diff options
author | bai | 2019-03-29 02:14:43 +0000 |
---|---|---|
committer | bai | 2019-03-29 02:14:43 +0000 |
commit | 95dfe14528663923ca2a88ec928f1d8d9df2402b (patch) | |
tree | 5bc88d1466957f1aa39043b056bde5c439648022 /cgi | |
download | weabot-95dfe14528663923ca2a88ec928f1d8d9df2402b.tar.gz weabot-95dfe14528663923ca2a88ec928f1d8d9df2402b.tar.xz weabot-95dfe14528663923ca2a88ec928f1d8d9df2402b.zip |
Init
Diffstat (limited to 'cgi')
82 files changed, 22557 insertions, 0 deletions
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 "<![CDATA[%s]]>" % 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 "<?%s?>" % self.toEncoding(output, encoding) + +class Comment(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "<!--%s-->" % NavigableString.__str__(self, encoding) + +class Declaration(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "<!%s>" % 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 = '</%s>' % 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, "<foo><bar></foo>" actually means + "<foo><bar></bar></foo>". + + [Another possible explanation is "<foo><bar /></foo>", 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('<!\s+([^<>]*)>'), + lambda x: '<!' + x.group(1) + '>') + ] + + 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: + + <br/> (No space between name of closing tag and tag close) + <! --Comment--> (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: + <p>Foo<b>Bar *<p>* should pop to 'p', not 'b'. + <p>Foo<table>Bar *<p>* should pop to 'table', not 'p'. + <p>Foo<table><tr>Bar *<p>* should pop to 'tr', not 'p'. + + <li><ul><li> *<li>* should pop to 'ul', not the first 'li'. + <tr><table><tr> *<tr>* should pop to 'table', not the first 'tr' + <td><tr><td> *<td>* 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 "</%s> is not real!" % name + self.handle_data('</%s>' % 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] == '<![CDATA[': + k = self.rawdata.find(']]>', 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 <p> tag should implicitly close the previous <p> tag. + + <p>Para1<p>Para2 + should be transformed into: + <p>Para1</p><p>Para2 + + Some tags can be nested arbitrarily. For instance, the occurance + of a <blockquote> tag should _not_ implicitly close the previous + <blockquote> tag. + + Alice said: <blockquote>Bob said: <blockquote>Blah + should NOT be transformed into: + Alice said: <blockquote>Bob said: </blockquote><blockquote>Blah + + Some tags can be nested, but the nesting is reset by the + interposition of other tags. For instance, a <tr> tag should + implicitly close the previous <tr> tag within the same <table>, + but not close a <tr> tag in another table. + + <table><tr>Blah<tr>Blah + should be transformed into: + <table><tr>Blah</tr><tr>Blah + but, + <tr>Blah<table><tr>Blah + should NOT be transformed into + <tr>Blah<table></tr><tr>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: + + <b>Foo<b>Bar</b></b> + + 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 + "<b>Foo<b>Bar", 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 '</b></b>' 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 + <script> tags contain Javascript and should not be parsed, that + META tags may contain encoding information, and so on. + + This also makes it better for subclassing than BeautifulStoneSoup + or BeautifulSoup.""" + + RESET_NESTING_TAGS = buildTagMap('noscript') + NESTABLE_TAGS = {} + +class BeautifulSOAP(BeautifulStoneSoup): + """This class will push a tag with only a single string child into + the tag's parent as an attribute. The attribute's name is the tag + name, and the value is the string child. An example should give + the flavor of the change: + + <foo><bar>baz</bar></foo> + => + <foo bar="baz"><bar>baz</bar></foo> + + You can then access fooTag['bar'] instead of fooTag.barTag.string. + + This is, of course, useful for scraping structures that tend to + use subelements instead of attributes, such as SOAP messages. Note + that it modifies its input, so don't print the modified version + out. + + I'm not sure how many people really want to use this class; let me + know if you do. Mainly I like the name.""" + + def popTag(self): + if len(self.tagStack) > 1: + tag = self.tagStack[-1] + parent = self.tagStack[-2] + parent._getAttrMap() + if (isinstance(tag, Tag) and len(tag.contents) == 1 and + isinstance(tag.contents[0], NavigableString) and + not parent.attrMap.has_key(tag.name)): + parent[tag.name] = tag.contents[0] + BeautifulStoneSoup.popTag(self) + +#Enterprise class names! It has come to our attention that some people +#think the names of the Beautiful Soup parser classes are too silly +#and "unprofessional" for use in enterprise screen-scraping. We feel +#your pain! For such-minded folk, the Beautiful Soup Consortium And +#All-Night Kosher Bakery recommends renaming this file to +#"RobustParser.py" (or, in cases of extreme enterprisiness, +#"RobustParserBeanInterface.class") and using the following +#enterprise-friendly class aliases: +class RobustXMLParser(BeautifulStoneSoup): + pass +class RobustHTMLParser(BeautifulSoup): + pass +class RobustWackAssHTMLParser(ICantBelieveItsBeautifulSoup): + pass +class RobustInsanelyWackAssHTMLParser(MinimalSoup): + pass +class SimplifyingSOAPParser(BeautifulSOAP): + pass + +###################################################### +# +# Bonus library: Unicode, Dammit +# +# This class forces XML data into a standard format (usually to UTF-8 +# or Unicode). It is heavily based on code from Mark Pilgrim's +# Universal Feed Parser. It does not rewrite the XML or HTML to +# reflect a new encoding: that happens in BeautifulStoneSoup.handle_pi +# (XML) and BeautifulSoup.start_meta (HTML). + +# Autodetects character encodings. +# Download from http://chardet.feedparser.org/ +try: + import chardet +# import chardet.constants +# chardet.constants._debug = 1 +except ImportError: + chardet = None + +# cjkcodecs and iconv_codec make Python know about more character encodings. +# Both are available from http://cjkpython.i18n.org/ +# They're built in if you use Python 2.4. +try: + import cjkcodecs.aliases +except ImportError: + pass +try: + import iconv_codec +except ImportError: + pass + +class UnicodeDammit: + """A class for detecting the encoding of a *ML document and + converting it to a Unicode string. If the source encoding is + windows-1252, can replace MS smart quotes with their HTML or XML + equivalents.""" + + # This dictionary maps commonly seen values for "charset" in HTML + # meta tags to the corresponding Python codec names. It only covers + # values that aren't in Python's aliases and can't be determined + # by the heuristics in find_codec. + CHARSET_ALIASES = { "macintosh" : "mac-roman", + "x-sjis" : "shift-jis" } + + def __init__(self, markup, overrideEncodings=[], + smartQuotesTo='xml', isHTML=False): + self.declaredHTMLEncoding = None + self.markup, documentEncoding, sniffedEncoding = \ + self._detectEncoding(markup, isHTML) + self.smartQuotesTo = smartQuotesTo + self.triedEncodings = [] + if markup == '' or isinstance(markup, unicode): + self.originalEncoding = None + self.unicode = unicode(markup) + return + + u = None + for proposedEncoding in overrideEncodings: + u = self._convertFrom(proposedEncoding) + if u: break + if not u: + for proposedEncoding in (documentEncoding, sniffedEncoding): + u = self._convertFrom(proposedEncoding) + if u: break + + # If no luck and we have auto-detection library, try that: + if not u and chardet and not isinstance(self.markup, unicode): + u = self._convertFrom(chardet.detect(self.markup)['encoding']) + + # As a last resort, try utf-8 and windows-1252: + if not u: + for proposed_encoding in ("utf-8", "windows-1252"): + u = self._convertFrom(proposed_encoding) + if u: break + + self.unicode = u + if not u: self.originalEncoding = None + + def _subMSChar(self, orig): + """Changes a MS smart quote character to an XML or HTML + entity.""" + sub = self.MS_CHARS.get(orig) + if isinstance(sub, tuple): + if self.smartQuotesTo == 'xml': + sub = '&#x%s;' % sub[1] + else: + sub = '&%s;' % sub[0] + return sub + + def _convertFrom(self, proposed): + proposed = self.find_codec(proposed) + if not proposed or proposed in self.triedEncodings: + return None + self.triedEncodings.append(proposed) + markup = self.markup + + # Convert smart quotes to HTML if coming from an encoding + # that might have them. + if self.smartQuotesTo and proposed.lower() in("windows-1252", + "iso-8859-1", + "iso-8859-2"): + markup = re.compile("([\x80-\x9f])").sub \ + (lambda(x): self._subMSChar(x.group(1)), + markup) + + try: + # print "Trying to convert document to %s" % proposed + u = self._toUnicode(markup, proposed) + self.markup = u + self.originalEncoding = proposed + except Exception, e: + # print "That didn't work!" + # print e + return None + #print "Correct encoding: %s" % proposed + return self.markup + + def _toUnicode(self, data, encoding): + '''Given a string and its encoding, decodes the string into Unicode. + %encoding is a string recognized by encodings.aliases''' + + # strip Byte Order Mark (if present) + if (len(data) >= 4) and (data[:2] == '\xfe\xff') \ + and (data[2:4] != '\x00\x00'): + encoding = 'utf-16be' + data = data[2:] + elif (len(data) >= 4) and (data[:2] == '\xff\xfe') \ + and (data[2:4] != '\x00\x00'): + encoding = 'utf-16le' + data = data[2:] + elif data[:3] == '\xef\xbb\xbf': + encoding = 'utf-8' + data = data[3:] + elif data[:4] == '\x00\x00\xfe\xff': + encoding = 'utf-32be' + data = data[4:] + elif data[:4] == '\xff\xfe\x00\x00': + encoding = 'utf-32le' + data = data[4:] + newdata = unicode(data, encoding) + return newdata + + def _detectEncoding(self, xml_data, isHTML=False): + """Given a document, tries to detect its XML encoding.""" + xml_encoding = sniffed_xml_encoding = None + try: + if xml_data[:4] == '\x4c\x6f\xa7\x94': + # EBCDIC + xml_data = self._ebcdic_to_ascii(xml_data) + elif xml_data[:4] == '\x00\x3c\x00\x3f': + # UTF-16BE + sniffed_xml_encoding = 'utf-16be' + xml_data = unicode(xml_data, 'utf-16be').encode('utf-8') + elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') \ + and (xml_data[2:4] != '\x00\x00'): + # UTF-16BE with BOM + sniffed_xml_encoding = 'utf-16be' + xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8') + elif xml_data[:4] == '\x3c\x00\x3f\x00': + # UTF-16LE + sniffed_xml_encoding = 'utf-16le' + xml_data = unicode(xml_data, 'utf-16le').encode('utf-8') + elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and \ + (xml_data[2:4] != '\x00\x00'): + # UTF-16LE with BOM + sniffed_xml_encoding = 'utf-16le' + xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8') + elif xml_data[:4] == '\x00\x00\x00\x3c': + # UTF-32BE + sniffed_xml_encoding = 'utf-32be' + xml_data = unicode(xml_data, 'utf-32be').encode('utf-8') + elif xml_data[:4] == '\x3c\x00\x00\x00': + # UTF-32LE + sniffed_xml_encoding = 'utf-32le' + xml_data = unicode(xml_data, 'utf-32le').encode('utf-8') + elif xml_data[:4] == '\x00\x00\xfe\xff': + # UTF-32BE with BOM + sniffed_xml_encoding = 'utf-32be' + xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8') + elif xml_data[:4] == '\xff\xfe\x00\x00': + # UTF-32LE with BOM + sniffed_xml_encoding = 'utf-32le' + xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8') + elif xml_data[:3] == '\xef\xbb\xbf': + # UTF-8 with BOM + sniffed_xml_encoding = 'utf-8' + xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8') + else: + sniffed_xml_encoding = 'ascii' + pass + except: + xml_encoding_match = None + xml_encoding_match = re.compile( + '^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data) + if not xml_encoding_match and isHTML: + regexp = re.compile('<\s*meta[^>]+charset=([^>]*?)[;\'">]', re.I) + xml_encoding_match = regexp.search(xml_data) + if xml_encoding_match is not None: + xml_encoding = xml_encoding_match.groups()[0].lower() + if isHTML: + self.declaredHTMLEncoding = xml_encoding + if sniffed_xml_encoding and \ + (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode', + 'iso-10646-ucs-4', 'ucs-4', 'csucs4', + 'utf-16', 'utf-32', 'utf_16', 'utf_32', + 'utf16', 'u16')): + xml_encoding = sniffed_xml_encoding + return xml_data, xml_encoding, sniffed_xml_encoding + + + def find_codec(self, charset): + return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \ + or (charset and self._codec(charset.replace("-", ""))) \ + or (charset and self._codec(charset.replace("-", "_"))) \ + or charset + + def _codec(self, charset): + if not charset: return charset + codec = None + try: + codecs.lookup(charset) + codec = charset + except (LookupError, ValueError): + pass + return codec + + EBCDIC_TO_ASCII_MAP = None + def _ebcdic_to_ascii(self, s): + c = self.__class__ + if not c.EBCDIC_TO_ASCII_MAP: + emap = (0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15, + 16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31, + 128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7, + 144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26, + 32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33, + 38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94, + 45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63, + 186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34, + 195,97,98,99,100,101,102,103,104,105,196,197,198,199,200, + 201,202,106,107,108,109,110,111,112,113,114,203,204,205, + 206,207,208,209,126,115,116,117,118,119,120,121,122,210, + 211,212,213,214,215,216,217,218,219,220,221,222,223,224, + 225,226,227,228,229,230,231,123,65,66,67,68,69,70,71,72, + 73,232,233,234,235,236,237,125,74,75,76,77,78,79,80,81, + 82,238,239,240,241,242,243,92,159,83,84,85,86,87,88,89, + 90,244,245,246,247,248,249,48,49,50,51,52,53,54,55,56,57, + 250,251,252,253,254,255) + import string + c.EBCDIC_TO_ASCII_MAP = string.maketrans( \ + ''.join(map(chr, range(256))), ''.join(map(chr, emap))) + return s.translate(c.EBCDIC_TO_ASCII_MAP) + + MS_CHARS = { '\x80' : ('euro', '20AC'), + '\x81' : ' ', + '\x82' : ('sbquo', '201A'), + '\x83' : ('fnof', '192'), + '\x84' : ('bdquo', '201E'), + '\x85' : ('hellip', '2026'), + '\x86' : ('dagger', '2020'), + '\x87' : ('Dagger', '2021'), + '\x88' : ('circ', '2C6'), + '\x89' : ('permil', '2030'), + '\x8A' : ('Scaron', '160'), + '\x8B' : ('lsaquo', '2039'), + '\x8C' : ('OElig', '152'), + '\x8D' : '?', + '\x8E' : ('#x17D', '17D'), + '\x8F' : '?', + '\x90' : '?', + '\x91' : ('lsquo', '2018'), + '\x92' : ('rsquo', '2019'), + '\x93' : ('ldquo', '201C'), + '\x94' : ('rdquo', '201D'), + '\x95' : ('bull', '2022'), + '\x96' : ('ndash', '2013'), + '\x97' : ('mdash', '2014'), + '\x98' : ('tilde', '2DC'), + '\x99' : ('trade', '2122'), + '\x9a' : ('scaron', '161'), + '\x9b' : ('rsaquo', '203A'), + '\x9c' : ('oelig', '153'), + '\x9d' : '?', + '\x9e' : ('#x17E', '17E'), + '\x9f' : ('Yuml', ''),} + +####################################################################### + + +#By default, act as an HTML pretty-printer. +if __name__ == '__main__': + import sys + soup = BeautifulSoup(sys.stdin) + print soup.prettify() diff --git a/cgi/GeoIP.dat b/cgi/GeoIP.dat Binary files differnew file mode 100644 index 0000000..b98993c --- /dev/null +++ b/cgi/GeoIP.dat diff --git a/cgi/anarkia.py b/cgi/anarkia.py new file mode 100644 index 0000000..6b7e5fd --- /dev/null +++ b/cgi/anarkia.py @@ -0,0 +1,439 @@ +# coding=utf-8 +import _mysql +from database import * +from framework import * +from template import * +from img import * +from post import * +from settings import Settings + +d_thread = {} +d_post = {} + +def anarkia(self, path_split): + setBoard('anarkia') + + if len(path_split) <= 2: + self.output = main() + return + + raise UserError, 'Ya fue, baisano...' + + if path_split[2] == 'opt': + self.output = boardoptions(self.formdata) + elif path_split[2] == 'mod': + self.output = mod(self.formdata) + elif path_split[2] == 'bans': + self.output = bans(self.formdata) + elif path_split[2] == 'css': + self.output = css(self.formdata) + elif path_split[2] == 'type': + self.output = type(self.formdata) + elif path_split[2] == 'emojis': + self.output = emojis(self.formdata) + else: + raise UserError, 'ke?' + +def main(): + board = Settings._.BOARD + + logs = FetchAll("SELECT * FROM `logs` WHERE `staff` = 'Anarko' ORDER BY `timestamp` DESC") + for log in logs: + log['timestamp_formatted'] = formatTimestamp(log['timestamp']) + + return renderTemplate('anarkia.html', {'mode': 0, 'logs': logs}) + +def type(formdata): + board = Settings._.BOARD + + if board['board_type'] == '1': + (type_now, type_do, do_num) = ('BBS', 'IB', '0') + else: + (type_now, type_do, do_num) = ('IB', 'BBS', '1') + + if formdata.get('transform') == 'do': + t = 0 + try: + with open('anarkia_time') as f: + t = int(f.read()) + except IOError: + pass + + dif = time.time() - t + if dif > (10 * 60): + #if True: + import re + t = time.time() + + board['board_type'] = do_num + board['force_css'] = Settings.HOME_URL + 'anarkia/style_' + type_do.lower() + '.css' + updateBoardSettings() + + # update posts + fix_board() + + # regenerate + setBoard('anarkia') + regenerateBoard(True) + + tf = timeTaken(t, time.time()) + + with open('anarkia_time', 'w') as f: + t = f.write(str(int(time.time()))) + + msg = 'Cambiada estructura de sección a %s. (%s)' % (type_do, tf) + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + else: + raise UserError, 'Esta acción sólo se puede realizar cada 10 minutos. Faltan: %d mins.' % (10-int(dif/60)) + + return renderTemplate('anarkia.html', {'mode': 7, 'type_now': type_now, 'type_do': type_do}) + +def fix_board(): + board = Settings._.BOARD + get_fix_dictionary() + + if board['board_type'] == '1': + to_fix = FetchAll("SELECT * FROM posts WHERE message LIKE '%%anarkia/res/%%' AND boardid = %s" % board['id']) + else: + to_fix = FetchAll("SELECT * FROM posts WHERE message LIKE '%%anarkia/read/%%' AND boardid = %s" % board['id']) + + for p in to_fix: + try: + if board['board_type'] == '1': + newmessage = re.sub(r'/anarkia/res/(\d+).html#(\d+)">>>(\d+)', fix_to_bbs, p['message']) + else: + newmessage = re.sub(r'/anarkia/read/(\d+)/(\d+)">>>(\d+)', fix_to_ib, p['message']) + + UpdateDb("UPDATE posts SET message = '%s' WHERE boardid = %s AND id = %s" % \ + (_mysql.escape_string(newmessage), board['id'], p['id'])) + except KeyError: + pass + + return True + +def fix_to_bbs(matchobj): + threadid = matchobj.group(1) + pid = matchobj.group(2) + new_thread = d_thread[threadid] + new_post = d_post[new_thread][pid] + return '/anarkia/read/%s/%s">>>%s' % (new_thread, new_post, new_post) + +def fix_to_ib(matchobj): + threadid = matchobj.group(1) + num = int(matchobj.group(2)) + new_thread = d_thread[threadid] + new_post = d_post[new_thread][num] + return '/anarkia/res/%s.html#%s">>>%s' % (new_thread, new_post, new_post) + +def get_fix_dictionary(): + global d_thread, d_post + board = Settings._.BOARD + res = FetchAll("SELECT id, timestamp, parentid FROM posts WHERE boardid = %s ORDER BY CASE parentid WHEN 0 THEN id ELSE parentid END ASC, `id` ASC" % board['id']) + num = 1 + thread = 0 + for p in res: + pid = p['id'] + if p['parentid'] == '0': + num = 1 + + time = p['timestamp'] + if board['board_type'] == '1': + d_thread[pid] = time + thread = time + else: + d_thread[time] = pid + thread = pid + + d_post[thread] = {} + + if board['board_type'] == '1': + d_post[thread][pid] = num + else: + d_post[thread][num] = pid + num += 1 + + return + +def css(formdata): + board = Settings._.BOARD + + if board['board_type'] == '1': + basename = 'style_bbs.css' + else: + basename = 'style_ib.css' + + fname = '%sanarkia/%s' % (Settings.HOME_DIR, basename) + + if formdata.get('cssfile'): + with open(fname, 'w') as f: + cssfile = f.write(formdata['cssfile']) + + msg = 'CSS actualizado.' + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + + with open(fname) as f: + cssfile = f.read() + + return renderTemplate('anarkia.html', {'mode': 6, 'basename': basename, 'cssfile': cssfile}) + +def bans(formdata): + board = Settings._.BOARD + + if formdata.get('unban'): + unban = int(formdata['unban']) + boardpickle = pickle.dumps(['anarkia']) + + ban = FetchOne("SELECT * FROM `bans` WHERE id = %d" % unban) + if not ban: + raise UserError, "Ban inválido." + if ban['boards'] != boardpickle: + raise USerError, "Ban inválido." + + UpdateDb('DELETE FROM `bans` WHERE id = %s' % ban['id']) + logAction("Usuario %s desbaneado." % ban['ip'][:4]) + regenerateAccess() + + bans = FetchAll('SELECT * FROM `bans` WHERE staff = \'anarko\'') + for ban in bans: + ban['added'] = formatTimestamp(ban['added']) + if ban['until'] == '0': + ban['until'] = _('Does not expire') + else: + ban['until'] = formatTimestamp(ban['until']) + return renderTemplate('anarkia.html', {'mode': 5, 'bans': bans}) + +def mod(formdata): + board = Settings._.BOARD + + if formdata.get('thread'): + parentid = int(formdata['thread']) + posts = FetchAll('SELECT * FROM `posts` WHERE (parentid = %d OR id = %d) AND boardid = %s ORDER BY `id` ASC' % (parentid, parentid, board['id'])) + return renderTemplate('anarkia.html', {'mode': 3, 'posts': posts}) + elif formdata.get('lock'): + postid = int(formdata['lock']) + post = FetchOne('SELECT id, locked FROM posts WHERE boardid = %s AND id = %d AND parentid = 0 LIMIT 1' % (board['id'], postid)) + if post['locked'] == '0': + setLocked = 1 + msg = "Hilo %s cerrado." % post['id'] + else: + setLocked = 0 + msg = "Hilo %s abierto." % post['id'] + + UpdateDb("UPDATE `posts` SET `locked` = %d WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % (setLocked, board["id"], post["id"])) + threadUpdated(post['id']) + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + elif formdata.get('del'): + postid = int(formdata['del']) + post = FetchOne('SELECT id, parentid FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % (board['id'], postid)) + if post['parentid'] != '0': + deletePost(post['id'], None, '3', False) + msg = "Mensaje %s eliminado." % post['id'] + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + else: + raise UserError, "jaj no" + elif formdata.get('restore'): + postid = int(formdata['restore']) + post = FetchOne('SELECT id, parentid FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % (board['id'], postid)) + + UpdateDb('UPDATE `posts` SET `IS_DELETED` = 0 WHERE `boardid` = %s AND `id` = %s LIMIT 1' % (board['id'], post['id'])) + if post['parentid'] != '0': + threadUpdated(post['parentid']) + else: + regenerateFrontPages() + msg = "Mensaje %s recuperado." % post['id'] + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + elif formdata.get('ban'): + postid = int(formdata['ban']) + post = FetchOne('SELECT id, ip FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % (board['id'], postid)) + + return renderTemplate('anarkia.html', {'mode': 4, 'post': post}) + elif formdata.get('banto'): + postid = int(formdata['banto']) + post = FetchOne('SELECT id, message, parentid, ip FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % (board['id'], postid)) + + reason = formdata.get('reason').replace('script', '').replace('meta', '') + if reason is not None: + if formdata['seconds'] != '0': + until = str(timestamp() + int(formdata['seconds'])) + else: + until = '0' + where = pickle.dumps(['anarkia']) + + ban = FetchOne("SELECT `id` FROM `bans` WHERE `ip` = '" + post['ip'] + "' AND `boards` = '" + _mysql.escape_string(where) + "' LIMIT 1") + if ban: + raise UserError, "Este usuario ya esta baneado." + + # Blind mode + if formdata.get('blind') == '1': + blind = '1' + else: + blind = '0' + + InsertDb("INSERT INTO `bans` (`ip`, `netmask`, `boards`, `added`, `until`, `staff`, `reason`, `blind`) VALUES ('" + post['ip'] + "', INET_ATON('255.255.255.255'), '" + _mysql.escape_string(where) + "', " + str(timestamp()) + ", " + until + ", 'anarko', '" + _mysql.escape_string(formdata['reason']) + "', '"+blind+"')") + + newmessage = post['message'] + '<hr /><span class="banned">A este usuario se le revocó el acceso. Razón: %s</span>' % reason + + UpdateDb("UPDATE posts SET message = '%s' WHERE boardid = %s AND id = %s" % (_mysql.escape_string(newmessage), board['id'], post['id'])) + if post['parentid'] != '0': + threadUpdated(post['parentid']) + else: + regenerateFrontPages() + regenerateAccess() + + msg = "Usuario %s baneado." % post['ip'][:4] + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + else: + reports = FetchAll("SELECT * FROM `reports` WHERE board = 'anarkia'") + threads = FetchAll('SELECT * FROM `posts` WHERE boardid = %s AND parentid = 0 ORDER BY `bumped` DESC' % board['id']) + return renderTemplate('anarkia.html', {'mode': 2, 'threads': threads, 'reports': reports}) + +def boardoptions(formdata): + board = Settings._.BOARD + + if formdata.get('longname'): + # submitted + board['longname'] = formdata['longname'].replace('script', '') + board['postarea_desc'] = formdata['postarea_desc'].replace('script', '').replace('meta', '') + board['postarea_extra'] = formdata['postarea_extra'].replace('script', '').replace('meta', '') + board['anonymous'] = formdata['anonymous'].replace('script', '') + board['subject'] = formdata['subject'].replace('script', '') + board['message'] = formdata['message'].replace('script', '') + board['useid'] = formdata['useid'] + if 'disable_name' in formdata.keys(): + board['disable_name'] = '1' + else: + board['disable_name'] = '0' + if 'disable_subject' in formdata.keys(): + board['disable_subject'] = '1' + else: + board['disable_subject'] = '0' + if 'allow_noimage' in formdata.keys(): + board['allow_noimage'] = '1' + else: + board['allow_noimage'] = '0' + if 'allow_images' in formdata.keys(): + board['allow_images'] = '1' + else: + board['allow_images'] = '0' + if 'allow_image_replies' in formdata.keys(): + board['allow_image_replies'] = '1' + else: + board['allow_image_replies'] = '0' + + # Update file types + UpdateDb("DELETE FROM `boards_filetypes` WHERE `boardid` = %s" % board['id']) + for filetype in filetypelist(): + if 'filetype'+filetype['ext'] in formdata.keys(): + UpdateDb("INSERT INTO `boards_filetypes` VALUES (%s, %s)" % (board['id'], filetype['id'])) + + try: + board['maxsize'] = int(formdata['maxsize']) + if board['maxsize'] > 10000: + board['maxsize'] = 10000 + except: + raise UserError, _("Max size must be numeric.") + + try: + board['thumb_px'] = int(formdata['thumb_px']) + if board['thumb_px'] > 500: + board['thumb_px'] = 500 + except: + raise UserError, _("Max thumb dimensions must be numeric.") + + try: + board['numthreads'] = int(formdata['numthreads']) + if board['numthreads'] > 15: + board['numthreads'] = 15 + except: + raise UserError, _("Max threads shown must be numeric.") + + try: + board['numcont'] = int(formdata['numcont']) + if board['numcont'] > 15: + board['numcont'] = 15 + except: + raise UserError, _("Max replies shown must be numeric.") + + t = time.time() + updateBoardSettings() + setBoard('anarkia') + regenerateBoard(True) + tf = timeTaken(t, time.time()) + + msg = 'Opciones cambiadas. %s' % tf + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + else: + return renderTemplate('anarkia.html', {'mode': 1, 'boardopts': board, 'filetypes': filetypelist(), 'supported_filetypes': board['filetypes_ext']}) + +def emojis(formdata): + board = Settings._.BOARD + board_pickle = _mysql.escape_string(pickle.dumps([board['dir']])) + + if formdata.get('new'): + import re + ext = {'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/png': 'png'} + + if not formdata['name']: + raise UserError, 'Ingresa nombre.' + if not re.match(r"^[0-9a-zA-Z]+$", formdata['name']): + raise UserError, 'Nombre inválido; solo letras/números.' + + name = ":%s:" % formdata['name'][:15] + data = formdata['file'] + + if not data: + raise UserError, 'Ingresa imagen.' + + # check if it exists + already = FetchOne("SELECT 1 FROM `filters` WHERE `boards` = '%s' AND `from` = '%s'" % (board_pickle, _mysql.escape_string(name))) + if already: + raise UserError, 'Este emoji ya existe.' + + # get image information + content_type, width, height, size, extra = getImageInfo(data) + + if content_type not in ext.keys(): + raise UserError, 'Formato inválido.' + if width > 500 or height > 500: + raise UserError, 'Dimensiones muy altas.' + if size > 150000: + raise UserError, 'Tamaño muy grande.' + + # create file names + thumb_width, thumb_height = getThumbDimensions(width, height, 32) + + file_path = Settings.ROOT_DIR + board["dir"] + "/e/" + formdata['name'][:15] + '.' + ext[content_type] + file_url = Settings.BOARDS_URL + board["dir"] + "/e/" + formdata['name'][:15] + '.' + ext[content_type] + to_filter = '<img src="%s" width="%d" height="%d" />' % (file_url, thumb_width, thumb_height) + + # start processing image + args = [Settings.CONVERT_PATH, "-", "-limit" , "thread", "1", "-resize", "%dx%d" % (thumb_width, thumb_height), "-quality", "80", file_path] + p = subprocess.Popen(args, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + out = p.communicate(input=data)[0] + + # insert into DB + sql = "INSERT INTO `filters` (`boards`, `type`, `action`, `from`, `to`, `staff`, `added`) VALUES ('%s', 0, 1, '%s', '%s', 'Anarko', '%s')" % (board_pickle, _mysql.escape_string(name), _mysql.escape_string(to_filter), timestamp()) + UpdateDb(sql) + + msg = "Emoji %s agregado." % name + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + elif formdata.get('del'): + return renderTemplate('anarkia.html', {'mode': 99, 'msg': 'Del.'}) + else: + filters = FetchAll("SELECT * FROM `filters` WHERE `boards` = '%s' ORDER BY `added` DESC" % board_pickle) + return renderTemplate('anarkia.html', {'mode': 8, 'emojis': filters}) + +def filetypelist(): + filetypes = FetchAll('SELECT * FROM `filetypes` ORDER BY `ext` ASC') + return filetypes + +def logAction(action): + InsertDb("INSERT INTO `logs` (`timestamp`, `staff`, `action`) VALUES (" + str(timestamp()) + ", 'Anarko', '" + _mysql.escape_string(action) + "')")
\ No newline at end of file diff --git a/cgi/api.py b/cgi/api.py new file mode 100644 index 0000000..8960578 --- /dev/null +++ b/cgi/api.py @@ -0,0 +1,392 @@ +# coding=utf-8 +import json +import _mysql +import time + +from framework import * +from database import * +from post import * + +def api(self, path_split): + if len(path_split) > 2: + try: + self.output = api_process(self, path_split) + except APIError, e: + self.output = api_error("error", e.message) + except UserError, e: + self.output = api_error("failed", e.message) + except Exception, e: + import sys, traceback + exc_type, exc_value, exc_traceback = sys.exc_info() + detail = ["%s : %s : %s : %s" % (os.path.basename(o[0]),o[1],o[2],o[3]) for o in traceback.extract_tb(exc_traceback)] + + self.output = api_error("exception", str(e), str(type(e)), detail) + else: + self.output = api_error("error", "No method specified") + +def api_process(self, path_split): + formdata = self.formdata + ip = self.environ["REMOTE_ADDR"] + t = time.time() + method = path_split[2] + + #bans = ['181.72.116.62'] + bans = [] + if ip in bans: + raise APIError, "You have been blacklisted." + + #with open('../api_log.txt', 'a') as f: + # logstr = "[%s] %s: %s\n" % (formatTimestamp(t), ip, repr(path_split)) + # f.write(logstr) + + values = {'state': 'success'} + + if method == 'boards': + boards = FetchAll('SELECT dir, name, board_type, allow_images, allow_image_replies, maxsize FROM `boards` WHERE `secret`=0 ORDER BY `name` ASC') + values['boards'] = boards + for board in values['boards']: + board['board_type'] = int(board['board_type']) + board['allow_images'] = int(board['allow_images']) + board['allow_image_replies'] = int(board['allow_image_replies']) + board['maxsize'] = int(board['maxsize']) + + elif method == 'last': + data_limit = formdata.get('limit') + data_since = formdata.get('since') + + limit = 10 + since = 0 + + if data_limit: + try: + limit = int(data_limit) + except ValueError: + raise APIError, "Limit must be numeric" + + if data_since: + try: + since = int(data_since) + except ValueError: + raise APIError, "Since must be numeric" + + if limit > 50: + raise APIError, "Maximum limit is 50" + + sql = "SELECT posts.id, boards.dir, timestamp, timestamp_formatted, posts.name, tripcode, email, posts.subject, posts.message, file, file_size, image_height, image_width, thumb, thumb_width, thumb_height, parentid FROM posts INNER JOIN boards ON boardid = boards.id WHERE timestamp > %d AND IS_DELETED = 0 AND email NOT LIKE '%%sage%%' AND boards.secret = 0 ORDER BY timestamp DESC LIMIT %d" % (since, limit) + values['posts'] = FetchAll(sql) + + for post in values['posts']: + post['id'] = int(post['id']) + post['timestamp'] = int(post['timestamp']) + post['parentid'] = int(post['parentid']) + post['file_size'] = int(post['file_size']) + post['image_width'] = int(post['image_width']) + post['image_height'] = int(post['image_height']) + post['thumb_width'] = int(post['thumb_width']) + post['thumb_height'] = int(post['thumb_height']) + post['message'] = post['message'].decode('utf-8', 'replace') + elif method == 'lastage': + data_limit = formdata.get('limit') + data_time = formdata.get('time', 0) + + limit = 30 + + if data_limit: + try: + limit = int(data_limit) + except ValueError: + raise APIError, "Limit must be numeric" + + if limit > 50: + raise APIError, "Maximum limit is 50" + + threads = getLastAge(limit) + if threads[0]['bumped'] > int(data_time): + values['threads'] = threads + else: + values['threads'] = [] + elif method == 'list': + data_board = formdata.get('dir') + data_offset = formdata.get('offset') + data_limit = formdata.get('limit') + data_replies = formdata.get('replies') + offset = 0 + limit = 10 + numreplies = 2 + + if not data_board: + raise APIError, "Missing parameters" + + if data_limit: + try: + limit = int(data_limit) + except ValueError: + raise APIError, "Limit must be numeric" + + if data_offset: + try: + offset = int(data_offset) + except ValueError: + raise APIError, "Offset must be numeric" + + if data_replies: + try: + numreplies = int(data_replies) + except ValueError: + raise APIError, "Replies must be numeric" + + if data_replies and limit > 30: + raise APIError, "Maximum limit is 30" + + board = setBoard(data_board) + + #sql = "SELECT id, timestamp, bumped, timestamp_formatted, name, tripcode, email, subject, message, file, thumb FROM posts WHERE boardid = %s AND parentid = 0 AND IS_DELETED = 0 ORDER BY bumped DESC LIMIT %d" % (board['id'], limit) + sql = "SELECT p.id, p.timestamp, p.bumped, p.expires, p.expires_formatted, p.timestamp_formatted, p.name, p.tripcode, p.email, p.subject, p.message, p.file, p.file_size, p.image_width, p.image_height, p.thumb, p.thumb_height, p.thumb_width, p.locked, coalesce(x.count,0) AS total_replies, coalesce(x.files,0) AS total_files FROM `posts` AS p LEFT JOIN (SELECT parentid, count(1) as count, count(nullif(file, '')) as files FROM `posts` WHERE boardid = %(board)s GROUP BY parentid) AS x ON p.id=x.parentid WHERE p.parentid = 0 AND p.boardid = %(board)s AND p.IS_DELETED = 0 ORDER BY `bumped` DESC LIMIT %(limit)d OFFSET %(offset)d" % {'board': board["id"], 'limit': limit, 'offset': offset} + + threads = FetchAll(sql) + + if numreplies: + for thread in threads: + lastreplies = FetchAll("SELECT id, timestamp, timestamp_formatted, name, tripcode, email, subject, message, file, file_size, image_height, image_width, thumb, thumb_width, thumb_height, IS_DELETED FROM `posts` WHERE parentid = %s AND boardid = %s ORDER BY `timestamp` DESC LIMIT %d" % (thread['id'], board['id'], numreplies)) + lastreplies = lastreplies[::-1] + thread['id'] = int(thread['id']) + thread['timestamp'] = int(thread['timestamp']) + thread['bumped'] = int(thread['bumped']) + thread['expires'] = int(thread['expires']) + thread['total_replies'] = int(thread['total_replies']) + thread['total_files'] = int(thread['total_files']) + thread['file_size'] = int(thread['file_size']) + thread['image_width'] = int(thread['image_width']) + thread['image_height'] = int(thread['image_height']) + thread['thumb_width'] = int(thread['thumb_width']) + thread['thumb_height'] = int(thread['thumb_height']) + thread['locked'] = int(thread['locked']) + + thread['replies'] = [] + + for post in lastreplies: + post['IS_DELETED'] = int(post['IS_DELETED']) + post['id'] = int(post['id']) + post['timestamp'] = int(post['timestamp']) + + if post['IS_DELETED']: + empty_post = {'id': post['id'], + 'IS_DELETED': post['IS_DELETED'], + 'timestamp': post['timestamp'], + } + thread['replies'].append(empty_post) + else: + post['file_size'] = int(post['file_size']) + post['image_width'] = int(post['image_width']) + post['image_height'] = int(post['image_height']) + post['thumb_width'] = int(post['thumb_width']) + post['thumb_height'] = int(post['thumb_height']) + post['message'] = post['message'].decode('utf-8', 'replace') + + thread['replies'].append(post) + + values['threads'] = threads + elif method == 'thread': + data_board = formdata.get('dir') + data_threadid = formdata.get('id') + data_threadts = formdata.get('ts') + data_offset = formdata.get('offset') + data_limit = formdata.get('limit') + data_striphtml = formdata.get('nohtml') + striphtml = False + offset = 0 + limit = 1000 + + if not data_board or (not data_threadid and not data_threadts): + raise APIError, "Missing parameters" + + if data_limit: + try: + limit = int(data_limit) + except ValueError: + raise APIError, "Limit must be numeric" + + if data_offset: + try: + offset = int(data_offset) + except ValueError: + raise APIError, "Offset must be numeric" + + if data_striphtml: + if int(data_striphtml) == 1: + striphtml = True + + board = setBoard(data_board) + search_field = 'id' + search_val = 0 + + try: + search_val = int(data_threadid) + except (ValueError, TypeError): + pass + + try: + search_val = int(data_threadts) + search_field = 'timestamp' + except (ValueError, TypeError): + pass + + if not search_val: + raise APIError, "No thread ID" + + op_post = FetchOne("SELECT id, timestamp, subject, locked FROM posts WHERE `%s` = '%d' AND boardid = '%s' AND parentid = 0" % (search_field, search_val, board["id"])) + + if not op_post: + raise APIError, "Not a thread" + + values['id'] = int(op_post['id']) + values['timestamp'] = int(op_post['timestamp']) + values['subject'] = op_post['subject'] + values['locked'] = int(op_post['locked']) + + total_replies = int(FetchOne("SELECT COUNT(1) FROM posts WHERE boardid = '%s' AND parentid = '%d'" % (board["id"], values['id']), 0)[0]) + + values['total_replies'] = total_replies + + sql = "SELECT id, parentid, timestamp, timestamp_formatted, name, tripcode, email, subject, message, file, file_size, image_width, image_height, thumb, thumb_width, thumb_height, IS_DELETED FROM posts WHERE boardid = %s AND (parentid = %s OR id = %s) ORDER BY id ASC LIMIT %d OFFSET %d" % (_mysql.escape_string(board['id']), values['id'], values['id'], limit, offset) + posts = FetchAll(sql) + + values['posts'] = [] + + for post in posts: + post['IS_DELETED'] = int(post['IS_DELETED']) + post['id'] = int(post['id']) + post['parentid'] = int(post['parentid']) + post['timestamp'] = int(post['timestamp']) + + if post['IS_DELETED']: + empty_post = {'id': post['id'], + 'IS_DELETED': post['IS_DELETED'], + 'parentid': post['parentid'], + 'timestamp': post['timestamp'], + } + values['posts'].append(empty_post) + else: + post['file_size'] = int(post['file_size']) + post['image_width'] = int(post['image_width']) + post['image_height'] = int(post['image_height']) + post['thumb_width'] = int(post['thumb_width']) + post['thumb_height'] = int(post['thumb_height']) + post['message'] = post['message'].decode('utf-8', 'replace') + if striphtml: + post['message'] = post['message'].replace("<br />", " ") + post['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub("", post['message']) + values['posts'].append(post) + elif method == 'get': + data_board = formdata.get('dir') + data_parentid = formdata.get('thread') + data_postid = formdata.get('id') + data_postnum = formdata.get('num') + + if not data_board and (not data_postid or (not data_postnum and not data_parentid)): + raise APIError, "Missing parameters" + + board = setBoard(data_board) + postid = 0 + + if data_postnum: + data_postid = getID(data_parentid, data_postid) + + try: + postid = int(data_postid) + except ValueError: + raise APIError, "Post ID must be numeric" + + post = FetchOne("SELECT id, parentid, timestamp, timestamp_formatted, name, tripcode, email, subject, message, file, file_size, image_width, image_height, thumb, thumb_width, thumb_height, IS_DELETED FROM posts WHERE `id`='%d' AND boardid='%s'" % (postid, board["id"])) + + if not post: + raise APIError, "Post ID cannot be found" + + values['posts'] = [] + + post['IS_DELETED'] = int(post['IS_DELETED']) + post['id'] = int(post['id']) + post['parentid'] = int(post['parentid']) + post['timestamp'] = int(post['timestamp']) + + if post['IS_DELETED']: + empty_post = {'id': post['id'], + 'IS_DELETED': post['IS_DELETED'], + 'parentid': post['parentid'], + 'timestamp': post['timestamp'], + } + values['posts'].append(empty_post) + else: + post['file_size'] = int(post['file_size']) + post['image_width'] = int(post['image_width']) + post['image_height'] = int(post['image_height']) + post['thumb_width'] = int(post['thumb_width']) + post['thumb_height'] = int(post['thumb_height']) + post['message'] = post['message'].decode('utf-8', 'replace') + values['posts'].append(post) + elif method == 'delete': + data_board = formdata.get('dir') + data_postid = formdata.get('id') + data_imageonly = formdata.get('imageonly') + data_password = formdata.get('password') + + if not data_board or not data_postid or not data_password: + raise APIError, "Missing parameters" + + imageonly = False + board = setBoard(data_board) + + try: + postid = int(data_postid) + except ValueError: + raise APIError, "Post ID must be numeric" + + if data_imageonly and data_imageonly == 1: + imageonly = True + + deletePost(postid, data_password, board['recyclebin'], imageonly) + elif method == 'post': + boarddir = formdata.get('board') + + if not boarddir: + raise APIError, "Missing parameters" + + parent = formdata.get('parent') + trap1 = formdata.get('name', '') + trap2 = formdata.get('email', '') + name = formdata.get('fielda', '') + email = formdata.get('fieldb', '') + subject = formdata.get('subject', '') + message = formdata.get('message', '') + file = formdata.get('file') + file_original = formdata.get('file_original') + spoil = formdata.get('spoil') + oek_file = formdata.get('oek_file') + password = formdata.get('password', '') + noimage = formdata.get('noimage') + mobile = ("mobile" in 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) + + values['post_url'] = post_url + values['time_taken'] = ttaken + else: + raise APIError, "Invalid method" + + values['time'] = int(t) + #values['time_taken'] = time.time() - t + return json.dumps(values, sort_keys=True, separators=(',',':')) + +def api_error(errtype, msg, type=None, detail=None): + values = {'state': errtype, 'message': msg} + + if type: + values['type'] = type + if detail: + values['detail'] = detail + + return json.dumps(values) + +class APIError(Exception): + pass diff --git a/cgi/database.py b/cgi/database.py new file mode 100644 index 0000000..c8611c5 --- /dev/null +++ b/cgi/database.py @@ -0,0 +1,69 @@ +# coding=utf-8 + +import threading +import _mysql +from settings import Settings + +database_lock = threading.Lock() + +try: + # Although SQLAlchemy is optional, it is highly recommended + import sqlalchemy.pool as pool + _mysql = pool.manage( module = _mysql, + pool_size = Settings.DATABASE_POOL_SIZE, + max_overflow = Settings.DATABASE_POOL_OVERFLOW) + Settings._.USING_SQLALCHEMY = True +except ImportError: + pass + +def OpenDb(): + if Settings._.CONN is None: + Settings._.CONN = _mysql.connect(host = Settings.DATABASE_HOST, + user = Settings.DATABASE_USERNAME, + passwd = Settings.DATABASE_PASSWORD, + db = Settings.DATABASE_DB) + +def FetchAll(query, method=1): + """ + Query and fetch all results as a list + """ + db = Settings._.CONN + + db.query(query) + r = db.use_result() + return r.fetch_row(0, method) + +def FetchOne(query, method=1): + """ + Query and fetch only the first result + """ + db = Settings._.CONN + + db.query(query) + r = db.use_result() + try: + return r.fetch_row(1, method)[0] + except: + return None + +def UpdateDb(query): + """ + Update the DB (UPDATE/DELETE) and return # of affected rows + """ + db = Settings._.CONN + + db.query(query) + return db.affected_rows() + +def InsertDb(query): + """ + Insert into the DB and return the primary key of new row + """ + db = Settings._.CONN + + db.query(query) + return db.insert_id() + +def CloseDb(): + if Settings._.CONN is not None: + Settings._.CONN.close() diff --git a/cgi/fcgi.py b/cgi/fcgi.py new file mode 100644 index 0000000..8677679 --- /dev/null +++ b/cgi/fcgi.py @@ -0,0 +1,1332 @@ +# Copyright (c) 2002, 2003, 2005, 2006 Allan Saddi <allan@saddi.com> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. +# +# $Id$ + +""" +fcgi - a FastCGI/WSGI gateway. + +For more information about FastCGI, see <http://www.fastcgi.com/>. + +For more information about the Web Server Gateway Interface, see +<http://www.python.org/peps/pep-0333.html>. + +Example usage: + + #!/usr/bin/env python + from myapplication import app # Assume app is your WSGI application object + from fcgi import WSGIServer + WSGIServer(app).run() + +See the documentation for WSGIServer/Server for more information. + +On most platforms, fcgi will fallback to regular CGI behavior if run in a +non-FastCGI context. If you want to force CGI behavior, set the environment +variable FCGI_FORCE_CGI to "Y" or "y". +""" + +__author__ = 'Allan Saddi <allan@saddi.com>' +__version__ = '$Revision$' + +import sys +import os +import signal +import struct +import cStringIO as StringIO +import select +import socket +import errno +import traceback + +try: + import thread + import threading + thread_available = True +except ImportError: + import dummy_thread as thread + import dummy_threading as threading + thread_available = False + +# Apparently 2.3 doesn't define SHUT_WR? Assume it is 1 in this case. +if not hasattr(socket, 'SHUT_WR'): + socket.SHUT_WR = 1 + +__all__ = ['WSGIServer'] + +# Constants from the spec. +FCGI_LISTENSOCK_FILENO = 0 + +FCGI_HEADER_LEN = 8 + +FCGI_VERSION_1 = 1 + +FCGI_BEGIN_REQUEST = 1 +FCGI_ABORT_REQUEST = 2 +FCGI_END_REQUEST = 3 +FCGI_PARAMS = 4 +FCGI_STDIN = 5 +FCGI_STDOUT = 6 +FCGI_STDERR = 7 +FCGI_DATA = 8 +FCGI_GET_VALUES = 9 +FCGI_GET_VALUES_RESULT = 10 +FCGI_UNKNOWN_TYPE = 11 +FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE + +FCGI_NULL_REQUEST_ID = 0 + +FCGI_KEEP_CONN = 1 + +FCGI_RESPONDER = 1 +FCGI_AUTHORIZER = 2 +FCGI_FILTER = 3 + +FCGI_REQUEST_COMPLETE = 0 +FCGI_CANT_MPX_CONN = 1 +FCGI_OVERLOADED = 2 +FCGI_UNKNOWN_ROLE = 3 + +FCGI_MAX_CONNS = 'FCGI_MAX_CONNS' +FCGI_MAX_REQS = 'FCGI_MAX_REQS' +FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS' + +FCGI_Header = '!BBHHBx' +FCGI_BeginRequestBody = '!HB5x' +FCGI_EndRequestBody = '!LB3x' +FCGI_UnknownTypeBody = '!B7x' + +FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody) +FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody) + +if __debug__: + import time + + # Set non-zero to write debug output to a file. + DEBUG = 0 + DEBUGLOG = '/tmp/fcgi.log' + + def _debug(level, msg): + if DEBUG < level: + return + + try: + f = open(DEBUGLOG, 'a') + f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg)) + f.close() + except: + pass + +class InputStream(object): + """ + File-like object representing FastCGI input streams (FCGI_STDIN and + FCGI_DATA). Supports the minimum methods required by WSGI spec. + """ + def __init__(self, conn): + self._conn = conn + + # See Server. + self._shrinkThreshold = conn.server.inputStreamShrinkThreshold + + self._buf = '' + self._bufList = [] + self._pos = 0 # Current read position. + self._avail = 0 # Number of bytes currently available. + + self._eof = False # True when server has sent EOF notification. + + def _shrinkBuffer(self): + """Gets rid of already read data (since we can't rewind).""" + if self._pos >= self._shrinkThreshold: + self._buf = self._buf[self._pos:] + self._avail -= self._pos + self._pos = 0 + + assert self._avail >= 0 + + def _waitForData(self): + """Waits for more data to become available.""" + self._conn.process_input() + + def read(self, n=-1): + if self._pos == self._avail and self._eof: + return '' + while True: + if n < 0 or (self._avail - self._pos) < n: + # Not enough data available. + if self._eof: + # And there's no more coming. + newPos = self._avail + break + else: + # Wait for more data. + self._waitForData() + continue + else: + newPos = self._pos + n + break + # Merge buffer list, if necessary. + if self._bufList: + self._buf += ''.join(self._bufList) + self._bufList = [] + r = self._buf[self._pos:newPos] + self._pos = newPos + self._shrinkBuffer() + return r + + def readline(self, length=None): + if self._pos == self._avail and self._eof: + return '' + while True: + # Unfortunately, we need to merge the buffer list early. + if self._bufList: + self._buf += ''.join(self._bufList) + self._bufList = [] + # Find newline. + i = self._buf.find('\n', self._pos) + if i < 0: + # Not found? + if self._eof: + # No more data coming. + newPos = self._avail + break + else: + # Wait for more to come. + self._waitForData() + continue + else: + newPos = i + 1 + break + if length is not None: + if self._pos + length < newPos: + newPos = self._pos + length + r = self._buf[self._pos:newPos] + self._pos = newPos + self._shrinkBuffer() + return r + + def readlines(self, sizehint=0): + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + def __iter__(self): + return self + + def next(self): + r = self.readline() + if not r: + raise StopIteration + return r + + def add_data(self, data): + if not data: + self._eof = True + else: + self._bufList.append(data) + self._avail += len(data) + +class MultiplexedInputStream(InputStream): + """ + A version of InputStream meant to be used with MultiplexedConnections. + Assumes the MultiplexedConnection (the producer) and the Request + (the consumer) are running in different threads. + """ + def __init__(self, conn): + super(MultiplexedInputStream, self).__init__(conn) + + # Arbitrates access to this InputStream (it's used simultaneously + # by a Request and its owning Connection object). + lock = threading.RLock() + + # Notifies Request thread that there is new data available. + self._lock = threading.Condition(lock) + + def _waitForData(self): + # Wait for notification from add_data(). + self._lock.wait() + + def read(self, n=-1): + self._lock.acquire() + try: + return super(MultiplexedInputStream, self).read(n) + finally: + self._lock.release() + + def readline(self, length=None): + self._lock.acquire() + try: + return super(MultiplexedInputStream, self).readline(length) + finally: + self._lock.release() + + def add_data(self, data): + self._lock.acquire() + try: + super(MultiplexedInputStream, self).add_data(data) + self._lock.notify() + finally: + self._lock.release() + +class OutputStream(object): + """ + FastCGI output stream (FCGI_STDOUT/FCGI_STDERR). By default, calls to + write() or writelines() immediately result in Records being sent back + to the server. Buffering should be done in a higher level! + """ + def __init__(self, conn, req, type, buffered=False): + self._conn = conn + self._req = req + self._type = type + self._buffered = buffered + self._bufList = [] # Used if buffered is True + self.dataWritten = False + self.closed = False + + def _write(self, data): + length = len(data) + while length: + toWrite = min(length, self._req.server.maxwrite - FCGI_HEADER_LEN) + + rec = Record(self._type, self._req.requestId) + rec.contentLength = toWrite + rec.contentData = data[:toWrite] + self._conn.writeRecord(rec) + + data = data[toWrite:] + length -= toWrite + + def write(self, data): + assert not self.closed + + if not data: + return + + self.dataWritten = True + + if self._buffered: + self._bufList.append(data) + else: + self._write(data) + + def writelines(self, lines): + assert not self.closed + + for line in lines: + self.write(line) + + def flush(self): + # Only need to flush if this OutputStream is actually buffered. + if self._buffered: + data = ''.join(self._bufList) + self._bufList = [] + self._write(data) + + # Though available, the following should NOT be called by WSGI apps. + def close(self): + """Sends end-of-stream notification, if necessary.""" + if not self.closed and self.dataWritten: + self.flush() + rec = Record(self._type, self._req.requestId) + self._conn.writeRecord(rec) + self.closed = True + +class TeeOutputStream(object): + """ + Simple wrapper around two or more output file-like objects that copies + written data to all streams. + """ + def __init__(self, streamList): + self._streamList = streamList + + def write(self, data): + for f in self._streamList: + f.write(data) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def flush(self): + for f in self._streamList: + f.flush() + +class StdoutWrapper(object): + """ + Wrapper for sys.stdout so we know if data has actually been written. + """ + def __init__(self, stdout): + self._file = stdout + self.dataWritten = False + + def write(self, data): + if data: + self.dataWritten = True + self._file.write(data) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def __getattr__(self, name): + return getattr(self._file, name) + +def decode_pair(s, pos=0): + """ + Decodes a name/value pair. + + The number of bytes decoded as well as the name/value pair + are returned. + """ + nameLength = ord(s[pos]) + if nameLength & 128: + nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff + pos += 4 + else: + pos += 1 + + valueLength = ord(s[pos]) + if valueLength & 128: + valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff + pos += 4 + else: + pos += 1 + + name = s[pos:pos+nameLength] + pos += nameLength + value = s[pos:pos+valueLength] + pos += valueLength + + return (pos, (name, value)) + +def encode_pair(name, value): + """ + Encodes a name/value pair. + + The encoded string is returned. + """ + nameLength = len(name) + if nameLength < 128: + s = chr(nameLength) + else: + s = struct.pack('!L', nameLength | 0x80000000L) + + valueLength = len(value) + if valueLength < 128: + s += chr(valueLength) + else: + s += struct.pack('!L', valueLength | 0x80000000L) + + return s + name + value + +class Record(object): + """ + A FastCGI Record. + + Used for encoding/decoding records. + """ + def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID): + self.version = FCGI_VERSION_1 + self.type = type + self.requestId = requestId + self.contentLength = 0 + self.paddingLength = 0 + self.contentData = '' + + def _recvall(sock, length): + """ + Attempts to receive length bytes from a socket, blocking if necessary. + (Socket may be blocking or non-blocking.) + """ + dataList = [] + recvLen = 0 + while length: + try: + data = sock.recv(length) + except socket.error, e: + if e[0] == errno.EAGAIN: + select.select([sock], [], []) + continue + else: + raise + if not data: # EOF + break + dataList.append(data) + dataLen = len(data) + recvLen += dataLen + length -= dataLen + return ''.join(dataList), recvLen + _recvall = staticmethod(_recvall) + + def read(self, sock): + """Read and decode a Record from a socket.""" + try: + header, length = self._recvall(sock, FCGI_HEADER_LEN) + except: + raise EOFError + + if length < FCGI_HEADER_LEN: + raise EOFError + + self.version, self.type, self.requestId, self.contentLength, \ + self.paddingLength = struct.unpack(FCGI_Header, header) + + if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, ' + 'contentLength = %d' % + (sock.fileno(), self.type, self.requestId, + self.contentLength)) + + if self.contentLength: + try: + self.contentData, length = self._recvall(sock, + self.contentLength) + except: + raise EOFError + + if length < self.contentLength: + raise EOFError + + if self.paddingLength: + try: + self._recvall(sock, self.paddingLength) + except: + raise EOFError + + def _sendall(sock, data): + """ + Writes data to a socket and does not return until all the data is sent. + """ + length = len(data) + while length: + try: + sent = sock.send(data) + except socket.error, e: + if e[0] == errno.EAGAIN: + select.select([], [sock], []) + continue + else: + raise + data = data[sent:] + length -= sent + _sendall = staticmethod(_sendall) + + def write(self, sock): + """Encode and write a Record to a socket.""" + self.paddingLength = -self.contentLength & 7 + + if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, ' + 'contentLength = %d' % + (sock.fileno(), self.type, self.requestId, + self.contentLength)) + + header = struct.pack(FCGI_Header, self.version, self.type, + self.requestId, self.contentLength, + self.paddingLength) + self._sendall(sock, header) + if self.contentLength: + self._sendall(sock, self.contentData) + if self.paddingLength: + self._sendall(sock, '\x00'*self.paddingLength) + +class Request(object): + """ + Represents a single FastCGI request. + + These objects are passed to your handler and is the main interface + between your handler and the fcgi module. The methods should not + be called by your handler. However, server, params, stdin, stdout, + stderr, and data are free for your handler's use. + """ + def __init__(self, conn, inputStreamClass): + self._conn = conn + + self.server = conn.server + self.params = {} + self.stdin = inputStreamClass(conn) + self.stdout = OutputStream(conn, self, FCGI_STDOUT) + self.stderr = OutputStream(conn, self, FCGI_STDERR, buffered=True) + self.data = inputStreamClass(conn) + + def run(self): + """Runs the handler, flushes the streams, and ends the request.""" + try: + protocolStatus, appStatus = self.server.handler(self) + except: + traceback.print_exc(file=self.stderr) + self.stderr.flush() + if not self.stdout.dataWritten: + self.server.error(self) + + protocolStatus, appStatus = FCGI_REQUEST_COMPLETE, 0 + + if __debug__: _debug(1, 'protocolStatus = %d, appStatus = %d' % + (protocolStatus, appStatus)) + + self._flush() + self._end(appStatus, protocolStatus) + + def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE): + self._conn.end_request(self, appStatus, protocolStatus) + + def _flush(self): + self.stdout.close() + self.stderr.close() + +class CGIRequest(Request): + """A normal CGI request disguised as a FastCGI request.""" + def __init__(self, server): + # These are normally filled in by Connection. + self.requestId = 1 + self.role = FCGI_RESPONDER + self.flags = 0 + self.aborted = False + + self.server = server + self.params = dict(os.environ) + self.stdin = sys.stdin + self.stdout = StdoutWrapper(sys.stdout) # Oh, the humanity! + self.stderr = sys.stderr + self.data = StringIO.StringIO() + + def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE): + sys.exit(appStatus) + + def _flush(self): + # Not buffered, do nothing. + pass + +class Connection(object): + """ + A Connection with the web server. + + Each Connection is associated with a single socket (which is + connected to the web server) and is responsible for handling all + the FastCGI message processing for that socket. + """ + _multiplexed = False + _inputStreamClass = InputStream + + def __init__(self, sock, addr, server): + self._sock = sock + self._addr = addr + self.server = server + + # Active Requests for this Connection, mapped by request ID. + self._requests = {} + + def _cleanupSocket(self): + """Close the Connection's socket.""" + try: + self._sock.shutdown(socket.SHUT_WR) + except: + return + try: + while True: + r, w, e = select.select([self._sock], [], []) + if not r or not self._sock.recv(1024): + break + except: + pass + self._sock.close() + + def run(self): + """Begin processing data from the socket.""" + self._keepGoing = True + while self._keepGoing: + try: + self.process_input() + except EOFError: + break + except (select.error, socket.error), e: + if e[0] == errno.EBADF: # Socket was closed by Request. + break + raise + + self._cleanupSocket() + + def process_input(self): + """Attempt to read a single Record from the socket and process it.""" + # Currently, any children Request threads notify this Connection + # that it is no longer needed by closing the Connection's socket. + # We need to put a timeout on select, otherwise we might get + # stuck in it indefinitely... (I don't like this solution.) + while self._keepGoing: + try: + r, w, e = select.select([self._sock], [], [], 1.0) + except ValueError: + # Sigh. ValueError gets thrown sometimes when passing select + # a closed socket. + raise EOFError + if r: break + if not self._keepGoing: + return + rec = Record() + rec.read(self._sock) + + if rec.type == FCGI_GET_VALUES: + self._do_get_values(rec) + elif rec.type == FCGI_BEGIN_REQUEST: + self._do_begin_request(rec) + elif rec.type == FCGI_ABORT_REQUEST: + self._do_abort_request(rec) + elif rec.type == FCGI_PARAMS: + self._do_params(rec) + elif rec.type == FCGI_STDIN: + self._do_stdin(rec) + elif rec.type == FCGI_DATA: + self._do_data(rec) + elif rec.requestId == FCGI_NULL_REQUEST_ID: + self._do_unknown_type(rec) + else: + # Need to complain about this. + pass + + def writeRecord(self, rec): + """ + Write a Record to the socket. + """ + rec.write(self._sock) + + def end_request(self, req, appStatus=0L, + protocolStatus=FCGI_REQUEST_COMPLETE, remove=True): + """ + End a Request. + + Called by Request objects. An FCGI_END_REQUEST Record is + sent to the web server. If the web server no longer requires + the connection, the socket is closed, thereby ending this + Connection (run() returns). + """ + rec = Record(FCGI_END_REQUEST, req.requestId) + rec.contentData = struct.pack(FCGI_EndRequestBody, appStatus, + protocolStatus) + rec.contentLength = FCGI_EndRequestBody_LEN + self.writeRecord(rec) + + if remove: + del self._requests[req.requestId] + + if __debug__: _debug(2, 'end_request: flags = %d' % req.flags) + + if not (req.flags & FCGI_KEEP_CONN) and not self._requests: + self._cleanupSocket() + self._keepGoing = False + + def _do_get_values(self, inrec): + """Handle an FCGI_GET_VALUES request from the web server.""" + outrec = Record(FCGI_GET_VALUES_RESULT) + + pos = 0 + while pos < inrec.contentLength: + pos, (name, value) = decode_pair(inrec.contentData, pos) + cap = self.server.capability.get(name) + if cap is not None: + outrec.contentData += encode_pair(name, str(cap)) + + outrec.contentLength = len(outrec.contentData) + self.writeRecord(outrec) + + def _do_begin_request(self, inrec): + """Handle an FCGI_BEGIN_REQUEST from the web server.""" + role, flags = struct.unpack(FCGI_BeginRequestBody, inrec.contentData) + + req = self.server.request_class(self, self._inputStreamClass) + req.requestId, req.role, req.flags = inrec.requestId, role, flags + req.aborted = False + + if not self._multiplexed and self._requests: + # Can't multiplex requests. + self.end_request(req, 0L, FCGI_CANT_MPX_CONN, remove=False) + else: + self._requests[inrec.requestId] = req + + def _do_abort_request(self, inrec): + """ + Handle an FCGI_ABORT_REQUEST from the web server. + + We just mark a flag in the associated Request. + """ + req = self._requests.get(inrec.requestId) + if req is not None: + req.aborted = True + + def _start_request(self, req): + """Run the request.""" + # Not multiplexed, so run it inline. + req.run() + + def _do_params(self, inrec): + """ + Handle an FCGI_PARAMS Record. + + If the last FCGI_PARAMS Record is received, start the request. + """ + req = self._requests.get(inrec.requestId) + if req is not None: + if inrec.contentLength: + pos = 0 + while pos < inrec.contentLength: + pos, (name, value) = decode_pair(inrec.contentData, pos) + req.params[name] = value + else: + self._start_request(req) + + def _do_stdin(self, inrec): + """Handle the FCGI_STDIN stream.""" + req = self._requests.get(inrec.requestId) + if req is not None: + req.stdin.add_data(inrec.contentData) + + def _do_data(self, inrec): + """Handle the FCGI_DATA stream.""" + req = self._requests.get(inrec.requestId) + if req is not None: + req.data.add_data(inrec.contentData) + + def _do_unknown_type(self, inrec): + """Handle an unknown request type. Respond accordingly.""" + outrec = Record(FCGI_UNKNOWN_TYPE) + outrec.contentData = struct.pack(FCGI_UnknownTypeBody, inrec.type) + outrec.contentLength = FCGI_UnknownTypeBody_LEN + self.writeRecord(rec) + +class MultiplexedConnection(Connection): + """ + A version of Connection capable of handling multiple requests + simultaneously. + """ + _multiplexed = True + _inputStreamClass = MultiplexedInputStream + + def __init__(self, sock, addr, server): + super(MultiplexedConnection, self).__init__(sock, addr, server) + + # Used to arbitrate access to self._requests. + lock = threading.RLock() + + # Notification is posted everytime a request completes, allowing us + # to quit cleanly. + self._lock = threading.Condition(lock) + + def _cleanupSocket(self): + # Wait for any outstanding requests before closing the socket. + self._lock.acquire() + while self._requests: + self._lock.wait() + self._lock.release() + + super(MultiplexedConnection, self)._cleanupSocket() + + def writeRecord(self, rec): + # Must use locking to prevent intermingling of Records from different + # threads. + self._lock.acquire() + try: + # Probably faster than calling super. ;) + rec.write(self._sock) + finally: + self._lock.release() + + def end_request(self, req, appStatus=0L, + protocolStatus=FCGI_REQUEST_COMPLETE, remove=True): + self._lock.acquire() + try: + super(MultiplexedConnection, self).end_request(req, appStatus, + protocolStatus, + remove) + self._lock.notify() + finally: + self._lock.release() + + def _do_begin_request(self, inrec): + self._lock.acquire() + try: + super(MultiplexedConnection, self)._do_begin_request(inrec) + finally: + self._lock.release() + + def _do_abort_request(self, inrec): + self._lock.acquire() + try: + super(MultiplexedConnection, self)._do_abort_request(inrec) + finally: + self._lock.release() + + def _start_request(self, req): + thread.start_new_thread(req.run, ()) + + def _do_params(self, inrec): + self._lock.acquire() + try: + super(MultiplexedConnection, self)._do_params(inrec) + finally: + self._lock.release() + + def _do_stdin(self, inrec): + self._lock.acquire() + try: + super(MultiplexedConnection, self)._do_stdin(inrec) + finally: + self._lock.release() + + def _do_data(self, inrec): + self._lock.acquire() + try: + super(MultiplexedConnection, self)._do_data(inrec) + finally: + self._lock.release() + +class Server(object): + """ + The FastCGI server. + + Waits for connections from the web server, processing each + request. + + If run in a normal CGI context, it will instead instantiate a + CGIRequest and run the handler through there. + """ + request_class = Request + cgirequest_class = CGIRequest + + # Limits the size of the InputStream's string buffer to this size + the + # server's maximum Record size. Since the InputStream is not seekable, + # we throw away already-read data once this certain amount has been read. + inputStreamShrinkThreshold = 102400 - 8192 + + def __init__(self, handler=None, maxwrite=8192, bindAddress=None, + umask=None, multiplexed=False): + """ + handler, if present, must reference a function or method that + takes one argument: a Request object. If handler is not + specified at creation time, Server *must* be subclassed. + (The handler method below is abstract.) + + maxwrite is the maximum number of bytes (per Record) to write + to the server. I've noticed mod_fastcgi has a relatively small + receive buffer (8K or so). + + bindAddress, if present, must either be a string or a 2-tuple. If + present, run() will open its own listening socket. You would use + this if you wanted to run your application as an 'external' FastCGI + app. (i.e. the webserver would no longer be responsible for starting + your app) If a string, it will be interpreted as a filename and a UNIX + socket will be opened. If a tuple, the first element, a string, + is the interface name/IP to bind to, and the second element (an int) + is the port number. + + Set multiplexed to True if you want to handle multiple requests + per connection. Some FastCGI backends (namely mod_fastcgi) don't + multiplex requests at all, so by default this is off (which saves + on thread creation/locking overhead). If threads aren't available, + this keyword is ignored; it's not possible to multiplex requests + at all. + """ + if handler is not None: + self.handler = handler + self.maxwrite = maxwrite + if thread_available: + try: + import resource + # Attempt to glean the maximum number of connections + # from the OS. + maxConns = resource.getrlimit(resource.RLIMIT_NOFILE)[0] + except ImportError: + maxConns = 100 # Just some made up number. + maxReqs = maxConns + if multiplexed: + self._connectionClass = MultiplexedConnection + maxReqs *= 5 # Another made up number. + else: + self._connectionClass = Connection + self.capability = { + FCGI_MAX_CONNS: maxConns, + FCGI_MAX_REQS: maxReqs, + FCGI_MPXS_CONNS: multiplexed and 1 or 0 + } + else: + self._connectionClass = Connection + self.capability = { + # If threads aren't available, these are pretty much correct. + FCGI_MAX_CONNS: 1, + FCGI_MAX_REQS: 1, + FCGI_MPXS_CONNS: 0 + } + self._bindAddress = bindAddress + self._umask = umask + + def _setupSocket(self): + if self._bindAddress is None: # Run as a normal FastCGI? + isFCGI = True + + if isFCGI: + sock = socket.fromfd(FCGI_LISTENSOCK_FILENO, socket.AF_INET, + socket.SOCK_STREAM) + try: + sock.getpeername() + except socket.error, e: + if e[0] == errno.ENOTSOCK: + # Not a socket, assume CGI context. + isFCGI = False + elif e[0] != errno.ENOTCONN: + raise + + # FastCGI/CGI discrimination is broken on Mac OS X. + # Set the environment variable FCGI_FORCE_CGI to "Y" or "y" + # if you want to run your app as a simple CGI. (You can do + # this with Apache's mod_env [not loaded by default in OS X + # client, ha ha] and the SetEnv directive.) + if not isFCGI or \ + os.environ.get('FCGI_FORCE_CGI', 'N').upper().startswith('Y'): + req = self.cgirequest_class(self) + req.run() + sys.exit(0) + else: + # Run as a server + oldUmask = None + if type(self._bindAddress) is str: + # Unix socket + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + os.unlink(self._bindAddress) + except OSError: + pass + if self._umask is not None: + oldUmask = os.umask(self._umask) + else: + # INET socket + assert type(self._bindAddress) is tuple + assert len(self._bindAddress) == 2 + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + sock.bind(self._bindAddress) + sock.listen(socket.SOMAXCONN) + + if oldUmask is not None: + os.umask(oldUmask) + + return sock + + def _cleanupSocket(self, sock): + """Closes the main socket.""" + sock.close() + + def _installSignalHandlers(self): + self._oldSIGs = [(x,signal.getsignal(x)) for x in + (signal.SIGHUP, signal.SIGINT, signal.SIGTERM)] + signal.signal(signal.SIGHUP, self._hupHandler) + signal.signal(signal.SIGINT, self._intHandler) + signal.signal(signal.SIGTERM, self._intHandler) + + def _restoreSignalHandlers(self): + for signum,handler in self._oldSIGs: + signal.signal(signum, handler) + + def _hupHandler(self, signum, frame): + self._hupReceived = True + self._keepGoing = False + + def _intHandler(self, signum, frame): + self._keepGoing = False + + def run(self, timeout=1.0): + """ + The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if + SIGHUP was received, False otherwise. + """ + web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS') + if web_server_addrs is not None: + web_server_addrs = map(lambda x: x.strip(), + web_server_addrs.split(',')) + + sock = self._setupSocket() + + self._keepGoing = True + self._hupReceived = False + + # Install signal handlers. + self._installSignalHandlers() + + while self._keepGoing: + try: + r, w, e = select.select([sock], [], [], timeout) + except select.error, e: + if e[0] == errno.EINTR: + continue + raise + + if r: + try: + clientSock, addr = sock.accept() + except socket.error, e: + if e[0] in (errno.EINTR, errno.EAGAIN): + continue + raise + + if web_server_addrs and \ + (len(addr) != 2 or addr[0] not in web_server_addrs): + clientSock.close() + continue + + # Instantiate a new Connection and begin processing FastCGI + # messages (either in a new thread or this thread). + conn = self._connectionClass(clientSock, addr, self) + thread.start_new_thread(conn.run, ()) + + self._mainloopPeriodic() + + # Restore signal handlers. + self._restoreSignalHandlers() + + self._cleanupSocket(sock) + + return self._hupReceived + + def _mainloopPeriodic(self): + """ + Called with just about each iteration of the main loop. Meant to + be overridden. + """ + pass + + def _exit(self, reload=False): + """ + Protected convenience method for subclasses to force an exit. Not + really thread-safe, which is why it isn't public. + """ + if self._keepGoing: + self._keepGoing = False + self._hupReceived = reload + + def handler(self, req): + """ + Default handler, which just raises an exception. Unless a handler + is passed at initialization time, this must be implemented by + a subclass. + """ + raise NotImplementedError, self.__class__.__name__ + '.handler' + + def error(self, req): + """ + Called by Request if an exception occurs within the handler. May and + should be overridden. + """ + import cgitb + req.stdout.write('Content-Type: text/html\r\n\r\n' + + cgitb.html(sys.exc_info())) + +class WSGIServer(Server): + """ + FastCGI server that supports the Web Server Gateway Interface. See + <http://www.python.org/peps/pep-0333.html>. + """ + def __init__(self, application, environ=None, umask=None, + multithreaded=True, **kw): + """ + environ, if present, must be a dictionary-like object. Its + contents will be copied into application's environ. Useful + for passing application-specific variables. + + Set multithreaded to False if your application is not MT-safe. + """ + if kw.has_key('handler'): + del kw['handler'] # Doesn't make sense to let this through + super(WSGIServer, self).__init__(**kw) + + if environ is None: + environ = {} + + self.application = application + self.environ = environ + self.multithreaded = multithreaded + + # Used to force single-threadedness + self._app_lock = thread.allocate_lock() + + def handler(self, req): + """Special handler for WSGI.""" + if req.role != FCGI_RESPONDER: + return FCGI_UNKNOWN_ROLE, 0 + + # Mostly taken from example CGI gateway. + environ = req.params + environ.update(self.environ) + + environ['wsgi.version'] = (1,0) + environ['wsgi.input'] = req.stdin + if self._bindAddress is None: + stderr = req.stderr + else: + stderr = TeeOutputStream((sys.stderr, req.stderr)) + environ['wsgi.errors'] = stderr + environ['wsgi.multithread'] = not isinstance(req, CGIRequest) and \ + thread_available and self.multithreaded + # Rationale for the following: If started by the web server + # (self._bindAddress is None) in either FastCGI or CGI mode, the + # possibility of being spawned multiple times simultaneously is quite + # real. And, if started as an external server, multiple copies may be + # spawned for load-balancing/redundancy. (Though I don't think + # mod_fastcgi supports this?) + environ['wsgi.multiprocess'] = True + environ['wsgi.run_once'] = isinstance(req, CGIRequest) + + if environ.get('HTTPS', 'off') in ('on', '1'): + environ['wsgi.url_scheme'] = 'https' + else: + environ['wsgi.url_scheme'] = 'http' + + self._sanitizeEnv(environ) + + headers_set = [] + headers_sent = [] + result = None + + def write(data): + assert type(data) is str, 'write() argument must be string' + assert headers_set, 'write() before start_response()' + + if not headers_sent: + status, responseHeaders = headers_sent[:] = headers_set + found = False + for header,value in responseHeaders: + if header.lower() == 'content-length': + found = True + break + if not found and result is not None: + try: + if len(result) == 1: + responseHeaders.append(('Content-Length', + str(len(data)))) + except: + pass + s = 'Status: %s\r\n' % status + for header in responseHeaders: + s += '%s: %s\r\n' % header + s += '\r\n' + req.stdout.write(s) + + req.stdout.write(data) + req.stdout.flush() + + def start_response(status, response_headers, exc_info=None): + if exc_info: + try: + if headers_sent: + # Re-raise if too late + raise exc_info[0], exc_info[1], exc_info[2] + finally: + exc_info = None # avoid dangling circular ref + else: + assert not headers_set, 'Headers already set!' + + assert type(status) is str, 'Status must be a string' + assert len(status) >= 4, 'Status must be at least 4 characters' + assert int(status[:3]), 'Status must begin with 3-digit code' + assert status[3] == ' ', 'Status must have a space after code' + assert type(response_headers) is list, 'Headers must be a list' + if __debug__: + for name,val in response_headers: + assert type(name) is str, 'Header names must be strings' + assert type(val) is str, 'Header values must be strings' + + headers_set[:] = [status, response_headers] + return write + + if not self.multithreaded: + self._app_lock.acquire() + try: + try: + result = self.application(environ, start_response) + try: + for data in result: + if data: + write(data) + if not headers_sent: + write('') # in case body was empty + finally: + if hasattr(result, 'close'): + result.close() + except socket.error, e: + if e[0] != errno.EPIPE: + raise # Don't let EPIPE propagate beyond server + finally: + if not self.multithreaded: + self._app_lock.release() + + return FCGI_REQUEST_COMPLETE, 0 + + def _sanitizeEnv(self, environ): + """Ensure certain values are present, if required by WSGI.""" + if not environ.has_key('SCRIPT_NAME'): + environ['SCRIPT_NAME'] = '' + if not environ.has_key('PATH_INFO'): + environ['PATH_INFO'] = '' + + # If any of these are missing, it probably signifies a broken + # server... + for name,default in [('REQUEST_METHOD', 'GET'), + ('SERVER_NAME', 'localhost'), + ('SERVER_PORT', '80'), + ('SERVER_PROTOCOL', 'HTTP/1.0')]: + if not environ.has_key(name): + environ['wsgi.errors'].write('%s: missing FastCGI param %s ' + 'required by WSGI!\n' % + (self.__class__.__name__, name)) + environ[name] = default + +if __name__ == '__main__': + def test_app(environ, start_response): + """Probably not the most efficient example.""" + import cgi + start_response('200 OK', [('Content-Type', 'text/html')]) + yield '<html><head><title>Hello World!</title></head>\n' \ + '<body>\n' \ + '<p>Hello World!</p>\n' \ + '<table border="1">' + names = environ.keys() + names.sort() + for name in names: + yield '<tr><td>%s</td><td>%s</td></tr>\n' % ( + name, cgi.escape(`environ[name]`)) + + form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, + keep_blank_values=1) + if form.list: + yield '<tr><th colspan="2">Form data</th></tr>' + + for field in form.list: + yield '<tr><td>%s</td><td>%s</td></tr>\n' % ( + field.name, field.value) + + yield '</table>\n' \ + '</body></html>\n' + + WSGIServer(test_app).run() diff --git a/cgi/formatting.py b/cgi/formatting.py new file mode 100644 index 0000000..d21bee2 --- /dev/null +++ b/cgi/formatting.py @@ -0,0 +1,425 @@ +# coding=utf-8 +import string +import cgi +import os +import re +import pickle +import time +import _mysql + +from database import * +from framework import * +from post import regenerateAccess +#from xhtml_clean import Cleaner + +from settings import Settings + +def format_post(message, ip, parentid, parent_timestamp=0): + """ + Formats posts using the specified format + """ + board = Settings._.BOARD + using_markdown = False + + # Escape any HTML if user is not using Markdown or HTML + if not Settings.USE_HTML: + message = cgi.escape(message) + + # Strip text + message = message.rstrip()[0:8000] + + # Treat HTML + if Settings.USE_MARKDOWN: + message = markdown(message) + using_markdown = True + if Settings.USE_HTML: + message = onlyAllowedHTML(message) + + # [code] tag + if board["dir"] == "tech": + message = re.compile(r"\[code\](.+)\[/code\]", re.DOTALL | re.IGNORECASE).sub(r"<pre><code>\1</code></pre>", message) + if board["allow_spoilers"]: + message = re.compile(r"\[spoiler\](.+)\[/spoiler\]", re.DOTALL | re.IGNORECASE).sub(r'<span class="spoil">\1</span>', message) + + if Settings.VIDEO_THUMBS: + (message, affected) = videoThumbs(message) + if affected: + message = close_html(message) + + message = clickableURLs(message) + message = checkRefLinks(message, parentid, parent_timestamp) + message = checkWordfilters(message, ip, board["dir"]) + + # If not using markdown quotes must be created and \n changed for HTML line breaks + if not using_markdown: + message = re.compile(r"^(\n)+").sub('', message) + message = checkQuotes(message) + message = message.replace("\n", "<br />") + + return message + +def tripcode(name): + """ + Calculate tripcode to match output of most imageboards + """ + if name == '': + return '', '' + + board = Settings._.BOARD + + name = name.decode('utf-8') + key = Settings.TRIP_CHAR.decode('utf-8') + + # if there's a trip + (namepart, marker, trippart) = name.partition('#') + if marker: + namepart = cleanString(namepart) + trip = '' + + # secure tripcode + if Settings.ALLOW_SECURE_TRIPCODES and '#' in trippart: + (trippart, securemarker, securepart) = trippart.partition('#') + try: + securepart = securepart.encode("sjis", "ignore") + except: + pass + + # encode secure tripcode + trip = getMD5(securepart + Settings.SECRET) + trip = trip.encode('base64').replace('\n', '') + trip = trip.encode('rot13') + trip = key+key+trip[2:12] + + # return it if we don't have a normal tripcode + if trippart == '': + return namepart.encode('utf-8'), trip.encode('utf-8') + + # do normal tripcode + from crypt import crypt + try: + trippart = trippart.encode("sjis", "ignore") + except: + pass + + trippart = cleanString(trippart, True, True) + salt = re.sub(r"[^\.-z]", ".", (trippart + "H..")[1:3]) + salt = salt.translate(string.maketrans(r":;=?@[\]^_`", "ABDFGabcdef")) + trip = key + crypt(trippart, salt)[-10:] + trip + + return namepart.encode('utf-8'), trip.encode('utf-8') + + return name.encode('utf-8'), '' + +def iphash(ip, post, t, useid, mobile, agent, cap_id, hide_end, has_countrycode): + current_t = time.time() + + if cap_id: + id = cap_id + elif 'sage' in post['email'] and useid == '1': + id = '???' + elif ip == "127.0.0.1": + id = '???' + else: + day = int((current_t + (Settings.TIME_ZONE*3600)) / 86400) + word = ',' + str(day) + + # Make difference by thread + word += ',' + str(t) + + id = hide_data(ip + word, 6, "id", Settings.SECRET) + + if hide_end: + id += '*' + elif addressIsTor(ip): + id += 'T' + elif 'Dalvik' in agent: + id += 'R' + elif 'Android' in agent: + id += 'a' + elif 'iPhone' in agent: + id += 'i' + elif useid == '3': + if 'Firefox' in agent: + id += 'F' + elif 'Safari' in agent and not 'Chrome' in agent: + id += 's' + elif 'Chrome' in agent: + id += 'C' + elif 'SeaMonkey' in agent: + id += 'S' + elif 'Edge' in agent: + id += 'E' + elif 'Opera' in agent or 'OPR' in agent: + id += 'o' + elif 'MSIE' in agent or 'Trident' in agent: + id += 'I' + elif mobile: + id += 'Q' + else: + id += '0' + elif mobile: + id += 'Q' + else: + id += '0' + + if addressIsBanned(ip, ""): + id += '#' + if (not has_countrycode and + not addressIsTor(ip) and + (addressIsProxy(ip) or not addressIsES(ip))): + id += '!' + + return id + +def cleanString(string, escape=True, quote=False): + string = string.strip() + if escape: + string = cgi.escape(string, quote) + return string + +def clickableURLs(message): + # URL + message = re.compile(r'( |^|:|\(|\[)((?:https?://|ftp://|mailto:|news:|irc:)[^\s<>()"]*?(?:\([^\s<>()"]*?\)[^\s<>()"]*?)*)((?:\s|<|>|"|\.|\|\]|!|\?|,|,|")*(?:[\s<>()"]|$))', re.M).sub(r'\1<a href="\2" rel="nofollow" target="_blank">\2</a>\3', message) + # Emails + message = re.compile(r"( |^|:)([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,6})", re.I | re.M).sub(r'\1<a href="mailto:\2" rel="nofollow"><\2></a>', message) + + return message + +def videoThumbs(message): + # Youtube + __RE = re.compile(r"^(?: +)?(https?://(?:www\.)?youtu(?:be\.com/watch\?v=|\.be/)([\w\-]+))(?: +)?$", re.M) + matches = __RE.finditer(message) + if matches: + import json + import urllib, urllib2 + + v_ids = [] + videos = {} + + for match in matches: + v_id = match.group(2) + if v_id not in v_ids: + v_ids.append(v_id) + videos[v_id] = { + 'span': match.span(0), + 'url': match.group(1), + } + if len(v_ids) >= Settings.VIDEO_THUMBS_LIMIT: + raise UserError, "Has incluído muchos videos en tu mensaje. El máximo es %d." % Settings.VIDEO_THUMBS_LIMIT + + if videos: + params = { + 'key': Settings.GOOGLE_API_KEY, + 'part': 'snippet,contentDetails', + 'id': ','.join(v_ids) + } + r_url = "https://www.googleapis.com/youtube/v3/videos?"+urllib.urlencode(params) + res = urllib2.urlopen(r_url) + res_json = json.load(res) + + offset = 0 + for item in res_json['items']: + v_id = item['id'] + (start, end) = videos[v_id]['span'] + end += 1 # remove endline + + try: + new_url = '<a href="%(url)s" target="_blank" class="yt"><span class="pvw"><img src="%(thumb)s" /></span><b>%(title)s</b> (%(secs)s)<br />%(channel)s</a><br />' \ + % {'title': item['snippet']['title'].encode('utf-8'), + 'channel': item['snippet']['channelTitle'].encode('utf-8'), + 'secs': parseIsoPeriod(item['contentDetails']['duration']).encode('utf-8'), + 'url': videos[v_id]['url'], + 'id': v_id.encode('utf-8'), + 'thumb': item['snippet']['thumbnails']['default']['url'].encode('utf-8'),} + except UnicodeDecodeError: + raise UserError, repr(v_id) + message = message[:start+offset] + new_url + message[end+offset:] + offset += len(new_url) - (end-start) + + return (message, len(videos)) + +def fixMobileLinks(message): + """ + Shorten long links; Convert >># links into a mobile version + """ + board = Settings._.BOARD + + # If textboard + if board["board_type"] == '1': + message = re.compile(r'<a href="/(\w+)/read/(\d+)(\.html)?/*(.+)"').sub(r'<a href="/cgi/mobileread/\1/\2/\4"', message) + else: + message = re.compile(r'<a href="/(\w+)/res/(\d+)\.html#(\d+)"').sub(r'<a href="/cgi/mobileread/\1/\2#\3"', message) + + return message + +def checkRefLinks(message, parentid, parent_timestamp): + """ + Check for >># links in posts and replace with the HTML to make them clickable + """ + board = Settings._.BOARD + + if board["board_type"] == '1': + # Textboard + if parentid != '0': + message = re.compile(r'>>(\d+(,\d+|-(?=[ \d\n])|\d+)*n?)').sub('<a href="' + Settings.BOARDS_URL + board['dir'] + '/read/' + str(parent_timestamp) + r'/\1">>>\1</a>', message) + else: + # Imageboard + quotes_id_array = re.findall(r">>([0-9]+)", message) + for quotes in quotes_id_array: + try: + post = FetchOne('SELECT * FROM `posts` WHERE `id` = ' + quotes + ' AND `boardid` = ' + board['id'] + ' LIMIT 1') + if post['parentid'] != '0': + message = re.compile(">>" + quotes).sub('<a href="' + Settings.BOARDS_URL + board['dir'] + '/res/' + post['parentid'] + '.html#' + quotes + '">>>' + quotes + '</a>', message) + else: + message = re.compile(">>" + quotes).sub('<a href="' + Settings.BOARDS_URL + board['dir'] + '/res/' + post['id'] + '.html#' + quotes + '">>>' + quotes + '</a>', message) + except: + message = re.compile(">>" + quotes).sub(r'<span class="q">>>'+quotes+'</span>', message) + + return message + +def checkQuotes(message): + """ + Check for >text in posts and add span around it to color according to the css + """ + message = re.compile(r"^>(.*)$", re.MULTILINE).sub(r'<span class="q">>\1</span>', message) + return message + +def escapeHTML(string): + string = string.replace('<', '<') + string = string.replace('>', '>') + return string + +def onlyAllowedHTML(message): + """ + Allow <b>, <i>, <u>, <strike>, and <pre> in posts, along with the special <aa> + """ + message = sanitize_html(message) + #message = re.compile(r"\[aa\](.+?)\[/aa\]", re.DOTALL | re.IGNORECASE).sub("<span class=\"sjis\">\\1</span>", message) + + return message + +def close_html(message): + """ + Old retarded version of sanitize_html, it just closes open tags. + """ + import BeautifulSoup + return unicode(BeautifulSoup.BeautifulSoup(message)).replace(' ', '').encode('utf-8') + +def sanitize_html(message, decode=True): + """ + Clean the code and allow only a few safe tags. + """ + import BeautifulSoup + + # Decode message from utf-8 if required + if decode: + message = message.decode('utf-8', 'replace') + + # Create HTML Cleaner with our allowed tags + whitelist_tags = ["a","b","br","blink","code","del","em","i","marquee","root","strike","strong","sub","sup","u"] + whitelist_attr = ["href"] + + soup = BeautifulSoup.BeautifulSoup(message) + + # Remove tags that aren't allowed + for tag in soup.findAll(): + if not tag.name.lower() in whitelist_tags: + tag.name = "span" + tag.attrs = [] + else: + for attr in [attr for attr in tag.attrs if attr not in whitelist_attr]: + del tag[attr] + + # We export the soup into a correct XHTML string + string = unicode(soup).encode('utf-8') + # We remove some anomalies we don't want + string = string.replace('<br/>', '<br />').replace(' ', '') + + return string + +def markdown(message): + import markdown + if message.strip() != "": + #return markdown.markdown(message).rstrip("\n").rstrip("<br />") + return markdown.markdown(message, extras=["cuddled-lists", "code-friendly"]).encode('utf-8') + else: + return "" + +def checkWordfilters(message, ip, board): + fixed_ip = inet_aton(ip) + wordfilters = FetchAll("SELECT * FROM `filters` WHERE `type` = '0' ORDER BY `id` ASC") + for wordfilter in wordfilters: + if wordfilter["boards"] != "": + boards = pickle.loads(wordfilter["boards"]) + if wordfilter["boards"] == "" or board in boards: + if wordfilter['action'] == '0': + if not re.search(wordfilter['from'], message, re.DOTALL | re.IGNORECASE) is None: + raise UserError, wordfilter['reason'] + elif wordfilter['action'] == '1': + message = re.compile(wordfilter['from'], re.DOTALL | re.IGNORECASE).sub(wordfilter['to'], message) + elif wordfilter['action'] == '2': + # Ban + if not re.search(wordfilter['from'], message, re.DOTALL | re.IGNORECASE) is None: + if wordfilter['seconds'] != '0': + until = str(timestamp() + int(wordfilter['seconds'])) + else: + until = '0' + + InsertDb("INSERT INTO `bans` (`ip`, `boards`, `added`, `until`, `staff`, `reason`, `note`, `blind`) VALUES (" + \ + "'" + str(fixed_ip) + "', '" + _mysql.escape_string(wordfilter['boards']) + \ + "', " + str(timestamp()) + ", " + until + ", 'System', '" + _mysql.escape_string(wordfilter['reason']) + \ + "', 'Word Auto-ban', '"+_mysql.escape_string(wordfilter['blind'])+"')") + regenerateAccess() + raise UserError, wordfilter['reason'] + elif wordfilter['action'] == '3': + if not re.search(wordfilter['from'], message, re.DOTALL | re.IGNORECASE) is None: + raise UserError, '<meta http-equiv="refresh" content="%s;url=%s" />%s' % (wordfilter['redirect_time'], wordfilter['redirect_url'], wordfilter['reason']) + return message + +def checkNamefilters(name, tripcode, ip, board): + namefilters = FetchAll("SELECT * FROM `filters` WHERE `type` = '1'") + + for namefilter in namefilters: + if namefilter["boards"] != "": + boards = pickle.loads(namefilter["boards"]) + if namefilter["boards"] == "" or board in boards: + # check if this filter applies + match = False + + if namefilter['from'] and namefilter['from_trip']: + # both name and trip filter + if re.search(namefilter['from'], name, re.DOTALL | re.IGNORECASE) and tripcode == namefilter['from_trip']: + match = True + elif namefilter['from'] and not namefilter['from_trip']: + # name filter + if re.search(namefilter['from'], name, re.DOTALL | re.IGNORECASE): + match = True + elif not namefilter['from'] and namefilter['from_trip']: + # trip filter + if tripcode == namefilter['from_trip']: + match = True + + if match: + # do action + if namefilter['action'] == '0': + raise UserError, namefilter['reason'] + elif namefilter['action'] == '1': + name = namefilter['to'] + tripcode = '' + return name, tripcode + elif namefilter['action'] == '2': + # Ban + if namefilter['seconds'] != '0': + until = str(timestamp() + int(namefilter['seconds'])) + else: + until = '0' + + InsertDb("INSERT INTO `bans` (`ip`, `boards`, `added`, `until`, `staff`, `reason`, `note`, `blind`) VALUES (" + \ + "'" + _mysql.escape_string(ip) + "', '" + _mysql.escape_string(namefilter['boards']) + \ + "', " + str(timestamp()) + ", " + until + ", 'System', '" + _mysql.escape_string(namefilter['reason']) + \ + "', 'Name Auto-ban', '"+_mysql.escape_string(namefilter['blind'])+"')") + regenerateAccess() + raise UserError, namefilter['reason'] + elif namefilter['action'] == '3': + raise UserError, '<meta http-equiv="refresh" content="%s;url=%s" />%s' % (namefilter['redirect_time'], namefilter['redirect_url'], namefilter['reason']) + return name, tripcode diff --git a/cgi/framework.py b/cgi/framework.py new file mode 100644 index 0000000..4c89bb7 --- /dev/null +++ b/cgi/framework.py @@ -0,0 +1,467 @@ +# coding=utf-8 +import os +import cgi +import datetime +import time +import hashlib +import pickle +import socket +import _mysql +import urllib +import re +from Cookie import SimpleCookie + +from settings import Settings +from database import * + +class CLT(datetime.tzinfo): + """ + Clase para zona horaria chilena. + Como el gobierno nos tiene los horarios de verano para la pura cagá, + por mientras dejo el DST como un boolean. Cuando lo fijen, dejarlo automático. + """ + def __init__(self): + self.isdst = False + + def utcoffset(self, dt): + #return datetime.timedelta(hours=-3) + self.dst(dt) + return datetime.timedelta(hours=Settings.TIME_ZONE) + + def dst(self, dt): + if self.isdst: + return datetime.timedelta(hours=1) + else: + return datetime.timedelta(0) + + def tzname(self,dt): + return "GMT -3" + +def setBoard(dir): + """ + Sets the board which the script is operating on by filling Settings._.BOARD + with the data from the db. + """ + if not dir: + raise UserError, _("The specified board is invalid.") + logTime("Seteando el board " + dir) + board = FetchOne("SELECT * FROM `boards` WHERE `dir` = '%s' LIMIT 1" % _mysql.escape_string(dir)) + if not board: + raise UserError, _("The specified board is invalid.") + + board["filetypes"] = FetchAll("SELECT * FROM `boards_filetypes` INNER JOIN `filetypes` ON filetypes.id = boards_filetypes.filetypeid WHERE `boardid` = %s ORDER BY `ext` ASC" % _mysql.escape_string(board['id'])) + board["filetypes_ext"] = [filetype['ext'] for filetype in board['filetypes']] + logTime("Board seteado.") + + Settings._.BOARD = board + + return board + +def addressIsBanned(ip, board): + packed_ip = inet_aton(ip) + 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`)") + logTime("SELECT * FROM `bans` WHERE (`netmask` IS NULL AND `ip` = '"+str(packed_ip)+"') OR (`netmask` IS NOT NULL AND '"+str(packed_ip)+"' & `netmask` = `ip`)") + for ban in bans: + if ban["boards"] != "": + boards = pickle.loads(ban["boards"]) + if ban["boards"] == "" or board in boards: + if board not in Settings.EXCLUDE_GLOBAL_BANS: + return True + return False + +def addressIsTor(ip): + if Settings._.IS_TOR is None: + res = False + nodes = [] + if ip == '127.0.0.1': # Tor proxy address + res = True + else: + with open('tor.txt') as f: + nodes = [line.rstrip() for line in f] + if ip in nodes: + res = True + Settings._.IS_TOR = res + return res + else: + return Settings._.IS_TOR + +def addressIsProxy(ip): + if Settings._.IS_PROXY is None: + res = False + proxies = [] + with open('proxy.txt') as f: + proxies = [line.rstrip() for line in f] + if ip in proxies: + res = True + Settings._.IS_PROXY = res + return res + else: + return Settings._.IS_PROXY + +def addressIsES(ip): + ES = ['AR', 'BO', 'CL', 'CO', 'CR', 'CU', 'EC', 'ES', 'GF', + 'GY', 'GT', 'HN', 'MX', 'NI', 'PA', 'PE', 'PY', 'PR', 'SR', 'UY', 'VE'] # 'BR', + return getCountry(ip) in ES + +def getCountry(ip): + import geoip + return geoip.country(ip) + +def getHost(ip): + if Settings._.HOST is None: + try: + Settings._.HOST = socket.gethostbyaddr(ip)[0] + return Settings._.HOST + except socket.herror: + return None + else: + return Settings._.HOST + +def hostIsBanned(ip): + host = getHost(ip) + if host: + banned_hosts = [] + for banned_host in banned_hosts: + if host.endswith(banned_host): + return True + return False + else: + return False + +def updateBoardSettings(): + """ + Pickle the board's settings and store it in the configuration field + """ + board = Settings._.BOARD + #UpdateDb("UPDATE `boards` SET `configuration` = '%s' WHERE `id` = %s LIMIT 1" % (_mysql.escape_string(configuration), board["id"])) + + del board["filetypes"] + del board["filetypes_ext"] + post_values = ["`" + _mysql.escape_string(str(key)) + "` = '" + _mysql.escape_string(str(value)) + "'" for key, value in board.iteritems()] + + UpdateDb("UPDATE `boards` SET %s WHERE `id` = '%s' LIMIT 1" % (", ".join(post_values), board["id"])) + +def timestamp(t=None): + """ + Create MySQL-safe timestamp from the datetime t if provided, otherwise create + the timestamp from datetime.now() + """ + if not t: + t = datetime.datetime.now() + return int(time.mktime(t.timetuple())) + +def formatDate(t=None, home=False): + """ + Format a datetime to a readable date + """ + if not t: + t = datetime.datetime.now(CLT()) + # Timezone fix + #t += datetime.timedelta(hours=1) + + days = {'en': ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], + 'es': ['lun', 'mar', 'mie', 'jue', 'vie', 'sab', 'dom'], + 'jp': ['月', '火', '水', '木', '金', '土', '日']} + + daylist = days[Settings.LANG] + format = "%d/%m/%y(%a)%H:%M:%S" + + if not home: + try: + board = Settings._.BOARD + if board["dir"] == 'world': + daylist = days['en'] + elif board["dir"] == '2d': + daylist = days['jp'] + except: + pass + + t = t.strftime(format) + + t = re.compile(r"mon", re.DOTALL | re.IGNORECASE).sub(daylist[0], t) + t = re.compile(r"tue", re.DOTALL | re.IGNORECASE).sub(daylist[1], t) + t = re.compile(r"wed", re.DOTALL | re.IGNORECASE).sub(daylist[2], t) + t = re.compile(r"thu", re.DOTALL | re.IGNORECASE).sub(daylist[3], t) + t = re.compile(r"fri", re.DOTALL | re.IGNORECASE).sub(daylist[4], t) + t = re.compile(r"sat", re.DOTALL | re.IGNORECASE).sub(daylist[5], t) + t = re.compile(r"sun", re.DOTALL | re.IGNORECASE).sub(daylist[6], t) + return t + +def formatTimestamp(t, home=False): + """ + Format a timestamp to a readable date + """ + return formatDate(datetime.datetime.fromtimestamp(int(t), CLT()), home) + +def timeTaken(time_start, time_finish): + return str(round(time_finish - time_start, 3)) + +def parseIsoPeriod(t_str): + m = re.match('P(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(\d+)S', t_str) + if m: + grps = [x for x in m.groups() if x] + if len(grps) == 1: + grps.insert(0, '0') + grps[-1] = grps[-1].zfill(2) + return ':'.join(grps) + else: + return '???' + +def getFormData(self): + """ + Process input sent to WSGI through a POST method and output it in an easy to + retrieve format: dictionary of dictionaries in the format of {key: value} + """ + wsgi_input = self.environ["wsgi.input"] + post_form = self.environ.get("wsgi.post_form") + if (post_form is not None + and post_form[0] is wsgi_input): + return post_form[2] + # This must be done to avoid a bug in cgi.FieldStorage + self.environ.setdefault("QUERY_STRING", "") + fs = cgi.FieldStorage(fp=wsgi_input, + environ=self.environ, + keep_blank_values=1) + new_input = InputProcessed() + post_form = (new_input, wsgi_input, fs) + self.environ["wsgi.post_form"] = post_form + self.environ["wsgi.input"] = new_input + + formdata = {} + for key in dict(fs): + try: + formdata.update({key: fs[key].value}) + if key == "file": + formdata.update({"file_original": secure_filename(fs[key].filename)}) + except AttributeError: + formdata.update({key: fs[key]}) + + return formdata + +class InputProcessed(object): + def read(self): + raise EOFError("El stream de wsgi.input ya se ha consumido.") + readline = readlines = __iter__ = read + +class UserError(Exception): + pass + +def secure_filename(path): + split = re.compile(r'[\0%s]' % re.escape(''.join([os.path.sep, os.path.altsep or '']))) + return cgi.escape(split.sub('', path)) + +def getMD5(data): + m = hashlib.md5() + m.update(data) + + return m.hexdigest() + +def nullstr(len): return "\0" * len + +def hide_data(data, length, key, secret): + """ + Encrypts data, useful for tripcodes and IDs + """ + crypt = rc4(nullstr(length), rc4(nullstr(32), key + secret) + data).encode('base64') + return crypt.rstrip('\n') + +def rc4(data, key): + """ + rc4 implementation + """ + x = 0 + box = range(256) + for i in range(256): + x = (x + box[i] + ord(key[i % len(key)])) % 256 + box[i], box[x] = box[x], box[i] + x = 0 + y = 0 + out = [] + for char in data: + x = (x + 1) % 256 + y = (y + box[x]) % 256 + box[x], box[y] = box[y], box[x] + out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256])) + + return ''.join(out) + +def getRandomLine(filename): + import random + f = open(filename, 'r') + lines = f.readlines() + num = random.randint(0, len(lines) - 1) + return lines[num] + +def getRandomIco(): + from glob import glob + from random import choice + icons = glob("../static/ico/*") + if icons: + return choice(icons).lstrip('..') + else: + return '' + +def N_(message): return message + +def getCookie(self, value=""): + return urllib.unquote_plus(self._cookies[value].value) + +def reCookie(self, key, value=""): + board = Settings._.BOARD + setCookie(self, key, value) + +def setCookie(self, key, value="", max_age=None, expires=None, path="/", domain=None, secure=None): + """ + Copied from Colubrid + """ + if self._cookies is None: + self._cookies = SimpleCookie() + self._cookies[key] = urllib.quote_plus(value) + if not max_age is None: + self._cookies[key]["max-age"] = max_age + if not expires is None: + if isinstance(expires, basestring): + self._cookies[key]["expires"] = expires + expires = None + elif isinstance(expires, datetime): + expires = expires.utctimetuple() + elif not isinstance(expires, (int, long)): + expires = datetime.datetime.gmtime(expires) + else: + raise ValueError("Se requiere de un entero o un datetime") + if not expires is None: + now = datetime.datetime.gmtime() + month = _([N_("Jan"), N_("Feb"), N_("Mar"), N_("Apr"), N_("May"), N_("Jun"), N_("Jul"), + N_("Aug"), N_("Sep"), N_("Oct"), N_("Nov"), N_("Dec")][now.tm_mon - 1]) + day = _([N_("Monday"), N_("Tuesday"), N_("Wednesday"), N_("Thursday"), + N_("Friday"), N_("Saturday"), N_("Sunday")][expires.tm_wday]) + date = "%02d-%s-%s" % ( + now.tm_mday, month, str(now.tm_year)[-2:] + ) + d = "%s, %s %02d:%02d:%02d GMT" % (day, date, now.tm_hour, + now.tm_min, now.tm_sec) + self._cookies[key]["expires"] = d + if not path is None: + self._cookies[key]["path"] = path + if not domain is None: + if domain != "THIS": + self._cookies[key]["domain"] = domain + else: + self._cookies[key]["domain"] = Settings.DOMAIN + if not secure is None: + self._cookies[key]["secure"] = secure + +def deleteCookie(self, key): + """ + Copied from Colubrid + """ + if self._cookies is None: + self._cookies = SimpleCookie() + if not key in self._cookies: + self._cookies[key] = "" + self._cookies[key]["max-age"] = 0 + +def elapsed_time(seconds, suffixes=['y','w','d','h','m','s'], add_s=False, separator=' '): + """ + Takes an amount of seconds and turns it into a human-readable amount of time. + """ + # the formatted time string to be returned + time = [] + + # the pieces of time to iterate over (days, hours, minutes, etc) + # - the first piece in each tuple is the suffix (d, h, w) + # - the second piece is the length in seconds (a day is 60s * 60m * 24h) + parts = [(suffixes[0], 60 * 60 * 24 * 7 * 52), + (suffixes[1], 60 * 60 * 24 * 7), + (suffixes[2], 60 * 60 * 24), + (suffixes[3], 60 * 60), + (suffixes[4], 60), + (suffixes[5], 1)] + + # for each time piece, grab the value and remaining seconds, and add it to + # the time string + for suffix, length in parts: + value = seconds / length + if value > 0: + seconds = seconds % length + time.append('%s%s' % (str(value), + (suffix, (suffix, suffix + 's')[value > 1])[add_s])) + if seconds < 1: + break + + return separator.join(time) + +def inet_aton(ip_string): + import socket, struct + return struct.unpack('!L',socket.inet_aton(ip_string))[0] + +def inet_ntoa(packed_ip): + import socket, struct + return socket.inet_ntoa(struct.pack('!L',packed_ip)) + +def is_bad_proxy(pip): + import urllib2 + import socket + socket.setdefaulttimeout(3) + + try: + proxy_handler = urllib2.ProxyHandler({'http': pip}) + opener = urllib2.build_opener(proxy_handler) + opener.addheaders = [('User-agent', 'Mozilla/5.0')] + urllib2.install_opener(opener) + req=urllib2.Request('http://bienvenidoainternet.org') + sock=urllib2.urlopen(req) + except urllib2.HTTPError, e: + return e.code + except Exception, detail: + return True + return False + +def send_mail(subject, srcmsg): + import smtplib + from email.mime.text import MIMEText + + msg = MIMEText(srcmsg) + me = 'weabot@bienvenidoainternet.org' + you = 'burocracia@bienvenidoainternet.org' + + msg['Subject'] = 'The contents of %s' % textfile + msg['From'] = me + msg['To'] = you + + s = smtplib.SMTP('localhost') + s.sendmail(me, [you], msg.as_string()) + s.quit() + +class weabotLogger: + def __init__(self): + self.times = [] + + def log(self, message): + self.times.append([time.time(), message]) + + def allTimes(self): + output = "Time Logged action\n--------------------------\n" + start = self.times[0][0] + for time in self.times: + difference = str(time[0] - start) + difference_split = difference.split(".") + if len(difference_split[0]) < 2: + difference_split[0] = "0" + difference_split[0] + + if len(difference_split[1]) < 7: + difference_split[1] = ("0" * (7 - len(difference_split[1]))) + difference_split[1] + elif len(difference_split[1]) > 7: + difference_split[1] = difference_split[1][:7] + + output += ".".join(difference_split) + " " + time[1] + "\n" + + return output + +logger = weabotLogger() +def logTime(message): + global logger + logger.log(message) + +def logTimes(): + global logger + return logger.allTimes() diff --git a/cgi/geoip.py b/cgi/geoip.py new file mode 100644 index 0000000..0bcb3d8 --- /dev/null +++ b/cgi/geoip.py @@ -0,0 +1,128 @@ +"""Python API that wraps GeoIP country database lookup into a simple function. + +Download the latest MaxMind GeoIP country database and read other docs here: + http://www.maxmind.com/app/geolitecountry + +Copyright (C) 2009 Ben Hoyt, released under the Lesser General Public License: + http://www.gnu.org/licenses/lgpl.txt + +Usage examples: + +>>> country('64.233.161.99') +'US' +>>> country('202.21.128.102') +'NZ' +>>> country('asdf') +'' +>>> country('127.0.0.1') +'' +""" + +# List of country codes (indexed by GeoIP country ID number) +countries = ( + '', 'AP', 'EU', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', + 'AR', 'AS', 'AT', 'AU', 'AW', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', + 'BI', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', + 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', + 'CV', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', + 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'FX', 'GA', 'GB', + 'GD', 'GE', 'GF', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', + 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IN', + 'IO', 'IQ', 'IR', 'IS', 'IT', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', + 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', + 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', + 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', + 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', + 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', + 'QA', 'RE', 'RO', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', + 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY', 'SZ', 'TC', 'TD', + 'TF', 'TG', 'TH', 'TJ', 'TK', 'TM', 'TN', 'TO', 'TL', 'TR', 'TT', 'TV', 'TW', + 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', + 'VU', 'WF', 'WS', 'YE', 'YT', 'RS', 'ZA', 'ZM', 'ME', 'ZW', 'A1', 'A2', 'O1', + 'AX', 'GG', 'IM', 'JE', 'BL', 'MF') + +def iptonum(ip): + """Convert IP address string to 32-bit integer, or return None if IP is bad. + + >>> iptonum('0.0.0.0') + 0 + >>> hex(iptonum('127.0.0.1')) + '0x7f000001' + >>> hex(iptonum('255.255.255.255')) + '0xffffffffL' + >>> iptonum('127.0.0.256') + >>> iptonum('1.2.3') + >>> iptonum('a.s.d.f') + >>> iptonum('1.2.3.-4') + >>> iptonum('') + """ + segments = ip.split('.') + if len(segments) != 4: + return None + num = 0 + for segment in segments: + try: + segment = int(segment) + except ValueError: + return None + if segment < 0 or segment > 255: + return None + num = num << 8 | segment + return num + +class DatabaseError(Exception): + pass + +class GeoIP(object): + """Wraps GeoIP country database lookup into a class.""" + + _record_length = 3 + _country_start = 16776960 + + def __init__(self, dbname='GeoIP.dat'): + """Init GeoIP instance with given GeoIP country database file.""" + self._dbfile = open(dbname, 'rb') + + def country(self, ip): + """Lookup IP address string and turn it into a two-letter country code + like 'NZ', or return empty string if unknown. + + >>> g = GeoIP() + >>> g.country('64.233.161.99') + 'US' + >>> g.country('202.21.128.102') + 'NZ' + >>> g.country('asdf') + '' + >>> g.country('127.0.0.1') + '' + """ + ipnum = iptonum(ip) + if ipnum is None: + return '' + return countries[self._country_id(ipnum)] + + def _country_id(self, ipnum): + """Look up and return country ID of given 32-bit IP address.""" + # Search algorithm from: http://code.google.com/p/pygeoip/ + offset = 0 + for depth in range(31, -1, -1): + self._dbfile.seek(offset * 2 * self._record_length) + data = self._dbfile.read(2 * self._record_length) + x = [0, 0] + for i in range(2): + for j in range(self._record_length): + x[i] += ord(data[self._record_length * i + j]) << (j * 8) + i = 1 if ipnum & (1 << depth) else 0 + if x[i] >= self._country_start: + return x[i] - self._country_start + offset = x[i] + raise DatabaseError('GeoIP database corrupt: offset=%s' % offset) + +def country(ip, dbname='GeoIP.dat'): + """Helper function that creates a GeoIP instance and calls country().""" + return GeoIP(dbname).country(ip) + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/cgi/img.py b/cgi/img.py new file mode 100644 index 0000000..21f326a --- /dev/null +++ b/cgi/img.py @@ -0,0 +1,416 @@ +# coding=utf-8 +import struct +import math +#import random +import os +import subprocess +from StringIO import StringIO + +from settings import Settings +from database import * +from framework import * + +try: # Windows needs stdio set for binary mode. + import msvcrt + msvcrt.setmode (0, os.O_BINARY) # stdin = 0 + msvcrt.setmode (1, os.O_BINARY) # stdout = 1 +except ImportError: + pass + +def processImage(post, data, t, originalname, spoiler=False): + """ + Take all post data from <post>, process uploaded file in <data>, and calculate + file names using datetime <t> + Returns updated <post> with file and thumb values + """ + board = Settings._.BOARD + + used_filetype = None + + # get image information + content_type, width, height, size, extra = getImageInfo(data) + + # check the size is fine + if size > int(board["maxsize"])*1024: + raise UserError, _("File too big. The maximum file size is: %s") % board['maxsize'] + + # check if file is supported + for filetype in board['filetypes']: + if content_type == filetype['mime']: + used_filetype = filetype + break + + if not used_filetype: + raise UserError, _("File type not supported.") + + # check if file is already posted + is_duplicate = checkFileDuplicate(data) + if checkFileDuplicate(data)[0]: + raise UserError, _("This image has already been posted %s.") % ('<a href="' + Settings.BOARDS_URL + board['dir'] + '/res/' + str(is_duplicate[1]) + '.html#' + str(is_duplicate[2]) + '">' + _("here") + '</a>') + + # prepare file names + if used_filetype['preserve_name'] == '1': + file_base = os.path.splitext(originalname)[0] # use original filename + else: + file_base = '%d' % int(t * 1000) # generate timestamp name + file_name = file_base + "." + used_filetype['ext'] + file_thumb_name = file_base + "s.jpg" + + # prepare paths + file_path = Settings.IMAGES_DIR + board["dir"] + "/src/" + file_name + file_thumb_path = Settings.IMAGES_DIR + board["dir"] + "/thumb/" + file_thumb_name + file_mobile_path = Settings.IMAGES_DIR + board["dir"] + "/mobile/" + file_thumb_name + file_cat_path = Settings.IMAGES_DIR + board["dir"] + "/cat/" + file_thumb_name + + # remove EXIF data if necessary for privacy + if content_type == 'image/jpeg': + data = removeExifData(data) + + # write file + f = open(file_path, "wb") + try: + f.write(data) + finally: + f.close() + + # set maximum dimensions + maxsize = int(board['thumb_px']) + + post["file"] = file_name + post["image_width"] = width + post["image_height"] = height + + # Do we need to thumbnail it? + if not used_filetype['image']: + # make thumbnail + file_thumb_width, file_thumb_height = getThumbDimensions(width, height, maxsize) + + if used_filetype['ffmpeg_thumb'] == '1': + # use ffmpeg to make thumbnail + logTime("Generating thumbnail") + + if used_filetype['mime'][:5] == 'video': + #duration_half = str(int(extra['duration'] / 2)) + retcode = subprocess.call([ + Settings.FFMPEG_PATH, '-strict', '-2', '-ss', '0', '-i', file_path, + '-v', 'quiet', '-an', '-vframes', '1', '-f', 'mjpeg', '-vf', 'scale=%d:%d' % (file_thumb_width, file_thumb_height), + '-threads', '1', file_thumb_path]) + if spoiler: + args = [Settings.CONVERT_PATH, file_thumb_path, "-limit", "thread", "1", "-background", "white", "-flatten", "-resize", "%dx%d" % (file_thumb_width, file_thumb_height), "-blur", "0x12", "-gravity", "center", "-fill", "rgba(0,0,0, .6)", "-draw", "rectangle 0,%d,%d,%d" % ((file_thumb_height/2)-10, file_thumb_width, (file_thumb_height/2)+7), "-fill", "white", "-annotate", "0", "Alerta de spoiler", "-quality", str(Settings.THUMB_QUALITY), file_thumb_path] + retcode = subprocess.call(args) + elif used_filetype['mime'][:5] == 'audio': + # we do an exception and use png for audio waveform thumbnails since they + # 1. are smaller 2. allow for transparency + file_thumb_name = file_thumb_name[:-3] + "png" + file_thumb_path = file_thumb_path[:-3] + "png" + file_mobile_path = file_mobile_path[:-3] + "png" + file_cat_path = file_cat_path[:-3] + "png" + + if int(board['thumb_px']) > 149: + file_thumb_width = board['thumb_px'] + file_thumb_height = float(int(board['thumb_px'])/2) + else: + file_thumb_width = 150 + file_thumb_height = 75 + + retcode = subprocess.call([ + Settings.FFMPEG_PATH, '-t', '300', '-i', file_path, + '-filter_complex', 'showwavespic=s=%dx%d:split_channels=1' % (int(file_thumb_width), int(file_thumb_height)), + '-frames:v', '1', '-threads', '1', file_thumb_path]) +# elif used_filetype['mime'] == 'application/x-shockwave-flash' or used_filetype['mime'] == 'mime/x-shockwave-flash': +# retcode = subprocess.call([ +# './ffmpeg', '-i', file_path, '-vcodec', 'mjpeg', '-vframes', '1', '-an', '-f', 'rawvideo', +# '-vf', 'scale=%d:%d' % (file_thumb_width, file_thumb_height), '-threads', '1', file_thumb_path]) + + if retcode != 0: + os.remove(file_path) + raise UserError, _("Thumbnail creation failure.") + ' ('+str(retcode)+')' + else: + # use imagemagick to make thumbnail + args = [Settings.CONVERT_PATH, file_path, "-limit", "thread", "1", "-background", "white", "-flatten", "-resize", "%dx%d" % (file_thumb_width, file_thumb_height)] + if spoiler: + args += ["-blur", "0x12", "-gravity", "center", "-fill", "rgba(0,0,0, .6)", "-draw", "rectangle 0,%d,%d,%d" % ((file_thumb_height/2)-10, file_thumb_width, (file_thumb_height/2)+7), "-fill", "white", "-annotate", "0", "Alerta de spoiler"] + args += ["-quality", str(Settings.THUMB_QUALITY), file_thumb_path] + + # generate thumbnails + logTime("Generating thumbnail") + retcode = subprocess.call(args) + if retcode != 0: + os.remove(file_path) + raise UserError, _("Thumbnail creation failure.") + ' ('+str(retcode)+')' + + # check if thumbnail was truly created + try: + open(file_thumb_path) + except: + os.remove(file_path) + raise UserError, _("Thumbnail creation failure.") + + # create extra thumbnails (catalog/mobile) + subprocess.call([Settings.CONVERT_PATH, file_thumb_path, "-limit" , "thread", "1", "-resize", "100x100", "-quality", "75", file_mobile_path]) + if not post["parentid"]: + subprocess.call([Settings.CONVERT_PATH, file_thumb_path, "-limit" , "thread", "1", "-resize", "150x150", "-quality", "60", file_cat_path]) + + post["thumb"] = file_thumb_name + post["thumb_width"] = file_thumb_width + post["thumb_height"] = file_thumb_height + else: + # Don't thumbnail and use mime image + if board["board_type"] == '0': + post["thumb"] = used_filetype['image'] + post["thumb_width"] = '120' + post["thumb_height"] = '120' + else: + post["thumb"] = used_filetype['image'].split(".")[0] + '_small.png' + post["thumb_width"] = '90' + post["thumb_height"] = '90' + + # calculate size (bytes) + post["file_size"] = len(data) + + # add additional metadata, if any + post["message"] += extraInfo(content_type, file_name, file_path) + + # file md5 + post["file_hex"] = getMD5(data) + + return post + +def extraInfo(mime, file_name, file_path): + board = Settings._.BOARD + + if mime in ['audio/ogg', 'audio/opus', 'audio/mpeg', 'video/webm']: + info = ffprobe_f(file_path) + extra = {} + credit_str = "" + + if mime == 'video/webm': + for s in info['streams']: + if 'width' in s: + stream = s + else: + stream = info['streams'][0] + + extra['codec'] = stream.get('codec_name', '').encode('utf-8') + format = info['format'] + + if 'bit_rate' in format: + extra['codec'] += ' ~%d kbps' % int(int(format['bit_rate']) / 1000) + if 'tags' in format: + extra['title'] = format['tags'].get('TITLE', format['tags'].get('title', '')).encode('utf-8') + extra['artist'] = format['tags'].get('ARTIST', format['tags'].get('artist', '')).encode('utf-8') + if extra['title'] or extra['artist']: + credit_str = ' - '.join((extra['artist'], extra['title'])) + ' ' + if 'tags' in stream: + extra['title'] = stream['tags'].get('TITLE', '').encode('utf-8') + extra['artist'] = stream['tags'].get('ARTIST', '').encode('utf-8') + if extra['title'] or extra['artist']: + credit_str = ' - '.join((extra['artist'], extra['title'])) + ' ' + + return '<hr /><small>%s(%s)</small>' % (credit_str, extra['codec']) + + elif mime in ['audio/mod', 'audio/xm', 'audio/s3m']: + ext = mime.split('/')[1].upper() + url = '/cgi/play/%s/%s' % (board['dir'], file_name) + return '<hr /><small>Módulo tracker (%s) [<a href="%s" target="_blank">Click para escuchar</a>]</small>' % (ext, url) + + return '' + +def getImageInfo(data): + data = str(data) + size = len(data) + height = -1 + width = -1 + extra = {} + content_type = "" + + # handle GIFs + if (size >= 10) and data[:6] in ("GIF87a", "GIF89a"): + # Check to see if content_type is correct + content_type = "image/gif" + w, h = struct.unpack("<HH", data[6:10]) + width = int(w) + height = int(h) + + # See PNG 2. Edition spec (http://www.w3.org/TR/PNG/) + # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR' + # and finally the 4-byte width, height + elif ((size >= 24) and data.startswith("\211PNG\r\n\032\n") + and (data[12:16] == "IHDR")): + content_type = "image/png" + w, h = struct.unpack(">LL", data[16:24]) + width = int(w) + height = int(h) + + # Maybe this is for an older PNG version. + elif (size >= 16) and data.startswith("\211PNG\r\n\032\n"): + # Check to see if we have the right content type + content_type = "image/png" + w, h = struct.unpack(">LL", data[8:16]) + width = int(w) + height = int(h) + + # handle JPEGs + elif (size >= 2) and data.startswith("\377\330"): + content_type = "image/jpeg" + jpeg = StringIO(data) + jpeg.read(2) + b = jpeg.read(1) + try: + while (b and ord(b) != 0xDA): + while (ord(b) != 0xFF): b = jpeg.read + while (ord(b) == 0xFF): b = jpeg.read(1) + if (ord(b) >= 0xC0 and ord(b) <= 0xC3): + jpeg.read(3) + h, w = struct.unpack(">HH", jpeg.read(4)) + break + else: + jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2) + b = jpeg.read(1) + width = int(w) + height = int(h) + except struct.error: + pass + except ValueError: + pass + + # handle WebM + elif (size >= 4) and data.startswith("\x1A\x45\xDF\xA3"): + content_type = "video/webm" + info = ffprobe(data) + + for stream in info['streams']: + if 'width' in stream: + width = stream['width'] + height = stream['height'] + break + + extra['duration'] = float(info['format']['duration']) + + # handle ogg formats (vorbis/opus) + elif (size >= 64) and data[:4] == "OggS": + if data[28:35] == "\x01vorbis": + content_type = "audio/ogg" + elif data[28:36] == "OpusHead": + content_type = "audio/opus" + + # handle MP3 + elif (size >= 64) and (data[:3] == "ID3" or data[:3] == "\xFF\xFB"): + content_type = "audio/mpeg" + + # handle MOD + elif (size >= 64) and data[1080:1084] == "M.K.": + content_type = "audio/mod" + + # handle XM + elif (size >= 64) and data.startswith("Extended Module:"): + content_type = "audio/xm" + + # handle S3M + elif (size >= 64) and data[25:32] == "\x00\x00\x00\x1A\x10\x00\x00": + content_type = "audio/s3m" + + # handle PDF + elif (size >= 4) and data[:7] == "%PDF-1.": + content_type = "application/pdf" + + # handle Shockwave Flash + elif (size >= 3) and data[:3] in ["CWS", "FWS"]: + content_type = "application/x-shockwave-flash" + + # handle torrent + elif (size >= 11) and data[:11] == "d8:announce": + content_type = "application/x-bittorrent" + + # handle PDF + elif (size >= 2) and data[:2] == "PK": + content_type = "application/epub+zip" + + return content_type, width, height, size, extra + +def ffprobe(data): + import json + p = subprocess.Popen([Settings.FFPROBE_PATH, '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', '-'], + stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + + out = p.communicate(input=data)[0] + return json.loads(out) + +def ffprobe_f(filename): + import json + + p = subprocess.Popen([Settings.FFPROBE_PATH, '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filename], + stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + + out = p.communicate()[0] + return json.loads(out) + +def getThumbDimensions(width, height, maxsize): + """ + Calculate dimensions to use for a thumbnail with maximum width/height of + <maxsize>, keeping aspect ratio + """ + wratio = (float(maxsize) / float(width)) + hratio = (float(maxsize) / float(height)) + + if (width <= maxsize) and (height <= maxsize): + return width, height + else: + if (wratio * height) < maxsize: + thumb_height = math.ceil(wratio * height) + thumb_width = maxsize + else: + thumb_width = math.ceil(hratio * width) + thumb_height = maxsize + + return int(thumb_width), int(thumb_height) + +def checkFileDuplicate(data): + """ + Check that the file <data> does not already exist in a live post on the + current board by calculating its hex and checking it against the database + """ + board = Settings._.BOARD + + file_hex = getMD5(data) + post = FetchOne("SELECT `id`, `parentid` FROM `posts` WHERE `file_hex` = '%s' AND `boardid` = %s AND IS_DELETED = 0 LIMIT 1" % (file_hex, board['id'])) + if post: + if int(post["parentid"]) != 0: + return True, post["parentid"], post["id"] + else: + return True, post["id"], post["id"] + else: + return False, 0, 0 + +def getJpegSegments(data): + if data[0:2] != b"\xff\xd8": + raise UserError("Given data isn't JPEG.") + + head = 2 + segments = [b"\xff\xd8"] + while 1: + if data[head: head + 2] == b"\xff\xda": + yield data[head:] + break + else: + length = struct.unpack(">H", data[head + 2: head + 4])[0] + endPoint = head + length + 2 + seg = data[head: endPoint] + yield seg + head = endPoint + + if (head >= len(data)): + raise UserDataError("Wrong JPEG data.") + +def removeExifData(src_data): + exif = None + + for seg in getJpegSegments(src_data): + if seg[0:2] == b"\xff\xe1" and seg[4:10] == b"Exif\x00\x00": + exif = seg + break + + if exif: + return src_data.replace(exif, b"") + else: + return src_data diff --git a/cgi/locale/es/LC_MESSAGES/weabot.mo b/cgi/locale/es/LC_MESSAGES/weabot.mo Binary files differnew file mode 100644 index 0000000..8e207a5 --- /dev/null +++ b/cgi/locale/es/LC_MESSAGES/weabot.mo diff --git a/cgi/manage.py b/cgi/manage.py new file mode 100644 index 0000000..44731ba --- /dev/null +++ b/cgi/manage.py @@ -0,0 +1,1823 @@ +# coding=utf-8 +import _mysql +import os +import cgi +import shutil +import imaplib +import poplib +import datetime + +from database import * +from settings import Settings +from framework import * +from formatting import * +from template import * +from post import * + +def manage(self, path_split): + page = '' + validated = False + administrator = False + moderator = True + skiptemplate = False + + try: + if self.formdata['username'] and self.formdata['password']: + # If no admin accounts available, create admin:admin + first_admin = FetchOne("SELECT 1 FROM `staff` WHERE `rights` = 0 LIMIT 1", 0) + if not first_admin: + InsertDb("INSERT INTO `staff` (`username`, `password`, `added`, `rights`) VALUES ('admin', '" + _mysql.escape_string(genPasswd("admin")) + "', 0, 0)") + + password = genPasswd(self.formdata['password']) + + valid_account = FetchOne("SELECT * FROM `staff` WHERE `username` = '" + _mysql.escape_string(self.formdata['username']) + "' AND `password` = '" + _mysql.escape_string(password) + "' LIMIT 1") + if valid_account: + setCookie(self, 'weabot_manage', self.formdata['username'] + ':' + valid_account['password'], domain='THIS') + UpdateDb('DELETE FROM `logs` WHERE `timestamp` < ' + str(timestamp() - 604800)) # one week + else: + page += _('Incorrect username/password.') + logAction('', 'Failed log-in. U:'+_mysql.escape_string(self.formdata['username'])+' IP:'+self.environ["REMOTE_ADDR"]) + except: + pass + + try: + manage_cookie = getCookie(self, 'weabot_manage') + if manage_cookie != '': + username, password = manage_cookie.split(':') + staff_account = FetchOne("SELECT * FROM `staff` WHERE `username` = '" + _mysql.escape_string(username) + "' AND `password` = '" + _mysql.escape_string(password) + "' LIMIT 1") + if staff_account: + validated = True + if staff_account['rights'] == '0' or staff_account['rights'] == '1' or staff_account['rights'] == '2': + administrator = True + if staff_account['rights'] == '2': + moderator = False + UpdateDb('UPDATE `staff` SET `lastactive` = ' + str(timestamp()) + ' WHERE `id` = ' + staff_account['id'] + ' LIMIT 1') + except: + pass + + #validated = True + #moderator = True + #staff_account = {} + #staff_account['username'] = '' + #staff_account['rights'] = '0' + #staff_account['added'] = '0' + + if not validated: + template_filename = "login.html" + template_values = {} + else: + if len(path_split) > 2: + if path_split[2] == 'rebuild': + if not administrator: + return + + try: + board_dir = path_split[3] + except: + board_dir = '' + + if board_dir == '': + template_filename = "rebuild.html" + template_values = {'boards': boardlist()} + else: + everything = ("everything" in self.formdata) + if board_dir == '!ALL': + t1 = time.time() + boards = FetchAll('SELECT `dir` FROM `boards` WHERE secret = 0') + for board in boards: + board = setBoard(board['dir']) + regenerateBoard(everything) + + message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('all boards'), 'time': timeTaken(t1, time.time())} + logAction(staff_account['username'], _('Rebuilt %s') % _('all boards')) + elif board_dir == '!BBS': + t1 = time.time() + boards = FetchAll('SELECT `dir` FROM `boards` WHERE `board_type` = 1') + for board in boards: + board = setBoard(board['dir']) + regenerateBoard(everything) + + message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('all boards'), 'time': timeTaken(t1, time.time())} + logAction(staff_account['username'], _('Rebuilt %s') % _('all boards')) + elif board_dir == '!IB': + t1 = time.time() + boards = FetchAll('SELECT `dir` FROM `boards` WHERE `board_type` = 1') + for board in boards: + board = setBoard(board['dir']) + regenerateBoard(everything) + + message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('all boards'), 'time': timeTaken(t1, time.time())} + logAction(staff_account['username'], _('Rebuilt %s') % _('all boards')) + elif board_dir == '!HOME': + t1 = time.time() + regenerateHome() + message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('home'), 'time': timeTaken(t1, time.time())} + logAction(staff_account['username'], _('Rebuilt %s') % _('home')) + elif board_dir == '!NEWS': + t1 = time.time() + regenerateNews() + message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('news'), 'time': timeTaken(t1, time.time())} + logAction(staff_account['username'], _('Rebuilt %s') % _('news')) + elif board_dir == '!KAKO': + t1 = time.time() + boards = FetchAll('SELECT `dir` FROM `boards` WHERE archive = 1') + for board in boards: + board = setBoard(board['dir']) + regenerateKako() + + message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': 'kako', 'time': timeTaken(t1, time.time())} + logAction(staff_account['username'], _('Rebuilt %s') % 'kako') + elif board_dir == '!HTACCESS': + t1 = time.time() + if regenerateAccess(): + message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('htaccess'), 'time': timeTaken(t1, time.time())} + logAction(staff_account['username'], _('Rebuilt %s') % _('htaccess')) + else: + message = _('htaccess regeneration deactivated by sysop.') + else: + t1 = time.time() + board = setBoard(board_dir) + regenerateBoard(everything) + + message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': '/' + board['dir'] + '/', 'time': timeTaken(t1, time.time())} + logAction(staff_account['username'], 'Rebuilt /' + board['dir'] + '/') + + template_filename = "message.html" + elif path_split[2] == 'mod': + if not moderator: + return + + try: + board = setBoard(path_split[3]) + except: + board = "" + + if not board: + template_filename = "mod.html" + template_values = {"mode": 1, 'boards': boardlist()} + elif self.formdata.get("thread"): + parentid = int(self.formdata["thread"]) + posts = FetchAll('SELECT id, timestamp, timestamp_formatted, name, message, file, thumb, IS_DELETED, locked, subject, length, INET_NTOA(ip) AS ip FROM `posts` WHERE (parentid = %d OR id = %d) AND boardid = %s ORDER BY `id` ASC' % (parentid, parentid, board['id'])) + template_filename = "mod.html" + template_values = {"mode": 3, "dir": board["dir"], "posts": posts} + else: + threads = FetchAll("SELECT * FROM `posts` WHERE boardid = %s AND parentid = 0 ORDER BY `bumped` DESC" % board["id"]) + template_filename = "mod.html" + template_values = {"mode": 2, "dir": board["dir"], "threads": threads} + elif path_split[2] == 'staff': + if staff_account['rights'] != '0': + return + action_taken = False + + if len(path_split) > 3: + if path_split[3] == 'add' or path_split[3] == 'edit': + member = None + member_username = '' + member_rights = '3' + + if path_split[3] == 'edit': + if len(path_split) > 4: + member = FetchOne('SELECT * FROM `staff` WHERE `id` = ' + _mysql.escape_string(path_split[4]) + ' LIMIT 1') + if member: + member_username = member['username'] + member_rights = member['rights'] + action = 'edit/' + member['id'] + + try: + if self.formdata['username'] != '': + if self.formdata['rights'] in ['0', '1', '2', '3']: + action_taken = True + if not ':' in self.formdata['username']: + UpdateDb("UPDATE `staff` SET `username` = '" + _mysql.escape_string(self.formdata['username']) + "', `rights` = " + self.formdata['rights'] + " WHERE `id` = " + member['id'] + " LIMIT 1") + message = _('Staff member updated.') + logAction(staff_account['username'], _('Updated staff account for %s') % self.formdata['username']) + else: + message = _('The character : can not be used in usernames.') + template_filename = "message.html" + except: + pass + else: + action = 'add' + try: + if self.formdata['username'] != '' and self.formdata['password'] != '': + username_taken = FetchOne('SELECT * FROM `staff` WHERE `username` = \'' + _mysql.escape_string(self.formdata['username']) + '\' LIMIT 1') + if not username_taken: + if self.formdata['rights'] in ['0', '1', '2', '3']: + action_taken = True + if not ':' in self.formdata['username']: + password = genPasswd(self.formdata['password']) + + InsertDb("INSERT INTO `staff` (`username`, `password`, `added`, `rights`) VALUES ('" + _mysql.escape_string(self.formdata['username']) + "', '" + _mysql.escape_string(password) + "', " + str(timestamp()) + ", " + self.formdata['rights'] + ")") + message = _('Staff member added.') + logAction(staff_account['username'], 'Added staff account for ' + self.formdata['username']) + else: + message = _('The character : can not be used in usernames.') + + template_filename = "message.html" + else: + action_taken = True + message = _('That username is already in use.') + template_filename = "message.html" + except: + pass + + if not action_taken: + action_taken = True + + if action == 'add': + submit = 'Agregar' + else: + submit = 'Editar' + + template_filename = "staff.html" + template_values = {'mode': 1, + 'action': action, + 'member': member, + 'member_username': member_username, + 'member_rights': member_rights, + 'submit': submit} + elif path_split[3] == 'delete': + if not moderator: + return + + action_taken = True + message = '<a href="' + Settings.CGI_URL + 'manage/staff/delete_confirmed/' + path_split[4] + '">' + _('Click here to confirm the deletion of that staff member') + '</a>' + template_filename = "message.html" + elif path_split[3] == 'delete_confirmed': + if not moderator: + return + + try: + action_taken = True + member = FetchOne('SELECT `username` FROM `staff` WHERE `id` = ' + _mysql.escape_string(path_split[4]) + ' LIMIT 1') + if member: + UpdateDb('DELETE FROM `staff` WHERE `id` = ' + _mysql.escape_string(path_split[4]) + ' LIMIT 1') + message = 'Staff member deleted.' + template_filename = "message.html" + logAction(staff_account['username'], _('Deleted staff account for %s') % member['username']) + else: + message = _('Unable to locate a staff account with that ID.') + template_filename = "message.html" + except: + pass + + if not action_taken: + staff = FetchAll('SELECT * FROM `staff` ORDER BY `rights`') + for member in staff: + if member['rights'] == '0': + member ['rights'] = _('Super-administrator') + elif member['rights'] == '1': + member ['rights'] = _('Administrator') + elif member['rights'] == '2': + member ['rights'] = _('Developer') + elif member['rights'] == '3': + member ['rights'] = _('Moderator') + if member['lastactive'] != '0': + member['lastactivestamp'] = member['lastactive'] + member['lastactive'] = formatTimestamp(member['lastactive']) + else: + member['lastactive'] = _('Never') + member['lastactivestamp'] = '0' + template_filename = "staff.html" + template_values = {'mode': 0, 'staff': staff} + elif path_split[2] == 'delete': + if not moderator: + return + + do_ban = False + try: + if self.formdata['ban'] == 'true': + do_ban = True + except: + pass + + template_filename = "delete.html" + template_values = {'do_ban': do_ban, 'curboard': path_split[3], 'postid': path_split[4]} + elif path_split[2] == 'delete_confirmed': + if not moderator: + return + + do_ban = self.formdata.get('ban') + permanently = self.formdata.get('perma') + imageonly = self.formdata.get('imageonly') + + board = setBoard(path_split[3]) + postid = int(path_split[4]) + post = FetchOne('SELECT id, message, parentid, INET_NTOA(ip) AS ip FROM posts WHERE boardid = %s AND id = %s' % (board['id'], postid)) + + if not permanently: + deletePost(path_split[4], None, '2', imageonly) + else: + deletePost(path_split[4], None, '0', imageonly) + regenerateHome() + + # Borrar denuncias + UpdateDb("DELETE FROM `reports` WHERE `postid` = '"+_mysql.escape_string(path_split[4])+"'") + boards = FetchAll('SELECT `name`, `dir` FROM `boards` ORDER BY `dir`') + + if imageonly: + message = 'Archivo de post /%s/%s eliminado.' % (board['dir'], post['id']) + elif permanently or post["parentid"] == '0': + message = 'Post /%s/%s eliminado permanentemente.' % (board['dir'], post['id']) + else: + message = 'Post /%s/%s enviado a la papelera.' % (board['dir'], post['id']) + template_filename = "message.html" + logAction(staff_account['username'], message + ' Contenido: ' + post['message'] + ' IP: ' + post['ip']) + + if do_ban: + message = _('Redirecting to ban page...') + '<meta http-equiv="refresh" content="0;url=' + Settings.CGI_URL + 'manage/ban?ip=' + post['ip'] + '" />' + template_filename = "message.html" + elif path_split[2] == 'lock': + setLocked = 0 + + # Nos vamos al board y ubicamos el post + board = setBoard(path_split[3]) + post = FetchOne('SELECT `parentid`, `locked` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[4]) + '\' LIMIT 1') + if not post: + message = _('Unable to locate a post with that ID.') + template_filename = "message.html" + else: + if post['parentid'] != '0': + message = _('Post is not a thread opener.') + template_filename = "message.html" + else: + if post['locked'] == '0': + # Cerrar si esta abierto + setLocked = 1 + else: + # Abrir si esta cerrado + setLocked = 0 + + UpdateDb("UPDATE `posts` SET `locked` = %d WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % (setLocked, board["id"], _mysql.escape_string(path_split[4]))) + threadUpdated(path_split[4]) + if setLocked == 1: + message = _('Thread successfully closed.') + logAction(staff_account['username'], _('Closed thread %s') % ('/' + path_split[3] + '/' + path_split[4])) + else: + message = _('Thread successfully opened.') + logAction(staff_account['username'], _('Opened thread %s') % ('/' + path_split[3] + '/' + path_split[4])) + template_filename = "message.html" + elif path_split[2] == 'permasage': + setPermasaged = 0 + + # Nos vamos al board y ubicamos el post + board = setBoard(path_split[3]) + post = FetchOne('SELECT `parentid`, `locked` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[4]) + '\' LIMIT 1') + if not post: + message = 'Unable to locate a post with that ID.' + template_filename = "message.html" + elif post['locked'] == '1': + message = 'Solo se puede aplicar permasage en un hilo abierto.' + template_filename = "message.html" + else: + if post['parentid'] != '0': + message = 'Post is not a thread opener.' + template_filename = "message.html" + else: + if post['locked'] == '2': + # Sacar permasage + setPermasaged = 0 + else: + # Colocar permasage + setPermasaged = 2 + + UpdateDb("UPDATE `posts` SET `locked` = %d WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % (setPermasaged, board["id"], _mysql.escape_string(path_split[4]))) + regenerateFrontPages() + threadUpdated(path_split[4]) + + if setPermasaged == 2: + message = 'Thread successfully permasaged.' + logAction(staff_account['username'], 'Enabled permasage in thread /' + path_split[3] + '/' + path_split[4]) + else: + message = 'Thread successfully un-permasaged.' + logAction(staff_account['username'], 'Disabled permasage in thread /' + path_split[3] + '/' + path_split[4]) + template_filename = "message.html" + elif path_split[2] == 'move': + if not moderator: + return + + oldboardid = "" + oldthread = "" + newboardid = "" + try: + oldboardid = path_split[3] + oldthread = path_split[4] + newboardid = path_split[5] + except: + pass + + try: + oldboardid = self.formdata['oldboardid'] + oldthread = self.formdata['oldthread'] + newboardid = self.formdata['newboardid'] + except: + pass + + if oldboardid and oldthread and newboardid: + message = "import" + import shutil + message += "ok" + + board = setBoard(oldboardid) + oldboard = board['dir'] + oldboardsubject = board['subject'] + + # get old posts + posts = FetchAll("SELECT * FROM `posts` WHERE (`id` = {0} OR `parentid` = {0}) AND `boardid` = {1} ORDER BY id ASC".format(oldthread, board['id'])) + + # switch to new board + board = setBoard(newboardid) + newboard = board['dir'] + + refs = {} + moved_files = [] + moved_thumbs = [] + moved_cats = [] + newthreadid = 0 + newthread = 0 + num = 1 + + message = "from total: %s<br>" % len(posts) + template_filename = "message.html" + + for p in posts: + # save old post ID + old_id = p['id'] + is_op = bool(p['parentid'] == '0') + + # copy post object but without ID and target boardid + post = Post() + post.post = p + post.post.pop("id") + post["boardid"] = board['id'] + post["parentid"] = newthreadid + + # save the files we need to move if any + if post['IS_DELETED'] == '0': + if post['file']: + moved_files.append(post['file']) + if post['thumb']: + moved_thumbs.append(post['thumb']) + if is_op: + moved_cats.append(post['thumb']) + + # fix subject if necessary + if post['subject'] and post['subject'] == oldboardsubject: + post['subject'] = board['subject'] + + # insert new post and get its new ID + new_id = post.insert() + + # save the reference (BBS = post number, IB = new ID) + refs[old_id] = num if board['board_type'] == '1' else new_id + + # this was an OP + message += "newthread = %s parentid = %s<br>" % (newthreadid, p['parentid']) + if is_op: + oldthread = old_id + newthreadid = new_id + oldbumped = post["bumped"] + + # BBS = new thread timestamp, IB = new thread ID + newthread = post['timestamp'] if board['board_type'] == '1' else new_id + + # log it + message += "%s -> %s<br>" % (old_id, new_id) + + num += 1 + + # fix anchors + for old, new in refs.iteritems(): + old_url = "/{oldboard}/res/{oldthread}.html#{oldpost}\">>>{oldpost}</a>".format(oldboard=oldboard, oldthread=oldthread, oldpost=old) + + if board['board_type'] == '1': + new_url = "/{newboard}/read/{newthread}/{newpost}\">>>{newpost}</a>".format(newboard=newboard, newthread=newthread, newpost=new) + else: + new_url = "/{newboard}/res/{newthread}.html#{newpost}\">>>{newpost}</a>".format(newboard=newboard, newthread=newthread, newpost=new) + + sql = "UPDATE `posts` SET `message` = REPLACE(message, '{old}', '{new}') WHERE `boardid` = {newboardid} AND (`id` = {newthreadid} OR `parentid` = {newthreadid})".format(old=old_url, new=new_url, newboardid=board['id'], newthreadid=newthreadid) + message += sql + "<br>" + UpdateDb(sql) + + # copy files + for file in moved_files: + if not os.path.isfile(Settings.IMAGES_DIR + newboard + "/src/" + file): + shutil.copyfile(Settings.IMAGES_DIR + oldboard + "/src/" + file, Settings.IMAGES_DIR + newboard + "/src/" + file) + for thumb in moved_thumbs: + if not os.path.isfile(Settings.IMAGES_DIR + newboard + "/thumb/" + thumb): + shutil.copyfile(Settings.IMAGES_DIR + oldboard + "/thumb/" + thumb, Settings.IMAGES_DIR + newboard + "/thumb/" + thumb) + if not os.path.isfile(Settings.IMAGES_DIR + newboard + "/mobile/" + thumb): + shutil.copyfile(Settings.IMAGES_DIR + oldboard + "/mobile/" + thumb, Settings.IMAGES_DIR + newboard + "/mobile/" + thumb) + for cat in moved_cats: + try: + if not os.path.isfile(Settings.IMAGES_DIR + newboard + "/cat/" + thumb): + shutil.copyfile(Settings.IMAGES_DIR + oldboard + "/cat/" + thumb, Settings.IMAGES_DIR + newboard + "/cat/" + thumb) + except: + pass + + # lock original, set expiration to 1 day + exp = timestamp()+86400 + exp_format = datetime.datetime.fromtimestamp(exp).strftime("%d/%m") + sql = "UPDATE `posts` SET `locked`=1, `expires`={exp}, `expires_formatted`=\"{exp_format}\" WHERE `boardid`=\"{oldboard}\" AND id=\"{oldthread}\"".format(exp=exp,exp_format=exp_format,oldboard=oldboardid,oldthread=oldthread) + UpdateDb(sql) + + # insert notice message + if 'msg' in self.formdata: + board = setBoard(oldboard) + + if board['board_type'] == '1': + thread_url = "/{newboard}/read/{newthread}".format(newboard=newboard, newthread=newthread) + else: + thread_url = "/{newboard}/res/{newthread}.html".format(newboard=newboard, newthread=newthread) + + notice_post = Post(board["id"]) + notice_post["parentid"] = oldthread + if board['board_type'] == "0": + notice_post["subject"] = "Aviso" + notice_post["name"] = "Sistema" + notice_post["message"] = "El hilo ha sido movido a <a href=\"{url}\">/{newboard}/{newthread}</a>.".format(url=thread_url, newboard=newboard, newthread=newthread) + notice_post["timestamp"] = timestamp()+1 + notice_post["timestamp_formatted"] = "Hilo movido" + notice_post["bumped"] = oldbumped + notice_post.insert() + + # regenerate + regenerateFrontPages() + regenerateThreadPage(newthreadid) + regenerateThreadPage(oldthread) + + message += "done" + + logAction(staff_account['username'], "Movido hilo %s/%s a %s/%s." % (oldboard, oldthread, newboard, newthread)) + else: + template_filename = "move.html" + template_values = {'boards': boardlist(), 'oldboardid': oldboardid, 'oldthread': oldthread} + elif path_split[2] == 'ban': + if not moderator: + return + + if len(path_split) > 4: + board = setBoard(path_split[3]) + post = FetchOne('SELECT `ip` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[4]) + '\' LIMIT 1') + formatted_ip = inet_ntoa(long(post['ip'])) + #Creo que esto no deberia ir aqui... -> UpdateDb('UPDATE `posts` SET `banned` = 1 WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[4]) + '\'') + if not post: + message = _('Unable to locate a post with that ID.') + template_filename = "message.html" + else: + message = '<meta http-equiv="refresh" content="0;url=' + Settings.CGI_URL + 'manage/ban?ip=' + formatted_ip + '" />Espere...' + template_filename = "message.html" + else: + #if path_split[3] == '': + try: + ip = self.formdata['ip'] + except: + ip = '' + try: + netmask = insnetmask = self.formdata['netmask'] + if netmask == '255.255.255.255': + insnetmask = '' + except: + netmask = instnetmask = '' + #else: + # ip = path_split[3] + if ip != '': + try: + reason = self.formdata['reason'] + except: + reason = None + if reason is not None: + if self.formdata['seconds'] != '0': + until = str(timestamp() + int(self.formdata['seconds'])) + else: + until = '0' + where = '' + if 'board_all' not in self.formdata.keys(): + where = [] + boards = FetchAll('SELECT `dir` FROM `boards`') + for board in boards: + keyname = 'board_' + board['dir'] + if keyname in self.formdata.keys(): + if self.formdata[keyname] == "1": + where.append(board['dir']) + if len(where) > 0: + where = pickle.dumps(where) + else: + self.error(_("You must select where the ban shall be placed")) + return + + if 'edit' in self.formdata.keys(): + UpdateDb("DELETE FROM `bans` WHERE `id` = '" + _mysql.escape_string(self.formdata['edit']) + "' LIMIT 1") + else: + ban = FetchOne("SELECT `id` FROM `bans` WHERE `ip` = '" + _mysql.escape_string(ip) + "' AND `boards` = '" + _mysql.escape_string(where) + "' LIMIT 1") + if ban: + self.error(_('There is already an identical ban for this IP.') + '<a href="'+Settings.CGI_URL+'manage/ban/' + ip + '?edit=' + ban['id']+'">' + _('Edit') + '</a>') + return + + # Blind mode + if 'blind' in self.formdata.keys() and self.formdata['blind'] == '1': + blind = '1' + else: + blind = '0' + + # Banear sin mensaje + InsertDb("INSERT INTO `bans` (`ip`, `netmask`, `boards`, `added`, `until`, `staff`, `reason`, `note`, `blind`) VALUES (INET_ATON('" + _mysql.escape_string(ip) + "') & INET_ATON('"+_mysql.escape_string(netmask)+"'), INET_ATON('"+_mysql.escape_string(insnetmask)+"'), '" + _mysql.escape_string(where) + "', " + str(timestamp()) + ", " + until + ", '" + _mysql.escape_string(staff_account['username']) + "', '" + _mysql.escape_string(self.formdata['reason']) + "', '" + _mysql.escape_string(self.formdata['note']) + "', '"+blind+"')") + + regenerateAccess() + if 'edit' in self.formdata.keys(): + message = _('Ban successfully edited.') + action = 'Edited ban for ' + ip + else: + message = _('Ban successfully placed.') + action = 'Banned ' + ip + if until != '0': + action += ' until ' + formatTimestamp(until) + else: + action += ' permanently' + logAction(staff_account['username'], action) + template_filename = 'message.html' + else: + startvalues = {'where': [], + 'netmask': '255.255.255.255', + 'reason': '', + 'note': '', + 'message': '(GET OUT)', + 'seconds': '0', + 'blind': '1'} + edit_id = 0 + if 'edit' in self.formdata.keys(): + edit_id = self.formdata['edit'] + ban = FetchOne("SELECT `id`, INET_NTOA(`ip`) AS 'ip', CASE WHEN `netmask` IS NULL THEN '255.255.255.255' ELSE INET_NTOA(`netmask`) END AS 'netmask', boards, added, until, staff, reason, note, blind FROM `bans` WHERE `id` = '" + _mysql.escape_string(edit_id) + "' ORDER BY `added` DESC") + if ban: + if ban['boards'] == '': + where = '' + else: + where = pickle.loads(ban['boards']) + if ban['until'] == '0': + until = 0 + else: + until = int(ban['until']) - timestamp() + startvalues = {'where': where, + 'netmask': ban['netmask'], + 'reason': ban['reason'], + 'note': ban['note'], + 'seconds': str(until), + 'blind': ban['blind'] + } + else: + edit_id = 0 + + template_filename = "bans.html" + template_values = {'mode': 1, + 'boards': boardlist(), + 'ip': ip, + 'startvalues': startvalues, + 'edit_id': edit_id} + elif path_split[2] == 'bans': + if not moderator: + return + + action_taken = False + if len(path_split) > 4: + if path_split[3] == 'delete': + ip = FetchOne("SELECT INET_NTOA(`ip`) AS 'ip' FROM `bans` WHERE `id` = '" + _mysql.escape_string(path_split[4]) + "' LIMIT 1", 0)[0] + if ip != '': + # Delete ban + UpdateDb('DELETE FROM `bans` WHERE `id` = ' + _mysql.escape_string(path_split[4]) + ' LIMIT 1') + regenerateAccess() + message = _('Ban successfully deleted.') + template_filename = "message.html" + logAction(staff_account['username'], _('Deleted ban for %s') % ip) + else: + message = _('There was a problem while deleting that ban. It may have already been removed, or recently expired.') + template_filename = "message.html" + + if not action_taken: + bans = FetchAll("SELECT `id`, INET_NTOA(`ip`) AS 'ip', CASE WHEN `netmask` IS NULL THEN '255.255.255.255' ELSE INET_NTOA(`netmask`) END AS 'netmask', boards, added, until, staff, reason, note, blind FROM `bans` ORDER BY `added` DESC") + if bans: + for ban in bans: + if ban['boards'] == '': + ban['boards'] = _('All boards') + else: + where = pickle.loads(ban['boards']) + if len(where) > 1: + ban['boards'] = '/' + '/, /'.join(where) + '/' + else: + ban['boards'] = '/' + where[0] + '/' + ban['added'] = formatTimestamp(ban['added']) + if ban['until'] == '0': + ban['until'] = _('Does not expire') + else: + ban['until'] = formatTimestamp(ban['until']) + if ban['blind'] == '1': + ban['blind'] = 'Sí' + else: + ban['blind'] = 'No' + template_filename = "bans.html" + template_values = {'mode': 0, 'bans': bans} + elif path_split[2] == 'changepassword': + form_submitted = False + try: + if self.formdata['oldpassword'] != '' and self.formdata['newpassword'] != '' and self.formdata['newpassword2'] != '': + form_submitted = True + except: + pass + if form_submitted: + if genPasswd(self.formdata['oldpassword']) == staff_account['password']: + if self.formdata['newpassword'] == self.formdata['newpassword2']: + UpdateDb('UPDATE `staff` SET `password` = \'' + genPasswd(self.formdata['newpassword']) + '\' WHERE `id` = ' + staff_account['id'] + ' LIMIT 1') + message = _('Password successfully changed. Please log out and log back in.') + template_filename = "message.html" + else: + message = _('Passwords did not match.') + template_filename = "message.html" + else: + message = _('Current password incorrect.') + template_filename = "message.html" + else: + template_filename = "changepassword.html" + template_values = {} + elif path_split[2] == 'board': + if not administrator: + return + + if len(path_split) > 3: + board = setBoard(path_split[3]) + form_submitted = False + try: + if self.formdata['name'] != '': + form_submitted = True + except: + pass + if form_submitted: + # Update board settings + board['name'] = self.formdata['name'] + board['longname'] = self.formdata['longname'] + board['subname'] = self.formdata['subname'] + board['anonymous'] = self.formdata['anonymous'] + board['subject'] = self.formdata['subject'] + board['message'] = self.formdata['message'] + if board['dir'] != 'anarkia': + board['board_type'] = self.formdata['type'] + board['useid'] = self.formdata['useid'] + board['slip'] = self.formdata['slip'] + board['countrycode'] = self.formdata['countrycode'] + if 'recyclebin' in self.formdata.keys(): + board['recyclebin'] = '1' + else: + board['recyclebin'] = '0' + if 'disable_name' in self.formdata.keys(): + board['disable_name'] = '1' + else: + board['disable_name'] = '0' + if 'disable_subject' in self.formdata.keys(): + board['disable_subject'] = '1' + else: + board['disable_subject'] = '0' + if 'secret' in self.formdata.keys(): + board['secret'] = '1' + else: + board['secret'] = '0' + if 'locked' in self.formdata.keys(): + board['locked'] = '1' + else: + board['locked'] = '0' + board['postarea_desc'] = self.formdata['postarea_desc'] + if 'allow_noimage' in self.formdata.keys(): + board['allow_noimage'] = '1' + else: + board['allow_noimage'] = '0' + if 'allow_images' in self.formdata.keys(): + board['allow_images'] = '1' + else: + board['allow_images'] = '0' + if 'allow_image_replies' in self.formdata.keys(): + board['allow_image_replies'] = '1' + else: + board['allow_image_replies'] = '0' + if 'allow_spoilers' in self.formdata.keys(): + board['allow_spoilers'] = '1' + else: + board['allow_spoilers'] = '0' + if 'allow_oekaki' in self.formdata.keys(): + board['allow_oekaki'] = '1' + else: + board['allow_oekaki'] = '0' + if 'archive' in self.formdata.keys(): + board['archive'] = '1' + else: + board['archive'] = '0' + board['postarea_extra'] = self.formdata['postarea_extra'] + board['force_css'] = self.formdata['force_css'] + + # Update file types + UpdateDb("DELETE FROM `boards_filetypes` WHERE `boardid` = %s" % board['id']) + for filetype in filetypelist(): + if 'filetype'+filetype['ext'] in self.formdata.keys(): + UpdateDb("INSERT INTO `boards_filetypes` VALUES (%s, %s)" % (board['id'], filetype['id'])) + + try: + board['numthreads'] = int(self.formdata['numthreads']) + except: + raise UserError, _("Max threads shown must be numeric.") + + try: + board['numcont'] = int(self.formdata['numcont']) + except: + raise UserError, _("Max replies shown must be numeric.") + + try: + board['numline'] = int(self.formdata['numline']) + except: + raise UserError, _("Max lines shown must be numeric.") + + try: + board['thumb_px'] = int(self.formdata['thumb_px']) + except: + raise UserError, _("Max thumb dimensions must be numeric.") + + try: + board['maxsize'] = int(self.formdata['maxsize']) + except: + raise UserError, _("Max size must be numeric.") + + try: + board['maxage'] = int(self.formdata['maxage']) + except: + raise UserError, _("Max age must be numeric.") + + try: + board['maxinactive'] = int(self.formdata['maxinactive']) + except: + raise UserError, _("Max inactivity must be numeric.") + + try: + board['threadsecs'] = int(self.formdata['threadsecs']) + except: + raise UserError, _("Time between new threads must be numeric.") + + try: + board['postsecs'] = int(self.formdata['postsecs']) + except: + raise UserError, _("Time between replies must be numeric.") + + updateBoardSettings() + message = _('Board options successfully updated.') + ' <a href="'+Settings.CGI_URL+'manage/rebuild/'+board['dir']+'">'+_('Rebuild')+'</a>' + template_filename = "message.html" + logAction(staff_account['username'], _('Updated options for /%s/') % board['dir']) + else: + template_filename = "boardoptions.html" + template_values = {'mode': 1, 'boardopts': board, 'filetypes': filetypelist(), 'supported_filetypes': board['filetypes_ext']} + else: + # List all boards + template_filename = "boardoptions.html" + template_values = {'mode': 0, 'boards': boardlist()} + elif path_split[2] == 'recyclebin': + if not administrator: + return + + message = None + if len(path_split) > 5: + if path_split[4] == 'restore': + board = setBoard(path_split[5]) + + post = FetchOne('SELECT `parentid` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[6]) + '\' LIMIT 1') + if not post: + message = _('Unable to locate a post with that ID.') + '<br />' + template_filename = "message.html" + else: + UpdateDb('UPDATE `posts` SET `IS_DELETED` = 0 WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[6]) + '\' LIMIT 1') + if post['parentid'] != '0': + threadUpdated(post['parentid']) + else: + regenerateFrontPages() + + message = _('Post successfully restored.') + logAction(staff_account['username'], _('Restored post %s') % ('/' + path_split[5] + '/' + path_split[6])) + + if path_split[4] == 'delete': + board = setBoard(path_split[5]) + post = FetchOne('SELECT `parentid` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[6]) + '\' LIMIT 1') + if not post: + message = _('Unable to locate a post with that ID.') + else: + deletePost(path_split[6], None) + + if post['parentid'] != '0': + threadUpdated(post['parentid']) + else: + regenerateFrontPages() + + message = _('Post successfully permadeleted.') + logAction(staff_account['username'], _('Permadeleted post %s') % ('/' + path_split[5] + '/' + path_split[6])) + + # Delete more than 1 post + if 'deleteall' in self.formdata.keys(): + return # TODO + deleted = 0 + for key in self.formdata.keys(): + if key[:2] == '!i': + dir = key[2:].split('/')[0] # Board where the post is + postid = key[2:].split('/')[1] # Post to delete + + # Delete post start + post = FetchOne('SELECT `parentid`, `dir` FROM `posts` INNER JOIN `boards` ON posts.boardid = boards.id WHERE `dir` = \'' + _mysql.escape_string(dir) + '\' AND posts.id = \'' + _mysql.escape_string(postid) + '\' LIMIT 1') + if not post: + message = _('Unable to locate a post with that ID.') + else: + board = setBoard(dir) + deletePost(int(postid), None) + if post['parentid'] != '0': + threadUpdated(post['parentid']) + else: + regenerateFrontPages() + deleted += 1 + # Delete post end + + logAction(staff_account['username'], _('Permadeleted %s post(s).') % str(deleted)) + message = _('Permadeleted %s post(s).') % str(deleted) + + ## Start + import math + pagesize = float(Settings.RECYCLEBIN_POSTS_PER_PAGE) + + try: + currentpage = int(path_split[3]) + except: + currentpage = 0 + + skip = False + if 'type' in self.formdata.keys(): + type = int(self.formdata["type"]) + else: + type = 0 + + # Generate board list + boards = FetchAll('SELECT `name`, `dir` FROM `boards` ORDER BY `dir`') + for board in boards: + if 'board' in self.formdata.keys() and self.formdata['board'] == board['dir']: + board['checked'] = True + else: + board['checked'] = False + + # Get type filter + if type != 0: + type_condition = "= " + str(type) + else: + type_condition = "!= 0" + + # Table + if 'board' in self.formdata.keys() and self.formdata['board'] != 'all': + cboard = self.formdata['board'] + posts = FetchAll("SELECT posts.id, posts.timestamp, timestamp_formatted, IS_DELETED, INET_NTOA(posts.ip) as ip, posts.message, dir, boardid FROM `posts` INNER JOIN `boards` ON boardid = boards.id WHERE `dir` = '%s' AND IS_DELETED %s ORDER BY `timestamp` DESC LIMIT %d, %d" % (_mysql.escape_string(self.formdata['board']), _mysql.escape_string(type_condition), currentpage*pagesize, pagesize)) + try: + totals = FetchOne("SELECT COUNT(id) FROM `posts` WHERE IS_DELETED %s AND `boardid` = %s" % (_mysql.escape_string(type_condition), _mysql.escape_string(posts[0]['boardid'])), 0) + except: + skip = True + else: + cboard = 'all' + posts = FetchAll("SELECT posts.id, posts.timestamp, timestamp_formatted, IS_DELETED, posts.ip, posts.message, dir FROM `posts` INNER JOIN `boards` ON boardid = boards.id WHERE IS_DELETED %s ORDER BY `timestamp` DESC LIMIT %d, %d" % (_mysql.escape_string(type_condition), currentpage*pagesize, pagesize)) + totals = FetchOne("SELECT COUNT(id) FROM `posts` WHERE IS_DELETED %s" % _mysql.escape_string(type_condition), 0) + + template_filename = "recyclebin.html" + template_values = {'message': message, + 'type': type, + 'boards': boards, + 'skip': skip} + + if not skip: + # Calculate number of pages + total = int(totals[0]) + pages = int(math.ceil(total / pagesize)) + + # Create delete form + if 'board' in self.formdata.keys(): + board = self.formdata['board'] + else: + board = None + + navigator = '' + if currentpage > 0: + navigator += '<a href="'+Settings.CGI_URL+'manage/recyclebin/'+str(currentpage-1)+'?type='+str(type)+'&board='+cboard+'"><</a> ' + else: + navigator += '< ' + + for i in range(pages): + if i != currentpage: + navigator += '<a href="'+Settings.CGI_URL+'manage/recyclebin/'+str(i)+'?type='+str(type)+'&board='+cboard+'">'+str(i)+'</a> ' + else: + navigator += str(i)+' ' + + if currentpage < (pages-1): + navigator += '<a href="'+Settings.CGI_URL+'manage/recyclebin/'+str(currentpage+1)+'?type='+str(type)+'&board='+cboard+'">></a> ' + else: + navigator += '> ' + + template_values.update({'currentpage': currentpage, + 'curboard': board, + 'posts': posts, + 'navigator': navigator}) + # End recyclebin + elif path_split[2] == 'lockboard': + if not administrator: + return + + try: + board_dir = path_split[3] + except: + board_dir = '' + + if board_dir == '': + template_filename = "lockboard.html" + template_values = {'boards': boardlist()} + elif path_split[2] == 'boardlock': + board = setBoard(path_split[3]) + if int(board['locked']): + # Si esta cerrado... abrir + board['locked'] = 0 + updateBoardSettings() + message = _('Board opened successfully.') + template_filename = "message.html" + else: + # Si esta abierta, cerrar + board['locked'] = 1 + updateBoardSettings() + message = _('Board closed successfully.') + template_filename = "message.html" + elif path_split[2] == 'addboard': + if not administrator: + return + + action_taken = False + board_dir = '' + + try: + if self.formdata['name'] != '': + board_dir = self.formdata['dir'] + except: + pass + + if board_dir != '': + action_taken = True + board_exists = FetchOne('SELECT * FROM `boards` WHERE `dir` = \'' + _mysql.escape_string(board_dir) + '\' LIMIT 1') + if not board_exists: + os.mkdir(Settings.ROOT_DIR + board_dir) + os.mkdir(Settings.ROOT_DIR + board_dir + '/res') + if not os.path.exists(Settings.IMAGES_DIR + board_dir): + os.mkdir(Settings.IMAGES_DIR + board_dir) + os.mkdir(Settings.IMAGES_DIR + board_dir + '/src') + os.mkdir(Settings.IMAGES_DIR + board_dir + '/thumb') + os.mkdir(Settings.IMAGES_DIR + board_dir + '/mobile') + os.mkdir(Settings.IMAGES_DIR + board_dir + '/cat') + if os.path.exists(Settings.ROOT_DIR + board_dir) and os.path.isdir(Settings.ROOT_DIR + board_dir): + UpdateDb('INSERT INTO `boards` (`dir`, `name`) VALUES (\'' + _mysql.escape_string(board_dir) + '\', \'' + _mysql.escape_string(self.formdata['name']) + '\')') + board = setBoard(board_dir) + f = open(Settings.ROOT_DIR + board['dir'] + '/.htaccess', 'w') + try: + f.write('DirectoryIndex index.html') + finally: + f.close() + regenerateFrontPages() + message = _('Board added') + template_filename = "message.html" + logAction(staff_account['username'], _('Added board %s') % ('/' + board['dir'] + '/')) + else: + message = _('There was a problem while making the directories.') + template_filename = "message.html" + else: + message = _('There is already a board with that directory.') + template_filename = "message.html" + + if not action_taken: + template_filename = "addboard.html" + template_values = {} + elif path_split[2] == 'trim': + if not administrator: + return + board = setBoard(path_split[3]) + trimThreads() + self.output = "done trimming" + return + elif path_split[2] == 'setexpires': + board = setBoard(path_split[3]) + parentid = int(path_split[4]) + days = int(path_split[5]) + t = time.time() + + expires = int(t) + (days * 86400) + date_format = '%d/%m' + expires_formatted = datetime.datetime.fromtimestamp(expires).strftime(date_format) + + sql = "UPDATE posts SET expires = timestamp + (%s * 86400), expires_formatted = FROM_UNIXTIME((timestamp + (%s * 86400)), '%s') WHERE boardid = %s AND id = %s" % (str(days), str(days), date_format, board["id"], str(parentid)) + UpdateDb(sql) + + self.output = "done " + sql + return + elif path_split[2] == 'fixflood': + if not administrator: + return + board = setBoard('zonavip') + threads = FetchAll("SELECT * FROM posts WHERE boardid = %s AND parentid = 0 AND subject LIKE 'querido mod%%'" % board['id']) + for thread in threads: + self.output += "%s<br>" % thread['id'] + #deletePost(thread['id'], None) + return + elif path_split[2] == 'fixico': + board = setBoard(path_split[3]) + + threads = FetchAll("SELECT * FROM posts WHERE boardid = %s AND parentid = 0 AND message NOT LIKE '<img%%'" % board['id']) + for t in threads: + img_src = '<img src="%s" alt="ico" /><br />' % getRandomIco() + newmessage = img_src + t["message"] + #UpdateDb("UPDATE posts SET message = '%s' WHERE boardid = %s AND id = %s" % (_mysql.escape_string(newmessage), board['id'], t['id'])) + + self.output = repr(threads) + return + elif path_split[2] == 'fixkako': + board = setBoard(path_split[3]) + + threads = FetchAll('SELECT * FROM archive WHERE boardid = %s ORDER BY timestamp DESC' % board['id']) + for item in threads: + t = time.time() + self.output += item['timestamp'] + '<br />' + fname = Settings.ROOT_DIR + board["dir"] + "/kako/" + str(item["timestamp"]) + ".json" + if os.path.isfile(fname): + import json + with open(fname) as f: + thread = json.load(f) + thread['posts'] = [dict(zip(thread['keys'], row)) for row in thread['posts']] + template_fname = "txt_archive.html" + + post_preview = cut_home_msg(thread['posts'][0]['message'], 0) + page = renderTemplate("txt_archive.html", {"threads": [thread], "preview": post_preview}, False) + with open(Settings.ROOT_DIR + board["dir"] + "/kako/" + str(thread['timestamp']) + ".html", "w") as f: + f.write(page) + + self.output += 'done' + str(time.time() - t) + '<br />' + else: + self.output += 'El hilo no existe.<br />' + elif path_split[2] == 'fixexpires': + board = setBoard(path_split[3]) + + if int(board["maxage"]): + date_format = '%d/%m' + date_format_y = '%m/%Y' + if int(board["maxage"]) >= 365: + date_format = date_format_y + sql = "UPDATE posts SET expires = timestamp + (%s * 86400), expires_formatted = FROM_UNIXTIME((timestamp + (%s * 86400)), '%s') WHERE boardid = %s AND parentid = 0" % (board["maxage"], board["maxage"], date_format, board["id"]) + UpdateDb(sql) + + alert_time = int(round(int(board['maxage']) * Settings.MAX_AGE_ALERT)) + sql = "UPDATE posts SET expires_alert = CASE WHEN UNIX_TIMESTAMP() > (expires - %d*86400) THEN 1 ELSE 0 END WHERE boardid = %s AND parentid = 0" % (alert_time, board["id"]) + UpdateDb(sql) + else: + sql = "UPDATE posts SET expires = 0, expires_formatted = '', expires_alert = 0 WHERE boardid = %s AND parentid = 0" % (board["id"]) + UpdateDb(sql) + + self.output = "done" + return + elif path_split[2] == 'fixid': + board = setBoard(path_split[3]) + posts = FetchAll('SELECT * FROM `posts` WHERE `boardid` = %s' % board['id']) + self.output = "total: %d<br />" % len(posts) + for post in posts: + new_timestamp_formatted = formatTimestamp(post['timestamp']) + tim = 0 + if board["useid"] != '0': + new_timestamp_formatted += ' ID:' + iphash(post['ip'], '', tim, '1', False, False, False, '0') + self.output += "%s - %s <br />" % (post['id'], new_timestamp_formatted) + query = "UPDATE `posts` SET timestamp_formatted = '%s' WHERE boardid = '%s' AND id = '%s'" % (new_timestamp_formatted, board['id'], post['id']) + UpdateDb(query) + return + elif path_split[2] == 'fixname': + board = setBoard(path_split[3]) + #posts = FetchAll('SELECT * FROM `posts` WHERE `boardid` = %s' % board['id']) + posts = FetchAll('SELECT * FROM `posts` WHERE `name` LIKE \'%s\'' % '%%') + new_name = board['anonymous'] + self.output = new_name + "<br />" + for post in posts: + self.output += "%s<br />" % (post['id']) + query = "UPDATE `posts` SET `name` = '%s' WHERE boardid = '%s' AND id = '%s'" % (new_name, board['id'], post['id']) + UpdateDb(query) + return + elif path_split[2] == 'setsub': + board = setBoard(path_split[3]) + thread = FetchOne('SELECT * FROM `posts` WHERE `parentid` = 0 AND `boardid` = %s' % board['id']) + subject = str(path_split[4]) + self.output = subject + "->" + thread['id'] + "<br />" + query = "UPDATE `posts` SET `subject` = '%s' WHERE boardid = '%s' AND id = '%s'" % (subject, board['id'], thread['id']) + UpdateDb(query) + return + elif path_split[2] == 'fixlength': + board = setBoard(path_split[3]) + threads = FetchAll('SELECT * FROM `posts` WHERE parentid = 0 AND `boardid` = %s' % board['id']) + for t in threads: + length = threadNumReplies(t['id']) + UpdateDb('UPDATE posts SET length = %d WHERE boardid = %s AND id = %s' % (length, board['id'], t['id'])) + + self.output='done' + return + elif path_split[2] == 'archive': + t = time.time() + board = setBoard(path_split[3]) + postid = int(path_split[4]) + archiveThread(postid) + self.output = "todo ok %s" % str(time.time() - t) + elif path_split[2] == 'filters': + action_taken = False + if len(path_split) > 3 and path_split[3] == 'add': + if "add" in self.formdata.keys(): + edit_id = 0 + if 'edit' in self.formdata.keys(): + edit_id = int(self.formdata['edit']) + + # We decide what type of filter it is. + # 0: Word / 1: Name/Trip + filter_type = int(self.formdata["type"]) + filter_action = int(self.formdata["action"]) + filter_from = '' + filter_tripcode = '' + + # I don't like pickles... oh well. + where = '' + if 'board_all' not in self.formdata.keys(): + where = [] + boards = FetchAll('SELECT `dir` FROM `boards`') + for board in boards: + keyname = 'board_' + board['dir'] + if keyname in self.formdata.keys(): + if self.formdata[keyname] == "1": + where.append(board['dir']) + if len(where) > 0: + where = _mysql.escape_string(pickle.dumps(where)) + else: + self.error(_("You must select what board the filter will affect")) + return + + if filter_type == 0: + # Word filter + if len(self.formdata["word"]) > 0: + filter_from = _mysql.escape_string(cgi.escape(self.formdata["word"])) + else: + self.error(_("You must enter a word.")) + return + elif filter_type == 1: + # Name/trip filter + can_add = False + if len(self.formdata["name"]) > 0: + filter_from = _mysql.escape_string(self.formdata["name"]) + can_add = True + if len(self.formdata["trip"]) > 0: + filter_tripcode = _mysql.escape_string(self.formdata["trip"]) + can_add = True + if not can_add: + self.error(_("You must enter a name and/or a tripcode.")) + return + + # Action + sql_query = '' + filter_reason = '' + if len(self.formdata["reason"]) > 0: + filter_reason = _mysql.escape_string(self.formdata["reason"]) + if filter_action == 0: + # Cancel post + sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \ + (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, str(timestamp()), _mysql.escape_string(staff_account['username'])) + elif filter_action == 1: + # Change to + if len(self.formdata["changeto"]) > 0: + filter_to = _mysql.escape_string(self.formdata["changeto"]) + sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `to`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \ + (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, filter_to, str(timestamp()), _mysql.escape_string(staff_account['username'])) + else: + self.error(_("You must enter a word to change to.")) + return + elif filter_action == 2: + # Ban + filter_seconds = '0' + if len(self.formdata["seconds"]) > 0: + filter_seconds = _mysql.escape_string(self.formdata["seconds"]) + if "blind" in self.formdata.keys() and self.formdata["blind"] == '1': + filter_blind = '1' + else: + filter_blind = '2' + + sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `seconds`, `blind`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \ + (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, filter_seconds, filter_blind, str(timestamp()), _mysql.escape_string(staff_account['username'])) + elif filter_action == 3: + # Redirect URL + if len(self.formdata['redirect_url']) > 0: + redirect_url = _mysql.escape_string(self.formdata['redirect_url']) + redirect_time = 0 + try: + redirect_time = int(self.formdata['redirect_time']) + except: + pass + else: + self.error(_("You must enter a URL to redirect to.")) + return + + sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `redirect_url`, `redirect_time`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \ + (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, redirect_url, str(redirect_time), str(timestamp()), _mysql.escape_string(staff_account['username'])) + # DO QUERY! + if edit_id > 0: + UpdateDb("DELETE FROM `filters` WHERE `id` = %s" % str(edit_id)) + UpdateDb(sql_query) + message = 'Filter edited.' + else: + filt = FetchOne("SELECT `id` FROM `filters` WHERE `boards` = '%s' AND `type` = '%s' AND `from` = '%s'" % (where, str(filter_type), filter_from)) + if not filt: + UpdateDb(sql_query) + message = 'Filter added.' + else: + message = 'This filter already exists here:' + ' <a href="'+Settings.CGI_URL+'manage/filters/add?edit='+filt['id']+'">edit</a>' + action_taken = True + template_filename = "message.html" + else: + # Create add form + edit_id = 0 + if 'edit' in self.formdata.keys() and int(self.formdata['edit']) > 0: + # Load values + edit_id = int(self.formdata['edit']) + filt = FetchOne("SELECT * FROM `filters` WHERE `id` = %s LIMIT 1" % str(edit_id)) + if filt['boards'] == '': + where = '' + else: + where = pickle.loads(filt['boards']) + startvalues = {'type': filt['type'], + 'trip': filt['from_trip'], + 'where': where, + 'action': filt['action'], + 'changeto': cgi.escape(filt['to'], True), + 'reason': filt['reason'], + 'seconds': filt['seconds'], + 'blind': filt['blind'], + 'redirect_url': filt['redirect_url'], + 'redirect_time': filt['redirect_time'],} + if filt['type'] == '1': + startvalues['name'] = filt['from'] + startvalues['word'] = '' + else: + startvalues['name'] = '' + startvalues['word'] = filt['from'] + else: + startvalues = {'type': '0', + 'word': '', + 'name': '', + 'trip': '', + 'where': [], + 'action': '0', + 'changeto': '', + 'reason': _('Forbidden word'), + 'seconds': '0', + 'blind': '1', + 'redirect_url': 'http://', + 'redirect_time': '5'} + + if edit_id > 0: + submit = "Editar Filtro" + else: + submit = "Agregar filtro" + + action_taken = True + template_filename = "filters.html" + template_values = {'mode': 1, + 'edit_id': edit_id, + 'boards': boardlist(), + 'startvalues': startvalues, + 'submit': submit} + elif len(path_split) > 4 and path_split[3] == 'delete': + delid = int(path_split[4]) + UpdateDb("DELETE FROM `filters` WHERE id = '%s' LIMIT 1" % str(delid)) + message = _('Deleted filter %s.') % str(delid) + template_filename = "message.html" + action_taken = True + + if not action_taken: + filters = FetchAll("SELECT * FROM `filters` ORDER BY `added` DESC") + for filter in filters: + if filter['boards'] == '': + filter['boards'] = _('All boards') + else: + where = pickle.loads(filter['boards']) + if len(where) > 1: + filter['boards'] = '/' + '/, /'.join(where) + '/' + else: + filter['boards'] = '/' + where[0] + '/' + if filter['type'] == '0': + filter['type_formatted'] = _('Word:') + ' <b>' + cgi.escape(filter['from']) + '</b>' + elif filter['type'] == '1': + filter['type_formatted'] = _('Name/Tripcode:')+' ' + if filter['from'] != '': + filter['type_formatted'] += '<b class="name">' + filter['from'] + '</b>' + if filter['from_trip'] != '': + filter['type_formatted'] += '<span class="trip">' + filter['from_trip'] + '</span>' + else: + filter['type_formatted'] = '?' + if filter['action'] == '0': + filter ['action_formatted'] = _('Abort post') + elif filter['action'] == '1': + filter ['action_formatted'] = _('Change to:') + ' <b>' + cgi.escape(filter['to']) + '</b>' + elif filter['action'] == '2': + if filter['blind'] == '1': + blind = _('Yes') + else: + blind = _('No') + filter ['action_formatted'] = _('Autoban:') + '<br />' + \ + (_('Length:')+' <i>%s</i><br />'+_('Blind:')+' <i>%s</i>') % (filter['seconds'], blind) + elif filter['action'] == '3': + filter ['action_formatted'] = (_('Redirect to:')+' %s ('+_('in %s secs')+')') % (filter['redirect_url'], filter['redirect_time']) + else: + filter ['action_formatted'] = '?' + filter['added'] = formatTimestamp(filter['added']) + + template_filename = "filters.html" + template_values = {'mode': 0, 'filters': filters} + elif path_split[2] == 'logs': + if staff_account['rights'] != '0' and staff_account['rights'] != '2': + return + + logs = FetchAll('SELECT * FROM `logs` ORDER BY `timestamp` DESC') + for log in logs: + log['timestamp_formatted'] = formatTimestamp(log['timestamp']) + template_filename = "logs.html" + template_values = {'logs': logs} + elif path_split[2] == 'logout': + message = _('Logging out...') + '<meta http-equiv="refresh" content="0;url=' + Settings.CGI_URL + 'manage" />' + setCookie(self, 'weabot_manage', '', domain='THIS') + setCookie(self, 'weabot_staff', '') + template_filename = "message.html" + elif path_split[2] == 'quotes': + # Quotes for the post screen + if "save" in self.formdata.keys(): + try: + f = open('quotes.conf', 'w') + f.write(self.formdata["data"]) + f.close() + message = 'Datos guardados.' + template_filename = "message.html" + except: + message = 'Error al guardar datos.' + template_filename = "message.html" + try: + f = open('quotes.conf', 'r') + data = cgi.escape(f.read(1048576), True) + f.close() + template_filename = "quotes.html" + template_values = {'data': data} + except: + message = 'Error al leer datos.' + template_filename = 'message.html' + elif path_split[2] == 'recent_images': + try: + if int(self.formdata['images']) > 100: + images = '100' + else: + images = self.formdata['images'] + posts = FetchAll('SELECT * FROM `posts` INNER JOIN `boards` ON boardid = boards.id WHERE CHAR_LENGTH(`thumb`) > 0 ORDER BY `timestamp` DESC LIMIT ' + _mysql.escape_string(images)) + except: + posts = FetchAll('SELECT * FROM `posts` INNER JOIN `boards` ON boardid = boards.id WHERE CHAR_LENGTH(`thumb`) > 0 ORDER BY `timestamp` DESC LIMIT 10') + template_filename = "recent_images.html" + template_values = {'posts': posts} + elif path_split[2] == 'news': + if not administrator: + return + + type = 1 + if 'type' in self.formdata: + type = int(self.formdata['type']) + + if type > 2: + raise UserError, "Tipo no soportado" + + # canal del home + if len(path_split) > 3: + if path_split[3] == 'add': + t = datetime.datetime.now() + + # Insertar el nuevo post + title = '' + message = self.formdata["message"].replace("\n", "<br />") + + # Titulo + if 'title' in self.formdata: + title = self.formdata["title"] + + # Post anonimo + if 'anonymous' in self.formdata.keys() and self.formdata['anonymous'] == '1': + to_name = "Staff ★" + else: + to_name = "%s ★" % staff_account['username'] + timestamp_formatted = formatDate(t) + if type > 0: + timestamp_formatted = re.sub(r"\(.+", "", timestamp_formatted) + else: + timestamp_formatted = re.sub(r"\(...\)", " ", timestamp_formatted) + + UpdateDb("INSERT INTO `news` (type, staffid, staff_name, title, message, name, timestamp, timestamp_formatted) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%d', '%s')" % (type, staff_account['id'], staff_account['username'], _mysql.escape_string(title), _mysql.escape_string(message), to_name, timestamp(t), timestamp_formatted)) + + regenerateNews() + regenerateHome() + message = _("Added successfully.") + template_filename = "message.html" + if path_split[3] == 'delete': + # Eliminar un post + id = int(path_split[4]) + UpdateDb("DELETE FROM `news` WHERE id = %d AND type = %d" % (id, type)) + regenerateNews() + regenerateHome() + message = _("Deleted successfully.") + template_filename = "message.html" + else: + posts = FetchAll("SELECT * FROM `news` WHERE type = %d ORDER BY `timestamp` DESC" % type) + template_filename = "news.html" + template_values = {'action': type, 'posts': posts} + elif path_split[2] == 'newschannel': + #if not administrator: + # return + + if len(path_split) > 3: + if path_split[3] == 'add': + t = datetime.datetime.now() + # Delete old posts + #posts = FetchAll("SELECT `id` FROM `news` WHERE `type` = '1' ORDER BY `timestamp` DESC LIMIT "+str(Settings.MODNEWS_MAX_POSTS)+",30") + #for post in posts: + # UpdateDb("DELETE FROM `news` WHERE id = " + post['id'] + " AND `type` = '0'") + + # Insert new post + message = '' + try: + # Cut long lines + message = self.formdata["message"] + message = clickableURLs(cgi.escape(message).rstrip()[0:8000]) + message = onlyAllowedHTML(message) + if Settings.USE_MARKDOWN: + message = markdown(message) + if not Settings.USE_MARKDOWN: + message = message.replace("\n", "<br />") + except: + pass + + # If it's preferred to remain anonymous... + if 'anonymous' in self.formdata.keys() and self.formdata['anonymous'] == '1': + to_name = "Staff ★" + else: + to_name = "%s ★" % staff_account['username'] + timestamp_formatted = formatDate(t) + + UpdateDb("INSERT INTO `news` (type, staffid, staff_name, title, message, name, timestamp, timestamp_formatted) VALUES ('0', '%s', '%s', '%s', '%s', '%s', '%d', '%s')" % (staff_account['id'], staff_account['username'], _mysql.escape_string(self.formdata['title']), _mysql.escape_string(message), to_name, timestamp(t), timestamp_formatted)) + + message = _("Added successfully.") + template_filename = "message.html" + if path_split[3] == 'delete': + if not administrator: + # We check that if he's not admin, he shouldn't be able to delete other people's posts + post = FetchOne("SELECT `staffid` FROM `news` WHERE id = '"+_mysql.escape_string(path_split[4])+"' AND type = '0'") + if post['staffid'] != staff_account['id']: + self.error(_('That post is not yours.')) + return + + # Delete! + UpdateDb("DELETE FROM `news` WHERE id = '" + _mysql.escape_string(path_split[4]) + "' AND type = '0'") + message = _("Deleted successfully.") + template_filename = "message.html" + else: + # If he's not admin, show only his own posts + if administrator: + posts = FetchAll("SELECT * FROM `news` WHERE type = '0' ORDER BY `timestamp` DESC") + else: + posts = FetchAll("SELECT * FROM `news` WHERE staffid = '"+staff_account['id']+"' AND type = '0' ORDER BY `timestamp` DESC") + + template_filename = "news.html" + template_values = {'action': 'newschannel', 'posts': posts} + elif path_split[2] == 'reports': + if not moderator: + return + + message = None + import math + pagesize = float(Settings.REPORTS_PER_PAGE) + totals = FetchOne("SELECT COUNT(id) FROM `reports`") + total = int(totals['COUNT(id)']) + pages = int(math.ceil(total / pagesize)) + + try: + currentpage = int(path_split[3]) + except: + currentpage = 0 + + if len(path_split) > 4: + if path_split[4] == 'ignore': + # Delete report + UpdateDb("DELETE FROM `reports` WHERE `id` = '"+_mysql.escape_string(path_split[5])+"'") + message = _('Report %s ignored.') % path_split[5] + if 'ignore' in self.formdata.keys(): + ignored = 0 + if 'board' in self.formdata.keys() and self.formdata['board'] != 'all': + reports = FetchAll("SELECT `id` FROM `reports` WHERE `board` = '%s' ORDER BY `timestamp` DESC LIMIT %d, %d" % (_mysql.escape_string(self.formdata['board']), currentpage*pagesize, pagesize)) + else: + reports = FetchAll("SELECT `id` FROM `reports` ORDER BY `timestamp` DESC LIMIT %d, %d" % (currentpage*pagesize, pagesize)) + + for report in reports: + keyname = 'i' + report['id'] + if keyname in self.formdata.keys(): + # Ignore here + UpdateDb("DELETE FROM `reports` WHERE `id` = '"+_mysql.escape_string(report['id'])+"'") + ignored += 1 + + message = _('Ignored %s report(s).') % str(ignored) + + # Generate board list + boards = FetchAll('SELECT `name`, `dir` FROM `boards` ORDER BY `dir`') + for board in boards: + if 'board' in self.formdata.keys() and self.formdata['board'] == board['dir']: + board['checked'] = True + else: + board['checked'] = False + + # Tabla + if 'board' in self.formdata.keys() and self.formdata['board'] != 'all': + reports = FetchAll("SELECT id, timestamp, timestamp_formatted, postid, parentid, link, board, INET_NTOA(ip) AS ip, reason, reporterip FROM `reports` WHERE `board` = '%s' ORDER BY `timestamp` DESC LIMIT %d, %d" % (_mysql.escape_string(self.formdata['board']), currentpage*pagesize, pagesize)) + else: + reports = FetchAll("SELECT id, timestamp, timestamp_formatted, postid, parentid, link, board, INET_NTOA(ip) AS ip, reason, reporterip FROM `reports` ORDER BY `timestamp` DESC LIMIT %d, %d" % (currentpage*pagesize, pagesize)) + + if 'board' in self.formdata.keys(): + curboard = self.formdata['board'] + else: + curboard = None + + #for report in reports: + # if report['parentid'] == '0': + # report['link'] = Settings.BOARDS_URL + report['board'] + '/res/' + report['postid'] + '.html#' + report['postid'] + # else: + # report['link'] = Settings.BOARDS_URL + report['board'] + '/res/' + report['parentid'] + '.html#' + report['postid'] + + navigator = '' + if currentpage > 0: + navigator += '<a href="'+Settings.CGI_URL+'manage/reports/'+str(currentpage-1)+'"><</a> ' + else: + navigator += '< ' + + for i in range(pages): + if i != currentpage: + navigator += '<a href="'+Settings.CGI_URL+'manage/reports/'+str(i)+'">'+str(i)+'</a> ' + else: + navigator += str(i)+' ' + + if currentpage < (pages-1): + navigator += '<a href="'+Settings.CGI_URL+'manage/reports/'+str(currentpage+1)+'">></a> ' + else: + navigator += '> ' + + template_filename = "reports.html" + template_values = {'message': message, + 'boards': boards, + 'reports': reports, + 'currentpage': currentpage, + 'curboard': curboard, + 'navigator': navigator} + # Show by IP + elif path_split[2] == 'ipshow': + if not moderator: + return + + if 'ip' in self.formdata.keys(): + # If an IP was given... + if self.formdata['ip'] != '': + formatted_ip = str(inet_aton(self.formdata['ip'])) + posts = FetchAll("SELECT posts.*, boards.dir, boards.board_type, boards.subject AS default_subject FROM `posts` JOIN `boards` ON boards.id = posts.boardid WHERE ip = '%s' ORDER BY posts.timestamp DESC" % _mysql.escape_string(formatted_ip)) + ip = self.formdata['ip'] + template_filename = "ipshow.html" + template_values = {"mode": 1, "ip": ip, "host": getHost(ip), "country": getCountry(ip), "tor": addressIsTor(ip), "posts": posts} + logAction(staff_account['username'], "ipshow on {}".format(ip)) + else: + # Generate form + template_filename = "ipshow.html" + template_values = {"mode": 0} + elif path_split[2] == 'ipdelete': + if not moderator: + return + + # Delete by IP + if 'ip' in self.formdata.keys(): + # If an IP was given... + if self.formdata['ip'] != '': + where = [] + if 'board_all' not in self.formdata.keys(): + # If he chose boards separately, add them to a list + boards = FetchAll('SELECT `id`, `dir` FROM `boards`') + for board in boards: + keyname = 'board_' + board['dir'] + if keyname in self.formdata.keys(): + if self.formdata[keyname] == "1": + where.append(board) + else: + # If all boards were selected="selected", all them all to the list + where = FetchAll('SELECT `id`, `dir` FROM `boards`') + + # If no board was chosen + if len(where) <= 0: + self.error(_("Select a board first.")) + return + + deletedPostsTotal = 0 + ip = inet_aton(self.formdata['ip']) + deletedPosts = 0 + for theboard in where: + board = setBoard(theboard['dir']) + isDeletedOP = False + + # delete all starting posts first + op_posts = FetchAll("SELECT `id`, `message` FROM posts WHERE parentid = 0 AND boardid = '" + board['id'] + "' AND ip = " + str(ip)) + for post in op_posts: + deletePost(post['id'], None) + + deletedPosts += 1 + deletedPostsTotal += 1 + + replies = FetchAll("SELECT `id`, `message`, `parentid` FROM posts WHERE parentid != 0 AND boardid = '" + board['id'] + "' AND ip = " + str(ip)) + for post in replies: + deletePost(post['id'], None, '2') + + deletedPosts += 1 + deletedPostsTotal += 1 + + regenerateHome() + + if deletedPosts > 0: + message = '%(posts)s post(s) were deleted from %(board)s.' % {'posts': str(deletedPosts), 'board': '/' + board['dir'] + '/'} + template_filename = "message.html" + #logAction(staff_account['username'], '%(posts)s post(s) were deleted from %(board)s. IP: %(ip)s' % \ + # {'posts': str(deletedPosts), + # 'board': '/' + board['dir'] + '/', + # 'ip': self.formdata['ip']}) + else: + self.error(_("Please enter an IP first.")) + return + + message = 'In total %(posts)s from IP %(ip)s were deleted.' % {'posts': str(deletedPosts), 'ip': self.formdata['ip']} + template_filename = "message.html" + else: + # Generate form... + template_filename = "ipdelete.html" + template_values = {'boards': boardlist()} + elif path_split[2] == 'search': + if not administrator: + return + search_logs = FetchAll('SELECT `id`,`timestamp`,`keyword`,`ita`,INET_NTOA(`ip`) AS `ip`,`res` FROM `search_log` ORDER BY `timestamp` DESC LIMIT 250') + for log in search_logs: + #log['ip'] = str(inet_ntoa(log['ip'])) + log['timestamp_formatted'] = formatTimestamp(log['timestamp']) + if log['keyword'].startswith('k '): + log['keyword'] = log['keyword'][2:] + log['archive'] = True + else: + log['archive'] = False + template_filename = "search.html" + template_values = {'search': search_logs} + else: + # Main page. + reports = FetchOne("SELECT COUNT(1) FROM `reports`", 0)[0] + posts = FetchAll("SELECT * FROM `news` WHERE type = '0' ORDER BY `timestamp` DESC") + + template_filename = "manage.html" + template_values = {'reports': reports, 'posts': posts} + + if not skiptemplate: + try: + if template_filename == 'message.html': + template_values = {'message': message} + except: + template_filename = 'message.html' + template_values = {'message': '???'} + + template_values.update({ + 'title': 'Manage', + 'validated': validated, + 'page': page, + }) + + if validated: + template_values.update({ + 'username': staff_account['username'], + 'site_title': Settings.SITE_TITLE, + 'rights': staff_account['rights'], + 'administrator': administrator, + 'added': formatTimestamp(staff_account['added']), + }) + + self.output += renderTemplate("manage/" + template_filename, template_values) + +def logAction(staff, action): + InsertDb("INSERT INTO `logs` (`timestamp`, `staff`, `action`) VALUES (" + str(timestamp()) + ", '" + _mysql.escape_string(staff) + "\', \'" + _mysql.escape_string(action) + "\')") + +def genPasswd(string): + return getMD5(string + Settings.SECRET) + +def boardlist(): + boards = FetchAll('SELECT * FROM `boards` ORDER BY `board_type`, `dir`') + return boards + +def filetypelist(): + filetypes = FetchAll('SELECT * FROM `filetypes` ORDER BY `ext` ASC') + return filetypes diff --git a/cgi/markdown.py b/cgi/markdown.py new file mode 100644 index 0000000..3ebfaab --- /dev/null +++ b/cgi/markdown.py @@ -0,0 +1,2044 @@ +#!/usr/bin/env python +# Copyright (c) 2007-2008 ActiveState Corp. +# License: MIT (http://www.opensource.org/licenses/mit-license.php) + +r"""A fast and complete Python implementation of Markdown. + +[from http://daringfireball.net/projects/markdown/] +> Markdown is a text-to-HTML filter; it translates an easy-to-read / +> easy-to-write structured text format into HTML. Markdown's text +> format is most similar to that of plain text email, and supports +> features such as headers, *emphasis*, code blocks, blockquotes, and +> links. +> +> Markdown's syntax is designed not as a generic markup language, but +> specifically to serve as a front-end to (X)HTML. You can use span-level +> HTML tags anywhere in a Markdown document, and you can use block level +> HTML tags (like <div> and <table> as well). + +Module usage: + + >>> import markdown2 + >>> markdown2.markdown("*boo!*") # or use `html = markdown_path(PATH)` + u'<p><em>boo!</em></p>\n' + + >>> markdowner = Markdown() + >>> markdowner.convert("*boo!*") + u'<p><em>boo!</em></p>\n' + >>> markdowner.convert("**boom!**") + u'<p><strong>boom!</strong></p>\n' + +This implementation of Markdown implements the full "core" syntax plus a +number of extras (e.g., code syntax coloring, footnotes) as described on +<http://code.google.com/p/python-markdown2/wiki/Extras>. +""" + +cmdln_desc = """A fast and complete Python implementation of Markdown, a +text-to-HTML conversion tool for web writers. + +Supported extras (see -x|--extras option below): +* code-friendly: Disable _ and __ for em and strong. +* code-color: Pygments-based syntax coloring of <code> sections. +* cuddled-lists: Allow lists to be cuddled to the preceding paragraph. +* footnotes: Support footnotes as in use on daringfireball.net and + implemented in other Markdown processors (tho not in Markdown.pl v1.0.1). +* html-classes: Takes a dict mapping html tag names (lowercase) to a + string to use for a "class" tag attribute. Currently only supports + "pre" and "code" tags. Add an issue if you require this for other tags. +* pyshell: Treats unindented Python interactive shell sessions as <code> + blocks. +* link-patterns: Auto-link given regex patterns in text (e.g. bug number + references, revision number references). +* xml: Passes one-liner processing instructions and namespaced XML tags. +""" + +# Dev Notes: +# - There is already a Python markdown processor +# (http://www.freewisdom.org/projects/python-markdown/). +# - Python's regex syntax doesn't have '\z', so I'm using '\Z'. I'm +# not yet sure if there implications with this. Compare 'pydoc sre' +# and 'perldoc perlre'. + +__version_info__ = (1, 0, 1, 17) # first three nums match Markdown.pl +__version__ = '1.0.1.17' +__author__ = "Trent Mick" + +import os +import sys +from pprint import pprint +import re +import logging +try: + from hashlib import md5 +except ImportError: + from md5 import md5 +import optparse +from random import random, randint +import codecs +from urllib import quote + + + +#---- Python version compat + +if sys.version_info[:2] < (2,4): + from sets import Set as set + def reversed(sequence): + for i in sequence[::-1]: + yield i + def _unicode_decode(s, encoding, errors='xmlcharrefreplace'): + return unicode(s, encoding, errors) +else: + def _unicode_decode(s, encoding, errors='strict'): + return s.decode(encoding, errors) + + +#---- globals + +DEBUG = False +log = logging.getLogger("markdown") + +DEFAULT_TAB_WIDTH = 4 + + +try: + import uuid +except ImportError: + SECRET_SALT = str(randint(0, 1000000)) +else: + SECRET_SALT = str(uuid.uuid4()) +def _hash_ascii(s): + #return md5(s).hexdigest() # Markdown.pl effectively does this. + return 'md5-' + md5(SECRET_SALT + s).hexdigest() +def _hash_text(s): + return 'md5-' + md5(SECRET_SALT + s.encode("utf-8")).hexdigest() + +# Table of hash values for escaped characters: +g_escape_table = dict([(ch, _hash_ascii(ch)) + for ch in '\\`*_{}[]()>#+-.!']) + + + +#---- exceptions + +class MarkdownError(Exception): + pass + + + +#---- public api + +def markdown_path(path, encoding="utf-8", + html4tags=False, tab_width=DEFAULT_TAB_WIDTH, + safe_mode=None, extras=None, link_patterns=None, + use_file_vars=False): + fp = codecs.open(path, 'r', encoding) + text = fp.read() + fp.close() + return Markdown(html4tags=html4tags, tab_width=tab_width, + safe_mode=safe_mode, extras=extras, + link_patterns=link_patterns, + use_file_vars=use_file_vars).convert(text) + +def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH, + safe_mode=None, extras=None, link_patterns=None, + use_file_vars=False): + return Markdown(html4tags=html4tags, tab_width=tab_width, + safe_mode=safe_mode, extras=extras, + link_patterns=link_patterns, + use_file_vars=use_file_vars).convert(text) + +class Markdown(object): + # The dict of "extras" to enable in processing -- a mapping of + # extra name to argument for the extra. Most extras do not have an + # argument, in which case the value is None. + # + # This can be set via (a) subclassing and (b) the constructor + # "extras" argument. + extras = None + + urls = None + titles = None + html_blocks = None + html_spans = None + html_removed_text = "[HTML_REMOVED]" # for compat with markdown.py + + # Used to track when we're inside an ordered or unordered list + # (see _ProcessListItems() for details): + list_level = 0 + + _ws_only_line_re = re.compile(r"^[ \t]+$", re.M) + + def __init__(self, html4tags=False, tab_width=4, safe_mode=None, + extras=None, link_patterns=None, use_file_vars=False): + if html4tags: + self.empty_element_suffix = ">" + else: + self.empty_element_suffix = " />" + self.tab_width = tab_width + + # For compatibility with earlier markdown2.py and with + # markdown.py's safe_mode being a boolean, + # safe_mode == True -> "replace" + if safe_mode is True: + self.safe_mode = "replace" + else: + self.safe_mode = safe_mode + + if self.extras is None: + self.extras = {} + elif not isinstance(self.extras, dict): + self.extras = dict([(e, None) for e in self.extras]) + if extras: + if not isinstance(extras, dict): + extras = dict([(e, None) for e in extras]) + self.extras.update(extras) + assert isinstance(self.extras, dict) + if "toc" in self.extras and not "header-ids" in self.extras: + self.extras["header-ids"] = None # "toc" implies "header-ids" + self._instance_extras = self.extras.copy() + self.link_patterns = link_patterns + self.use_file_vars = use_file_vars + self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M) + + def reset(self): + self.urls = {} + self.titles = {} + self.html_blocks = {} + self.html_spans = {} + self.list_level = 0 + self.extras = self._instance_extras.copy() + if "footnotes" in self.extras: + self.footnotes = {} + self.footnote_ids = [] + if "header-ids" in self.extras: + self._count_from_header_id = {} # no `defaultdict` in Python 2.4 + + def convert(self, text): + """Convert the given text.""" + # Main function. The order in which other subs are called here is + # essential. Link and image substitutions need to happen before + # _EscapeSpecialChars(), so that any *'s or _'s in the <a> + # and <img> tags get encoded. + + # Clear the global hashes. If we don't clear these, you get conflicts + # from other articles when generating a page which contains more than + # one article (e.g. an index page that shows the N most recent + # articles): + self.reset() + + if not isinstance(text, unicode): + #TODO: perhaps shouldn't presume UTF-8 for string input? + text = unicode(text, 'utf-8') + + if self.use_file_vars: + # Look for emacs-style file variable hints. + emacs_vars = self._get_emacs_vars(text) + if "markdown-extras" in emacs_vars: + splitter = re.compile("[ ,]+") + for e in splitter.split(emacs_vars["markdown-extras"]): + if '=' in e: + ename, earg = e.split('=', 1) + try: + earg = int(earg) + except ValueError: + pass + else: + ename, earg = e, None + self.extras[ename] = earg + + # Standardize line endings: + text = re.sub("\r\n|\r", "\n", text) + + # Make sure $text ends with a couple of newlines: + text += "\n\n" + + # Convert all tabs to spaces. + text = self._detab(text) + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ \t]*\n+/ . + text = self._ws_only_line_re.sub("", text) + + if self.safe_mode: + text = self._hash_html_spans(text) + + # Turn block-level HTML blocks into hash entries + text = self._hash_html_blocks(text, raw=True) + + # Strip link definitions, store in hashes. + if "footnotes" in self.extras: + # Must do footnotes first because an unlucky footnote defn + # looks like a link defn: + # [^4]: this "looks like a link defn" + text = self._strip_footnote_definitions(text) + text = self._strip_link_definitions(text) + + text = self._run_block_gamut(text) + + if "footnotes" in self.extras: + text = self._add_footnotes(text) + + text = self._unescape_special_chars(text) + + if self.safe_mode: + text = self._unhash_html_spans(text) + + #text += "\n" + + rv = UnicodeWithAttrs(text) + if "toc" in self.extras: + rv._toc = self._toc + return rv + + _emacs_oneliner_vars_pat = re.compile(r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE) + # This regular expression is intended to match blocks like this: + # PREFIX Local Variables: SUFFIX + # PREFIX mode: Tcl SUFFIX + # PREFIX End: SUFFIX + # Some notes: + # - "[ \t]" is used instead of "\s" to specifically exclude newlines + # - "(\r\n|\n|\r)" is used instead of "$" because the sre engine does + # not like anything other than Unix-style line terminators. + _emacs_local_vars_pat = re.compile(r"""^ + (?P<prefix>(?:[^\r\n|\n|\r])*?) + [\ \t]*Local\ Variables:[\ \t]* + (?P<suffix>.*?)(?:\r\n|\n|\r) + (?P<content>.*?\1End:) + """, re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE) + + def _get_emacs_vars(self, text): + """Return a dictionary of emacs-style local variables. + + Parsing is done loosely according to this spec (and according to + some in-practice deviations from this): + http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables + """ + emacs_vars = {} + SIZE = pow(2, 13) # 8kB + + # Search near the start for a '-*-'-style one-liner of variables. + head = text[:SIZE] + if "-*-" in head: + match = self._emacs_oneliner_vars_pat.search(head) + if match: + emacs_vars_str = match.group(1) + assert '\n' not in emacs_vars_str + emacs_var_strs = [s.strip() for s in emacs_vars_str.split(';') + if s.strip()] + if len(emacs_var_strs) == 1 and ':' not in emacs_var_strs[0]: + # While not in the spec, this form is allowed by emacs: + # -*- Tcl -*- + # where the implied "variable" is "mode". This form + # is only allowed if there are no other variables. + emacs_vars["mode"] = emacs_var_strs[0].strip() + else: + for emacs_var_str in emacs_var_strs: + try: + variable, value = emacs_var_str.strip().split(':', 1) + except ValueError: + log.debug("emacs variables error: malformed -*- " + "line: %r", emacs_var_str) + continue + # Lowercase the variable name because Emacs allows "Mode" + # or "mode" or "MoDe", etc. + emacs_vars[variable.lower()] = value.strip() + + tail = text[-SIZE:] + if "Local Variables" in tail: + match = self._emacs_local_vars_pat.search(tail) + if match: + prefix = match.group("prefix") + suffix = match.group("suffix") + lines = match.group("content").splitlines(0) + #print "prefix=%r, suffix=%r, content=%r, lines: %s"\ + # % (prefix, suffix, match.group("content"), lines) + + # Validate the Local Variables block: proper prefix and suffix + # usage. + for i, line in enumerate(lines): + if not line.startswith(prefix): + log.debug("emacs variables error: line '%s' " + "does not use proper prefix '%s'" + % (line, prefix)) + return {} + # Don't validate suffix on last line. Emacs doesn't care, + # neither should we. + if i != len(lines)-1 and not line.endswith(suffix): + log.debug("emacs variables error: line '%s' " + "does not use proper suffix '%s'" + % (line, suffix)) + return {} + + # Parse out one emacs var per line. + continued_for = None + for line in lines[:-1]: # no var on the last line ("PREFIX End:") + if prefix: line = line[len(prefix):] # strip prefix + if suffix: line = line[:-len(suffix)] # strip suffix + line = line.strip() + if continued_for: + variable = continued_for + if line.endswith('\\'): + line = line[:-1].rstrip() + else: + continued_for = None + emacs_vars[variable] += ' ' + line + else: + try: + variable, value = line.split(':', 1) + except ValueError: + log.debug("local variables error: missing colon " + "in local variables entry: '%s'" % line) + continue + # Do NOT lowercase the variable name, because Emacs only + # allows "mode" (and not "Mode", "MoDe", etc.) in this block. + value = value.strip() + if value.endswith('\\'): + value = value[:-1].rstrip() + continued_for = variable + else: + continued_for = None + emacs_vars[variable] = value + + # Unquote values. + for var, val in emacs_vars.items(): + if len(val) > 1 and (val.startswith('"') and val.endswith('"') + or val.startswith('"') and val.endswith('"')): + emacs_vars[var] = val[1:-1] + + return emacs_vars + + # Cribbed from a post by Bart Lateur: + # <http://www.nntp.perl.org/group/perl.macperl.anyperl/154> + _detab_re = re.compile(r'(.*?)\t', re.M) + def _detab_sub(self, match): + g1 = match.group(1) + return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width)) + def _detab(self, text): + r"""Remove (leading?) tabs from a file. + + >>> m = Markdown() + >>> m._detab("\tfoo") + ' foo' + >>> m._detab(" \tfoo") + ' foo' + >>> m._detab("\t foo") + ' foo' + >>> m._detab(" foo") + ' foo' + >>> m._detab(" foo\n\tbar\tblam") + ' foo\n bar blam' + """ + if '\t' not in text: + return text + return self._detab_re.subn(self._detab_sub, text)[0] + + _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del' + _strict_tag_block_re = re.compile(r""" + ( # save in \1 + ^ # start of line (with re.M) + <(%s) # start tag = \2 + \b # word break + (.*\n)*? # any number of lines, minimally matching + </\2> # the matching end tag + [ \t]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + ) + """ % _block_tags_a, + re.X | re.M) + + _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math' + _liberal_tag_block_re = re.compile(r""" + ( # save in \1 + ^ # start of line (with re.M) + <(%s) # start tag = \2 + \b # word break + (.*\n)*? # any number of lines, minimally matching + .*</\2> # the matching end tag + [ \t]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + ) + """ % _block_tags_b, + re.X | re.M) + + def _hash_html_block_sub(self, match, raw=False): + html = match.group(1) + if raw and self.safe_mode: + html = self._sanitize_html(html) + key = _hash_text(html) + self.html_blocks[key] = html + return "\n\n" + key + "\n\n" + + def _hash_html_blocks(self, text, raw=False): + """Hashify HTML blocks + + We only want to do this for block-level HTML tags, such as headers, + lists, and tables. That's because we still want to wrap <p>s around + "paragraphs" that are wrapped in non-block-level tags, such as anchors, + phrase emphasis, and spans. The list of tags we're looking for is + hard-coded. + + @param raw {boolean} indicates if these are raw HTML blocks in + the original source. It makes a difference in "safe" mode. + """ + if '<' not in text: + return text + + # Pass `raw` value into our calls to self._hash_html_block_sub. + hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw) + + # First, look for nested blocks, e.g.: + # <div> + # <div> + # tags for inner block must be indented. + # </div> + # </div> + # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `<div>` and stop at the first `</div>`. + text = self._strict_tag_block_re.sub(hash_html_block_sub, text) + + # Now match more liberally, simply from `\n<tag>` to `</tag>\n` + text = self._liberal_tag_block_re.sub(hash_html_block_sub, text) + + # Special case just for <hr />. It was easier to make a special + # case than to make the other regex more complicated. + if "<hr" in text: + _hr_tag_re = _hr_tag_re_from_tab_width(self.tab_width) + text = _hr_tag_re.sub(hash_html_block_sub, text) + + # Special case for standalone HTML comments: + if "<!--" in text: + start = 0 + while True: + # Delimiters for next comment block. + try: + start_idx = text.index("<!--", start) + except ValueError, ex: + break + try: + end_idx = text.index("-->", start_idx) + 3 + except ValueError, ex: + break + + # Start position for next comment block search. + start = end_idx + + # Validate whitespace before comment. + if start_idx: + # - Up to `tab_width - 1` spaces before start_idx. + for i in range(self.tab_width - 1): + if text[start_idx - 1] != ' ': + break + start_idx -= 1 + if start_idx == 0: + break + # - Must be preceded by 2 newlines or hit the start of + # the document. + if start_idx == 0: + pass + elif start_idx == 1 and text[0] == '\n': + start_idx = 0 # to match minute detail of Markdown.pl regex + elif text[start_idx-2:start_idx] == '\n\n': + pass + else: + break + + # Validate whitespace after comment. + # - Any number of spaces and tabs. + while end_idx < len(text): + if text[end_idx] not in ' \t': + break + end_idx += 1 + # - Must be following by 2 newlines or hit end of text. + if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'): + continue + + # Escape and hash (must match `_hash_html_block_sub`). + html = text[start_idx:end_idx] + if raw and self.safe_mode: + html = self._sanitize_html(html) + key = _hash_text(html) + self.html_blocks[key] = html + text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:] + + if "xml" in self.extras: + # Treat XML processing instructions and namespaced one-liner + # tags as if they were block HTML tags. E.g., if standalone + # (i.e. are their own paragraph), the following do not get + # wrapped in a <p> tag: + # <?foo bar?> + # + # <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="chapter_1.md"/> + _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width) + text = _xml_oneliner_re.sub(hash_html_block_sub, text) + + return text + + def _strip_link_definitions(self, text): + # Strips link definitions from text, stores the URLs and titles in + # hash references. + less_than_tab = self.tab_width - 1 + + # Link defs are in the form: + # [id]: url "optional title" + _link_def_re = re.compile(r""" + ^[ ]{0,%d}\[(.+)\]: # id = \1 + [ \t]* + \n? # maybe *one* newline + [ \t]* + <?(.+?)>? # url = \2 + [ \t]* + (?: + \n? # maybe one newline + [ \t]* + (?<=\s) # lookbehind for whitespace + ['"(] + ([^\n]*) # title = \3 + ['")] + [ \t]* + )? # title is optional + (?:\n+|\Z) + """ % less_than_tab, re.X | re.M | re.U) + return _link_def_re.sub(self._extract_link_def_sub, text) + + def _extract_link_def_sub(self, match): + id, url, title = match.groups() + key = id.lower() # Link IDs are case-insensitive + self.urls[key] = self._encode_amps_and_angles(url) + if title: + self.titles[key] = title.replace('"', '"') + return "" + + def _extract_footnote_def_sub(self, match): + id, text = match.groups() + text = _dedent(text, skip_first_line=not text.startswith('\n')).strip() + normed_id = re.sub(r'\W', '-', id) + # Ensure footnote text ends with a couple newlines (for some + # block gamut matches). + self.footnotes[normed_id] = text + "\n\n" + return "" + + def _strip_footnote_definitions(self, text): + """A footnote definition looks like this: + + [^note-id]: Text of the note. + + May include one or more indented paragraphs. + + Where, + - The 'note-id' can be pretty much anything, though typically it + is the number of the footnote. + - The first paragraph may start on the next line, like so: + + [^note-id]: + Text of the note. + """ + less_than_tab = self.tab_width - 1 + footnote_def_re = re.compile(r''' + ^[ ]{0,%d}\[\^(.+)\]: # id = \1 + [ \t]* + ( # footnote text = \2 + # First line need not start with the spaces. + (?:\s*.*\n+) + (?: + (?:[ ]{%d} | \t) # Subsequent lines must be indented. + .*\n+ + )* + ) + # Lookahead for non-space at line-start, or end of doc. + (?:(?=^[ ]{0,%d}\S)|\Z) + ''' % (less_than_tab, self.tab_width, self.tab_width), + re.X | re.M) + return footnote_def_re.sub(self._extract_footnote_def_sub, text) + + + _hr_res = [ + re.compile(r"^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$", re.M), + re.compile(r"^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$", re.M), + re.compile(r"^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$", re.M), + ] + + def _run_block_gamut(self, text): + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + + #text = self._do_headers(text) + + # Do Horizontal Rules: + #hr = "\n<hr"+self.empty_element_suffix+"\n" + #for hr_re in self._hr_res: + # text = hr_re.sub(hr, text) + + text = self._do_lists(text) + + if "pyshell" in self.extras: + text = self._prepare_pyshell_blocks(text) + + text = self._do_code_blocks(text) + + text = self._do_block_quotes(text) + + # We already ran _HashHTMLBlocks() before, in Markdown(), but that + # was to escape raw HTML in the original Markdown source. This time, + # we're escaping the markup we've just created, so that we don't wrap + # <p> tags around block-level tags. + text = self._hash_html_blocks(text) + + text = self._form_paragraphs(text) + + return text + + def _pyshell_block_sub(self, match): + lines = match.group(0).splitlines(0) + _dedentlines(lines) + indent = ' ' * self.tab_width + s = ('\n' # separate from possible cuddled paragraph + + indent + ('\n'+indent).join(lines) + + '\n\n') + return s + + def _prepare_pyshell_blocks(self, text): + """Ensure that Python interactive shell sessions are put in + code blocks -- even if not properly indented. + """ + if ">>>" not in text: + return text + + less_than_tab = self.tab_width - 1 + _pyshell_block_re = re.compile(r""" + ^([ ]{0,%d})>>>[ ].*\n # first line + ^(\1.*\S+.*\n)* # any number of subsequent lines + ^\n # ends with a blank line + """ % less_than_tab, re.M | re.X) + + return _pyshell_block_re.sub(self._pyshell_block_sub, text) + + def _run_span_gamut(self, text): + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + + #text = self._do_code_spans(text) - El AA ! + + text = self._escape_special_chars(text) + + # Process anchor and image tags. + text = self._do_links(text) + + # Make links out of things like `<http://example.com/>` + # Must come after _do_links(), because you can use < and > + # delimiters in inline links like [this](<url>). + #text = self._do_auto_links(text) + + if "link-patterns" in self.extras: + text = self._do_link_patterns(text) + + text = self._encode_amps_and_angles(text) + + text = self._do_italics_and_bold(text) + + # Do hard breaks: + text = re.sub(r"\n", "<br%s" % self.empty_element_suffix, text) + + return text + + # "Sorta" because auto-links are identified as "tag" tokens. + _sorta_html_tokenize_re = re.compile(r""" + ( + # tag + </? + (?:\w+) # tag name + (?:\s+(?:[\w-]+:)?[\w-]+=(?:".*?"|'.*?'))* # attributes + \s*/?> + | + # auto-link (e.g., <http://www.activestate.com/>) + <\w+[^>]*> + | + <!--.*?--> # comment + | + <\?.*?\?> # processing instruction + ) + """, re.X) + + def _escape_special_chars(self, text): + # Python markdown note: the HTML tokenization here differs from + # that in Markdown.pl, hence the behaviour for subtle cases can + # differ (I believe the tokenizer here does a better job because + # it isn't susceptible to unmatched '<' and '>' in HTML tags). + # Note, however, that '>' is not allowed in an auto-link URL + # here. + escaped = [] + is_html_markup = False + for token in self._sorta_html_tokenize_re.split(text): + if is_html_markup: + # Within tags/HTML-comments/auto-links, encode * and _ + # so they don't conflict with their use in Markdown for + # italics and strong. We're replacing each such + # character with its corresponding MD5 checksum value; + # this is likely overkill, but it should prevent us from + # colliding with the escape values by accident. + escaped.append(token.replace('*', g_escape_table['*']) + .replace('_', g_escape_table['_'])) + else: + escaped.append(self._encode_backslash_escapes(token)) + is_html_markup = not is_html_markup + return ''.join(escaped) + + def _hash_html_spans(self, text): + # Used for safe_mode. + + def _is_auto_link(s): + if ':' in s and self._auto_link_re.match(s): + return True + elif '@' in s and self._auto_email_link_re.match(s): + return True + return False + + tokens = [] + is_html_markup = False + for token in self._sorta_html_tokenize_re.split(text): + if is_html_markup and not _is_auto_link(token): + sanitized = self._sanitize_html(token) + key = _hash_text(sanitized) + self.html_spans[key] = sanitized + tokens.append(key) + else: + tokens.append(token) + is_html_markup = not is_html_markup + return ''.join(tokens) + + def _unhash_html_spans(self, text): + for key, sanitized in self.html_spans.items(): + text = text.replace(key, sanitized) + return text + + def _sanitize_html(self, s): + if self.safe_mode == "replace": + return self.html_removed_text + elif self.safe_mode == "escape": + replacements = [ + ('&', '&'), + ('<', '<'), + ('>', '>'), + ] + for before, after in replacements: + s = s.replace(before, after) + return s + else: + raise MarkdownError("invalid value for 'safe_mode': %r (must be " + "'escape' or 'replace')" % self.safe_mode) + + _tail_of_inline_link_re = re.compile(r''' + # Match tail of: [text](/url/) or [text](/url/ "title") + \( # literal paren + [ \t]* + (?P<url> # \1 + <.*?> + | + .*? + ) + [ \t]* + ( # \2 + (['"]) # quote char = \3 + (?P<title>.*?) + \3 # matching quote + )? # title is optional + \) + ''', re.X | re.S) + _tail_of_reference_link_re = re.compile(r''' + # Match tail of: [text][id] + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + \[ + (?P<id>.*?) + \] + ''', re.X | re.S) + + def _do_links(self, text): + """Turn Markdown link shortcuts into XHTML <a> and <img> tags. + + This is a combination of Markdown.pl's _DoAnchors() and + _DoImages(). They are done together because that simplified the + approach. It was necessary to use a different approach than + Markdown.pl because of the lack of atomic matching support in + Python's regex engine used in $g_nested_brackets. + """ + MAX_LINK_TEXT_SENTINEL = 3000 # markdown2 issue 24 + + # `anchor_allowed_pos` is used to support img links inside + # anchors, but not anchors inside anchors. An anchor's start + # pos must be `>= anchor_allowed_pos`. + anchor_allowed_pos = 0 + + curr_pos = 0 + while True: # Handle the next link. + # The next '[' is the start of: + # - an inline anchor: [text](url "title") + # - a reference anchor: [text][id] + # - an inline img: ![text](url "title") + # - a reference img: ![text][id] + # - a footnote ref: [^id] + # (Only if 'footnotes' extra enabled) + # - a footnote defn: [^id]: ... + # (Only if 'footnotes' extra enabled) These have already + # been stripped in _strip_footnote_definitions() so no + # need to watch for them. + # - a link definition: [id]: url "title" + # These have already been stripped in + # _strip_link_definitions() so no need to watch for them. + # - not markup: [...anything else... + try: + start_idx = text.index('[', curr_pos) + except ValueError: + break + text_length = len(text) + + # Find the matching closing ']'. + # Markdown.pl allows *matching* brackets in link text so we + # will here too. Markdown.pl *doesn't* currently allow + # matching brackets in img alt text -- we'll differ in that + # regard. + bracket_depth = 0 + for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL, + text_length)): + ch = text[p] + if ch == ']': + bracket_depth -= 1 + if bracket_depth < 0: + break + elif ch == '[': + bracket_depth += 1 + else: + # Closing bracket not found within sentinel length. + # This isn't markup. + curr_pos = start_idx + 1 + continue + link_text = text[start_idx+1:p] + + # Possibly a footnote ref? + if "footnotes" in self.extras and link_text.startswith("^"): + normed_id = re.sub(r'\W', '-', link_text[1:]) + if normed_id in self.footnotes: + self.footnote_ids.append(normed_id) + result = '<sup class="footnote-ref" id="fnref-%s">' \ + '<a href="#fn-%s">%s</a></sup>' \ + % (normed_id, normed_id, len(self.footnote_ids)) + text = text[:start_idx] + result + text[p+1:] + else: + # This id isn't defined, leave the markup alone. + curr_pos = p+1 + continue + + # Now determine what this is by the remainder. + p += 1 + if p == text_length: + return text + + # Inline anchor or img? + if text[p] == '(': # attempt at perf improvement + match = self._tail_of_inline_link_re.match(text, p) + if match: + # Handle an inline anchor or img. + #is_img = start_idx > 0 and text[start_idx-1] == "!" + #if is_img: + # start_idx -= 1 + is_img = False + + url, title = match.group("url"), match.group("title") + if url and url[0] == '<': + url = url[1:-1] # '<url>' -> 'url' + # We've got to encode these to avoid conflicting + # with italics/bold. + url = url.replace('*', g_escape_table['*']) \ + .replace('_', g_escape_table['_']) + if title: + title_str = ' title="%s"' \ + % title.replace('*', g_escape_table['*']) \ + .replace('_', g_escape_table['_']) \ + .replace('"', '"') + else: + title_str = '' + if is_img: + result = '<img src="%s" alt="%s"%s%s' \ + % (url.replace('"', '"'), + link_text.replace('"', '"'), + title_str, self.empty_element_suffix) + curr_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + elif start_idx >= anchor_allowed_pos: + result_head = '<a href="%s"%s>' % (url, title_str) + result = '%s%s</a>' % (result_head, link_text) + # <img> allowed from curr_pos on, <a> from + # anchor_allowed_pos on. + curr_pos = start_idx + len(result_head) + anchor_allowed_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + else: + # Anchor not allowed here. + curr_pos = start_idx + 1 + continue + + # Reference anchor or img? + else: + match = self._tail_of_reference_link_re.match(text, p) + if match: + # Handle a reference-style anchor or img. + #is_img = start_idx > 0 and text[start_idx-1] == "!" + #if is_img: + # start_idx -= 1 + is_img = False + + link_id = match.group("id").lower() + if not link_id: + link_id = link_text.lower() # for links like [this][] + if link_id in self.urls: + url = self.urls[link_id] + # We've got to encode these to avoid conflicting + # with italics/bold. + url = url.replace('*', g_escape_table['*']) \ + .replace('_', g_escape_table['_']) + title = self.titles.get(link_id) + if title: + title = title.replace('*', g_escape_table['*']) \ + .replace('_', g_escape_table['_']) + title_str = ' title="%s"' % title + else: + title_str = '' + if is_img: + result = '<img src="%s" alt="%s"%s%s' \ + % (url.replace('"', '"'), + link_text.replace('"', '"'), + title_str, self.empty_element_suffix) + curr_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + elif start_idx >= anchor_allowed_pos: + result = '<a href="%s"%s>%s</a>' \ + % (url, title_str, link_text) + result_head = '<a href="%s"%s>' % (url, title_str) + result = '%s%s</a>' % (result_head, link_text) + # <img> allowed from curr_pos on, <a> from + # anchor_allowed_pos on. + curr_pos = start_idx + len(result_head) + anchor_allowed_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + else: + # Anchor not allowed here. + curr_pos = start_idx + 1 + else: + # This id isn't defined, leave the markup alone. + curr_pos = match.end() + continue + + # Otherwise, it isn't markup. + curr_pos = start_idx + 1 + + return text + + def header_id_from_text(self, text, prefix): + """Generate a header id attribute value from the given header + HTML content. + + This is only called if the "header-ids" extra is enabled. + Subclasses may override this for different header ids. + """ + header_id = _slugify(text) + if prefix: + header_id = prefix + '-' + header_id + if header_id in self._count_from_header_id: + self._count_from_header_id[header_id] += 1 + header_id += '-%s' % self._count_from_header_id[header_id] + else: + self._count_from_header_id[header_id] = 1 + return header_id + + _toc = None + def _toc_add_entry(self, level, id, name): + if self._toc is None: + self._toc = [] + self._toc.append((level, id, name)) + + _setext_h_re = re.compile(r'^(.+)[ \t]*\n(=+|-+)[ \t]*\n+', re.M) + def _setext_h_sub(self, match): + n = {"=": 1, "-": 2}[match.group(2)[0]] + demote_headers = self.extras.get("demote-headers") + if demote_headers: + n = min(n + demote_headers, 6) + header_id_attr = "" + if "header-ids" in self.extras: + header_id = self.header_id_from_text(match.group(1), + prefix=self.extras["header-ids"]) + header_id_attr = ' id="%s"' % header_id + html = self._run_span_gamut(match.group(1)) + if "toc" in self.extras: + self._toc_add_entry(n, header_id, html) + return "<h%d%s>%s</h%d>\n\n" % (n, header_id_attr, html, n) + + _atx_h_re = re.compile(r''' + ^(\#{1,6}) # \1 = string of #'s + [ \t]* + (.+?) # \2 = Header text + [ \t]* + (?<!\\) # ensure not an escaped trailing '#' + \#* # optional closing #'s (not counted) + \n+ + ''', re.X | re.M) + def _atx_h_sub(self, match): + n = len(match.group(1)) + demote_headers = self.extras.get("demote-headers") + if demote_headers: + n = min(n + demote_headers, 6) + header_id_attr = "" + if "header-ids" in self.extras: + header_id = self.header_id_from_text(match.group(2), + prefix=self.extras["header-ids"]) + header_id_attr = ' id="%s"' % header_id + html = self._run_span_gamut(match.group(2)) + if "toc" in self.extras: + self._toc_add_entry(n, header_id, html) + return "<h%d%s>%s</h%d>\n\n" % (n, header_id_attr, html, n) + + def _do_headers(self, text): + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + text = self._setext_h_re.sub(self._setext_h_sub, text) + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + text = self._atx_h_re.sub(self._atx_h_sub, text) + + return text + + + _marker_ul_chars = '*+-' + _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars + _marker_ul = '(?:[%s])' % _marker_ul_chars + _marker_ol = r'(?:\d+\.)' + + def _list_sub(self, match): + lst = match.group(1) + lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol" + result = self._process_list_items(lst) + if self.list_level: + return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type) + else: + return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type) + + def _do_lists(self, text): + # Form HTML ordered (numbered) and unordered (bulleted) lists. + + for marker_pat in (self._marker_ul, self._marker_ol): + # Re-usable pattern to match any entire ul or ol list: + less_than_tab = self.tab_width - 1 + whole_list = r''' + ( # \1 = whole list + ( # \2 + [ ]{0,%d} + (%s) # \3 = first list item marker + [ \t]+ + ) + (?:.+?) + ( # \4 + \Z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ \t]* + %s[ \t]+ + ) + ) + ) + ''' % (less_than_tab, marker_pat, marker_pat) + + # We use a different prefix before nested lists than top-level lists. + # See extended comment in _process_list_items(). + # + # Note: There's a bit of duplication here. My original implementation + # created a scalar regex pattern as the conditional result of the test on + # $g_list_level, and then only ran the $text =~ s{...}{...}egmx + # substitution once, using the scalar as the pattern. This worked, + # everywhere except when running under MT on my hosting account at Pair + # Networks. There, this caused all rebuilds to be killed by the reaper (or + # perhaps they crashed, but that seems incredibly unlikely given that the + # same script on the same server ran fine *except* under MT. I've spent + # more time trying to figure out why this is happening than I'd like to + # admit. My only guess, backed up by the fact that this workaround works, + # is that Perl optimizes the substition when it can figure out that the + # pattern will never change, and when this optimization isn't on, we run + # afoul of the reaper. Thus, the slightly redundant code to that uses two + # static s/// patterns rather than one conditional pattern. + + if self.list_level: + sub_list_re = re.compile("^"+whole_list, re.X | re.M | re.S) + text = sub_list_re.sub(self._list_sub, text) + else: + list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list, + re.X | re.M | re.S) + text = list_re.sub(self._list_sub, text) + + return text + + _list_item_re = re.compile(r''' + (\n)? # leading line = \1 + (^[ \t]*) # leading whitespace = \2 + (?P<marker>%s) [ \t]+ # list marker = \3 + ((?:.+?) # list item text = \4 + (\n{1,2})) # eols = \5 + (?= \n* (\Z | \2 (?P<next_marker>%s) [ \t]+)) + ''' % (_marker_any, _marker_any), + re.M | re.X | re.S) + + _last_li_endswith_two_eols = False + def _list_item_sub(self, match): + item = match.group(4) + leading_line = match.group(1) + leading_space = match.group(2) + if leading_line or "\n\n" in item or self._last_li_endswith_two_eols: + item = self._run_block_gamut(self._outdent(item)) + else: + # Recursion for sub-lists: + item = self._do_lists(self._outdent(item)) + if item.endswith('\n'): + item = item[:-1] + item = self._run_span_gamut(item) + self._last_li_endswith_two_eols = (len(match.group(5)) == 2) + return "<li>%s</li>\n" % item + + def _process_list_items(self, list_str): + # Process the contents of a single ordered or unordered list, + # splitting it into individual list items. + + # The $g_list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + self.list_level += 1 + self._last_li_endswith_two_eols = False + list_str = list_str.rstrip('\n') + '\n' + list_str = self._list_item_re.sub(self._list_item_sub, list_str) + self.list_level -= 1 + return list_str + + def _get_pygments_lexer(self, lexer_name): + try: + from pygments import lexers, util + except ImportError: + return None + try: + return lexers.get_lexer_by_name(lexer_name) + except util.ClassNotFound: + return None + + def _color_with_pygments(self, codeblock, lexer, **formatter_opts): + import pygments + import pygments.formatters + + class HtmlCodeFormatter(pygments.formatters.HtmlFormatter): + def _wrap_code(self, inner): + """A function for use in a Pygments Formatter which + wraps in <code> tags. + """ + yield 0, "<code>" + for tup in inner: + yield tup + yield 0, "</code>" + + def wrap(self, source, outfile): + """Return the source with a code, pre, and div.""" + return self._wrap_div(self._wrap_pre(self._wrap_code(source))) + + formatter = HtmlCodeFormatter(cssclass="codehilite", **formatter_opts) + return pygments.highlight(codeblock, lexer, formatter) + + def _code_block_sub(self, match): + codeblock = match.group(1) + codeblock = self._outdent(codeblock) + codeblock = self._detab(codeblock) + codeblock = codeblock.lstrip('\n') # trim leading newlines + codeblock = codeblock.rstrip() # trim trailing whitespace + + if "code-color" in self.extras and codeblock.startswith(":::"): + lexer_name, rest = codeblock.split('\n', 1) + lexer_name = lexer_name[3:].strip() + lexer = self._get_pygments_lexer(lexer_name) + codeblock = rest.lstrip("\n") # Remove lexer declaration line. + if lexer: + formatter_opts = self.extras['code-color'] or {} + colored = self._color_with_pygments(codeblock, lexer, + **formatter_opts) + return "\n\n%s\n\n" % colored + + codeblock = self._encode_code(codeblock) + pre_class_str = self._html_class_str_from_tag("pre") + code_class_str = self._html_class_str_from_tag("code") + return "\n\n<pre%s><code%s>%s\n</code></pre>\n\n" % ( + pre_class_str, code_class_str, codeblock) + + def _html_class_str_from_tag(self, tag): + """Get the appropriate ' class="..."' string (note the leading + space), if any, for the given tag. + """ + if "html-classes" not in self.extras: + return "" + try: + html_classes_from_tag = self.extras["html-classes"] + except TypeError: + return "" + else: + if tag in html_classes_from_tag: + return ' class="%s"' % html_classes_from_tag[tag] + return "" + + def _do_code_blocks(self, text): + """Process Markdown `<pre><code>` blocks.""" + code_block_re = re.compile(r''' + (?:\n\n|\A) + ( # $1 = the code block -- one or more lines, starting with a space/tab + (?: + (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc + ''' % (self.tab_width, self.tab_width), + re.M | re.X) + + return code_block_re.sub(self._code_block_sub, text) + + + # Rules for a code span: + # - backslash escapes are not interpreted in a code span + # - to include one or or a run of more backticks the delimiters must + # be a longer run of backticks + # - cannot start or end a code span with a backtick; pad with a + # space and that space will be removed in the emitted HTML + # See `test/tm-cases/escapes.text` for a number of edge-case + # examples. + _code_span_re = re.compile(r''' + (?<!\\) + (`+) # \1 = Opening run of ` + (?!`) # See Note A test/tm-cases/escapes.text + (.+?) # \2 = The code block + (?<!`) + \1 # Matching closer + (?!`) + ''', re.X | re.S) + + def _code_span_sub(self, match): + c = match.group(2).strip(" \t") + c = self._encode_code(c) + return "<code>%s</code>" % c + + def _do_code_spans(self, text): + # * Backtick quotes are used for <code></code> spans. + # + # * You can use multiple backticks as the delimiters if you want to + # include literal backticks in the code span. So, this input: + # + # Just type ``foo `bar` baz`` at the prompt. + # + # Will translate to: + # + # <p>Just type <code>foo `bar` baz</code> at the prompt.</p> + # + # There's no arbitrary limit to the number of backticks you + # can use as delimters. If you need three consecutive backticks + # in your code, use four for delimiters, etc. + # + # * You can use spaces to get literal backticks at the edges: + # + # ... type `` `bar` `` ... + # + # Turns to: + # + # ... type <code>`bar`</code> ... + return self._code_span_re.sub(self._code_span_sub, text) + + def _encode_code(self, text): + """Encode/escape certain characters inside Markdown code runs. + The point is that in code, these characters are literals, + and lose their special Markdown meanings. + """ + replacements = [ + # Encode all ampersands; HTML entities are not + # entities within a Markdown code span. + ('&', '&'), + # Do the angle bracket song and dance: + ('<', '<'), + ('>', '>'), + # Now, escape characters that are magic in Markdown: + ('*', g_escape_table['*']), + ('_', g_escape_table['_']), + ('{', g_escape_table['{']), + ('}', g_escape_table['}']), + ('[', g_escape_table['[']), + (']', g_escape_table[']']), + ('\\', g_escape_table['\\']), + ] + for before, after in replacements: + text = text.replace(before, after) + return text + + _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S) + _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S) + #_spoiler_re = re.compile(r"###(?=\S)(.+?[*_]*)(?<=\S)###", re.S) + + _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S) + _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S) + def _do_italics_and_bold(self, text): + # <strong> must go first: + if "code-friendly" in self.extras: + text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text) + text = self._code_friendly_em_re.sub(r"<em>\1</em>", text) + else: + text = self._strong_re.sub(r"<strong>\2</strong>", text) + text = self._em_re.sub(r"<em>\2</em>", text) + + #text = self._spoiler_re.sub("<del>\\1</del>", text) + return text + + + _block_quote_re = re.compile(r''' + ( # Wrap whole match in \1 + ( + ^[ \t]*>[^>] # '>' at the start of a line + .+\n # rest of the first line + \n* # blanks + )+ + ) + ''', re.M | re.X) + _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M); + + _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S) + def _dedent_two_spaces_sub(self, match): + return re.sub(r'(?m)^ ', '', match.group(1)) + + def _block_quote_sub(self, match): + bq = match.group(1) + #bq = self._bq_one_level_re.sub('', bq) # trim one level of quoting + bq = self._ws_only_line_re.sub('', bq) # trim whitespace-only lines + bq = bq.strip('\n') + bq = self._run_span_gamut(bq) + #bq = self._run_block_gamut(bq) # recurse + + bq = re.sub('(?m)^', ' ', bq) + # These leading spaces screw with <pre> content, so we need to fix that: + bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq) + + return "<blockquote>\n%s\n</blockquote>\n\n" % bq + + def _do_block_quotes(self, text): + if '>' not in text: + return text + return self._block_quote_re.sub(self._block_quote_sub, text) + + def _form_paragraphs(self, text): + # Strip leading and trailing lines: + text = text.strip('\n') + + # Wrap <p> tags. + grafs = [] + for i, graf in enumerate(re.split(r"\n{2,}", text)): + if graf in self.html_blocks: + # Unhashify HTML blocks + grafs.append(self.html_blocks[graf]) + else: + cuddled_list = None + if "cuddled-lists" in self.extras: + # Need to put back trailing '\n' for `_list_item_re` + # match at the end of the paragraph. + li = self._list_item_re.search(graf + '\n') + # Two of the same list marker in this paragraph: a likely + # candidate for a list cuddled to preceding paragraph + # text (issue 33). Note the `[-1]` is a quick way to + # consider numeric bullets (e.g. "1." and "2.") to be + # equal. + if (li and len(li.group(2)) <= 3 and li.group("next_marker") + and li.group("marker")[-1] == li.group("next_marker")[-1]): + start = li.start() + cuddled_list = self._do_lists(graf[start:]).rstrip("\n") + assert cuddled_list.startswith("<ul>") or cuddled_list.startswith("<ol>") + graf = graf[:start] + + # Wrap <p> tags. + graf = self._run_span_gamut(graf) + grafs.append("<p>" + graf.lstrip(" \t") + "</p>") + + if cuddled_list: + grafs.append(cuddled_list) + + return "\n\n".join(grafs) + + def _add_footnotes(self, text): + if self.footnotes: + footer = [ + '<div class="footnotes">', + '<hr' + self.empty_element_suffix, + '<ol>', + ] + for i, id in enumerate(self.footnote_ids): + if i != 0: + footer.append('') + footer.append('<li id="fn-%s">' % id) + footer.append(self._run_block_gamut(self.footnotes[id])) + backlink = ('<a href="#fnref-%s" ' + 'class="footnoteBackLink" ' + 'title="Jump back to footnote %d in the text.">' + '↩</a>' % (id, i+1)) + if footer[-1].endswith("</p>"): + footer[-1] = footer[-1][:-len("</p>")] \ + + ' ' + backlink + "</p>" + else: + footer.append("\n<p>%s</p>" % backlink) + footer.append('</li>') + footer.append('</ol>') + footer.append('</div>') + return text + '\n\n' + '\n'.join(footer) + else: + return text + + # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + # http://bumppo.net/projects/amputator/ + _ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)') + _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I) + _naked_gt_re = re.compile(r'''(?<![a-z?!/'"-])>''', re.I) + + def _encode_amps_and_angles(self, text): + # Smart processing for ampersands and angle brackets that need + # to be encoded. + text = self._ampersand_re.sub('&', text) + + # Encode naked <'s + text = self._naked_lt_re.sub('<', text) + + # Encode naked >'s + # Note: Other markdown implementations (e.g. Markdown.pl, PHP + # Markdown) don't do this. + text = self._naked_gt_re.sub('>', text) + return text + + def _encode_backslash_escapes(self, text): + for ch, escape in g_escape_table.items(): + text = text.replace("\\"+ch, escape) + return text + + _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I) + def _auto_link_sub(self, match): + g1 = match.group(1) + return '<a href="%s">%s</a>' % (g1, g1) + + _auto_email_link_re = re.compile(r""" + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-\w]+(\.[-\w]+)*\.[a-z]+ + ) + > + """, re.I | re.X | re.U) + def _auto_email_link_sub(self, match): + return self._encode_email_address( + self._unescape_special_chars(match.group(1))) + + def _do_auto_links(self, text): + text = self._auto_link_re.sub(self._auto_link_sub, text) + text = self._auto_email_link_re.sub(self._auto_email_link_sub, text) + return text + + def _encode_email_address(self, addr): + # Input: an email address, e.g. "foo@example.com" + # + # Output: the email address as a mailto link, with each character + # of the address encoded as either a decimal or hex entity, in + # the hopes of foiling most address harvesting spam bots. E.g.: + # + # <a href="mailto:foo@e + # xample.com">foo + # @example.com</a> + # + # Based on a filter by Matthew Wickline, posted to the BBEdit-Talk + # mailing list: <http://tinyurl.com/yu7ue> + chars = [_xml_encode_email_char_at_random(ch) + for ch in "mailto:" + addr] + # Strip the mailto: from the visible part. + addr = '<a href="%s">%s</a>' \ + % (''.join(chars), ''.join(chars[7:])) + return addr + + def _do_link_patterns(self, text): + """Caveat emptor: there isn't much guarding against link + patterns being formed inside other standard Markdown links, e.g. + inside a [link def][like this]. + + Dev Notes: *Could* consider prefixing regexes with a negative + lookbehind assertion to attempt to guard against this. + """ + link_from_hash = {} + for regex, repl in self.link_patterns: + replacements = [] + for match in regex.finditer(text): + if hasattr(repl, "__call__"): + href = repl(match) + else: + href = match.expand(repl) + replacements.append((match.span(), href)) + for (start, end), href in reversed(replacements): + escaped_href = ( + href.replace('"', '"') # b/c of attr quote + # To avoid markdown <em> and <strong>: + .replace('*', g_escape_table['*']) + .replace('_', g_escape_table['_'])) + link = '<a href="%s">%s</a>' % (escaped_href, text[start:end]) + hash = _hash_text(link) + link_from_hash[hash] = link + text = text[:start] + hash + text[end:] + for hash, link in link_from_hash.items(): + text = text.replace(hash, link) + return text + + def _unescape_special_chars(self, text): + # Swap back in all the special characters we've hidden. + for ch, hash in g_escape_table.items(): + text = text.replace(hash, ch) + return text + + def _outdent(self, text): + # Remove one level of line-leading tabs or spaces + return self._outdent_re.sub('', text) + + +class MarkdownWithExtras(Markdown): + """A markdowner class that enables most extras: + + - footnotes + - code-color (only has effect if 'pygments' Python module on path) + + These are not included: + - pyshell (specific to Python-related documenting) + - code-friendly (because it *disables* part of the syntax) + - link-patterns (because you need to specify some actual + link-patterns anyway) + """ + extras = ["footnotes", "code-color"] + + +#---- internal support functions + +class UnicodeWithAttrs(unicode): + """A subclass of unicode used for the return value of conversion to + possibly attach some attributes. E.g. the "toc_html" attribute when + the "toc" extra is used. + """ + _toc = None + @property + def toc_html(self): + """Return the HTML for the current TOC. + + This expects the `_toc` attribute to have been set on this instance. + """ + if self._toc is None: + return None + + def indent(): + return ' ' * (len(h_stack) - 1) + lines = [] + h_stack = [0] # stack of header-level numbers + for level, id, name in self._toc: + if level > h_stack[-1]: + lines.append("%s<ul>" % indent()) + h_stack.append(level) + elif level == h_stack[-1]: + lines[-1] += "</li>" + else: + while level < h_stack[-1]: + h_stack.pop() + if not lines[-1].endswith("</li>"): + lines[-1] += "</li>" + lines.append("%s</ul></li>" % indent()) + lines.append(u'%s<li><a href="#%s">%s</a>' % ( + indent(), id, name)) + while len(h_stack) > 1: + h_stack.pop() + if not lines[-1].endswith("</li>"): + lines[-1] += "</li>" + lines.append("%s</ul>" % indent()) + return '\n'.join(lines) + '\n' + + +_slugify_strip_re = re.compile(r'[^\w\s-]') +_slugify_hyphenate_re = re.compile(r'[-\s]+') +def _slugify(value): + """ + Normalizes string, converts to lowercase, removes non-alpha characters, + and converts spaces to hyphens. + + From Django's "django/template/defaultfilters.py". + """ + import unicodedata + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') + value = unicode(_slugify_strip_re.sub('', value).strip().lower()) + return _slugify_hyphenate_re.sub('-', value) + +# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549 +def _curry(*args, **kwargs): + function, args = args[0], args[1:] + def result(*rest, **kwrest): + combined = kwargs.copy() + combined.update(kwrest) + return function(*args + rest, **combined) + return result + +# Recipe: regex_from_encoded_pattern (1.0) +def _regex_from_encoded_pattern(s): + """'foo' -> re.compile(re.escape('foo')) + '/foo/' -> re.compile('foo') + '/foo/i' -> re.compile('foo', re.I) + """ + if s.startswith('/') and s.rfind('/') != 0: + # Parse it: /PATTERN/FLAGS + idx = s.rfind('/') + pattern, flags_str = s[1:idx], s[idx+1:] + flag_from_char = { + "i": re.IGNORECASE, + "l": re.LOCALE, + "s": re.DOTALL, + "m": re.MULTILINE, + "u": re.UNICODE, + } + flags = 0 + for char in flags_str: + try: + flags |= flag_from_char[char] + except KeyError: + raise ValueError("unsupported regex flag: '%s' in '%s' " + "(must be one of '%s')" + % (char, s, ''.join(flag_from_char.keys()))) + return re.compile(s[1:idx], flags) + else: # not an encoded regex + return re.compile(re.escape(s)) + +# Recipe: dedent (0.1.2) +def _dedentlines(lines, tabsize=8, skip_first_line=False): + """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines + + "lines" is a list of lines to dedent. + "tabsize" is the tab width to use for indent width calculations. + "skip_first_line" is a boolean indicating if the first line should + be skipped for calculating the indent width and for dedenting. + This is sometimes useful for docstrings and similar. + + Same as dedent() except operates on a sequence of lines. Note: the + lines list is modified **in-place**. + """ + DEBUG = False + if DEBUG: + print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\ + % (tabsize, skip_first_line) + indents = [] + margin = None + for i, line in enumerate(lines): + if i == 0 and skip_first_line: continue + indent = 0 + for ch in line: + if ch == ' ': + indent += 1 + elif ch == '\t': + indent += tabsize - (indent % tabsize) + elif ch in '\r\n': + continue # skip all-whitespace lines + else: + break + else: + continue # skip all-whitespace lines + if DEBUG: print "dedent: indent=%d: %r" % (indent, line) + if margin is None: + margin = indent + else: + margin = min(margin, indent) + if DEBUG: print "dedent: margin=%r" % margin + + if margin is not None and margin > 0: + for i, line in enumerate(lines): + if i == 0 and skip_first_line: continue + removed = 0 + for j, ch in enumerate(line): + if ch == ' ': + removed += 1 + elif ch == '\t': + removed += tabsize - (removed % tabsize) + elif ch in '\r\n': + if DEBUG: print "dedent: %r: EOL -> strip up to EOL" % line + lines[i] = lines[i][j:] + break + else: + raise ValueError("unexpected non-whitespace char %r in " + "line %r while removing %d-space margin" + % (ch, line, margin)) + if DEBUG: + print "dedent: %r: %r -> removed %d/%d"\ + % (line, ch, removed, margin) + if removed == margin: + lines[i] = lines[i][j+1:] + break + elif removed > margin: + lines[i] = ' '*(removed-margin) + lines[i][j+1:] + break + else: + if removed: + lines[i] = lines[i][removed:] + return lines + +def _dedent(text, tabsize=8, skip_first_line=False): + """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text + + "text" is the text to dedent. + "tabsize" is the tab width to use for indent width calculations. + "skip_first_line" is a boolean indicating if the first line should + be skipped for calculating the indent width and for dedenting. + This is sometimes useful for docstrings and similar. + + textwrap.dedent(s), but don't expand tabs to spaces + """ + lines = text.splitlines(1) + _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line) + return ''.join(lines) + + +class _memoized(object): + """Decorator that caches a function's return value each time it is called. + If called later with the same arguments, the cached value is returned, and + not re-evaluated. + + http://wiki.python.org/moin/PythonDecoratorLibrary + """ + def __init__(self, func): + self.func = func + self.cache = {} + def __call__(self, *args): + try: + return self.cache[args] + except KeyError: + self.cache[args] = value = self.func(*args) + return value + except TypeError: + # uncachable -- for instance, passing a list as an argument. + # Better to not cache than to blow up entirely. + return self.func(*args) + def __repr__(self): + """Return the function's docstring.""" + return self.func.__doc__ + + +def _xml_oneliner_re_from_tab_width(tab_width): + """Standalone XML processing instruction regex.""" + return re.compile(r""" + (?: + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + [ ]{0,%d} + (?: + <\?\w+\b\s+.*?\?> # XML processing instruction + | + <\w+:\w+\b\s+.*?/> # namespaced single tag + ) + [ \t]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + ) + """ % (tab_width - 1), re.X) +_xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width) + +def _hr_tag_re_from_tab_width(tab_width): + return re.compile(r""" + (?: + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in \1 + [ ]{0,%d} + <(hr) # start tag = \2 + \b # word break + ([^<>])*? # + /?> # the matching end tag + [ \t]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + ) + """ % (tab_width - 1), re.X) +_hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width) + + +def _xml_encode_email_char_at_random(ch): + r = random() + # Roughly 10% raw, 45% hex, 45% dec. + # '@' *must* be encoded. I [John Gruber] insist. + # Issue 26: '_' must be encoded. + if r > 0.9 and ch not in "@_": + return ch + elif r < 0.45: + # The [1:] is to drop leading '0': 0x63 -> x63 + return '&#%s;' % hex(ord(ch))[1:] + else: + return '&#%s;' % ord(ch) + + + +#---- mainline + +class _NoReflowFormatter(optparse.IndentedHelpFormatter): + """An optparse formatter that does NOT reflow the description.""" + def format_description(self, description): + return description or "" + +def _test(): + import doctest + doctest.testmod() + +def main(argv=None): + if argv is None: + argv = sys.argv + if not logging.root.handlers: + logging.basicConfig() + + usage = "usage: %prog [PATHS...]" + version = "%prog "+__version__ + parser = optparse.OptionParser(prog="markdown2", usage=usage, + version=version, description=cmdln_desc, + formatter=_NoReflowFormatter()) + parser.add_option("-v", "--verbose", dest="log_level", + action="store_const", const=logging.DEBUG, + help="more verbose output") + parser.add_option("--encoding", + help="specify encoding of text content") + parser.add_option("--html4tags", action="store_true", default=False, + help="use HTML 4 style for empty element tags") + parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode", + help="sanitize literal HTML: 'escape' escapes " + "HTML meta chars, 'replace' replaces with an " + "[HTML_REMOVED] note") + parser.add_option("-x", "--extras", action="append", + help="Turn on specific extra features (not part of " + "the core Markdown spec). See above.") + parser.add_option("--use-file-vars", + help="Look for and use Emacs-style 'markdown-extras' " + "file var to turn on extras. See " + "<http://code.google.com/p/python-markdown2/wiki/Extras>.") + parser.add_option("--link-patterns-file", + help="path to a link pattern file") + parser.add_option("--self-test", action="store_true", + help="run internal self-tests (some doctests)") + parser.add_option("--compare", action="store_true", + help="run against Markdown.pl as well (for testing)") + parser.set_defaults(log_level=logging.INFO, compare=False, + encoding="utf-8", safe_mode=None, use_file_vars=False) + opts, paths = parser.parse_args() + log.setLevel(opts.log_level) + + if opts.self_test: + return _test() + + if opts.extras: + extras = {} + for s in opts.extras: + splitter = re.compile("[,;: ]+") + for e in splitter.split(s): + if '=' in e: + ename, earg = e.split('=', 1) + try: + earg = int(earg) + except ValueError: + pass + else: + ename, earg = e, None + extras[ename] = earg + else: + extras = None + + if opts.link_patterns_file: + link_patterns = [] + f = open(opts.link_patterns_file) + try: + for i, line in enumerate(f.readlines()): + if not line.strip(): continue + if line.lstrip().startswith("#"): continue + try: + pat, href = line.rstrip().rsplit(None, 1) + except ValueError: + raise MarkdownError("%s:%d: invalid link pattern line: %r" + % (opts.link_patterns_file, i+1, line)) + link_patterns.append( + (_regex_from_encoded_pattern(pat), href)) + finally: + f.close() + else: + link_patterns = None + + from os.path import join, dirname, abspath, exists + markdown_pl = join(dirname(dirname(abspath(__file__))), "test", + "Markdown.pl") + for path in paths: + if opts.compare: + print "==== Markdown.pl ====" + perl_cmd = 'perl %s "%s"' % (markdown_pl, path) + o = os.popen(perl_cmd) + perl_html = o.read() + o.close() + sys.stdout.write(perl_html) + print "==== markdown2.py ====" + html = markdown_path(path, encoding=opts.encoding, + html4tags=opts.html4tags, + safe_mode=opts.safe_mode, + extras=extras, link_patterns=link_patterns, + use_file_vars=opts.use_file_vars) + sys.stdout.write( + html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) + if extras and "toc" in extras: + log.debug("toc_html: " + + html.toc_html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) + if opts.compare: + test_dir = join(dirname(dirname(abspath(__file__))), "test") + if exists(join(test_dir, "test_markdown2.py")): + sys.path.insert(0, test_dir) + from test_markdown2 import norm_html_from_html + norm_html = norm_html_from_html(html) + norm_perl_html = norm_html_from_html(perl_html) + else: + norm_html = html + norm_perl_html = perl_html + print "==== match? %r ====" % (norm_perl_html == norm_html) + + +if __name__ == "__main__": + sys.exit( main(sys.argv) ) + diff --git a/cgi/oekaki.py b/cgi/oekaki.py new file mode 100644 index 0000000..f0bada7 --- /dev/null +++ b/cgi/oekaki.py @@ -0,0 +1,176 @@ +# coding=utf-8 +import _mysql +import os +import cgi +import random + +from database import * +from settings import Settings +from framework import * +from formatting import * +from template import * +from post import * + +def oekaki(self, path_split): + """ + Este script hace todo lo que tiene que hacer con los + archivos de Oekaki. + """ + page = '' + skiptemplate = False + + if len(path_split) > 2: + # Inicia el applet. Lo envia luego a este mismo script, a "Finish". + if path_split[2] == 'paint': + # Veamos que applet usar + applet = self.formdata['oek_applet'].split('|') + + applet_name = applet[0] + + if len(applet) > 1 and applet[1] == 'y': + applet_str = 'pro' + else: + applet_str = '' + + if len(applet) > 2 and applet[2] == 'y': + use_selfy = True + else: + use_selfy = False + + # Obtenemos el board + board = setBoard(self.formdata['board']) + + if board['allow_oekaki'] != '1': + raise UserError, 'Esta sección no soporta oekaki.' + + # Veamos a quien le estamos respondiendo + try: + parentid = int(self.formdata['parent']) + except: + parentid = 0 + + # Vemos si el usuario quiere una animacion + if 'oek_animation' in self.formdata.keys(): + animation = True + animation_str = 'animation' + else: + animation = False + animation_str = '' + + # Nos aseguramos que la entrada es numerica + try: + width = int(self.formdata['oek_x']) + height = int(self.formdata['oek_y']) + except: + raise UserError, 'Valores de tamaño inválidos (%s)' % repr(self.formdata) + + params = { + 'dir_resource': Settings.BOARDS_URL + 'oek_temp/', + 'tt.zip': 'tt_def.zip', + 'res.zip': 'res.zip', + 'MAYSCRIPT': 'true', + 'scriptable': 'true', + 'tools': applet_str, + 'layer_count': '5', + 'undo': '90', + 'undo_in_mg': '15', + 'url_save': Settings.BOARDS_URL + 'oek_temp/save.php?applet=shi'+applet_str, + 'poo': 'false', + 'send_advance': 'true', + 'send_language': 'utf8', + 'send_header': '', + 'send_header_image_type': 'false', + 'thumbnail_type': animation_str, + 'image_jpeg': 'false', + 'image_size': '92', + 'compress_level': '4' + } + + if 'oek_edit' in self.formdata.keys(): + # Si hay que editar, cargar la imagen correspondiente en el canvas + pid = int(self.formdata['oek_edit']) + post = FetchOne('SELECT id, file, image_width, image_height FROM posts WHERE id = %d AND boardid = %s' % (pid, board['id'])) + editfile = Settings.BOARDS_URL + board['dir'] + '/src/' + post['file'] + + params['image_canvas'] = edit + params['image_width'] = file['image_width'] + params['image_height'] = file['image_height'] + width = int(file['image_width']) + height = int(file['image_height']) + else: + editfile = None + params['image_width'] = str(width) + params['image_height'] = str(height) + + if 'canvas' in self.formdata.keys(): + editfile = self.formdata['canvas'] + + # Darle las dimensiones al exit script + params['url_exit'] = Settings.CGI_URL + 'oekaki/finish/' + board['dir'] + '/' + str(parentid) + + page += renderTemplate("paint.html", {'applet': applet_name, 'edit': editfile, 'replythread': parentid, 'width': width, 'height': height, 'params': params, 'selfy': use_selfy}) + elif path_split[2] == 'finish': + # path splits: + # 3: Board + # 4: Parentid + if path_split > 7: + # Al terminar de dibujar, llegamos aqui. Damos la opcion de postearlo. + board = setBoard(path_split[3]) + try: + parentid = int(path_split[4]) + except: + parentid = None + + ts = int(time.time()) + ip = inet_aton(self.environ["REMOTE_ADDR"]) + fname = "%s/oek_temp/%d.png" % (Settings.HOME_DIR, ip) + oek = 'no' + + if 'filebase' in self.formdata: + img = self.formdata['filebase'] + if img.startswith("data:image/png;base64,"): + img = img[22:] + img = img.replace(' ', '+') + img = img.decode('base64') + with open(fname, 'wb') as f: + f.write(img) + + if os.path.isfile(fname): + oek = ip + + try: + timetaken = timestamp() - int(path_split[5][:-2]) + except: + timetaken = 0 + + page += renderTemplate("board.html", {"threads": None, "oek_finish": oek, "replythread": parentid, "ts": ts}) + + elif path_split[2] == 'animation': + try: + board = setBoard(path_split[3]) + file = int(path_split[4]) + except: + raise UserError, 'Board o archivo de animación inválido.' + + params = { + 'pch_file': Settings.BOARDS_URL + board['dir'] + '/src/' + str(file) + '.pch', + 'run': 'true', + 'buffer_progress': 'false', + 'buffer_canvas': 'true', + 'speed': '2', + 'res.zip': Settings.BOARDS_URL + 'oek_temp/res/' +'res.zip', + 'tt.zip': Settings.BOARDS_URL + 'oek_temp/res/' + 'tt.zip', + 'tt_size': '31' + } + page += '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' + \ + '<html xmlns="http://www.w3.org/1999/xhtml">\n<head><style type="text/css">html, body{margin: 0; padding: 0;height:100%;} .full{width:100%;height:100%;}</style>\n<title>Bienvenido a Internet | Oekaki</title>\n</head>\n' + \ + '<body bgcolor="#CFCFFF" text="#800000" link="#003399" vlink="#808080" alink="#11FF11">\n' + \ + '<table cellpadding="0" cellspacing="0" class="full"><tr><td class="full">\n' + page += '<applet name="pch" code="pch2.PCHViewer.class" archive="' + Settings.BOARDS_URL + 'oek_temp/PCHViewer123.jar" width="100%" height="100%">' + for key in params.keys(): + page += '<param name="' + key + '" value="' + cleanString(params[key]) + '" />' + "\n" + page += '<div align="center">Java must be installed and enabled to use this applet. Please refer to our Java setup tutorial for more information.</div>' + page += '</applet>\n</td></tr></table>\n</body>\n</html>' + + if not skiptemplate: + self.output = page diff --git a/cgi/post.py b/cgi/post.py new file mode 100644 index 0000000..f0ee814 --- /dev/null +++ b/cgi/post.py @@ -0,0 +1,1260 @@ +# coding=utf-8 +import math +import os +import shutil +import time +import threading +import Queue +import _mysql +import formatting + +from database import * +from template import * +from settings import Settings +from framework import * + +class Post(object): + def __init__(self, boardid=0): + self.post = { + "boardid": boardid, + "parentid": 0, + "name": "", + "tripcode": "", + "email": "", + "subject": "", + "message": "", + "password": "", + "file": "", + "file_hex": "", + "file_size": 0, + "thumb": "", + "image_width": 0, + "image_height": 0, + "thumb_width": 0, + "thumb_height": 0, + "ip": "", + "timestamp_formatted": "", + "timestamp": 0, + "bumped": 0, + "locked": 0, + } + + def __getitem__(self, key): + return self.post[key] + + def __setitem__(self, key, value): + self.post[key] = value + + def __iter__(self): + return self.post + + def insert(self): + logTime("Insertando Post") + post_values = [_mysql.escape_string(str(value)) for key, value in self.post.iteritems()] + + return InsertDb("INSERT INTO `posts` (`%s`) VALUES ('%s')" % ( + "`, `".join(self.post.keys()), + "', '".join(post_values) + )) + +class RegenerateThread(threading.Thread): + def __init__(self, threadid, request_queue): + threading.Thread.__init__(self, name="RegenerateThread-%d" % (threadid,)) + self.request_queue = request_queue + self.board = Settings._.BOARD + + def run(self): + Settings._.BOARD = self.board + while 1: + action = self.request_queue.get() + if action is None: + break + if action == "front": + regenerateFrontPages() + else: + regenerateThreadPage(action) + +def threadNumReplies(post): + """ + Get how many replies a thread has + """ + board = Settings._.BOARD + + num = FetchOne("SELECT COUNT(1) FROM `posts` WHERE `parentid` = '%s' AND `boardid` = '%s'" % (post, board['id']), 0) + return int(num[0])+1 + +def get_parent_post(post_id, board_id): + post = FetchOne("SELECT `id`, `email`, `message`, `locked`, `subject`, `timestamp`, `bumped`, `last`, `length` FROM `posts` WHERE `id` = %s AND `parentid` = 0 AND `IS_DELETED` = 0 AND `boardid` = %s LIMIT 1" % (post_id, board_id)) + if post: + return post + else: + raise UserError, _("The ID of the parent post is invalid.") + +def getThread(postid=0, mobile=False, timestamp=0): + board = Settings._.BOARD + total_bytes = 0 + + database_lock.acquire() + try: + if timestamp: + cond = "`timestamp` = %s" % str(timestamp) + else: + cond = "`id` = %s" % str(postid) + + op_post = FetchOne("SELECT IS_DELETED, email, file, file_size, id, image_height, image_width, ip, message, name, subject, thumb, thumb_height, thumb_width, timestamp_formatted, tripcode, parentid, locked, expires, expires_alert, expires_formatted, timestamp FROM `posts` WHERE %s AND `boardid` = %s AND parentid = 0 LIMIT 1" % (cond, board["id"])) + if op_post: + op_post['num'] = 1 + if mobile: + op_post['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", op_post['timestamp_formatted']) + thread = {"id": op_post["id"], "posts": [op_post], "omitted": 0} + #thread = {"id": op_post["id"], "posts": [op_post], "omitted": 0, "omitted_img": 0} + total_bytes += len(op_post["message"])+80 + + replies = FetchAll("SELECT IS_DELETED, email, file, file_size, id, image_height, image_width, ip, message, name, subject, thumb, thumb_height, thumb_width, timestamp_formatted, tripcode, parentid, locked, expires, expires_alert, expires_formatted, timestamp FROM `posts` WHERE `parentid` = %s AND `boardid` = %s ORDER BY `id` ASC" % (op_post["id"], board["id"])) + thread["length"] = 1 + if replies: + for reply in replies: + thread["length"] += 1 + reply['num'] = thread["length"] + if mobile: + reply['message'] = formatting.fixMobileLinks(reply['message']) + reply['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", reply['timestamp_formatted']) + thread["posts"].append(reply) + total_bytes += len(reply["message"])+57 + + # An imageboard needs subject + if board["board_type"] in ['1', '5']: + thread["timestamp"] = op_post["timestamp"] + thread["subject"] = op_post["subject"] + thread["message"] = op_post["message"] + thread["locked"] = op_post["locked"] + thread["size"] = "%d KB" % int(total_bytes / 1000) + + #threads = [thread] + else: + return None + finally: + database_lock.release() + + return thread + +def getID(threadid, postnum): + board = Settings._.BOARD + + database_lock.acquire() + try: + posts = FetchAll("SELECT id FROM `posts` WHERE `parentid`=%s AND `boardid`=%s ORDER BY `id` ASC" % (thread["id"], board["id"])) + if posts: + post = posts[int(postnum)-1] + postid = post["id"] + else: + return None + finally: + database_lock.release() + + return postid + +def shortenMsg(message, elid='0', elboard='0'): + """ + Intenta acortar el mensaje si es necesario + Algoritmo traducido desde KusabaX + """ + board = Settings._.BOARD + + limit = 100 * int(board['numline']) + + message_exploded = message.split('<br />') + if len(message) > limit or len(message_exploded) > int(board['numline']): + message_shortened = '' + for i in range(int(board['numline'])): + if i >= len(message_exploded): + break + + message_shortened += message_exploded[i] + '<br />' + + #try: + message_shortened = message_shortened.decode('utf-8', 'replace') + #except: + + if len(message_shortened) > limit: + message_shortened = message_shortened[:limit] + + message_shortened = formatting.close_html(message_shortened) + + return True, message_shortened + else: + return False, message + +def threadUpdated(postid): + """ + Shortcut to update front pages and thread page by passing a thread ID. Uses + the simple threading module to do both regenerateFrontPages() and + regenerateThreadPage() asynchronously + """ + # Use queues only if multithreading is enabled + if Settings.USE_MULTITHREADING: + request_queue = Queue.Queue() + threads = [RegenerateThread(i, request_queue) for i in range(2)] + for t in threads: + t.start() + + request_queue.put("front") + request_queue.put(postid) + + for i in range(2): + request_queue.put(None) + + for t in threads: + t.join + else: + regenerateFrontPages() + regenerateThreadPage(postid) + +def regenerateFrontPages(): + """ + Regenerates index.html and #.html for each page after that according to the number + of live threads in the database + """ + board = Settings._.BOARD + threads = [] + if board['board_type'] == '1': + threads_to_fetch = int(board['numthreads']) + threads_to_limit = threads_to_fetch + 50 + else: + if board['dir'] == 'o': + threads_to_fetch = threads_to_limit = int(board['numthreads'])*21 + else: + threads_to_fetch = threads_to_limit = int(board['numthreads'])*11 + + database_lock.acquire() + try: + # fetch necessary threads and calculate how many posts we need + allthreads_query = "SELECT id, timestamp, subject, locked, length FROM `posts` WHERE `boardid` = '%s' AND parentid = 0 AND IS_DELETED = 0 ORDER BY `bumped` DESC, `id` ASC LIMIT %d" % \ + (board["id"], threads_to_limit) + allthreads = FetchAll(allthreads_query) + posts_to_fetch = 0 + for t in allthreads[:threads_to_fetch]: + posts_to_fetch += int(t["length"]) + more_threads = allthreads[threads_to_fetch:50] + + # get the needed posts for the front page and order them + posts_query = "SELECT * FROM `posts` WHERE `boardid` = '%s' ORDER BY `bumped` DESC, CASE parentid WHEN 0 THEN id ELSE parentid END ASC, `id` ASC LIMIT %d" % \ + (board["id"], posts_to_fetch) + posts = FetchAll(posts_query) + + threads = [] + if posts: + thread = None + post_num = 0 + + for post in posts: + if post["parentid"] == '0': + skipThread = False + if post["IS_DELETED"] == '0': + # OP; Make new thread + if thread is not None: + thread["length"] = post_num + threads.append(thread) + post_num = post["num"] = 1 + thread = {"id": post["id"], "timestamp": post["timestamp"], "subject": post["subject"], "locked": post["locked"], "posts": [post]} + else: + skipThread = True + else: + if not skipThread: + post_num += 1 + post["num"] = post_num + thread["posts"].append(post) + + if post_num: + thread["length"] = post_num + threads.append(thread) + finally: + database_lock.release() + + pages = [] + is_omitted = False + if len(threads) > 0: + # Todo : Make this better + if board['board_type'] == '1': + page_count = 1 # Front page only + threads_per_page = int(board['numthreads']) + else: + if board['dir'] == 'o': + front_limit = int(board['numthreads'])*21 + else: + front_limit = int(board['numthreads'])*11 + + if len(threads) >= front_limit: + is_omitted = True + + page_count = int(math.ceil(float(len(threads)) / float(int(board['numthreads'])))) + threads_per_page = int(board['numthreads']) + + for i in xrange(page_count): + pages.append([]) + start = i * threads_per_page + end = start + threads_per_page + for thread in threads[start:end]: + pages[i].append(thread) + else: + page_count = 0 + is_omitted = False + pages.append({}) + + page_num = 0 + for pagethreads in pages: + regeneratePage(page_num, page_count, pagethreads, is_omitted, more_threads) + page_num += 1 + +def regeneratePage(page_num, page_count, threads, is_omitted=False, more_threads=[]): + """ + Regenerates a single page and writes it to .html + """ + board = Settings._.BOARD + + for thread in threads: + replylimit = int(board['numcont']) + + # Create reply list + parent = thread["posts"].pop(0) + replies = thread["posts"] + thread["omitted"] = 0 + #thread["omitted_img"] = 0 + + # Omit posts + while(len(replies) > replylimit): + post = replies.pop(0) + thread["omitted"] += 1 + #if post["file"]: + # thread["omitted_img"] += 1 + + # Remake thread with necessary replies only + replies.insert(0, parent) + thread["posts"] = replies + + # Shorten messages + for post in thread["posts"]: + post["shortened"], post["message"] = shortenMsg(post["message"]) + + # Build page according to page number + if page_num == 0: + file_name = "index" + else: + file_name = str(page_num) + + if board['board_type'] == '1': + templatename = "txt_board.html" + else: + templatename = "board.html" + + page_rendered = renderTemplate(templatename, {"threads": threads, "pagenav": pageNavigator(page_num, page_count, is_omitted), "more_threads": more_threads}) + + f = open(Settings.ROOT_DIR + board["dir"] + "/" + file_name + ".html", "w") + try: + f.write(page_rendered) + finally: + f.close() + +def threadList(mode=0): + board = Settings._.BOARD + + if mode == 1: + mobile = True + maxthreads = 20 + cutFactor = 100 + elif mode == 2: + mobile = True + maxthreads = 1000 + cutFactor = 50 + elif mode == 3: + mobile = True + maxthreads = 1000 + cutFactor = 100 + else: + mobile = False + maxthreads = 1000 + cutFactor = 70 + + if board['board_type'] == '1': + filename = "txt_threadlist.html" + full_threads = FetchAll("SELECT id, timestamp, timestamp_formatted, subject, length, last FROM `posts` WHERE parentid = 0 AND boardid = %(board)s AND IS_DELETED = 0 ORDER BY `bumped` DESC LIMIT %(limit)s" \ + % {'board': board["id"], 'limit': maxthreads}) + else: + filename = "threadlist.html" + full_threads = FetchAll("SELECT p.*, coalesce(x.count,1) AS length, coalesce(x.t,p.timestamp) AS last FROM `posts` AS p LEFT JOIN (SELECT parentid, count(1)+1 as count, max(timestamp) as t FROM `posts` " +\ + "WHERE boardid = %(board)s GROUP BY parentid) AS x ON p.id=x.parentid WHERE p.parentid = 0 AND p.boardid = %(board)s AND p.IS_DELETED = 0 ORDER BY `bumped` DESC LIMIT %(limit)s" \ + % {'board': board["id"], 'limit': maxthreads}) + + # Generate threadlist + timestamps = [] + for thread in full_threads: + if board['board_type'] == '1': + thread["timestamp_formatted"] = thread["timestamp_formatted"].split(" ")[0] + timestamps.append([thread["last"], formatTimestamp(thread["last"])]) + if mobile: + timestamps[-1][1] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", timestamps[-1][1]) + else: + if len(thread['message']) > cutFactor: + thread['shortened'] = True + else: + thread['shortened'] = False + thread['message'] = thread['message'].replace('<br />', ' ') + thread['message'] = thread['message'].split("<hr />")[0] + thread['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub('', thread['message']) + thread['message'] = thread['message'].decode('utf-8')[:cutFactor].encode('utf-8') + thread['message'] = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub('', thread['message']) # Removes incomplete HTML entities + thread['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", thread['timestamp_formatted']) + + # Get last reply if in mobile mode + if mode == 1: + thread['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", thread['timestamp_formatted']) + lastreply = FetchOne("SELECT * FROM `posts` WHERE parentid = %s AND boardid = %s AND IS_DELETED = 0 ORDER BY `timestamp` DESC LIMIT 1" % (thread['id'], board['id'])) + if lastreply: + if len(lastreply['message']) > 60: + lastreply['shortened'] = True + else: + lastreply['shortened'] = False + lastreply['message'] = lastreply['message'].replace('<br />', ' ') + lastreply['message'] = lastreply['message'].split("<hr />")[0] + lastreply['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub('', lastreply['message']) + lastreply['message'] = lastreply['message'].decode('utf-8')[:60].encode('utf-8') + lastreply['message'] = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub('', lastreply['message']) # Removes incomplete HTML entities + lastreply['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", lastreply['timestamp_formatted']) + thread["lastreply"] = lastreply + else: + thread["lastreply"] = None + elif mode == 2: + lastreply = FetchOne("SELECT timestamp_formatted FROM `posts` WHERE parentid = %s AND boardid = %s AND IS_DELETED = 0 ORDER BY `timestamp` DESC LIMIT 1" % (thread['id'], board['id'])) + if lastreply: + lastreply['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", lastreply['timestamp_formatted']) + thread["lastreply"] = lastreply + + return renderTemplate(filename, {"more_threads": full_threads, "timestamps": timestamps, "mode": mode}, mobile) + +def catalog(sort=''): + board = Settings._.BOARD + + if board['board_type'] != '0': + raise UserError, "No hay catálogo disponible para esta sección." + + cutFactor = 500 + + q_sort = '`bumped` DESC, `id` ASC' + if sort: + if sort == '1': + q_sort = '`timestamp` DESC' + elif sort == '2': + q_sort = '`timestamp` ASC' + elif sort == '3': + q_sort = '`length` DESC' + elif sort == '4': + q_sort = '`length` ASC' + + threads = FetchAll("SELECT id, subject, message, length, thumb, expires_formatted FROM `posts` " +\ + "WHERE parentid = 0 AND boardid = %(board)s AND IS_DELETED = 0 ORDER BY %(sort)s" \ + % {'board': board["id"], 'sort': q_sort}) + + for thread in threads: + if len(thread['message']) > cutFactor: + thread['shortened'] = True + else: + thread['shortened'] = False + thread['message'] = thread['message'].replace('<br />', ' ') + thread['message'] = thread['message'].split("<hr />")[0] + thread['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub('', thread['message']) + thread['message'] = thread['message'].decode('utf-8')[:cutFactor].encode('utf-8') + thread['message'] = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub('', thread['message']) # Removes incomplete HTML entities + + return renderTemplate("catalog.html", {"threads": threads, "i_sort": sort}) + +def regenerateThreadPage(postid): + """ + Regenerates /res/#.html for supplied thread id + """ + board = Settings._.BOARD + + thread = getThread(postid) + + if board['board_type'] in ['1', '5']: + template_filename = "txt_thread.html" + outname = Settings.ROOT_DIR + board["dir"] + "/read/" + str(thread["timestamp"]) + ".html" + title_matome = thread['subject'] + post_preview = cut_home_msg(thread['posts'][0]['message'], 0) + else: + template_filename = "board.html" + outname = Settings.ROOT_DIR + board["dir"] + "/res/" + str(postid) + ".html" + post_preview = cut_home_msg(thread['posts'][0]['message'], len(board['name'])) + + if thread['posts'][0]['subject'] != board['subject']: + title_matome = thread['posts'][0]['subject'] + else: + title_matome = post_preview + + page = renderTemplate(template_filename, {"threads": [thread], "replythread": postid, "matome": title_matome, "preview": post_preview}, False) + + f = open(outname, "w") + try: + f.write(page) + finally: + f.close() + +def threadPage(postid, mobile=False, timestamp=0): + board = Settings._.BOARD + + if board['board_type'] in ['1', '5']: + template_filename = "txt_thread.html" + else: + template_filename = "board.html" + + threads = [getThread(postid, mobile, timestamp)] + + return renderTemplate(template_filename, {"threads": threads, "replythread": postid}, mobile) + +def dynamicRead(parentid, ranges, mobile=False): + import re + board = Settings._.BOARD + + if board['board_type'] != '1': + raise UserError, "Esta sección no es un BBS y como tal no soporta lectura dinámica." + + # get entire thread + template_fname = "txt_thread.html" + thread = getThread(timestamp=parentid, mobile=mobile) + + if not thread: + # Try the archive + fname = Settings.ROOT_DIR + board["dir"] + "/kako/" + str(parentid) + ".json" + if os.path.isfile(fname): + import json + with open(fname) as f: + thread = json.load(f) + thread['posts'] = [dict(zip(thread['keys'], row)) for row in thread['posts']] + template_fname = "txt_archive.html" + else: + raise UserError, 'El hilo no existe.' + + filtered_thread = { + "id": thread['id'], + "timestamp": thread['timestamp'], + "length": thread['length'], + "subject": thread['subject'], + "locked": thread['locked'], + "posts": [], + } + + if 'size' in thread: + filtered_thread['size'] = thread['size'] + + no_op = False + if ranges.endswith('n'): + no_op = True + ranges = ranges[:-1] + + # get thread length + total = thread["length"] + + # compile regex + __multiple_ex = re.compile("^([0-9]*)-([0-9]*)$") + __single_ex = re.compile("^([0-9]+)$") + __last_ex = re.compile("^l([0-9]+)$") + start = 0 + end = 0 + + # separate by commas (,) + for range in ranges.split(','): + # single post (#) + range_match = __single_ex.match(range) + if range_match: + postid = int(range_match.group(1)) + if postid > 0 and postid <= total: + filtered_thread["posts"].append(thread["posts"][postid-1]) + + # go to next range + continue + + # post range (#-#) + range_match = __multiple_ex.match(range) + if range_match: + start = int(range_match.group(1) or 1) + end = int(range_match.group(2) or total) + + if start > total: + start = total + if end > total: + end = total + + if start < end: + filtered_thread["posts"].extend(thread["posts"][start-1:end]) + else: + list = thread["posts"][end-1:start] + list.reverse() + filtered_thread["posts"].extend(list) + + # go to next range + continue + + # last posts (l#) + range_match = __last_ex.match(range) + if range_match: + length = int(range_match.group(1)) + start = total - length + 1 + end = total + if start < 1: + start = 1 + + filtered_thread["posts"].extend(thread["posts"][start-1:]) + + continue + + # calculate previous and next ranges + prevrange = None + nextrange = None + if __multiple_ex.match(ranges) or __last_ex.match(ranges): + if mobile: + range_n = 50 + else: + range_n = 100 + + prev_start = start-range_n + prev_end = start-1 + next_start = end+1 + next_end = end+range_n + + if prev_start < 1: + prev_start = 1 + if next_end > total: + next_end = total + + if start > 1: + prevrange = '%d-%d' % (prev_start, prev_end) + if end < total: + nextrange = '%d-%d' % (next_start, next_end) + + if not no_op and start > 1 and end > 1: + filtered_thread["posts"].insert(0, thread["posts"][0]) + + if not filtered_thread["posts"]: + raise UserError, "No hay posts que mostrar." + + post_preview = cut_home_msg(filtered_thread["posts"][0]["message"], 0) + + return renderTemplate(template_fname, {"threads": [filtered_thread], "replythread": parentid, "prevrange": prevrange, "nextrange": nextrange, "preview": post_preview}, mobile, noindex=True) + +def regenerateBoard(everything=False): + """ + Update front pages and every thread res HTML page + """ + board = Settings._.BOARD + op_posts = [] + + if everything: + op_posts = FetchAll("SELECT `id` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0" % board["id"]) + + # Use queues only if multithreading is enabled + if Settings.USE_MULTITHREADING: + request_queue = Queue.Queue() + threads = [RegenerateThread(i, request_queue) for i in range(Settings.MAX_PROGRAM_THREADS)] + for t in threads: + t.start() + + request_queue.put("front") + + for post in op_posts: + request_queue.put(post["id"]) + + for i in range(Settings.MAX_PROGRAM_THREADS): + request_queue.put(None) + + for t in threads: + t.join() + else: + regenerateFrontPages() + for post in op_posts: + regenerateThreadPage(post["id"]) + +def deletePost(postid, password, deltype='0', imageonly=False, quick=False): + """ + Remove post from database and unlink file (if present), along with all replies + if supplied post is a thread + """ + board = Settings._.BOARD + + # make sure postid is numeric + postid = int(postid) + + # get post + post = FetchOne("SELECT `id`, `timestamp`, `parentid`, `file`, `thumb`, `password`, `length` FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(postid))) + + # abort if the post doesn't exist + if not post: + raise UserError, _("There isn't a post with this ID. It was probably deleted.") + + if password: + if password != post['password']: + raise UserError, "No tienes permiso para eliminar este mensaje." + if post["parentid"] == '0' and int(post["length"]) >= Settings.DELETE_FORBID_LENGTH: + raise UserError, "No puedes eliminar un hilo con tantas respuestas." + if (int(time.time()) - int(post["timestamp"])) > 86400: + raise UserError, "No puedes eliminar un post tan viejo." + + # just update the DB if deleting only the image, otherwise delete whole post + if imageonly: + if post["file"]: + deleteFile(post) + + UpdateDb("UPDATE `posts` SET `file` = '', `file_hex` = '', `thumb` = '', `thumb_width` = 0, `thumb_height` = 0 WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(post['id']))) + else: + if int(post["parentid"]) == 0: + deleteReplies(post) + + logTime("Deleting post " + str(postid)) + if deltype != '0' and post["parentid"] != '0': + # Soft delete (recycle bin) + UpdateDb("UPDATE `posts` SET `IS_DELETED` = %s WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (deltype, board["id"], post["id"])) + else: + # Hard delete + if post["file"]: + deleteFile(post) + + UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], post["id"])) + if post['parentid'] != '0': + UpdateDb("UPDATE `posts` SET length = %d WHERE `id` = '%s' AND `boardid` = '%s'" % (threadNumReplies(post["parentid"]), post["parentid"], board["id"])) + + if post['parentid'] == '0': + if board['board_type'] == '1': + os.unlink(Settings.ROOT_DIR + board["dir"] + "/read/" + post["timestamp"] + ".html") + else: + os.unlink(Settings.ROOT_DIR + board["dir"] + "/res/" + post["id"] + ".html") + + regenerateHome() + + # rebuild thread and fronts if reply; rebuild only fronts if not + if post["parentid"] != '0': + threadUpdated(post["parentid"]) + else: + regenerateFrontPages() + +def deleteReplies(thread): + board = Settings._.BOARD + + # delete files first + replies = FetchAll("SELECT `parentid`, `file`, `thumb` FROM `posts` WHERE `boardid` = %s AND `parentid` = %s AND `file` != ''" % (board["id"], thread["id"])) + for post in replies: + deleteFile(post) + + # delete all replies from DB + UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `parentid` = %s" % (board["id"], thread["id"])) + +def deleteFile(post): + """ + Unlink file and thumb of supplied post + """ + board = Settings._.BOARD + + try: + os.unlink(Settings.IMAGES_DIR + board["dir"] + "/src/" + post["file"]) + except: + pass + + # we don't want to delete mime thumbnails + if post["thumb"].startswith("mime"): + return + + try: + os.unlink(Settings.IMAGES_DIR + board["dir"] + "/thumb/" + post["thumb"]) + except: + pass + + try: + os.unlink(Settings.IMAGES_DIR + board["dir"] + "/mobile/" + post["thumb"]) + except: + pass + + if int(post["parentid"]) == 0: + try: + os.unlink(Settings.IMAGES_DIR + board["dir"] + "/cat/" + post["thumb"]) + except: + pass + +def trimThreads(): + """ + Delete any threads which have passed the MAX_THREADS setting + """ + logTime("Trimming threads") + board = Settings._.BOARD + archived = False + + # Use limit of the board type + if board['board_type'] == '1': + limit = Settings.TXT_MAX_THREADS + else: + limit = Settings.MAX_THREADS + + # trim expiring threads first + if board['maxage'] != '0': + t = time.time() + + alert_time = int(round(int(board['maxage']) * Settings.MAX_AGE_ALERT)) + time_limit = t + (alert_time * 86400) + old_ops = FetchAll("SELECT `id`, `timestamp`, `expires`, `expires_alert`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `expires` > 0 AND `expires` < %s LIMIT 50" % (board['id'], time_limit)) + + for op in old_ops: + if t >= int(op['expires']): + # Trim old threads + if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH: + archiveThread(op["id"]) + archived = True + + deletePost(op["id"], None) + else: + # Add alert to threads approaching deletion + UpdateDb("UPDATE `posts` SET expires_alert = 1 WHERE `boardid` = %s AND `id` = %s" % (board['id'], op['id'])) + + # trim inactive threads next + if board['maxinactive'] != '0': + t = time.time() + + oldest_last = t - (int(board['maxinactive']) * 86400) + old_ops = FetchAll("SELECT `id`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `last` < %d LIMIT 50" % (board['id'], oldest_last)) + + for op in old_ops: + if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH: + archiveThread(op["id"]) + archived = True + + deletePost(op["id"], None) + + # select trim type by board + if board['board_type'] == '1': + trim_method = Settings.TXT_TRIM_METHOD + else: + trim_method = Settings.TRIM_METHOD + + # select order by trim + if trim_method == 1: + order = 'last DESC' + elif trim_method == 2: + order = 'bumped DESC' + else: + order = 'timestamp DESC' + + # Trim the last thread + op_posts = FetchAll("SELECT `id`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 ORDER BY %s" % (board["id"], order)) + if len(op_posts) > limit: + posts = op_posts[limit:] + for post in posts: + if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH: + archiveThread(post["id"]) + archived = True + + deletePost(post["id"], None) + pass + + if archived: + regenerateKako() + +def autoclose_thread(parentid, t, replies): + """ + If the thread is crossing the reply limit, close it with a message. + """ + board = Settings._.BOARD + + # decide the replylimit + if board['board_type'] == '1' and Settings.TXT_CLOSE_THREAD_ON_REPLIES > 0: + replylimit = Settings.TXT_CLOSE_THREAD_ON_REPLIES + elif Settings.CLOSE_THREAD_ON_REPLIES > 0: + replylimit = Settings.CLOSE_THREAD_ON_REPLIES + else: + return # do nothing + + # close it if passing replylimit + #if replies >= replylimit or board["dir"] == "polka": + if replies >= replylimit: + notice_post = Post(board["id"]) + notice_post["parentid"] = parentid + notice_post["name"] = "Sistema" + notice_post["message"] = "El hilo ha llegado al límite de respuestas.<br />Si quieres continuarlo, por favor crea otro." + notice_post["timestamp"] = t+1 + notice_post["bumped"] = get_parent_post(parentid, board["id"])["bumped"] + notice_post["timestamp_formatted"] = str(replylimit) + " mensajes" + notice_post.insert() + UpdateDb("UPDATE `posts` SET `locked` = 1 WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % (board["id"], _mysql.escape_string(parentid))) + +def pageNavigator(page_num, page_count, is_omitted=False): + """ + Create page navigator in the format of [0], [1], [2]... + """ + board = Settings._.BOARD + + # No threads? + if page_count == 0: + return '' + + # TODO nijigen HACK + first_str = "Primera página" + last_str = "Última página" + previous_str = _("Previous") + next_str = _("Next") + omitted_str = "Resto omitido" + + pagenav = "<span>" + if page_num == 0: + pagenav += first_str + else: + previous = str(page_num - 1) + if previous == "0": + previous = "" + else: + previous = previous + ".html" + pagenav += '<form method="get" action="' + Settings.BOARDS_URL + board["dir"] + '/' + previous + '"><input value="'+previous_str+'" type="submit" /></form>' + + pagenav += "</span><span>" + + for i in xrange(page_count): + if i == page_num: + pagenav += "[<strong>%d</strong>]" % i + else: + if i == 0: + pagenav += '[<a href="%s%s/">%d</a>]' % (Settings.BOARDS_URL, board['dir'], i) + else: + pagenav += '[<a href="%s%s/%d.html">%d</a>]' % (Settings.BOARDS_URL, board['dir'], i, i) + + if i > 0 and (i % 10) == 0 and not is_omitted: + pagenav += '<br />' + elif i < 10: + pagenav += ' ' + + if is_omitted: + pagenav += "[" + omitted_str + "]" + + pagenav += "<!-- "+repr(is_omitted)+"-->" + pagenav += "</span><span>" + + next = (page_num + 1) + if next == page_count: + pagenav += last_str + "</span>" + else: + pagenav += '<form method="get" action="' + Settings.BOARDS_URL + board["dir"] + '/' + str(next) + '.html"><input value="'+next_str+'" type="submit" /></form></span>' + + return pagenav + +def flood_check(t,post,boardid): + board = Settings._.BOARD + + if not post["parentid"]: + maxtime = t - int(board['threadsecs']) + #lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = '%s' and `parentid` = 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, maxtime), 0) + + # NO MATTER THE IP + lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `parentid` = 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (boardid, maxtime), 0) + pass + else: + maxtime = t - int(board['postsecs']) + lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = '%s' and `parentid` != 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, maxtime), 0) + + if int(lastpost[0]): + if post["parentid"]: + raise UserError, _("Flood detected. Please wait a moment before posting again.") + else: + lastpost = FetchOne("SELECT `timestamp` FROM `posts` WHERE `parentid`=0 and `boardid`='%s' and IS_DELETED = 0 ORDER BY `timestamp` DESC" % (boardid), 0) + wait = int(int(board['threadsecs']) - (t - int(lastpost[0]))) + raise UserError, "Por favor espera " + str(wait) + " segundos antes de crear otro hilo." + +def cut_home_msg(message, boardlength=0): + short_message = message.replace("<br />", " ") + short_message = short_message.split("<hr />")[0] + short_message = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub("", short_message) # Removes HTML tags + limit = Settings.HOME_LASTPOSTS_LENGTH - boardlength + + if len(short_message) > limit: + if isinstance(short_message, unicode): + short_message = short_message[:limit].encode('utf-8') + "…" + else: + short_message = short_message.decode('utf-8')[:limit].encode('utf-8') + "…" + short_message = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub("", short_message) # Removes incomplete HTML + return short_message + +def getLastAge(limit): + threads = [] + sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, bumped, last, length, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND posts.locked < 3 ORDER BY bumped DESC LIMIT %d" % limit + threads = FetchAll(sql) + + for post in threads: + post['id'] = int(post['id']) + post['bumped'] = int(post['bumped']) + post['last'] = int(post['last']) + post['length'] = int(post['length']) + post['board_type'] = int(post['board_type']) + post['timestamp'] = int(post['timestamp']) + post['content'] = cut_home_msg(post['content'], 0) + + if post['board_type'] == 1: + post['url'] = '/%s/read/%d/l10' % (post['dir'], post['timestamp']) + else: + post['url'] = '/%s/res/%d.html' % (post['dir'], post['id']) + + return threads + +def getNewThreads(limit): + threads = [] + sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND boards.id <> 34 AND boards.id <> 13 AND posts.locked = 0 ORDER BY timestamp DESC LIMIT %d" % (limit) + threads = FetchAll(sql) + + for post in threads: + post['id'] = int(post['id']) + post['board_type'] = int(post['board_type']) + post['timestamp'] = int(post['timestamp']) + post['timestamp_formatted'] = formatTimestamp(post['timestamp'], True) + post['timestamp_formatted'] = post['timestamp_formatted'][:8] + ' ' + post['timestamp_formatted'][13:] + post['content'] = cut_home_msg(post['content'], 0) + if post['board_type'] == 1: + post['url'] = '/%s/read/%d' % (post['dir'], post['timestamp']) + else: + post['url'] = '/%s/res/%d.html' % (post['dir'], post['id']) + + return threads + +def regenerateHome(): + """ + Update index.html in the boards directory with useful data for users + """ + logTime("Updating home") + t = datetime.datetime.now() + + limit = Settings.HOME_LASTPOSTS + template_values = { + 'header': Settings.SITE_TITLE, + 'slogan': Settings.SITE_SLOGAN, + 'latest_news': FetchAll("SELECT `timestamp`, `message`, `timestamp_formatted` FROM `news` WHERE `type` = '2' ORDER BY `timestamp` DESC LIMIT " + str(Settings.HOME_NEWS)), + 'latest_age': getLastAge(limit), + 'latest_age_num': limit, + 'new_threads': getNewThreads(Settings.HOME_NEWTHREADS), + } + + page_rendered = renderTemplate('home.html', template_values) + f = open(Settings.HOME_DIR + "home.html", "w") + try: + f.write(page_rendered) + finally: + f.close() + + if Settings.ENABLE_RSS: + sql = "SELECT id, boardid, board_name, timestamp, timestamp_formatted, content, url FROM last ORDER BY timestamp DESC LIMIT 10" + rss = FetchAll(sql) + rss_rendered = renderTemplate('home.rss', {'posts': rss}) + f = open(Settings.HOME_DIR + "bai.rss", "w") + try: + f.write(rss_rendered) + finally: + f.close() + +def regenerateNews(): + """ + Update news.html in the boards directory with older news + """ + posts = FetchAll("SELECT * FROM `news` WHERE `type` = '1' ORDER BY `timestamp` DESC") + template_values = { + 'title': 'Noticias', + 'posts': posts, + 'header': Settings.SITE_TITLE, + 'slogan': Settings.SITE_SLOGAN, + 'navbar': False, + } + + page_rendered = renderTemplate('news.html', template_values) + + f = open(Settings.HOME_DIR + "noticias.html", "w") + try: + f.write(page_rendered) + finally: + f.close() + +def regenerateAccess(): + if not Settings.HTACCESS_GEN: + return False + + bans = FetchAll("SELECT INET_NTOA(`ip`) AS 'ip', INET_NTOA(`netmask`) AS 'netmask', `boards` FROM `bans` WHERE `blind` = '1'") + listbans = dict() + #listbans_global = list() + + boarddirs = FetchAll('SELECT `dir` FROM `boards`') + for board in boarddirs: + listbans[board['dir']] = list() + + for ban in bans: + ipmask = ban["ip"] + if ban["netmask"] is not None: + ipmask += '/' + ban["netmask"] + + if ban["boards"] != "": + boards = pickle.loads(ban["boards"]) + for board in boards: + listbans[board].append(ipmask) + else: + #listbans_global.append(ban["ip"]) + for board in boarddirs: + if board['dir'] not in Settings.EXCLUDE_GLOBAL_BANS: + listbans[board['dir']].append(ipmask) + + # Generate .htaccess for each board + for board in listbans.keys(): + template_values = { + 'ips': listbans[board], + 'dir': board, + } + + page_rendered = renderTemplate('htaccess', template_values) + f = open(Settings.ROOT_DIR + board + "/.htaccess", "w") + try: + f.write(page_rendered) + finally: + f.close() + + return True + +def regenerateKako(): + board = Settings._.BOARD + + threads = FetchAll("SELECT * FROM archive WHERE boardid = %s ORDER BY timestamp DESC" % board['id']) + page = renderTemplate('kako.html', {'threads': threads}) + with open(Settings.ROOT_DIR + board["dir"] + "/kako/index.html", "w") as f: + f.write(page) + +def make_url(postid, post, parent_post, noko, mobile): + board = Settings._.BOARD + + parentid = post["parentid"] + if not parentid: + parentid = postid + + if mobile: + if not noko: + url = Settings.CGI_URL + 'mobile/' + board["dir"] + elif board["board_type"] == '1': + url = "%s/mobileread/%s/%s/l10#form" % (Settings.CGI_URL, board["dir"], parent_post['timestamp']) + else: + url = "%s/mobileread/%s/%s#%s" % (Settings.CGI_URL, board["dir"], parentid, postid) + else: + if not noko: + url = Settings.BOARDS_URL + board["dir"] + "/" + elif board["board_type"] == '1': + url = "%s/read/%s/l50#bottom" % (Settings.BOARDS_URL + board["dir"], str(parent_post['timestamp'])) + else: + url = "%s/res/%s.html#%s" % (Settings.BOARDS_URL + board["dir"], str(parentid), postid) + + return url + +def make_redirect(url, timetaken=None): + board = Settings._.BOARD + randomPhrase = getRandomLine('quotes.conf') + + return renderTemplate('redirect.html', {'url': url, 'message': randomPhrase, 'timetaken': timetaken}) + +def latestAdd(post, postnum, postid, parent_post): + board = Settings._.BOARD + + #UpdateDb("DELETE FROM last LIMIT 15, 500#") + + if post['subject'] and post['subject'] != board["subject"]: + content = post['subject'] + else: + content = cut_home_msg(post['message'], len(board['name'])) + + timestamp_formatted = datetime.datetime.fromtimestamp(post['timestamp']).strftime('%Y-%m-%dT%H:%M:%S%Z') + parentid = parent_post['id'] if post['parentid'] else postid + + if board['board_type'] == '1': + url = '/%s/read/%s/%d' % (board['dir'], (parent_post['timestamp'] if post['parentid'] else post['timestamp']), (postnum if postnum else 1)) + else: + url = '/%s/res/%s.html#%s' % (board['dir'], parentid, postid) + + sql = "INSERT INTO last (id, boardid, board_name, timestamp, timestamp_formatted, content, url) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s')" % (str(postid), board['id'], _mysql.escape_string(board['name']), post['timestamp'], _mysql.escape_string(timestamp_formatted), _mysql.escape_string(content), _mysql.escape_string(url)) + UpdateDb(sql) + +def latestRemove(postid): + board = Settings._.BOARD + UpdateDb("DELETE FROM last WHERE id = %s AND boardid = %s" % (str(postid), board['id'])) + +def archiveThread(postid): + import json + board = Settings._.BOARD + + thread = getThread(postid, False) + post_preview = cut_home_msg(thread['posts'][0]['message'], 0) + + page = renderTemplate("txt_archive.html", {"threads": [thread], "replythread": postid, "preview": post_preview}, False) + with open(Settings.ROOT_DIR + board["dir"] + "/kako/" + str(thread['timestamp']) + ".html", "w") as f: + f.write(page) + + thread['keys'] = ['num', 'IS_DELETED', 'name', 'tripcode', 'email', 'message', 'timestamp_formatted'] + thread['posts'] = [[row[key] for key in thread['keys']] for row in thread['posts']] + try: + with open(Settings.ROOT_DIR + board["dir"] + "/kako/" + str(thread['timestamp']) + ".json", "w") as f: + json.dump(thread, f, indent=0) + except: + raise UserError, "Can't archive: %s" % thread['timestamp'] + + UpdateDb("REPLACE INTO archive (id, boardid, timestamp, subject, length) VALUES ('%s', '%s', '%s', '%s', '%s')" % (thread['id'], board['id'], thread['timestamp'], _mysql.escape_string(thread['subject']), thread['length'])) + +def throw_dice(dice): + qty = int(dice[0][1:]) + if qty == 0: + raise UserError, "No tienes dados para lanzar." + if qty > 100: + qty = 100 + sides = int(dice[1][1:]) if dice[1] else 6 + if sides == 0: + raise UserError, "Tus dados no tienen caras." + if sides > 100: + sides = 100 + + string = "Lanzas " + string += "un dado de " if qty == 1 else (str(qty) + " dados de ") + string += "una cara." if sides == 1 else (str(sides) + " caras.") + string += " Resultado: <b>" + + total = i = 0 + while (i < qty): + total += random.randint(1,sides) + i += 1 + + string += str(total) + "</b>" + + return string + +def magic_ball(): + string = "La bola 8 mágica dice: <b>" + results = ["Sí.", "Es seguro.", "En mi opinión, sí.", "Está decidido que sí.", "Definitivamente sí.", "Es lo más probable.", "Buen pronóstico.", "Puedes confiar en ello.", "Todo apunta a que sí.", "Sin duda.", "Respuesta vaga, vuelve a intentarlo.", "Pregunta en otro momento.", "Mejor que no te lo diga ahora.", "No puedo predecirlo ahora.", "Concéntrate y vuelve a preguntar.", "No cuentes con ello.", "Mi respuesta es no.", "Mis fuentes me dicen que no.", "Pronóstico no muy bueno.", "Muy dudoso."] + string += random.choice(results) + "</b>" + + return string + +def discord_hook(post, url): + import urllib2 + import json + + board = Settings._.BOARD + + WEBHOOK_URL = "https://discordapp.com/api/webhooks/428025764974166018/msYu1-R3JRnG-cxrhAu3J7LbIPvzpBlJwbW5PFe5VEQaxVzjros9CXOpjZDahUE42Jgn" + + data = {"content": "", + "ts": post['timestamp'], + "embeds": [{ + "title": post['subject'], + "description": cut_home_msg(post['message'], 30), + "url": "https://bienvenidoainternet.org" + url, + "color": 11910504, + "timestamp": datetime.datetime.utcfromtimestamp(post['timestamp']).isoformat(), + "footer": { "text": board['name'] }, + "thumbnail": { "url": "https://bienvenidoainternet.org/%s/thumb/%s" % (board['dir'], post['thumb']) }, + "author": { + "name": "Nuevo hilo", + "icon_url": "https://bienvenidoainternet.org/0/junk/w/shobon.gif" + }}] + } + jsondata = json.dumps(data, separators=(',',':')) + + opener = urllib2.build_opener() + opener.addheaders = [('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0')] + response = opener.open(WEBHOOK_URL, jsondata, 6) + the_page = response.read() diff --git a/cgi/proxy.txt b/cgi/proxy.txt new file mode 100644 index 0000000..5fc9460 --- /dev/null +++ b/cgi/proxy.txt @@ -0,0 +1,3251 @@ +190.151.94.45 +186.67.46.229 +109.107.112.139 +109.107.112.250 +109.107.97.45 +114.127.246.36 +114.224.167.170 +115.99.10.9 +116.17.92.221 +116.26.61.105 +116.48.224.179 +116.5.232.33 +116.72.165.208 +116.72.189.248 +117.102.81.138 +118.175.22.21 +118.220.175.207 +118.98.161.122 +118.98.165.42 +118.98.168.18 +119.115.52.109 +119.235.25.242 +119.62.128.38 +119.63.86.78 +119.70.40.101 +12.13.94.227 +12.208.118.96 +12.208.14.76 +12.208.168.97 +12.208.190.71 +12.240.37.195 +12.4.27.18 +12.47.164.114 +12.51.72.38 +12.72.169.156 +120.118.254.248 +120.125.80.133 +120.138.117.249 +120.138.80.226 +120.28.64.69 +121.108.31.23 +121.207.206.137 +121.207.42.244 +121.242.41.18 +121.245.199.202 +121.246.207.88 +121.52.147.45 +121.52.49.218 +121.58.193.10 +121.8.98.90 +121.9.230.162 +122.103.167.189 +122.103.185.182 +122.107.124.56 +122.116.65.95 +122.166.15.47 +122.17.74.118 +122.224.97.85 +122.6.245.14 +123.127.110.247 +123.131.44.66 +123.175.204.208 +123.233.121.164 +123.6.19.97 +124.115.177.53 +124.124.19.3 +124.129.174.114 +124.138.92.206 +124.195.158.155 +124.207.168.48 +124.217.198.4 +124.237.86.10 +124.30.233.111 +124.40.121.3 +124.53.159.169 +124.53.159.185 +124.81.224.174 +124.81.231.1 +125.135.15.181 +125.14.35.130 +125.160.244.159 +125.163.255.154 +125.240.55.130 +125.243.140.2 +125.243.149.130 +125.245.187.2 +125.245.196.194 +125.245.211.2 +125.248.165.178 +125.34.135.4 +125.34.30.201 +125.40.59.193 +125.46.24.134 +125.46.34.53 +125.46.95.230 +125.65.113.61 +129.22.90.248 +129.70.142.34 +129.93.193.140 +130.63.177.192 +131.196.115.10 +131.196.13.5 +134.39.27.37 +138.0.152.162 +138.0.231.66 +138.117.109.154 +138.117.143.226 +138.117.149.68 +138.117.84.90 +138.122.191.224 +138.186.176.175 +138.186.178.161 +138.186.179.219 +138.186.201.214 +138.36.98.107 +138.59.176.244 +138.59.176.246 +138.59.176.67 +140.113.169.49 +140.116.23.198 +140.117.178.234 +140.134.104.195 +140.134.208.93 +141.13.16.201 +141.13.16.202 +141.85.118.1 +141.85.254.118 +142.150.3.77 +142.59.52.201 +142.59.90.148 +143.255.142.99 +143.50.28.215 +146.101.147.140 +147.75.123.141 +147.75.123.142 +148.233.239.23 +148.233.239.24 +148.243.37.101 +148.244.102.98 +149.202.5.57 +151.11.232.92 +151.204.42.140 +151.79.46.58 +152.169.230.136 +152.204.130.62 +152.231.29.196 +152.231.74.252 +152.231.81.122 +156.17.93.77 +156.34.176.45 +157.182.52.224 +16.225.151.192 +160.7.251.98 +161.10.228.42 +161.132.100.69 +161.132.111.83 +161.132.176.28 +162.129.45.61 +163.17.151.126 +163.17.189.254 +163.24.133.117 +163.29.225.250 +163.30.113.254 +163.30.32.90 +164.77.134.13 +164.77.170.68 +164.78.252.25 +165.228.128.11 +165.228.132.11 +165.229.203.100 +168.196.112.250 +168.196.114.254 +168.234.75.142 +168.90.12.125 +168.90.12.126 +170.239.84.248 +170.239.86.37 +170.254.230.226 +170.78.46.53 +170.79.16.19 +170.84.126.164 +172.162.51.225 +172.163.102.206 +172.163.122.212 +172.163.195.118 +172.163.2.27 +172.164.11.216 +172.165.127.246 +172.166.67.103 +172.190.25.229 +172.192.219.161 +173.45.229.206 +174.0.50.242 +174.129.225.131 +174.142.104.57 +174.142.24.201 +174.142.36.90 +174.49.93.138 +176.56.122.156 +176.56.122.194 +176.56.98.135 +177.230.105.9 +177.234.12.202 +177.234.7.66 +177.250.223.82 +178.60.28.98 +179.42.186.120 +179.43.98.74 +179.49.114.110 +179.60.240.233 +179.60.241.246 +179.63.253.42 +181.10.176.50 +181.111.56.248 +181.112.138.198 +181.112.147.218 +181.112.147.90 +181.112.152.222 +181.112.188.90 +181.112.218.146 +181.112.219.187 +181.112.221.182 +181.112.221.2 +181.112.227.142 +181.112.228.126 +181.112.37.38 +181.112.39.186 +181.112.47.246 +181.112.51.174 +181.112.53.114 +181.112.55.6 +181.112.60.62 +181.112.61.146 +181.112.61.162 +181.112.61.178 +181.112.62.26 +181.113.116.134 +181.113.121.158 +181.113.123.58 +181.113.20.186 +181.113.28.150 +181.113.30.222 +181.113.5.142 +181.113.5.146 +181.113.5.186 +181.114.21.154 +181.115.241.90 +181.119.115.17 +181.129.1.154 +181.129.131.74 +181.129.2.186 +181.129.30.190 +181.129.39.42 +181.129.40.42 +181.129.51.106 +181.143.103.170 +181.143.123.114 +181.143.162.189 +181.143.186.82 +181.143.210.61 +181.143.221.226 +181.143.47.28 +181.143.65.117 +181.143.73.34 +181.143.9.34 +181.15.156.190 +181.167.159.174 +181.168.140.136 +181.168.148.151 +181.170.196.130 +181.174.79.53 +181.177.141.233 +181.177.242.82 +181.177.242.84 +181.188.130.14 +181.188.156.51 +181.188.199.152 +181.189.210.1 +181.189.235.11 +181.192.13.15 +181.196.145.106 +181.196.150.166 +181.196.178.106 +181.196.207.66 +181.196.27.162 +181.196.27.198 +181.196.28.34 +181.196.50.238 +181.196.57.54 +181.196.77.194 +181.199.178.12 +181.199.202.248 +181.211.101.10 +181.211.114.110 +181.211.13.126 +181.211.187.250 +181.211.191.228 +181.211.2.82 +181.211.3.190 +181.211.57.10 +181.211.59.22 +181.225.78.142 +181.225.79.164 +181.229.121.134 +181.29.113.251 +181.29.126.81 +181.39.17.138 +181.39.223.96 +181.39.28.228 +181.39.96.154 +181.40.115.186 +181.40.46.204 +181.40.93.118 +181.41.246.100 +181.44.24.56 +181.45.61.153 +181.47.9.239 +181.48.167.142 +181.48.216.38 +181.48.47.26 +181.49.121.21 +181.49.160.90 +181.49.200.90 +181.49.213.166 +181.49.41.206 +181.49.44.14 +181.49.50.226 +181.52.172.253 +181.55.148.39 +181.55.151.170 +181.55.188.184 +181.56.9.161 +181.57.156.186 +181.64.106.201 +181.64.233.120 +181.65.133.210 +181.66.51.109 +181.67.238.120 +181.74.133.166 +185.155.67.160 +185.177.74.179 +185.19.214.48 +185.65.186.192 +185.74.192.176 +185.98.232.119 +185.98.232.182 +186.0.195.194 +186.0.95.78 +186.1.180.214 +186.1.180.217 +186.1.183.102 +186.10.5.141 +186.10.5.142 +186.10.79.138 +186.101.136.10 +186.101.33.146 +186.103.154.101 +186.103.169.164 +186.103.169.166 +186.103.239.190 +186.117.128.92 +186.121.252.131 +186.147.161.185 +186.156.228.41 +186.156.236.98 +186.156.85.228 +186.178.10.78 +186.178.7.142 +186.18.225.108 +186.3.1.187 +186.38.79.28 +186.4.186.119 +186.4.192.202 +186.4.200.145 +186.42.161.30 +186.42.185.114 +186.42.188.110 +186.42.198.218 +186.42.97.154 +186.46.120.138 +186.46.128.102 +186.46.136.42 +186.46.138.14 +186.46.138.186 +186.46.151.22 +186.46.152.142 +186.46.153.174 +186.46.156.202 +186.46.163.102 +186.46.192.242 +186.46.232.34 +186.46.232.38 +186.46.250.14 +186.46.27.70 +186.46.28.14 +186.46.85.154 +186.46.88.226 +186.46.94.18 +186.46.94.54 +186.47.100.30 +186.47.102.58 +186.47.212.166 +186.47.23.118 +186.47.23.130 +186.47.232.250 +186.47.46.6 +186.47.72.198 +186.47.72.58 +186.47.96.90 +186.65.81.140 +186.67.90.12 +186.68.85.26 +186.68.93.46 +186.74.135.245 +186.74.190.82 +186.83.66.119 +186.88.21.87 +186.89.125.167 +186.89.227.238 +186.89.98.56 +186.91.185.237 +186.91.186.112 +186.91.222.58 +186.92.138.184 +186.92.144.24 +186.92.158.52 +186.92.167.175 +186.92.25.250 +186.92.85.128 +186.93.86.242 +186.95.18.58 +186.95.181.177 +186.95.220.5 +186.95.243.219 +186.95.57.249 +186.95.59.223 +187.11.250.36 +187.134.221.231 +187.141.79.55 +187.160.116.145 +187.160.149.88 +187.160.154.147 +187.160.245.156 +187.161.209.78 +187.161.228.47 +187.161.3.226 +187.188.182.116 +187.189.115.24 +187.189.26.10 +187.189.26.61 +187.189.47.196 +187.189.73.164 +187.189.75.110 +187.190.221.61 +187.190.64.28 +187.217.189.229 +187.237.138.186 +187.243.251.110 +187.243.251.30 +187.243.251.42 +187.246.183.227 +187.250.102.60 +187.49.217.2 +189.109.46.210 +189.111.166.103 +189.111.166.127 +189.122.171.234 +189.122.197.251 +189.126.103.252 +189.127.143.70 +189.14.68.130 +189.166.116.138 +189.19.10.23 +189.19.168.149 +189.19.60.123 +189.195.162.222 +189.196.15.9 +189.198.199.122 +189.198.239.74 +189.199.112.138 +189.20.207.150 +189.204.116.221 +189.204.219.149 +189.209.110.202 +189.211.209.44 +189.218.126.199 +189.218.127.200 +189.218.214.244 +189.218.68.175 +189.219.103.120 +189.219.229.50 +189.219.24.155 +189.219.241.16 +189.219.241.174 +189.219.76.118 +189.219.80.167 +189.219.88.243 +189.219.92.138 +189.219.99.41 +189.221.102.190 +189.23.208.37 +189.23.6.3 +189.26.245.173 +189.29.117.58 +189.29.27.185 +189.3.176.130 +189.37.28.147 +189.39.115.185 +189.47.129.62 +189.47.137.189 +189.53.181.9 +189.55.174.28 +189.55.219.176 +189.55.219.252 +189.56.61.33 +189.60.224.13 +189.79.63.28 +189.8.41.58 +190.0.22.10 +190.0.25.242 +190.0.35.6 +190.1.174.162 +190.101.137.157 +190.102.206.48 +190.103.31.61 +190.104.156.126 +190.104.179.138 +190.104.179.254 +190.104.195.210 +190.104.233.24 +190.104.69.9) +190.104.71.30 +190.107.16.37 +190.108.192.24 +190.109.169.41 +190.11.115.133 +190.11.121.126 +190.11.26.58 +190.110.192.124 +190.111.233.208 +190.111.243.170 +190.112.108.29 +190.112.126.143 +190.112.40.138 +190.112.40.54 +190.112.41.116 +190.112.42.186 +190.116.175.2 +190.117.101.146 +190.117.115.150 +190.117.167.168 +190.117.167.239 +190.117.181.60 +190.12.102.205 +190.12.18.134 +190.12.48.158 +190.12.58.187 +190.12.63.166 +190.12.65.66 +190.121.158.114 +190.121.158.122 +190.121.167.218 +190.121.29.235 +190.122.20.85 +190.128.30.14 +190.129.1.141 +190.129.25.59 +190.13.174.19 +190.130.207.201 +190.131.203.90 +190.131.223.210 +190.131.235.134 +190.131.254.91 +190.138.249.182 +190.139.101.154 +190.139.49.20 +190.14.213.26 +190.14.233.90 +190.14.237.66 +190.14.245.142 +190.141.37.122 +190.142.221.66 +190.142.221.81 +190.145.30.188 +190.145.45.150 +190.145.80.114 +190.147.1.8 +190.147.100.219 +190.147.134.57 +190.147.160.240 +190.15.207.242 +190.15.213.137 +190.15.221.21 +190.15.222.24 +190.151.10.226 +190.151.68.116 +190.152.149.114 +190.152.150.62 +190.152.16.43 +190.152.18.182 +190.152.19.190 +190.152.37.114 +190.152.37.58 +190.152.4.54 +190.153.137.15 +190.153.142.191 +190.153.210.237 +190.163.22.64 +190.171.215.160 +190.18.134.27 +190.18.207.50 +190.180.62.246 +190.184.31.194 +190.186.111.59 +190.186.242.12 +190.186.46.14 +190.186.46.7 +190.186.5.54 +190.186.55.194 +190.186.58.170 +190.186.65.201 +190.186.7.114 +190.186.7.38 +190.196.168.46 +190.196.4.51 +190.198.30.0 +190.198.89.58 +190.199.213.29 +190.199.245.82 +190.200.63.90 +190.202.15.83 +190.203.229.8 +190.203.51.177 +190.204.0.202 +190.205.220.125 +190.205.45.146 +190.206.1.33 +190.206.143.42 +190.206.2.24 +190.207.254.204 +190.210.37.42 +190.210.99.221 +190.210.99.241 +190.211.80.154 +190.214.1.26 +190.214.10.54 +190.214.14.38 +190.214.31.230 +190.214.44.74 +190.214.56.138 +190.214.56.142 +190.215.253.251 +190.216.198.123 +190.217.3.162 +190.217.4.102 +190.217.55.10 +190.217.6.121 +190.219.217.140 +190.221.138.29 +190.225.18.133 +190.226.227.140 +190.228.202.235 +190.228.33.114 +190.228.38.78 +190.228.47.10 +190.23.175.185 +190.232.115.179 +190.232.168.242 +190.234.168.144 +190.234.181.250 +190.236.177.213 +190.236.188.8 +190.238.11.199 +190.238.68.190 +190.239.144.37 +190.239.148.244 +190.24.145.124 +190.242.119.68 +190.248.136.229 +190.249.160.169 +190.253.230.54 +190.254.148.213 +190.3.36.105 +190.33.255.26 +190.37.117.144 +190.38.40.46 +190.39.158.1 +190.39.195.72 +190.4.1.150 +190.4.30.195 +190.40.230.185 +190.42.124.87 +190.42.184.52 +190.42.34.95 +190.43.120.91 +190.43.40.153 +190.44.84.131 +190.49.46.80 +190.5.117.153 +190.5.122.174 +190.5.92.18 +190.52.168.58 +190.52.175.76 +190.53.89.103 +190.57.144.102 +190.57.144.42 +190.57.169.226 +190.57.189.78 +190.6.203.124 +190.6.29.173 +190.6.35.203 +190.60.215.172 +190.60.234.131 +190.60.251.44 +190.60.4.91 +190.63.154.197 +190.63.182.230 +190.63.187.230 +190.64.135.122 +190.64.160.101 +190.7.144.202 +190.7.149.53 +190.73.0.234 +190.73.55.61 +190.74.169.21 +190.75.198.179 +190.75.222.40 +190.78.165.211 +190.78.82.216 +190.79.140.223 +190.79.143.123 +190.79.91.74 +190.81.177.26 +190.82.117.18 +190.82.117.20 +190.85.122.134 +190.85.146.156 +190.9.56.170 +190.9.59.198 +190.9.60.10 +190.90.122.26 +190.90.193.212 +190.90.218.253 +190.93.179.126 +190.94.247.254 +190.95.19.236 +190.95.7.235 +190.96.47.237 +190.96.91.243 +191.102.122.3 +191.102.125.74 +191.102.125.75 +191.102.84.196 +191.102.89.54 +191.103.252.169 +191.103.253.25 +191.103.253.89 +191.98.183.138 +191.98.183.139 +192.115.104.88 +192.116.226.69 +193.111.120.47 +193.153.38.221 +193.171.32.6 +193.173.119.83 +193.188.95.146 +193.194.69.155 +193.220.32.246 +193.251.1.244 +193.251.35.18 +193.30.164.3 +193.40.59.83 +193.52.195.6 +193.69.186.83 +193.86.86.86 +194.117.157.72 +194.149.220.21 +194.149.222.33 +194.176.176.82 +194.177.202.247 +194.30.228.83 +194.46.229.3 +194.55.138.53 +194.6.1.219 +194.79.113.83 +194.9.85.141 +194.90.179.13 +195.103.8.10 +195.135.236.215 +195.146.78.214 +195.160.188.163 +195.167.64.193 +195.209.224.91 +195.242.192.18 +195.246.155.219 +195.47.14.193 +195.54.22.74 +195.55.85.254 +195.76.242.227 +195.89.143.211 +195.97.171.76 +196.12.145.125 +196.20.7.74 +196.202.252.244 +196.203.172.166 +196.23.147.34 +196.23.52.170 +196.25.52.36 +196.27.116.60 +196.38.62.210 +196.40.43.34 +198.164.83.28 +198.49.131.250 +198.83.124.250 +199.193.10.202 +199.193.13.202 +199.216.215.21 +199.253.99.202 +2.136.85.132 +20.132.16.22 +200.101.13.202 +200.101.92.148 +200.102.191.228 +200.102.217.207 +200.104.104.91 +200.104.250.92 +200.105.148.74 +200.105.227.182 +200.107.29.70 +200.109.108.137 +200.109.119.126 +200.109.228.66 +200.109.72.53 +200.11.138.149 +200.110.13.169 +200.111.102.66 +200.111.121.18 +200.111.121.19 +200.111.121.21 +200.111.122.107 +200.112.216.5 +200.112.228.211 +200.112.70.53 +200.112.84.5 +200.114.104.4 +200.114.97.14 +200.115.25.118 +200.115.25.119 +200.115.25.120 +200.116.132.2 +200.116.209.58 +200.116.227.138 +200.116.227.99 +200.116.69.6 +200.117.240.56 +200.118.104.79 +200.118.168.237 +200.119.239.252 +200.119.56.48 +200.120.195.140 +200.120.224.207 +200.122.211.14 +200.123.102.149 +200.123.254.177 +200.123.50.43 +200.123.55.253 +200.125.202.198 +200.128.6.235 +200.135.246.2 +200.139.78.114 +200.14.126.36 +200.140.171.24 +200.142.99.166 +200.146.85.16 +200.148.230.217 +200.150.139.211 +200.152.107.56 +200.158.26.223 +200.159.216.247 +200.16.208.187 +200.160.244.246 +200.160.96.39 +200.161.108.72 +200.161.118.198 +200.161.125.6 +200.161.31.11 +200.161.81.98 +200.162.113.32 +200.165.140.104 +200.167.145.129 +200.167.86.209 +200.168.152.152 +200.171.17.23 +200.171.175.157 +200.171.232.140 +200.174.85.193 +200.174.85.195 +200.187.136.122 +200.192.210.98 +200.195.95.38 +200.2.125.90 +200.202.214.4 +200.204.121.196 +200.204.154.29 +200.204.235.123 +200.205.87.106 +200.206.46.178 +200.207.126.232 +200.207.48.252 +200.207.9.168 +200.209.175.243 +200.21.225.82 +200.21.24.79 +200.217.16.202 +200.217.53.234 +200.217.76.37 +200.221.10.104 +200.225.0.174 +200.232.184.253 +200.232.94.34 +200.233.77.95 +200.242.95.171 +200.245.35.131 +200.25.250.233 +200.252.201.144 +200.27.164.196 +200.29.191.151 +200.3.207.39 +200.30.101.2 +200.31.137.58 +200.31.42.3 +200.35.51.206 +200.37.231.66 +200.37.80.11 +200.4.253.27 +200.40.113.134 +200.41.230.102 +200.41.230.105 +200.42.225.106 +200.42.45.211 +200.43.138.196 +200.43.187.129 +200.44.217.52 +200.45.22.134 +200.45.32.150 +200.46.109.82 +200.48.106.34 +200.48.129.123 +200.49.181.162 +200.5.83.150 +200.50.170.38 +200.52.4.82 +200.54.103.76 +200.54.108.54 +200.54.194.12 +200.54.212.234 +200.55.208.203 +200.55.219.178 +200.59.9.18 +200.6.180.2 +200.6.225.122 +200.60.16.22 +200.61.19.208 +200.61.6.50 +200.65.129.1 +200.65.129.2 +200.67.117.246 +200.67.85.1 +200.68.34.99 +200.68.97.116 +200.69.245.33 +200.7.252.182 +200.72.187.74 +200.72.187.75 +200.74.158.86 +200.75.12.213 +200.75.9.35 +200.80.230.10 +200.81.172.98 +200.82.157.103 +200.83.4.60 +200.84.128.104 +200.84.138.166 +200.84.14.108 +200.84.150.113 +200.84.151.133 +200.84.181.210 +200.84.47.228 +200.85.123.154 +200.85.37.254 +200.85.59.250 +200.87.180.226 +200.87.43.50 +200.88.223.99 +200.89.129.99 +200.90.148.195 +200.93.43.43 +200.93.82.36 +200.94.92.230 +200.95.239.254 +200.96.193.100 +201.0.17.76 +201.1.113.10 +201.1.63.111 +201.10.42.166 +201.100.42.12 +201.116.199.243 +201.116.70.1 +201.12.116.112 +201.122.180.91 +201.13.169.167 +201.13.176.9 +201.13.187.229 +201.132.155.10 +201.132.155.70 +201.132.160.210 +201.132.162.254 +201.14.225.222 +201.144.14.229 +201.148.23.69 +201.15.143.25 +201.15.218.158 +201.15.30.1 +201.155.194.182 +201.16.232.37 +201.160.37.2 +201.165.55.14 +201.166.150.158 +201.166.23.226 +201.167.56.18 +201.17.163.70 +201.17.188.5 +201.172.123.9 +201.172.139.205 +201.172.194.92 +201.172.80.223 +201.173.158.27 +201.173.223.180 +201.173.240.145 +201.183.235.77 +201.184.224.98 +201.184.227.178 +201.184.237.194 +201.184.244.186 +201.184.250.76 +201.184.252.42 +201.184.72.34 +201.184.81.178 +201.184.86.218 +201.187.110.174 +201.190.181.76 +201.20.89.10 +201.208.117.43 +201.208.133.140 +201.208.155.57 +201.208.38.95 +201.208.43.80 +201.209.12.56 +201.209.173.54 +201.211.154.15 +201.211.189.154 +201.213.54.67 +201.216.213.201 +201.217.217.26 +201.217.245.108 +201.217.246.34 +201.219.184.227 +201.219.218.17 +201.219.218.18 +201.220.84.146 +201.222.52.30 +201.222.55.93 +201.222.99.12 +201.228.89.170 +201.229.208.2 +201.229.208.3 +201.238.239.88 +201.24.125.218 +201.240.212.1 +201.240.52.170 +201.242.120.234 +201.242.192.120 +201.243.173.251 +201.243.63.12 +201.245.190.38 +201.246.116.96 +201.249.52.100 +201.249.88.226 +201.251.126.164 +201.252.106.88 +201.252.14.124 +201.252.211.201 +201.253.124.65 +201.253.144.1 +201.253.9.185 +201.255.141.247 +201.255.178.224 +201.26.133.204 +201.26.169.10 +201.26.19.180 +201.26.200.105 +201.26.212.10 +201.26.8.186 +201.27.2.220 +201.3.184.222 +201.31.247.225 +201.39.29.50 +201.42.69.73 +201.43.31.164 +201.44.24.98 +201.45.188.169 +201.53.148.108 +201.53.73.44 +201.59.184.124 +201.66.27.218 +201.68.18.124 +201.68.227.8 +201.68.244.150 +201.68.77.129 +201.73.45.70 +201.73.73.130 +201.74.205.134 +201.75.20.103 +201.75.78.76 +201.76.141.210 +201.76.29.82 +201.80.187.222 +201.80.207.132 +201.86.70.162 +201.88.248.243 +201.93.128.110 +202.102.73.145 +202.104.189.20 +202.104.20.181 +202.105.138.19 +202.105.182.12 +202.105.182.13 +202.105.182.15 +202.105.182.16 +202.105.182.20 +202.105.182.33 +202.105.182.87 +202.105.230.226 +202.106.139.88 +202.108.122.38 +202.110.204.18 +202.114.66.170 +202.115.202.250 +202.12.73.20 +202.129.181.242 +202.134.202.251 +202.141.25.92 +202.143.140.250 +202.143.154.242 +202.145.3.101 +202.147.168.58 +202.153.41.102 +202.164.191.186 +202.168.193.131 +202.177.119.4 +202.188.222.2 +202.194.133.31 +202.194.202.7 +202.239.243.116 +202.29.137.147 +202.3.217.125 +202.39.6.27 +202.41.105.162 +202.41.181.24 +202.44.8.100 +202.54.169.233 +202.54.217.164 +202.54.61.99 +202.58.163.201 +202.58.86.8 +202.63.177.3 +202.65.43.172 +202.70.36.242 +202.70.88.138 +202.71.240.33 +202.78.225.1 +202.78.227.32 +202.80.127.29 +202.87.179.206 +202.9.136.40 +202.94.203.89 +202.94.212.199 +202.95.169.175 +202.98.141.200 +202.98.23.114 +202.98.23.116 +202.99.225.45 +202.99.29.27 +203.101.61.76 +203.110.240.22 +203.113.137.66 +203.113.34.239 +203.117.67.122 +203.123.240.112 +203.124.21.224 +203.129.53.177 +203.130.203.41 +203.149.32.30 +203.151.40.4 +203.155.16.130 +203.158.167.152 +203.160.1.103 +203.160.1.112 +203.160.1.121 +203.160.1.130 +203.160.1.66 +203.160.1.75 +203.160.1.85 +203.160.1.94 +203.162.183.222 +203.186.92.119 +203.193.138.148 +203.196.67.107 +203.197.37.46 +203.200.75.165 +203.202.203.8 +203.67.172.29 +203.69.244.194 +203.69.39.251 +203.76.185.189 +203.78.11.73 +203.82.52.210 +203.86.31.92 +204.111.219.182 +204.196.104.27 +204.85.72.128 +205.138.18.80 +205.200.37.111 +206.174.3.131 +206.230.106.206 +206.49.33.250 +206.51.224.46 +207.102.0.15 +207.161.20.188 +207.167.236.137 +207.181.207.36 +207.182.152.72 +207.192.207.240 +207.192.209.237 +207.248.102.18 +207.248.230.70 +207.249.163.139 +207.38.251.111 +207.50.148.37 +207.61.241.100 +207.61.38.67 +207.99.23.198 +208.107.124.142 +208.107.18.112 +208.110.73.34 +208.34.14.113 +208.34.14.165 +208.53.196.161 +208.53.199.48 +208.53.199.75 +208.62.125.146 +208.66.171.217 +208.77.219.76 +208.96.122.142 +208.96.133.198 +208.96.213.149 +208.98.17.40 +209.1.163.63 +209.124.242.193 +209.137.150.138 +209.145.114.173 +209.159.184.219 +209.159.204.250 +209.159.228.202 +209.159.229.219 +209.159.241.112 +209.17.186.25 +209.195.4.27 +209.211.7.12 +209.218.218.171 +209.4.229.39 +209.45.108.175 +209.45.48.205 +209.47.38.116 +209.79.65.6 +209.89.66.6 +21.11.27.110 +210.12.86.181 +210.163.167.162 +210.192.111.173 +210.204.118.194 +210.21.12.94 +210.219.227.52 +210.23.124.178 +210.240.54.8 +210.245.63.218 +210.245.80.10 +210.245.80.15 +210.254.8.52 +210.34.14.166 +210.4.10.134 +210.51.4.173 +210.52.15.210 +210.74.254.35 +210.8.92.2 +210.82.40.243 +210.86.181.202 +210.92.128.194 +210.96.65.4 +211.108.62.230 +211.114.116.60 +211.115.185.41 +211.115.185.42 +211.115.185.44 +211.138.198.6 +211.139.120.69 +211.140.138.39 +211.140.151.214 +211.140.192.186 +211.15.62.123 +211.161.197.182 +211.21.111.227 +211.233.21.166 +211.45.21.165 +211.76.175.5 +211.90.114.199 +211.90.22.106 +211.93.108.113 +211.99.188.218 +212.102.0.104 +212.116.219.202 +212.117.63.145 +212.119.69.187 +212.12.157.130 +212.123.91.61 +212.138.84.62 +212.15.44.9 +212.165.156.74 +212.17.86.109 +212.179.127.188 +212.225.233.154 +212.231.65.8 +212.24.238.155 +212.243.183.5 +212.38.100.62 +212.4.98.158 +212.44.61.185 +212.60.65.206 +212.72.120.135 +212.76.90.2 +212.93.193.82 +213.137.130.166 +213.154.216.55 +213.158.112.202 +213.16.133.130 +213.16.20.140 +213.171.255.2 +213.174.145.1 +213.174.145.193 +213.174.145.250 +213.180.131.135 +213.185.116.152 +213.186.116.57 +213.212.75.12 +213.231.139.130 +213.249.237.196 +213.25.170.98 +213.25.29.12 +213.250.162.237 +213.255.229.205 +213.27.152.15 +213.27.71.250 +213.4.106.85 +213.4.106.86 +213.55.87.105 +213.55.87.205 +213.82.91.94 +213.97.52.28 +216.114.194.18 +216.117.225.240 +216.119.183.110 +216.127.32.22 +216.168.43.11 +216.19.216.44 +216.217.98.100 +216.228.57.247 +216.23.162.169 +216.230.72.76 +216.241.14.94 +216.241.36.82 +216.36.141.205 +216.72.196.21 +216.72.63.198 +216.80.118.13 +216.93.253.150 +217.10.246.2 +217.10.246.4 +217.117.136.88 +217.126.5.224 +217.147.30.24 +217.153.114.66 +217.174.98.198 +217.218.243.61 +217.219.211.89 +217.31.51.76 +217.31.51.77 +217.70.56.161 +218.104.180.228 +218.118.100.86 +218.127.146.43 +218.14.227.197 +218.14.227.198 +218.15.63.46 +218.202.48.81 +218.206.194.247 +218.229.29.128 +218.241.81.219 +218.248.20.160 +218.248.31.212 +218.249.83.87 +218.252.37.227 +218.26.204.66 +218.28.46.250 +218.28.58.86 +218.5.133.146 +218.50.52.210 +218.56.32.230 +218.56.64.210 +218.56.64.211 +218.56.64.212 +218.56.64.213 +218.58.136.14 +218.64.88.30 +218.7.48.22 +218.70.172.2 +218.75.100.114 +218.75.25.137 +218.75.76.74 +218.75.83.98 +218.76.207.31 +218.9.114.85 +218.94.9.38 +218.97.194.94 +219.113.113.52 +219.191.64.95 +219.208.194.33 +219.22.30.44 +219.240.36.173 +219.240.36.175 +219.25.212.32 +219.34.2.65 +219.37.208.150 +219.43.150.92 +219.43.228.154 +219.58.72.191 +219.64.91.118 +220.128.189.35 +220.130.81.188 +220.173.139.172 +220.194.55.160 +220.225.225.148 +220.227.138.82 +220.227.47.2 +220.227.47.20 +220.227.47.6 +220.231.29.99 +220.33.204.136 +220.5.108.156 +220.53.245.8 +220.56.96.67 +220.58.24.40 +220.6.120.99 +220.70.2.137 +221.11.27.110 +221.120.196.138 +221.122.66.84 +221.130.177.147 +221.139.50.83 +221.16.4.11 +221.178.141.159 +221.192.132.194 +221.2.216.38 +221.202.118.17 +221.224.95.158 +221.233.134.87 +221.3.2.61 +221.6.62.90 +221.8.74.211 +222.124.172.220 +222.191.242.225 +222.221.6.144 +222.240.208.14 +222.247.62.195 +222.35.90.16 +222.68.206.11 +222.68.207.11 +222.73.205.27 +222.83.228.17 +222.83.228.34 +222.86.132.20 +222.92.116.39 +24.10.186.31 +24.10.84.226 +24.100.18.152 +24.108.129.42 +24.108.35.246 +24.11.124.76 +24.11.20.98 +24.11.68.191 +24.11.90.41 +24.111.38.223 +24.116.11.52 +24.116.206.33 +24.118.147.89 +24.118.240.6 +24.119.169.151 +24.119.54.123 +24.12.214.237 +24.12.3.143 +24.125.125.26 +24.125.155.7 +24.125.158.91 +24.125.244.207 +24.125.73.200 +24.125.77.116 +24.127.113.227 +24.13.108.167 +24.131.171.96 +24.136.244.198 +24.137.215.227 +24.139.68.242 +24.14.107.77 +24.14.112.139 +24.140.13.237 +24.143.226.7 +24.151.126.249 +24.153.249.240 +24.154.129.8 +24.156.135.87 +24.16.192.63 +24.161.131.67 +24.167.83.34 +24.168.26.236 +24.170.37.75 +24.170.82.144 +24.170.90.111 +24.174.246.62 +24.175.116.55 +24.175.131.67 +24.175.136.94 +24.175.147.159 +24.182.38.24 +24.185.121.80 +24.185.21.20 +24.188.121.167 +24.188.125.225 +24.188.251.54 +24.189.5.235 +24.190.104.34 +24.190.214.200 +24.192.240.240 +24.193.87.7 +24.197.130.9 +24.2.24.6 +24.2.69.26 +24.20.45.101 +24.205.202.45 +24.208.222.208 +24.208.37.143 +24.210.148.161 +24.211.221.167 +24.211.49.0 +24.215.67.225 +24.215.89.254 +24.217.148.83 +24.217.194.73 +24.22.86.147 +24.228.49.186 +24.23.182.99 +24.23.199.14 +24.23.29.41 +24.230.163.136 +24.230.181.207 +24.230.182.225 +24.230.46.187 +24.237.24.27 +24.238.20.152 +24.242.236.98 +24.250.66.218 +24.254.113.238 +24.254.28.207 +24.254.34.183 +24.255.169.250 +24.255.247.79 +24.3.105.116 +24.3.253.43 +24.30.58.77 +24.30.62.26 +24.30.90.20 +24.4.239.144 +24.44.199.190 +24.44.219.167 +24.44.95.243 +24.45.174.137 +24.46.74.16 +24.47.247.186 +24.59.34.24 +24.61.52.46 +24.67.14.108 +24.7.230.76 +24.70.39.70 +24.77.207.136 +24.77.22.225 +24.78.169.73 +24.78.186.206 +24.8.191.246 +24.83.40.206 +24.83.69.55 +24.85.205.35 +24.89.198.236 +24.89.218.113 +24.9.22.230 +24.90.184.242 +24.98.12.137 +24.98.204.26 +24.98.81.111 +24.98.99.4 +31.25.181.98 +37.46.158.141 +37.46.158.199 +38.123.201.17 +41.205.107.65 +41.210.252.11 +41.211.224.66 +41.211.232.39 +41.221.177.29 +45.4.88.66 +45.7.132.58 +46.37.120.155 +5.154.37.28 +5.40.117.250 +5.40.117.253 +5.61.212.42 +58.137.8.85 +58.17.3.2 +58.181.22.190 +58.221.41.86 +58.222.254.13 +58.29.56.2 +58.56.108.114 +58.68.51.46 +58.69.190.50 +58.8.150.146 +58.83.197.27 +59.120.104.60 +59.144.175.48 +59.165.1.214 +59.177.176.21 +59.36.98.154 +59.39.145.178 +59.42.250.145 +59.45.207.24 +59.56.174.199 +59.92.21.80 +59.92.3.208 +59.94.177.245 +59.94.38.39 +59.94.41.39 +59.95.205.216 +60.12.227.209 +60.191.246.17 +60.191.96.90 +60.213.185.214 +60.218.99.18 +60.247.2.241 +60.250.139.213 +60.250.27.242 +60.28.182.205 +60.28.209.8 +60.32.115.204 +60.49.216.82 +60.49.51.63 +60.5.108.142 +61.12.149.73 +61.120.148.32 +61.131.48.219 +61.133.196.36 +61.133.196.40 +61.135.158.109 +61.135.158.125 +61.135.158.129 +61.135.158.130 +61.138.130.229 +61.139.73.6 +61.142.169.98 +61.142.81.37 +61.144.109.96 +61.152.154.19 +61.153.140.106 +61.156.42.123 +61.159.214.215 +61.159.235.36 +61.166.68.71 +61.166.68.72 +61.167.117.83 +61.17.232.227 +61.172.246.180 +61.172.249.94 +61.172.249.96 +61.175.139.6 +61.175.226.78 +61.178.128.208 +61.180.73.66 +61.182.66.53 +61.187.187.28 +61.19.158.212 +61.19.78.44 +61.234.254.69 +61.236.87.104 +61.238.104.200 +61.244.119.198 +61.27.47.49 +61.32.11.130 +61.54.82.130 +61.6.163.30 +61.60.55.220 +61.8.73.59 +61.91.242.19 +62.103.76.22 +62.119.28.242 +62.15.205.71 +62.159.143.172 +62.164.251.180 +62.168.41.61 +62.192.132.126 +62.193.246.10 +62.215.195.85 +62.49.143.76 +62.76.32.166 +62.81.76.18 +62.90.57.24 +62.99.77.116 +63.119.14.20 +64.111.32.54 +64.114.203.21 +64.17.66.234 +64.179.170.189 +64.184.203.60 +64.203.49.117 +64.207.224.70 +64.30.123.252 +64.58.28.250 +64.66.192.61 +64.79.197.36 +64.79.199.35 +64.79.209.203 +64.83.209.110 +64.86.28.118 +64.94.19.10 +65.190.207.153 +65.191.10.80 +65.255.71.17 +65.255.74.9 +65.28.103.187 +65.28.107.26 +65.28.109.2 +65.28.8.13 +65.29.182.114 +65.29.85.76 +65.30.216.140 +65.30.92.48 +65.35.247.52 +65.7.5.33 +65.75.189.33 +65.87.170.39 +66.128.83.112 +66.131.89.59 +66.135.163.52 +66.158.95.37 +66.165.197.13 +66.166.1.181 +66.167.100.59 +66.167.228.62 +66.168.71.215 +66.177.219.202 +66.178.127.24 +66.197.167.90 +66.198.41.11 +66.199.247.42 +66.214.17.189 +66.214.213.172 +66.219.59.95 +66.225.182.65 +66.229.205.251 +66.233.165.31 +66.234.204.35 +66.244.214.230 +66.244.214.249 +66.25.114.65 +66.253.168.169 +66.29.36.95 +66.38.121.88 +66.41.14.103 +66.41.150.79 +66.41.189.96 +66.56.175.206 +66.56.183.16 +66.57.1.142 +66.57.230.14 +66.57.75.68 +66.67.106.227 +66.90.237.241 +66.91.101.52 +67.100.121.150 +67.149.165.201 +67.149.215.109 +67.163.161.226 +67.168.222.227 +67.182.204.248 +67.184.38.23 +67.188.156.177 +67.19.148.234 +67.191.141.209 +67.191.220.137 +67.201.77.7 +67.48.16.231 +67.48.22.73 +67.48.23.137 +67.49.150.210 +67.49.162.117 +67.60.113.96 +67.65.242.94 +67.69.254.240 +67.69.254.241 +67.69.254.242 +67.69.254.244 +67.69.254.245 +67.69.254.246 +67.69.254.247 +67.69.254.248 +67.69.254.249 +67.69.254.250 +67.69.254.251 +67.69.254.252 +67.69.254.254 +67.69.254.255 +67.80.47.215 +67.80.81.65 +67.81.185.139 +67.81.225.132 +67.81.235.37 +67.82.22.33 +67.84.115.34 +67.84.148.144 +67.84.35.131 +67.84.84.89 +67.86.193.156 +67.87.64.23 +67.9.1.214 +67.9.20.215 +67.9.23.8 +67.9.25.132 +67.9.255.2 +67.9.28.224 +67.9.5.59 +67.9.5.92 +68.1.131.15 +68.10.87.155 +68.100.213.42 +68.102.90.174 +68.104.55.221 +68.105.0.173 +68.105.12.164 +68.108.224.145 +68.108.23.74 +68.109.170.127 +68.11.145.150 +68.11.181.113 +68.11.181.82 +68.11.182.166 +68.11.226.141 +68.11.237.184 +68.11.249.230 +68.111.127.37 +68.111.158.24 +68.111.231.178 +68.111.57.10 +68.113.102.37 +68.114.149.164 +68.115.77.233 +68.117.211.122 +68.118.245.35 +68.12.148.195 +68.12.169.13 +68.13.220.63 +68.144.70.254 +68.148.10.200 +68.148.86.171 +68.149.66.202 +68.173.44.218 +68.184.185.242 +68.184.56.83 +68.192.166.141 +68.198.151.89 +68.198.72.147 +68.199.107.24 +68.199.140.101 +68.2.143.105 +68.201.24.46 +68.201.46.56 +68.205.170.214 +68.207.186.253 +68.207.187.211 +68.224.71.23 +68.227.129.204 +68.228.236.251 +68.229.158.96 +68.36.188.103 +68.42.247.66 +68.44.106.173 +68.45.42.160 +68.51.214.245 +68.52.31.249 +68.54.70.19 +68.55.215.207 +68.55.225.102 +68.59.217.62 +68.59.44.32 +68.60.168.230 +68.60.188.125 +68.60.189.199 +68.62.176.8 +68.62.218.171 +68.62.240.77 +68.63.48.36 +68.8.224.217 +68.8.227.134 +68.81.191.233 +68.83.114.81 +68.83.156.112 +68.84.126.225 +68.84.47.147 +68.9.156.171 +68.9.242.26 +68.97.121.200 +68.97.127.248 +68.97.206.212 +68.98.0.233 +69.113.232.218 +69.114.237.205 +69.114.82.211 +69.115.86.239 +69.116.109.16 +69.116.42.119 +69.118.237.19 +69.119.206.230 +69.122.153.0 +69.122.222.90 +69.123.43.126 +69.123.44.118 +69.126.24.134 +69.127.102.247 +69.127.115.255 +69.127.175.231 +69.136.136.125 +69.136.58.38 +69.136.70.21 +69.138.220.71 +69.138.46.194 +69.139.124.172 +69.139.167.203 +69.142.108.83 +69.142.201.72 +69.151.73.128 +69.154.135.199 +69.161.78.160 +69.180.245.32 +69.180.8.201 +69.181.89.167 +69.212.49.215 +69.224.140.241 +69.23.105.226 +69.23.122.70 +69.242.176.42 +69.242.220.173 +69.245.152.207 +69.245.52.76 +69.246.117.136 +69.246.123.195 +69.246.123.26 +69.246.16.28 +69.246.218.125 +69.246.45.182 +69.246.61.14 +69.249.151.19 +69.250.19.191 +69.250.8.55 +69.253.188.82 +69.253.79.204 +69.254.246.123 +69.255.105.108 +69.46.16.232 +69.47.174.178 +69.62.141.46 +69.64.38.125 +69.65.42.44 +69.7.105.30 +69.71.85.202 +69.71.95.69 +69.81.17.45 +69.86.12.190 +69.86.90.176 +69.88.35.197 +69.92.244.253 +69.92.76.23 +70.117.243.108 +70.118.212.66 +70.119.146.106 +70.123.89.57 +70.125.110.220 +70.127.205.107 +70.161.20.242 +70.161.72.245 +70.161.93.238 +70.172.242.76 +70.176.119.94 +70.176.124.127 +70.176.191.105 +70.177.51.135 +70.177.53.179 +70.177.61.32 +70.180.206.70 +70.180.62.153 +70.186.168.130 +70.186.174.186 +70.225.202.96 +70.238.144.197 +70.238.47.146 +70.44.209.4 +70.45.34.75 +70.64.225.85 +70.64.250.176 +70.64.252.25 +70.66.218.123 +70.67.105.211 +70.72.152.13 +70.74.213.38 +70.76.83.81 +70.78.22.121 +70.82.140.29 +70.86.138.210 +70.89.83.173 +70.95.110.195 +71.10.72.221 +71.11.157.62 +71.14.40.151 +71.14.94.250 +71.14.95.198 +71.178.155.167 +71.192.111.90 +71.192.233.196 +71.192.234.31 +71.194.0.41 +71.194.217.243 +71.197.189.88 +71.200.233.55 +71.201.60.149 +71.203.142.40 +71.203.162.68 +71.203.213.53 +71.205.102.196 +71.205.107.223 +71.205.109.70 +71.205.111.45 +71.205.113.223 +71.205.123.131 +71.205.238.140 +71.205.238.236 +71.205.37.198 +71.206.111.22 +71.207.56.148 +71.224.107.188 +71.224.87.71 +71.225.31.105 +71.229.16.100 +71.233.34.93 +71.235.86.254 +71.237.41.13 +71.237.98.13 +71.238.39.207 +71.239.246.54 +71.250.251.214 +71.252.182.73 +71.41.99.190 +71.57.11.142 +71.7.246.230 +71.8.7.200 +71.8.98.36 +71.80.99.54 +71.82.77.13 +71.82.9.16 +71.85.121.118 +71.86.144.144 +71.86.150.78 +71.86.152.122 +71.89.55.232 +71.90.230.116 +72.128.40.214 +72.135.0.206 +72.137.122.212 +72.140.93.121 +72.141.35.81 +72.172.203.25 +72.174.123.174 +72.174.161.51 +72.175.137.173 +72.178.207.128 +72.178.248.236 +72.190.118.76 +72.190.122.130 +72.196.135.11 +72.197.212.200 +72.200.193.225 +72.203.130.111 +72.207.200.241 +72.207.59.75 +72.213.190.99 +72.213.194.229 +72.216.12.173 +72.219.39.216 +72.222.172.142 +72.222.180.102 +72.227.236.241 +72.227.36.24 +72.229.126.142 +72.24.129.249 +72.24.145.190 +72.24.212.232 +72.39.117.31 +72.4.82.241 +72.52.96.51 +72.9.82.131 +74.105.84.51 +74.129.180.10 +74.131.139.186 +74.137.109.66 +74.141.111.159 +74.15.86.86 +74.171.2.153 +74.174.5.68 +74.193.33.187 +74.194.57.223 +74.194.61.154 +74.196.148.50 +74.197.219.75 +74.210.84.24 +74.222.1.99 +74.223.183.66 +74.54.156.73 +74.55.86.131 +74.56.176.187 +74.71.197.158 +74.73.99.82 +74.77.117.65 +75.109.46.197 +75.136.135.2 +75.153.242.91 +75.179.140.16 +75.180.26.184 +75.183.7.150 +75.184.41.3 +75.34.137.36 +75.64.250.79 +75.64.35.123 +75.65.64.163 +75.66.105.218 +75.71.42.32 +75.74.84.122 +75.81.22.134 +75.83.57.219 +75.85.136.141 +75.87.150.14 +75.87.189.110 +75.93.212.146 +75.94.80.132 +76.100.211.252 +76.102.95.54 +76.105.105.96 +76.106.127.211 +76.107.108.144 +76.107.116.228 +76.107.117.227 +76.107.125.246 +76.107.136.73 +76.107.136.74 +76.107.137.6 +76.107.142.104 +76.107.151.18 +76.107.156.235 +76.107.208.13 +76.107.38.217 +76.107.42.95 +76.107.44.181 +76.107.93.147 +76.109.161.247 +76.11.23.85 +76.110.119.179 +76.110.138.122 +76.110.211.162 +76.112.137.136 +76.112.150.1 +76.112.25.186 +76.113.8.160 +76.115.37.7 +76.116.82.97 +76.117.113.157 +76.117.201.40 +76.122.108.158 +76.123.128.94 +76.123.18.157 +76.124.36.104 +76.127.22.84 +76.160.138.68 +76.17.104.79 +76.17.69.212 +76.17.88.209 +76.170.85.232 +76.173.155.23 +76.173.95.124 +76.176.208.180 +76.178.184.139 +76.182.57.113 +76.183.112.143 +76.187.117.138 +76.20.228.53 +76.214.55.31 +76.22.0.234 +76.22.128.2 +76.244.189.250 +76.247.168.177 +76.25.236.65 +76.27.54.31 +76.28.1.186 +76.28.208.70 +76.28.250.36 +76.29.10.61 +76.29.243.55 +76.69.32.139 +76.85.159.130 +76.88.67.220 +76.89.19.252 +76.89.23.238 +76.9.42.163 +76.94.48.145 +76.98.47.148 +77.100.110.112 +77.100.241.213 +77.101.103.239 +77.101.103.91 +77.101.58.117 +77.102.121.34 +77.102.178.145 +77.102.220.82 +77.102.25.197 +77.103.130.91 +77.103.153.29 +77.103.254.43 +77.103.84.134 +77.226.237.178 +77.226.240.50 +77.237.91.60 +77.240.82.6 +77.242.169.70 +77.242.233.44 +77.242.33.5 +77.244.218.34 +77.41.140.70 +77.71.0.158 +77.78.1.119 +77.88.66.251 +77.96.105.84 +77.96.143.223 +77.97.103.232 +77.97.84.28 +77.98.146.168 +77.98.146.183 +77.99.11.82 +77.99.113.100 +77.99.162.166 +77.99.183.136 +77.99.30.244 +78.109.149.162 +78.129.239.35 +78.138.131.150 +78.154.132.241 +78.162.45.2 +78.224.128.22 +78.24.49.96 +78.31.64.74 +78.38.244.2 +78.38.244.9 +78.43.175.129 +79.152.2.193 +79.156.24.91 +80.0.76.158 +80.105.84.250 +80.127.3.115 +80.143.220.5 +80.150.14.123 +80.153.156.21 +80.192.214.147 +80.192.55.191 +80.192.75.52 +80.193.155.208 +80.193.158.197 +80.193.189.226 +80.193.72.145 +80.195.248.30 +80.195.3.136 +80.195.53.253 +80.227.1.101 +80.237.140.233 +80.237.38.77 +80.247.71.56 +80.25.120.104 +80.26.178.158 +80.34.164.229 +80.36.35.109 +80.4.59.69 +80.4.60.88 +80.88.242.32 +80.89.58.59 +80.90.160.194 +80.98.201.218 +81.101.145.245 +81.101.146.0 +81.104.137.210 +81.104.140.27 +81.104.254.45 +81.177.3.10 +81.18.116.70 +81.189.106.138 +81.192.153.91 +81.203.116.165 +81.21.97.68 +81.211.104.58 +81.214.184.188 +81.31.157.38 +81.34.252.228 +81.43.23.51 +81.52.167.82 +81.82.192.93 +81.96.121.31 +81.96.127.75 +81.97.147.154 +81.98.109.201 +82.0.100.211 +82.0.70.181 +82.11.211.202 +82.12.101.34 +82.12.118.67 +82.13.13.173 +82.13.85.245 +82.130.196.153 +82.130.196.97 +82.130.246.64 +82.146.33.201 +82.154.126.143 +82.158.219.108 +82.200.165.143 +82.21.1.166 +82.21.184.178 +82.21.51.247 +82.22.138.43 +82.230.7.246 +82.234.51.250 +82.238.32.72 +82.239.187.75 +82.24.15.141 +82.24.250.31 +82.245.149.3 +82.28.185.247 +82.28.30.130 +82.29.230.10 +82.3.162.235 +82.3.225.58 +82.3.97.212 +82.32.221.58 +82.33.108.2 +82.33.117.189 +82.33.168.194 +82.33.46.103 +82.33.67.71 +82.34.108.122 +82.34.224.141 +82.35.201.216 +82.35.243.181 +82.35.91.170 +82.36.209.11 +82.36.86.70 +82.37.169.145 +82.38.36.40 +82.39.199.238 +82.4.211.107 +82.4.47.67 +82.40.215.66 +82.40.28.187 +82.40.48.179 +82.41.10.6 +82.41.198.251 +82.41.21.126 +82.41.221.10 +82.41.4.227 +82.41.5.12 +82.41.56.62 +82.41.57.26 +82.42.57.203 +82.43.58.68 +82.43.63.99 +82.44.235.63 +82.44.34.27 +82.44.97.222 +82.45.110.245 +82.45.117.238 +82.45.253.25 +82.45.254.205 +82.45.59.203 +82.46.144.165 +82.46.169.181 +82.46.23.204 +82.46.44.15 +82.47.59.57 +82.5.60.63 +82.6.16.219 +82.6.69.14 +82.7.105.26 +82.76.17.46 +82.8.80.191 +82.9.52.183 +83.100.149.29 +83.111.81.109 +83.142.23.194 +83.17.123.186 +83.2.212.9 +83.218.188.208 +83.220.195.232 +83.231.34.133 +83.231.34.192 +83.231.34.236 +83.236.157.231 +83.36.162.217 +83.61.22.207 +83.64.115.103 +83.70.106.206 +83.85.27.225 +83.96.39.196 +84.12.135.98 +84.194.92.1 +84.198.148.132 +84.198.202.74 +84.204.73.154 +84.235.0.182 +84.236.171.66 +84.247.24.127 +84.255.246.20 +84.67.132.233 +84.71.161.123 +85.114.131.54 +85.131.161.84 +85.134.160.128 +85.142.20.122 +85.168.233.221 +85.214.52.253 +85.214.59.79 +85.219.5.109 +85.24.89.199 +85.31.91.114 +85.70.156.138 +85.84.213.189 +86.0.224.116 +86.10.109.253 +86.10.147.26 +86.101.185.109 +86.101.185.112 +86.101.185.97 +86.101.185.99 +86.105.181.238 +86.109.100.80 +86.11.208.239 +86.12.57.51 +86.12.7.19 +86.125.142.141 +86.15.193.138 +86.2.31.207 +86.20.87.54 +86.21.200.186 +86.22.7.232 +86.24.213.144 +86.25.180.145 +86.3.40.90 +86.4.20.251 +86.4.25.128 +86.42.180.157 +86.42.243.36 +86.46.156.172 +86.61.76.7 +86.9.124.75 +87.116.164.85 +87.120.67.39 +87.252.3.67 +87.66.29.96 +87.86.13.29 +87.94.43.58 +88.104.209.63 +88.104.67.106 +88.108.166.245 +88.110.83.81 +88.12.16.211 +88.165.169.130 +88.171.218.44 +88.172.20.212 +88.173.201.9 +88.173.228.213 +88.174.252.233 +88.183.152.141 +88.191.63.104 +88.191.63.27 +88.191.66.131 +88.191.69.101 +88.191.98.15 +88.198.57.182 +88.2.237.249 +88.208.219.155 +88.22.10.27 +88.26.196.190 +88.5.211.84 +89.140.79.175 +89.146.71.82 +89.163.30.175 +89.19.172.22 +89.206.8.242 +89.21.137.70 +89.212.253.19 +89.234.27.15 +89.241.213.95 +89.248.194.212 +89.29.195.27 +89.39.142.121 +89.96.169.141 +90.155.218.74 +90.157.115.140 +90.173.78.226 +90.199.136.7 +91.110.151.89 +91.121.147.12 +91.121.48.207 +91.121.91.61 +91.151.106.127 +91.203.136.191 +91.235.51.238 +91.235.51.247 +91.78.100.114 +92.233.166.55 +92.233.226.34 +92.233.3.150 +92.234.144.16 +92.235.253.182 +92.236.102.208 +92.236.137.151 +92.236.16.51 +92.236.18.113 +92.236.222.129 +92.236.249.98 +92.236.26.72 +92.237.70.251 +92.237.9.240 +92.238.148.101 +92.238.184.16 +92.238.25.211 +92.238.40.83 +92.239.120.214 +92.243.17.151 +92.63.49.201 +92.64.178.98 +92.9.76.236 +93.156.180.97 +93.184.0.20 +93.92.34.238 +94.102.60.89 +94.23.192.228 +94.23.81.70 +94.25.81.45 +95.62.147.8 +96.21.139.56 +96.28.116.40 +96.28.160.240 +96.3.152.82 +96.3.172.29 +97.85.152.126 +97.87.65.118 +97.91.188.113 +97.97.255.95 +98.126.15.16 +98.126.15.27 +98.126.27.165 +98.141.23.139 +98.155.147.62 +98.16.253.47 +98.163.204.145 +98.164.75.175 +98.165.245.250 +98.166.26.87 +98.169.171.231 +98.181.60.131 +98.181.63.133 +98.192.95.181 +98.197.219.216 +98.202.107.151 +98.202.188.75 +98.204.164.207 +98.206.20.88 +98.208.46.176 +98.210.139.101 +98.210.147.111 +98.223.204.15 +98.229.212.211 +98.230.6.223 +98.233.228.159 +98.240.186.255 +98.243.17.13 +98.244.161.239 +98.28.33.20 +99.155.153.203 +99.199.237.158 +99.225.136.21 +99.228.104.199 +99.232.137.243 +99.232.189.8 +99.237.129.44 +99.242.140.117 +99.247.210.73 +99.254.157.217 +99.254.203.191 +103.94.125.244 +177.126.218.67 +176.122.98.51 +196.27.116.162 +93.188.45.157 +103.37.30.66 +75.149.141.145 +160.202.157.254 +84.10.1.82 +181.29.65.2 +75.71.253.42 +117.74.124.129 +79.106.41.15 +5.175.69.113 +93.123.196.160 +159.255.165.221 +200.98.141.76 +108.61.191.181 +195.178.207.241 +176.111.33.152 +202.142.169.123 +95.78.113.84 +194.187.216.182 +178.57.82.102 +45.6.100.90 +68.15.42.194 +186.221.152.223 +46.99.255.235 +180.183.221.144 +91.214.179.24 +110.37.201.75 +128.199.239.109 +200.29.101.77 +31.211.130.169 +72.47.105.234 +185.29.255.195 +185.5.183.101 +175.111.182.153 +103.225.174.13 +140.227.70.107 +188.191.29.15 +95.137.240.222 +85.187.194.13 +91.186.121.19 +181.114.56.242 +105.22.72.26 +81.82.200.134 +45.71.240.82 +222.124.2.186 +91.92.79.137 +190.147.208.143 +180.250.165.200 +79.173.97.45 +212.90.180.154 +85.172.109.18 +69.85.70.37 +94.102.124.139 +217.126.85.147 +62.213.57.218 +12.52.30.83 +41.164.31.154 +36.72.34.50 +202.6.224.52 +139.255.95.194 +46.19.47.114 +24.134.35.197 +190.85.70.110 +202.91.82.81 +213.174.10.58 +185.247.136.198 +201.190.190.250 +46.40.7.131 +89.165.218.82 +170.239.46.145 +190.185.180.166 +103.83.205.57 +95.93.98.73 +36.89.143.185 +202.52.126.3 +212.46.220.214 +201.247.175.50 +191.102.64.15 +27.123.221.51 +188.17.149.0 +176.99.110.182 +103.197.49.14 +103.106.119.154 +89.31.45.115 +93.187.161.119 +103.254.209.68 +195.78.101.162 +187.19.62.7 +197.232.51.81 +162.253.153.80 +82.144.130.13 +14.1.102.218 +36.89.53.195 +170.254.229.154 +139.0.29.18 +78.107.250.181 +89.135.125.133 +177.85.91.40 +37.187.116.199 +96.74.196.249 +85.175.226.106 +103.218.26.110 +197.253.67.68 +49.236.220.238 +145.239.86.210 +170.80.86.1 +95.46.1.130 +90.150.181.35 +77.247.88.10 +193.93.49.193 +181.143.51.50 +196.192.185.142 +109.74.50.14 +106.12.32.43 +91.197.204.139 +189.205.61.147 +103.241.205.66 +212.164.234.207 +83.223.132.172 +31.28.0.204 +81.16.246.44 +69.206.51.199 +76.10.246.164 +185.15.0.187 +85.105.222.29 +77.242.18.96 +181.39.165.155 +197.210.152.38 +31.45.246.187 +119.40.87.94 +202.179.186.238 +37.252.65.183 +93.170.113.241 +103.232.67.18 +154.117.159.226 +66.210.170.9 +138.41.25.163 +69.51.6.201 +36.89.235.35 +78.130.241.7 +109.230.60.3 +176.192.124.98 +201.150.149.79 +87.117.1.150 +2.92.106.53 +178.134.79.18 +185.162.62.206 +125.236.241.235 +118.144.114.134 +118.91.181.153 +193.85.30.78 +170.79.9.54 +190.104.215.14 +31.22.29.229 +209.206.113.193 +37.53.83.40 +36.67.8.245 +200.13.243.178 +82.114.68.58 +154.117.157.5 +193.218.149.91 +85.66.27.165 +185.75.206.131 +154.119.49.238 +84.42.56.237 +95.165.182.18 +181.129.148.138 +193.43.95.139 +83.19.160.122 +185.98.233.39 +91.202.207.110 +191.7.212.178 +80.211.109.96 +103.194.250.182 +188.242.249.116 +181.143.36.163 +92.51.75.203 +45.234.16.138 +183.88.174.196 +206.189.217.206 +80.90.25.160 +185.62.188.84 +95.64.253.177 +91.102.80.82 +139.255.113.98 +92.86.32.150 +87.229.89.144 +196.44.98.162 +159.192.97.83 +45.221.72.58 +85.100.108.84 +81.161.67.240 +83.208.168.199 +213.6.139.42 +24.139.73.82 +131.72.96.202 +85.200.245.65 +95.79.116.84 +181.129.45.154 +110.136.119.140 +195.222.61.29 +91.93.168.227 +178.140.116.185 +83.146.67.32 +159.203.174.2 +182.16.173.74 +5.188.102.81 +27.123.1.46 +209.203.6.246 +176.215.197.128 +109.188.64.248 +27.116.60.250 +74.93.145.1 +185.190.149.34 +212.96.201.128 +24.113.168.225 +103.87.236.154 +92.247.187.132 +24.226.159.195 +45.64.158.150 +187.180.18.52 +141.105.35.11 +185.141.11.118 +145.239.90.169 +186.225.50.211 +114.57.238.254 +177.10.21.154 +103.206.230.26 +83.169.214.53 +46.238.248.116 +193.178.190.173 +182.52.134.121 +177.39.56.223 +217.61.172.12 +95.167.241.242 +85.133.207.14 +103.111.83.26 +193.107.228.222 +80.106.247.145 +91.189.131.114 +184.69.57.210 +194.125.224.43 +200.75.204.150 +43.255.114.53 +111.119.225.168 +183.91.66.210 +103.65.192.211 +181.48.203.198 +195.112.122.197 +185.91.166.83 +190.42.32.154 +124.191.118.110 +37.57.163.30 +173.219.56.28 +94.45.155.45 +5.39.79.90 +181.196.50.130 +35.184.125.195 +104.248.122.203 +181.28.17.171 +178.150.191.73 +187.28.39.146 +109.236.211.171 +86.120.46.88 +103.112.9.31 +85.106.7.4 +41.50.88.56 +5.56.122.183 +103.100.80.42 +43.245.186.105 +140.190.17.195 +45.114.68.123 +5.53.114.152 +42.115.72.254 +203.82.197.34 +182.52.51.13 +103.255.234.189 +37.150.188.120 +93.170.119.242 +93.86.163.238 +45.126.46.140 +36.66.61.155 +188.32.95.148 +202.148.2.254 +77.232.137.35 +90.154.120.202 +85.207.99.213 +36.89.132.210 +156.236.71.29 +195.138.91.102 +84.241.41.150 +112.78.143.26 +103.254.59.122 +93.87.41.14 +170.178.151.250 +197.149.128.56 +167.114.250.199 +223.197.56.102 +52.68.71.75 +103.103.182.19 +196.203.55.18 +41.60.216.43 +180.94.87.157 +103.5.172.182 +103.251.176.106 +77.242.26.57 +130.0.31.226 +91.187.85.98 +105.174.18.118 +105.174.19.194 +181.170.223.14 +200.91.48.20 +5.77.240.130 +37.252.68.84 +109.75.34.188 +35.194.213.161 +104.199.234.56 +115.70.2.73 +101.167.169.229 +202.174.46.113 +178.189.11.134 +77.119.245.150 +91.133.123.12 +109.127.9.96 +85.132.18.222 +91.135.241.110 +64.150.235.214 +103.216.59.81 +49.0.39.153 +115.127.63.10 +63.175.159.29 +37.17.12.131 +86.57.181.122 +78.20.210.244 +81.82.209.76 +94.226.211.106 +160.238.136.125 +138.185.76.78 +160.238.136.121 +41.74.9.238 +41.86.244.89 +201.131.41.254 +181.114.115.105 +81.93.93.251 +81.93.73.27 +185.12.79.59 +83.143.31.254 +83.143.26.70 +129.205.201.56 +201.74.174.141 +177.136.5.166 +187.108.114.108 +95.158.153.145 +89.106.101.191 +79.110.125.201 +41.216.148.140 +154.73.40.70 +196.2.15.144 +196.2.10.56 +111.118.150.193 +119.82.253.32 +103.239.54.188 +41.204.84.161 +169.239.40.1 +24.122.184.158 +197.149.128.233 +169.239.123.69 +200.54.44.140 +179.57.108.159 +200.27.66.222 +101.76.214.72 +221.218.102.146 +190.85.153.139 +201.236.248.216 +200.58.213.178 +197.255.179.179 +41.75.76.75 +41.190.232.158 +41.243.13.50 +170.81.34.22 +186.26.121.98 +186.4.4.90 +195.29.45.100 +213.147.102.122 +194.30.157.117 +31.209.104.71 +193.86.125.62 +84.42.202.253 +89.111.104.109 +212.112.129.203 +158.248.219.169 +93.176.85.228 +196.201.206.219 +154.66.245.47 +199.127.197.12 +190.166.249.44 +190.211.182.7 +190.122.97.138 +180.189.167.34 +103.55.48.170 +181.211.97.18 +201.183.249.226 +186.178.10.20 +197.51.146.78 +41.65.99.133 +200.89.87.242 +168.227.20.27 +190.53.46.134 +105.235.235.156 +95.153.30.58 +80.79.114.240 +95.210.45.189 +130.117.175.134 +130.117.169.119 +95.216.160.51 +95.216.95.226 +163.172.146.169 +41.78.243.198 +41.78.243.218 +31.192.17.25 +188.121.193.44 +185.164.111.197 +46.4.24.166 +94.16.120.75 +102.177.101.9 +41.139.51.98 +62.103.22.100 +185.186.84.66 +201.216.168.238 +190.115.9.113 +160.119.130.10 +41.242.90.178 +160.119.129.42 +186.1.206.99 +190.185.119.91 +190.6.205.134 +122.115.78.240 +37.191.6.61 +109.74.61.67 +46.107.226.220 +27.0.183.67 +103.194.250.110 +219.91.142.237 +36.89.75.57 +36.37.124.226 +185.129.212.4 +212.86.75.9 +37.238.132.186 +37.237.63.82 +89.101.215.90 +52.169.139.131 +109.226.17.8 +109.226.26.134 +192.116.49.15 +212.66.97.185 +185.26.65.82 +208.131.186.162 +74.116.59.8 +208.163.39.218 +61.113.193.98 +92.46.54.114 +91.185.2.102 +41.139.141.254 +217.21.125.225 +41.90.102.34 +222.121.116.26 +27.255.91.146 +94.128.135.77 +158.181.171.9 +92.245.114.23 +158.181.19.120 +188.112.142.182 +195.13.161.141 +185.142.43.35 +185.104.252.10 +185.9.137.114 +154.66.109.182 +41.191.204.146 +41.191.205.29 +197.215.217.122 +196.250.176.67 +197.215.217.150 +62.68.34.86 +91.187.188.115 +78.157.79.120 +104.244.72.171 +195.218.3.241 +95.86.56.113 +92.55.84.202 +62.162.106.49 +41.77.23.238 +196.216.13.27 +41.190.95.66 +154.66.122.142 +211.24.98.29 +182.54.207.74 +217.64.109.231 +197.155.158.22 +196.200.60.142 +212.56.152.162 +88.203.36.209 +41.72.213.22 +41.72.192.170 +197.231.186.148 +148.243.240.156 +177.234.0.218 +93.116.185.57 +95.65.1.200 +89.28.101.79 +202.21.124.226 +202.131.248.66 +196.92.3.193 +165.90.67.102 +196.3.97.86 +196.3.97.68 +203.81.74.46 +122.248.100.13 +196.20.12.29 +196.20.12.9 +196.20.12.41 +103.1.94.213 +103.235.199.33 +202.166.196.34 +185.179.204.210 +185.179.204.226 +185.179.204.175 +190.4.186.20 +190.2.132.17 +203.147.79.184 +202.27.212.17 +114.134.164.166 +202.49.183.168 +165.98.135.6 +186.1.6.62 +197.234.45.230 +105.112.83.35 +155.93.122.186 +195.204.130.149 +81.166.242.208 +46.183.169.64 +125.209.118.42 +202.142.191.75 +213.6.69.113 +213.6.136.118 +213.6.198.230 +190.218.77.236 +179.63.195.140 +103.103.182.22 +203.83.20.53 +181.40.40.166 +190.128.228.54 +190.52.177.128 +200.37.16.253 +45.5.56.62 +202.57.63.206 +210.4.97.193 +111.125.87.199 +195.205.218.53 +46.238.120.149 +93.108.234.238 +217.129.163.102 +193.126.23.235 +24.139.244.238 +192.254.109.11 +65.38.222.162 +95.76.196.92 +82.79.83.78 +95.154.72.55 +95.31.13.55 +41.242.140.157 +196.223.246.11 +212.69.18.150 +178.222.246.55 +154.70.175.155 +41.86.54.160 +41.86.57.65 +196.216.220.204 +196.216.220.130 +150.107.124.32 +150.107.124.37 +150.107.124.35 +213.81.178.97 +86.110.229.38 +5.22.154.13 +188.230.234.67 +77.38.21.145 +77.94.144.162 +103.21.231.132 +103.21.231.131 +103.21.230.67 +41.79.198.36 +154.73.45.206 +41.162.53.130 +196.22.229.210 +41.193.101.122 +160.119.211.50 +69.63.67.44 +160.119.210.180 +84.217.82.227 +78.73.14.128 +82.145.149.5 +217.11.47.9 +179.43.144.19 +177.154.139.196
\ No newline at end of file diff --git a/cgi/quotes.conf b/cgi/quotes.conf new file mode 100644 index 0000000..faf5221 --- /dev/null +++ b/cgi/quotes.conf @@ -0,0 +1,13 @@ +Eres una buena persona.
+Fue un mensaje de calidad.
+キタ━━━━━\( ゚∀゚ )/━━━━━!!!
+Te invitaría a un café.
+Plataformas del futuro para la web 1.0.
+Plataformas del pasado para la web 2.0.
+Suenas como un bot muy desarrollado.
+Elegiste bien. Elegiste calidad.
+Gracias por usar Internet.
+Gracias por tu papiro.
+`·.¸¸.·´´¯`··._.·[GrAcIaS pOr El PoSt]`·.¸¸.·´´¯`··._.·
+Ha sido un éxito.
+Funcionó.
\ No newline at end of file diff --git a/cgi/template.py b/cgi/template.py new file mode 100644 index 0000000..0a7c530 --- /dev/null +++ b/cgi/template.py @@ -0,0 +1,117 @@ +# coding=utf-8 +import tenjin +import random +import re +from tenjin.helpers import * # Used when templating + +from settings import Settings +from database import * + +def renderTemplate(template, template_values={}, mobile=False, noindex=False): + """ + Run Tenjin on the supplied template name, with the extra values + template_values (if supplied) + """ + values = { + "title": Settings.NAME, + "board": None, + "board_name": None, + "board_long": None, + "is_page": "false", + "noindex": None, + "replythread": 0, + "home_url": Settings.HOME_URL, + "boards_url": Settings.BOARDS_URL, + "images_url": Settings.IMAGES_URL, + "static_url": Settings.STATIC_URL, + "cgi_url": Settings.CGI_URL, + "banner_url": None, + "banner_width": None, + "banner_height": None, + "disable_name": None, + "disable_subject": None, + "styles": Settings.STYLES, + "styles_default": Settings.STYLES_DEFAULT, + "txt_styles": Settings.TXT_STYLES, + "txt_styles_default": Settings.TXT_STYLES_DEFAULT, + "pagenav": "", + "reports_enable": Settings.REPORTS_ENABLE, + "force_css": "" + } + + engine = tenjin.Engine(pp=[tenjin.TrimPreprocessor(True)]) + board = Settings._.BOARD + + #if board: + if template in ["board.html", "threadlist.html", "catalog.html", "kako.html", "paint.html"] or template[0:3] == "txt": + # TODO HACK + if board['dir'] == 'world' and not mobile and (template == 'txt_board.html' or template == 'txt_thread.html'): + template = template[:-4] + 'en.html' + elif board['dir'] == '2d' and template == 'board.html' and not mobile: + template = template[:-4] + 'jp.html' + elif board['dir'] == '0' and template == 'board.html' and not mobile: + template = template[:-4] + '0.html' + + try: + banners = Settings.banners[board['dir']] + if banners: + banner_width = Settings.banners[board['dir']] + banner_height = Settings.banners[board['dir']] + except KeyError: + banners = Settings.banners['default'] + banner_width = Settings.banners['default'] + banner_height = Settings.banners['default'] + + values.update({ + "board": board["dir"], + "board_name": board["name"], + "board_long": board["longname"], + "board_type": board["board_type"], + "oek_finish": 0, + "disable_name": (board["disable_name"] == '1'), + "disable_subject": (board["disable_subject"] == '1'), + "default_subject": board["subject"], + "postarea_desc": board["postarea_desc"], + "postarea_extra": board["postarea_extra"], + "allow_images": (board["allow_images"] == '1'), + "allow_image_replies": (board["allow_image_replies"] == '1'), + "allow_noimage": (board["allow_noimage"] == '1'), + "allow_spoilers": (board["allow_spoilers"] == '1'), + "allow_oekaki": (board["allow_oekaki"] == '1'), + "archive": (board["archive"] == '1'), + "force_css": board["force_css"], + "noindex": (board["secret"] == '1'), + "useid": board["useid"], + "maxsize": board["maxsize"], + "maxage": board["maxage"], + "maxdimensions": board["thumb_px"], + "supported_filetypes": board["filetypes_ext"], + "prevrange": '', + "nextrange": '', + }) + else: + banners = Settings.banners['default'] + banner_width = Settings.banners['default'] + banner_height = Settings.banners['default'] + + if Settings.ENABLE_BANNERS: + if len(banners) > 1: + random_number = random.randrange(0, len(banners)) + BANNER_URL = Settings.banners_folder + banners[random_number][0] + BANNER_WIDTH = banners[random_number][1] + BANNER_HEIGHT = banners[random_number][2] + else: + BANNER_URL = Settings.banners_folder + banners[0][0] + BANNER_WIDTH = banners[0][1] + BANNER_HEIGHT = banners[0][2] + + values.update({"banner_url": BANNER_URL, "banner_width": BANNER_WIDTH, "banner_height": BANNER_HEIGHT}) + + values.update(template_values) + + if mobile: + template_folder = "templates/mobile/" + else: + template_folder = "templates/" + + return engine.render(template_folder + template, values)
\ No newline at end of file diff --git a/cgi/templates/anarkia.html b/cgi/templates/anarkia.html new file mode 100644 index 0000000..3ded9da --- /dev/null +++ b/cgi/templates/anarkia.html @@ -0,0 +1,329 @@ +<?py include('templates/base_top.html') ?> +<?py from tenjin.helpers.html import * ?> +<style>.anarkiahead {width:1000px; text-align:left} +.anarkiahead h2 {margin-top: 0} +.anarkiamenu a {font-size:20pt;display:inline-block;width:300px;padding:10px 0} +.logs {font-size:small;max-height:300px;overflow-y:auto;width:600px} +.long {white-space:nowrap} +.full {width:100%} +.return {font-size:24pt}</style> +<center> +<div class="replymode" style="font-size:26pt;color:red;font-weight:bold">ⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶ</div> +<br /> +<?py if mode == 0: ?> + <div class="anarkiahead"> + <h2 style="border-bottom:1px solid;width:100%;">Anarkía @ B.a.I.</h2> + <p>Anarkía es una sección especial sin moderación y con acceso libre a su panel de administración.</p> + <ul> + <li>El staff de B.a.I. no interferirá de ninguna manera en esta sección y cualquiera es libre de modificar + sus parámetros, de eliminar mensajes o banear usuarios dentro de ella.</li> + <li>Los hilos de otras secciones que sean eliminados por su baja calidad, denuncias u otra razón, caerán por defecto a esta sección.</li> + <li>Los bans en esta sección son independientes del resto del sitio. Es decir, usuarios baneados en BaI son libres de usar esta sección.</li> + <li>Cualquier problema en su funcionamiento por favor reportar en la sección <a href="/bai/">Meta</a>.</li> + </div> + <div class="anarkiamenu"> + <a href="#{cgi_url}anarkia/opt"><img src="#{boards_url}anarkia/opt.jpg" width="250" height="175"><br />Opciones generales</a> + <a href="#{cgi_url}anarkia/mod"><img src="#{boards_url}anarkia/mod.jpg" width="250" height="175"><br />Panel de moderación</a> + <a href="#{cgi_url}anarkia/css"><img src="#{boards_url}anarkia/css.jpg" width="250" height="175"><br />Editar CSS</a> + <br /> + <a href="#{cgi_url}anarkia/emojis"><img src="#{boards_url}anarkia/emojis.jpg" width="250" height="175"><br />Emojis</a> + <a href="#{cgi_url}anarkia/bans"><img src="#{boards_url}anarkia/bans.jpg" width="250" height="175"><br />Bans</a> + <a href="#{cgi_url}anarkia/type"><img src="#{boards_url}anarkia/type.jpg" width="250" height="175"><br />Cambiar tipo de board</a> + </div> + <hr /> + <input type="hidden" name="board" value="anarkia" /> + <div class="logs"> + <table class="managertable full"> + <tr><th colspan="2">Logs</th></tr> + <tr><th>Fecha</th><th class="full">Acción</th></tr> + <?py for log in logs: ?> + <tr><td class="date" data-unix="${log['timestamp']}">${log['timestamp_formatted']}</td><td>${log['action']}</td></tr> + <?py #endfor ?> + </table> + </div> + <hr /><a href="#{boards_url}anarkia" class="return">Volver a la sección</a> +<?py elif mode == 1: ?> +<div class="replymode">Opciones de Board</div> +<form action="#{cgi_url}anarkia/opt" method="post"> +<table> + <tr> + <td class="postblock">Nombre de sección</td> + <td><input type="text" name="longname" size="50" value="${boardopts['longname']}" maxlength="128" class="full" /></td> + </tr> + <tr> + <td class="postblock">Descripción</td> + <td> + <textarea id="patop" name="postarea_desc" rows="10" cols="50" class="full" oninput="pvw('patop')">${boardopts['postarea_desc']}</textarea> + <div id="p_patop" style="border:1px dotted gray;width:100%;"></div> + </td> + </tr> + <tr> + <td class="postblock">Caja extra</td> + <td><textarea name="postarea_extra" rows="5" cols="50" class="full">${boardopts['postarea_extra']}</textarea></td> + </tr> + <tr> + <td class="postblock">Nombre por defecto</td> + <td><input type="text" name="anonymous" size="50" maxlength="128" value="${boardopts['anonymous']}" class="full" /></td> + </tr> + <tr> + <td class="postblock">Título por defecto</td> + <td><input type="text" name="subject" size="50" maxlength="64" value="${boardopts['subject']}" class="full" /></td> + </tr> + <tr> + <td class="postblock">Mensaje por defecto</td> + <td><input type="text" name="message" size="50" maxlength="128" value="${boardopts['message']}" class="full" /></td> + </tr> + <tr> + <td class="postblock">ID</td> + <td> + <select name="useid" class="full"> + <option value="0">Desactivado</option> + <option value="1"#{selected(boardopts['useid'] == '1')}>Activado</option> + <option value="2"#{selected(boardopts['useid'] == '2')}>Activado siempre</option> + <option value="3"#{selected(boardopts['useid'] == '3')}>Activado siempre, detallado</option> + </select> + </td> + </tr> + <tr> + <td class="postblock">Desactivar nombre</td> + <td><input type="checkbox" name="disable_name" id="noname" value="1"#{checked(boardopts['disable_name'] == '1')} /><label for="noname"></label></td> + </tr> + <tr> + <td class="postblock">Desactivar asunto</td> + <td><input type="checkbox" name="disable_subject" id="nosub" value="1"#{checked(boardopts['disable_subject'] == '1')} /><label for="nosub"></label></td> + </tr> + <tr> + <td class="postblock">Permitir crear hilos sin imagen</td> + <td><input type="checkbox" name="allow_noimage" id="noimgallow" value="1"#{checked(boardopts['allow_noimage'] == '1')} /><label for="noimgallow"></label></td> + </tr> + <tr> + <td class="postblock">Permitir subida</td> + <td><input type="checkbox" name="allow_images" id="img" value="1"#{checked(boardopts['allow_images'] == '1')} /><label for="img">Al crear un hilo</label><br /><input type="checkbox" name="allow_image_replies" id="imgres" value="1"#{checked(boardopts['allow_image_replies'] == '1')} /><label for="imgres">Al responder</label></td> + </tr> + <tr> + <td class="postblock">Tipos de archivo</td> + <td> + <?py for filetype in filetypes: ?> + <input type="checkbox" name="filetype#{filetype['ext']}" id="#{filetype['ext']}" value="1"#{checked(filetype['ext'] in supported_filetypes)} /><label for="#{filetype['ext']}">${filetype['ext'].upper()}</label><br /> + <?py #endfor ?> + </td> + </tr> + <tr> + <td class="postblock">Tamaño máximo <span style="font-weight:normal;">(KB)</span></td> + <td><input type="text" name="maxsize" value="#{boardopts['maxsize']}" maxlength="5" size="11" /></td> + </tr> + <tr> + <td class="postblock">Dimensión de miniatura <span style="font-weight:normal;">(px)</span></td> + <td><input type="text" name="thumb_px" value="#{boardopts['thumb_px']}" maxlength="3" size="11" /></td> + </tr> + <tr> + <td class="postblock">Hilos en página frontal</td> + <td><input type="text" name="numthreads" value="#{boardopts['numthreads']}" maxlength="2" size="11" /></td> + </tr> + <tr> + <td class="postblock">Respuestas a mostrar</td> + <td><input type="text" name="numcont" value="#{boardopts['numcont']}" maxlength="2" size="11" /></td> + </tr> +</table> +<hr /> +<input type="submit" value="Guardar cambios" /> +</form> +<hr /> +<a href="#{cgi_url}anarkia" class="return">Volver al menú</a> +<?py elif mode == 2: ?> +<div class="replymode">Denuncias</div> +<?py if reports: ?> + <table class="managertable" style="max-width:1000px"> + <tr> + <th>Post</th> + <th>Fecha</th> + <th style="min-width:200px;">Razón</th> + </tr> + <?py for r in reports: ?> + <tr> + <td><a href="?thread=#{r['parentid'] if r['parentid'] != "0" else r['postid']}##{r['postid']}" style="font-weight:bold">#{r['postid']}</td> + <td>${r['timestamp_formatted']}</td> + <td>#{r['reason']}</a></td> + </tr> + <?py #endfor ?> + </table> +<?py else: ?> + No hay denuncias.<br /> +<?py #endif ?> +<br /> +<div class="replymode">Lista de hilos</div> +<table class="managertable" style="max-width:1000px;"> +<tr> + <th>#</th> + <th>ID</th> + <th style="width:50%;">Asunto</th> + <th>Fecha</th> + <th style="width:50%;">Mensaje</th> + <th>Posts</th> + <th>Acción</th> +</tr> +<?py i = 1 ?> +<?py for thread in threads: ?> +<tr> + <td>#{i}</td> + <td>#{thread['id']}</td> + <td><a href="?thread=#{thread['id']}" style="font-size:16pt;"><b>#{thread['subject'][:30]}</b></a></td> + <td>#{thread['timestamp_formatted'][:21]}</td> + <td>${thread['message'][:250]}</td> + <td>#{thread['length']}</td> + <td>[<a href="?lock=#{thread['id']}">#{"<b>Abrir</b>" if thread['locked'] == "1" else "Cerrar"}</a>]</td> +</tr> +<?py i += 1 ?> +<?py #endfor ?> +</table> +<hr /><a href="#{cgi_url}anarkia" class="return">Volver al menú</a> +<?py elif mode == 3: ?> +<div class="replymode" style="font-size:16pt">Hilo: ${posts[0]['subject']} (#{posts[0]['length']})</div> +<table class="managertable" style="width:1000px;"> + <tr> + <th>#</th> + <th>ID</th> + <th>Fecha</th> + <th>Nombre</th> + <th>Mensaje</th> + <th>Usuario</th> + </tr> +<?py i = 1 ?> +<?py for p in posts: ?> +<?py if p['IS_DELETED'] == '0': ?> + <tr> + <td>#{i}</td> + <td class="long"> + <b>#{p['id']}</b> + <?py if p['parentid'] != '0': ?> + [<a href="?del=#{p['id']}">Eliminar</a>] + <?py else: ?> + [<a href="?lock=#{p['id']}">#{"<b>Abrir</b>" if p['locked'] == "1" else "Cerrar"}</a>] + <?py #endif ?> + </td> + <td>${p['timestamp_formatted']}</td> + <td><span class="postername">${p['name']}</span></td> + <td>${p['message']}</td> + <td class="long">#{p['ip'][:4]} [<a href="?ban=#{p['id']}">Ban</a>]</td> + </tr> +<?py else: ?> + <tr> + <td>#{i}</td> + <td class="long"><b>#{p['id']}</b> [<a href="?restore=#{p['id']}">Recuperar</a>]</td> + <td colspan="4">Eliminado.</td> + </tr> +<?py #endif ?> +<?py i += 1 ?> +<?py #endfor ?> +</table> +<hr /><a href="#{cgi_url}anarkia/mod" class="return">Volver al panel de moderación</a> +<?py elif mode == 4: ?> +<div class="replymode">Colocar ban</div> +<form action="#{cgi_url}anarkia/mod" name="banform" method="post"> +<input type="hidden" name="banto" value="#{post['id']}" /> +<table> + <tr><td class="postblock">Ban para usuario</td><td><b>#{post['ip'][-4:]}</b></td></tr> + <tr><td class="postblock">Mensaje</td><td><textarea name="reason" class="full" maxlength="512"></textarea></td></tr> + <tr><td class="postblock">Ciego</td><td><input type="checkbox" name="blind" value="1" checked="checked" /></td></tr> + <tr><td class="postblock">Expira en <span style="font-weight:normal;">(segundos)</span></td> + <td><input type="text" name="seconds" class="full" value="3600" maxlength="8" /><br /> + <a href="#" onclick="document.banform.seconds.value='0';return false;">Nunca</a> + <a href="#" onclick="document.banform.seconds.value='3600';return false;">1hr</a> + <a href="#" onclick="document.banform.seconds.value='43200';return false;">12hr</a> + <a href="#" onclick="document.banform.seconds.value='86400';return false;">1d</a> + <a href="#" onclick="document.banform.seconds.value='259200';return false;">3d</a> + <a href="#" onclick="document.banform.seconds.value='604800';return false;">1w</a> + <a href="#" onclick="document.banform.seconds.value='2592000';return false;">1m</a> + <a href="#" onclick="document.banform.seconds.value='31536000';return false;">1yr</a> + </td> + </tr> + <tr><td colspan="2"><input type="submit" value="Banear" class="full" /></td></tr> +</table> +</form> +<hr /> +<a href="#{cgi_url}anarkia/mod" class="return">Volver al panel de moderación</a> +<?py elif mode == 5: ?> +<div class="replymode">Lista de bans</div> +<table class="managertable" style="max-width:1000px;"> +<tr> + <th>ID</th> + <th>Usuario</th> + <th>Puesto</th> + <th>Expira</th> + <th>Ciego</th> + <th style="min-width:200px;">Razón</th> + <th>Acción</th> +</tr> +<?py if bans: ?> + <?py for ban in bans: ?> + <tr> + <td class="long">#{ban['id']}</td> + <td>#{ban['ip'][-4:]}</td> + <td>${ban['added']}</td> + <td>${ban['until']}</td> + <td>${"Sí" if ban['blind'] == "1" else "No"}</td> + <td>${ban['reason']}</td> + <td>[<a href="?unban=#{ban['id']}">Eliminar ban</a>]</td> + </tr> + <?py #endfor ?> +<?py else: ?> + <tr><td colspan="7" style="text-align:center;">No hay bans.</td></tr> +<?py #endif ?> +</table> +<hr /> +<a href="#{cgi_url}anarkia" class="return">Volver al menú</a> +<?py elif mode == 6: ?> +<div class="replymode">Editar CSS</div> +<p><b>Editando:</b> <code>${basename}</code></p> +<p style="font-size:small">Dominios permitidos: https://bienvenidoainternet.org https://i.imgur.com</p> +<form action="#{cgi_url}anarkia/css" name="cssform" method="post" style="display:inline-block;"> +<textarea name="cssfile" cols="100" rows="30">${cssfile}</textarea><br /> +<input type="submit" value="Guardar cambios" class="full" /> +</form> +<hr /> +<a href="#{cgi_url}anarkia" class="return">Volver al menú</a> +<?py elif mode == 7: ?> +<div class="replymode">Cambiar tipo de sección</div> +<h1 style="color:red;font-size:26pt;text-decoration:underline;">ATENCIÓN</h1> +<p style="font-size:19pt">Estás a punto de cambiar la estructura de esta sección a #{type_do}.</p> +<p style="font-size:15pt">Esta sección es actualmente un #{type_now} y si prosigues transformarás su estructura a un #{type_do}.</p> +<p style="color:red;font-size:15pt;">Nótese que este cambio se puede hacer sólo una vez cada 10 minutos.</p> +<div style="display:inline-block;"> + <p style="margin-top:0;">¿Seguro que deseas convertir esta sección a #{type_do}? + <form method="get"> + <input type="hidden" name="transform" value="do"> + <input type="submit" value="Transformar a #{type_do}" class="full" /> + </form> + </p> +</div> +<hr /> +<a href="#{cgi_url}anarkia" class="return">Volver al menú</a> +<?py elif mode == 8: ?> +<div class="replymode">Emojis</div> +<table class="managertable"> + <tr><th>Nombre</th><th>Img</th></tr> + <?py for emoji in emojis: ?> + <tr><td>${emoji['from']}</td><td>#{emoji['to']}</td></tr> + <?py #endfor ?> +</table> +<hr /> +<form method="post" action="" enctype="multipart/form-data"> +<table> +<tr> + <td class="postblock">Nombre</td> + <td><input type="text" name="name" size="15" maxlength="15" class="full" /></td> + <td><input type="submit" name="new" value="Agregar emoji" class="full" /></td> +</tr> +<tr><td class="postblock">Archivo</td><td colspan="2"><input type="file" name="file" size="15" class="full" /></td></tr> +</table> +<small>(Sólo letras y/o números. Máximo: 500x500px, 500 KB.)</small> +</form> +<hr /> +<a href="#{cgi_url}anarkia" class="return">Volver al menú</a> +<?py elif mode == 99: ?> +<div>${msg}<br /><br /><a href="#{cgi_url}anarkia" class="return">Volver al menú</a></div> +<?py #endif ?> +</center> +<hr /> +<div class="replymode" style="font-size:26pt;color:red;font-weight:bold">ⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶ</div> +<?py include('templates/base_bottom.html') ?>
\ No newline at end of file diff --git a/cgi/templates/banned.html b/cgi/templates/banned.html new file mode 100644 index 0000000..23b6636 --- /dev/null +++ b/cgi/templates/banned.html @@ -0,0 +1,34 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<title>Acceso prohibido@B.a.I.</title> +<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" /> +<style type="text/css"> +html { text-align:center; } +body { background:#fe7777;color:#6a0000;display:inline-block;font-size:13pt;max-width:1000px;text-align:left; } +h1 { color:red;margin:0; } +h2 { margin:0.5em 0; } +</style> +</head> +<body> +<h1>Mensaje de Bienvenido a Internet BBS/IB</h1> +<h2>Se te ha prohibido el acceso :-(</h2> +<p>¡Tu IP (o rango) ha sido bloqueado!</p> +<?py if reason != "": ?> + <p>La razón dejada fue: <b>#{reason}</b> y tu ban fue puesto el <b>#{added}</b> para las siguientes secciones: <b>#{boards_str}</b></p> +<?py else: ?> + <p>No sabemos qué es lo que pudo causar tu ban, ¿qué hiciste?</p> + <p>Tu ban fue puesto el <b>#{added}</b> para las siguientes secciones: <b>#{boards_str}</b></p> +<?py #endif ?> +<?py if expire != "": ?> + <p>Pero no te preocupes, se te concederá nuevamente el acceso en la siguiente fecha y hora: <b>#{expire}</b>.</p> +<?py #endif ?> +<p>Si tu expulsión fue puesta incorrectamente no dudes en <a href="mailto:burocracia@bienvenidoainternet.org">contactarnos</a> dando tu IP, razón y explicación de los hechos.</p> +<p>¡Gracias por usar Bienvenido a Internet BBS/IB!</p> +<hr /> +<p><small><i>En muchos casos a pesar de que hayas sido expulsado del sitio se concede el acceso a las secciones <a href="/bai/">Meta</a> y Anarkía. Bajo cualquier consulta o reclamo <a href="mailto:burocracia@bienvenidoainternet.org">contáctanos</a>.</i></small></p> +<hr /> +<div style="text-align:right;">Bienvenido a Internet 2010-2018</div> +</body> +</html>
\ No newline at end of file diff --git a/cgi/templates/base_bottom.html b/cgi/templates/base_bottom.html new file mode 100644 index 0000000..102f8f2 --- /dev/null +++ b/cgi/templates/base_bottom.html @@ -0,0 +1,3 @@ +<div class="footer">- <a href="//www.bienvenidoainternet.org" target="_top">weabot</a> <?py include('templates/revision.html') ?> -</div> +</body> +</html>
\ No newline at end of file diff --git a/cgi/templates/base_top.html b/cgi/templates/base_top.html new file mode 100644 index 0000000..5389617 --- /dev/null +++ b/cgi/templates/base_top.html @@ -0,0 +1,55 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<?py if 'matome' in _context: ?> + <title>#{matome} - #{board_long}</title> +<?py elif board: ?> + <title>#{board_long}</title> + <?py else: ?> + <title>#{title}</title> +<?py #endif ?> + <meta http-equiv="Content-Type" content="application/xhtml+xml;charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> +<?py if replythread and 'threads' in _context and 'preview' in _context: ?> + <meta property="og:site_name" content="Bienvenido a Internet IB" /> + <meta property="twitter:site" content="Bienvenido a Internet IB" /> + <meta name="description" content="${preview}" /> + <meta property="og:title" content="${threads[0]['posts'][0]['subject']} - ${board_name}" /> + <meta property="og:description" content="${preview}" /> + <?py if threads[0]['posts'][0]['thumb']: ?> + <meta property="twitter:image" content="https://bienvenidoainternet.org/#{board}/thumb/#{threads[0]['posts'][0]['thumb']}" /> + <meta property="og:image" content="https://bienvenidoainternet.org/#{board}/thumb/#{threads[0]['posts'][0]['thumb']}" /> + <?py #endif ?> + <meta property="twitter:title" content="${threads[0]['posts'][0]['subject']} - ${board_name}" /> + <meta name="twitter:description" content="${preview}" /> +<?py #endif ?> + <meta name="robots" content="#{"noindex" if noindex else "index, follow"}" /> + <link rel="shortcut icon" href="/favicon.ico" /> + <link rel="stylesheet" href="#{static_url}css/ib.css" /> +<?py if not force_css: ?> + <link rel="stylesheet" id="css" href="#{static_url}css/#{styles[styles_default].lower()}.css" /> +<?py else: ?> + <link rel="stylesheet" type="text/css" href="#{force_css}" /> +<?py #endif ?> +<?py if board == "2d": ?> + <link rel="stylesheet" href="#{static_url}css/txt/sjis.css" /> +<?py #endif ?> + <script type="text/javascript" src="#{static_url}js/weabot.js?v=5"></script> + <script type="text/javascript" src="#{static_url}js/aquiencitas.js"></script> + <script type="text/javascript" src="#{static_url}js/autorefresh.js?v=3"></script> +</head> +<body#{' class="res"' if replythread else ''}> + <div id="main_nav">[<a href="/" target="_top">Bienvenido a Internet</a>] [<?py include('templates/navbar.html') ?>] + <?py if not force_css: ?> + <span>[<span>Apariencia:</span> + <?py for title in styles: ?> <a href="#" class="ss">#{title}</a><?py #endfor ?>]</span> + <?py #endif ?></div> + <div class="logo"> + <?py if board: ?> + #{board_long} + <?py else: ?> + <img src="/static/img/default.png" width="500" height="81" /> + <?py #endif ?> + </div> + <hr width="90%" size="1" /> 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 @@ +<?py include('templates/base_top.html') ?> +<?py if replythread or oek_finish: ?> + [<a href="#{boards_url}#{board}/">Volver al IB</a>] +<?py #endif ?> +<?py if replythread: ?> + [<a href="/cgi/catalog/${board}">Catálogo</a>] + [<a href="#bottom" name="top">Bajar</a>] + <div class="replymode">Modo Respuesta</div> +<?py #endif ?> +<a name="postbox"></a> +<div class="postarea"> +<?py if allow_oekaki and not oek_finish: ?> + <center><form class="oekform" action="#{cgi_url}oekaki/paint" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /> + <?py if replythread: ?> + <input type="hidden" name="parent" value="#{replythread}" /> + <?py #endif ?> + Usar: <select name="oek_applet"> + <option value="neo">PaintBBS NEO</option> + <option value="tegaki">Tegaki</option> + <option value="wpaint">wPaint</option> + <option value="shipainter|n|n">Shi-Painter</option> + <option value="shipainter|y|n">Shi-Painter Pro</option> + </select> + <span id="oek_size"><input type="text" name="oek_x" size="4" maxlength="4" value="300" /> x <input type="text" name="oek_y" size="4" maxlength="4" value="300" /></span> + <input type="submit" value="Dibujar" /><br /><a href="#{cgi_url}oekaki/finish/#{board}/#{replythread}">Recuperar dibujo guardado</a> + </form></center> +<?py #endif ?> +<?py if oek_finish: ?> +<center style="margin-bottom:0.5em;"><table border=""><tr><td> + <?py if oek_finish == "no": ?> + <font size="+3">No hay dibujo</font> + <?py else: ?> + <img src="#{boards_url}oek_temp/#{oek_finish}.png?ts=#{ts}" /> + <?py #endif ?> +</td></tr></table></center> +<?py #endif ?> +<form name="postform" id="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /> + <?py if replythread: ?> + <input type="hidden" name="parent" value="#{replythread}" /> + <input type="hidden" name="default_subject" value="#{default_subject}" /> + <?py #endif ?> + <div style="display:none;"><input type="text" name="name" size="25" /> <input type="text" name="email" size="25" /></div> + <table class="postform"> + <tr> + <td class="postblock">mediumo</td> + <td> + <input type="text" name="fieldb" size="25" accesskey="e" /> + <?py if disable_subject: ?> + <?py if replythread: ?> + <input type="submit" value="🤡" accesskey="z" /> + <?py else: ?> + <input type="submit" value="🤡" accesskey="z" /> + <?py #endif ?> + <?py #endif ?> + </td> + </tr> + <?py if not disable_subject: ?> + <tr> + <td class="postblock">Asunto</td> + <td> + <input type="text" name="subject" size="35" maxlength="100" accesskey="s" /> + <?py if replythread: ?> + <input type="submit" value="Responder" accesskey="z" /> + <?py else: ?> + <input type="submit" value="Crear hilo" accesskey="z" /> + <?py #endif ?> + </td> + </tr> + <?py #endif ?> + <tr> + <td class="postblock">molekuloj</td> + <td><textarea name="message" cols="50" rows="6" accesskey="m"></textarea></td> + </tr> + <?py if (replythread and allow_image_replies) or (not replythread and allow_images): ?> + <tr> + <td class="postblock">amiko</td> + <td> + <input type="file" name="file" id="file" accesskey="f" /> + <span id="filepreview" style="display:none;"></span> + <?py if allow_spoilers: ?> + <label>[<input type="checkbox" name="spoil" id="spoil" />Spoiler]</label> + <?py #endif ?> + <?py if allow_noimage and not replythread: ?> + <label>[<input type="checkbox" name="noimage" id="noimage" />Sin imagen]</label> + <?py #endif ?> + </td> + </tr> + <?py #endif ?> + <tr class="pass"> + <td class="postblock">timo</td> + <td><input type="password" name="password" size="8" accesskey="p" /> (uzata por post forigo)</td> + </tr> + <tr> + <td colspan="2" class="rules"> + <ul> + #{postarea_desc} + <li>ni ne vivas timi, ni vivas konekti.</li> + <?py if supported_filetypes: ?> + <li>elekti la veneno: <span id="filetypes">#{', '.join(supported_filetypes).upper()}</span>. ĝis: <span id="maxsize">#{maxsize}</span>KB. paŝo: #{maxdimensions}x#{maxdimensions}px</li> + <?py #endif ?> + </ul> + </td> + </tr> + </table> +</form> +</div> +<hr /> +<?py if postarea_extra: ?> +<center>#{postarea_extra}</center> +<hr /> +<?py #endif ?> +<form id="delform" action="#{cgi_url}delete" method="post"> + <input type="hidden" name="board" value="#{board}" /> + <?py if threads: ?> + <?py for thread in threads: ?> + <?py if not replythread: ?> + <span id="unhide#{thread['id']}#{board}" style="display:none;">Hilo <a href="#{boards_url}#{board}/res/#{thread['id']}.html">#{thread['id']}</a> oculto. <a class="tt" href="#">Ver hilo</a></span> + <?py #endif ?> + <div id="thread#{thread['id']}#{board}" class="thread" data-length="#{thread['length']}"> + <?py for post in thread['posts']: ?> + <?py if int(post['parentid']) != 0: ?> + <table><tr><td class="ell">…</td> + <td class="reply" id="reply#{post['id']}"> + <?py elif post['file']: ?> + <?py if post['image_width'] != '0': ?> + <div class="fs"><span>Nombre de archivo:</span><a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + <?py else: ?> + <div class="fs"><span>Nombre de archivo:</span><a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B) + <?py #endif ?> + <?py if not replythread: ?> + [<a href="#" title="Ocultar hilo" class="tt">Ocultar hilo</a>] + <?py #endif ?> + </div> + <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}"> + <?py if post['thumb'].startswith('mime'): ?> + <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py elif post['file'][-3:] == 'gif': ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py else: ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py #endif ?> + </a> + <?py #endif ?> + <a name="#{post['id']}"></a> + <?py if post['IS_DELETED'] == '1': ?> + <span class="deleted">No.#{post['id']} eliminado por usuario.</span> + <?py elif post['IS_DELETED'] == '2': ?> + <span class="deleted">No.#{post['id']} eliminado por miembro del staff.</span> + <?py else: ?> + <div class="info"><label><input type="checkbox" name="delete" value="#{post['id']}" /> + <?py if post['subject'] : ?> + <span class="subj">#{post['subject']}</span> + <?py #endif ?></label> + <?py if post['tripcode']: ?> + <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span> + <?py else: ?> + <span class="name"><b>#{post['name']}</b></span> + <?py #endif ?> + <span class="date" data-unix="#{random.randint(1,2147483647)}">#{post['timestamp_formatted']}</span> + <span class="reflink"><a>No.#{random.randint(1,999999)}</a></span> + <a class="rep" href="#{cgi_url}report/#{board}/#{post['id']}" rel="nofollow">rep</a> + <?py if int(post['parentid']) != 0: ?> + <?py if post['file']: ?> + <div class="fs"> + <?py if post['image_width'] != '0': ?> + <a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + <?py else: ?> + <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B) + <?py #endif ?> + </div> + <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}"> + <?py if post['thumb'].startswith('mime'): ?> + <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py elif post['file'][-3:] == 'gif': ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py else: ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py #endif ?> + </a> + <?py #endif ?> + <?py #endif ?> + <?py if int(post['parentid']) == 0 and not replythread: ?> + [<a href="#{boards_url}#{board}/res/#{post['id']}.html" class="hsbn">Responder</a>] + <?py if post['file'] == '': ?> + [<a href="#" title="Ocultar Hilo" class="tt">Ocultar</a>] + <?py #endif ?> + <?py #endif ?> + </div> + <?py if post['thumb_width'] != '0' and post['parentid'] != '0': ?> + <blockquote style="margin-left:#{int(post['thumb_width'])+40}px;"> + <?py else: ?> + <blockquote> + <?py #endif ?> + #{post['message']} + </blockquote> + <?py if not replythread and post['shortened']: ?> + <blockquote class="abbrev">(Post muy largo... Presiona <a href="#{boards_url}#{board}/res/#{post['id'] if post['parentid'] == "0" else post['parentid']}.html##{post['id']}">aquí</a> para verlo completo.)</blockquote> + <?py #endif ?> + <?py #endif ?> + <?py if post['parentid'] == "0": ?> + <?py if not replythread: ?> + <?py if int(thread['omitted']) == 1: ?> + <div class="omitted">Un post omitido. Haz clic en Responder para ver.</div> + <?py elif int(thread['omitted']) > 1: ?> + <div class="omitted">#{thread['omitted']} posts omitidos. Haz clic en Responder para ver.</div> + <?py #endif ?> + <?py #endif ?> + <?py else: ?> + </td></tr></table> + <?py #endif ?> + <?py #endfor ?> + <div class="cut"></div></div> + <?py if replythread: ?> + <hr /> + <div class="nav">[<a href="#{boards_url}#{board}/">Volver al IB</a>] + [<a href="/cgi/catalog/${board}">Catálogo</a>] + [<a href="#top" name="bottom">Subir</a>]</div> + <?py #endif ?> + <hr /> + <?py #endfor ?> + <div class="userdel">Eliminar post <label>[<input type="checkbox" name="imageonly" id="imageonly" />Sólo imagen]</label><br /> + Clave <input type="password" name="password" size="8" /> <input name="deletepost" value="Eliminar" type="submit" /></div> + <?py #endif ?> +</form> +<?py if pagenav: ?> + <div class="pg">#{pagenav}</div> +<?py #endif ?> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py if replythread or oek_finish: ?> + [<a href="#{boards_url}#{board}/">Volver al IB</a>] +<?py #endif ?> +<?py if replythread: ?> + [<a href="/cgi/catalog/${board}">Catálogo</a>] + [<a href="#bottom" name="top">Bajar</a>] + <div class="replymode">Modo Respuesta</div> +<?py #endif ?> +<a name="postbox"></a> +<div class="postarea"> +<?py if allow_oekaki and not oek_finish: ?> + <center><form class="oekform" action="#{cgi_url}oekaki/paint" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /> + <?py if replythread: ?> + <input type="hidden" name="parent" value="#{replythread}" /> + <?py #endif ?> + Usar: <select name="oek_applet"> + <option value="neo">PaintBBS NEO</option> + <option value="tegaki">Tegaki</option> + <option value="wpaint">wPaint</option> + <option value="shipainter|n|n">Shi-Painter</option> + <option value="shipainter|y|n">Shi-Painter Pro</option> + </select> + <span id="oek_size"><input type="text" name="oek_x" size="4" maxlength="4" value="300" /> x <input type="text" name="oek_y" size="4" maxlength="4" value="300" /></span> + <input type="submit" value="Dibujar" /><br /><a href="#{cgi_url}oekaki/finish/#{board}/#{replythread}">Recuperar dibujo guardado</a> + </form></center> +<?py #endif ?> +<?py if oek_finish: ?> +<center style="margin-bottom:0.5em;"><table border=""><tr><td> + <?py if oek_finish == "no": ?> + <font size="+3">No hay dibujo</font> + <?py else: ?> + <img src="#{boards_url}oek_temp/#{oek_finish}.png?ts=#{ts}" /> + <?py #endif ?> +</td></tr></table></center> +<?py #endif ?> +<form name="postform" id="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /> + <?py if replythread: ?> + <input type="hidden" name="parent" value="#{replythread}" /> + <input type="hidden" name="default_subject" value="#{default_subject}" /> + <?py #endif ?> + <div style="display:none;">Trampa: <input type="text" name="name" size="25" /> <input type="text" name="email" size="25" /></div> + <table class="postform"> + <?py if not disable_name: ?> + <tr> + <td class="postblock">Nombre</td> + <td><input type="text" name="fielda" size="25" accesskey="n" /></td> + </tr> + <?py #endif ?> + <tr> + <td class="postblock">E-mail</td> + <td> + <input type="text" name="fieldb" size="25" accesskey="e" /> + <?py if disable_subject: ?> + <?py if replythread: ?> + <input type="submit" value="Responder" accesskey="z" /> + <?py else: ?> + <input type="submit" value="Crear hilo" accesskey="z" /> + <?py #endif ?> + <?py #endif ?> + </td> + </tr> + <?py if not disable_subject: ?> + <tr> + <td class="postblock">Asunto</td> + <td> + <input type="text" name="subject" size="35" maxlength="100" accesskey="s" /> + <?py if replythread: ?> + <input type="submit" value="Responder" accesskey="z" /> + <?py else: ?> + <input type="submit" value="Crear hilo" accesskey="z" /> + <?py #endif ?> + </td> + </tr> + <?py #endif ?> + <tr> + <td class="postblock">Mensaje</td> + <td><textarea name="message" cols="50" rows="6" accesskey="m"></textarea></td> + </tr> + <?py if not oek_finish: ?> + <?py if (replythread and allow_image_replies) or (not replythread and allow_images): ?> + <tr> + <td class="postblock">Archivo</td> + <td> + <input type="file" name="file" id="file" accesskey="f" /> + <span id="filepreview" style="display:none;"></span> + <?py if allow_spoilers: ?> + <label>[<input type="checkbox" name="spoil" id="spoil" />Spoiler]</label> + <?py #endif ?> + <?py if allow_noimage and not replythread: ?> + <label>[<input type="checkbox" name="noimage" id="noimage" />Sin imagen]</label> + <?py #endif ?> + </td> + </tr> + <?py #endif ?> + <?py elif oek_finish != 'no': ?> + <input type="hidden" name="oek_file" value="#{oek_finish}" /> + <?py #endif ?> + <tr class="pass"> + <td class="postblock">Clave</td> + <td><input type="password" name="password" size="8" accesskey="p" /> (para eliminar el post)</td> + </tr> + <tr> + <td colspan="2" class="rules"> + <ul> + #{postarea_desc} + <?py if supported_filetypes: ?> + <li>Archivos permitidos: <span id="filetypes">#{', '.join(supported_filetypes).upper()}</span>. Hasta <span id="maxsize">#{maxsize}</span>KB. Miniaturas: #{maxdimensions}x#{maxdimensions}px</li> + <?py #endif ?> + <?py if not replythread: ?> + <li><a href="/cgi/catalog/${board}">Catálogo de hilos</a> (Orden: <a href="/cgi/catalog/${board}?sort=1">Nuevo</a>/<a href="/cgi/catalog/${board}?sort=2">Viejo</a>/<a href="/cgi/catalog/${board}?sort=3">Más</a>/<a href="/cgi/catalog/${board}?sort=4">Menos</a>)</li> + <?py #endif ?> + <?py if int(maxage) != 0: ?> + <li>Los hilos son automáticamente eliminados a los <b>#{maxage}</b> días de edad.</li> + <?py #endif ?> + <li>¿Eres nuevo? <a href="/guia.html">Leer antes de postear</a> · <a href="/faq.html">Preguntas frecuentes</a> · <a href="/bai/">Contacto</a></li> + </ul> + </td> + </tr> + </table> +</form> +</div> +<hr /> +<?py if postarea_extra: ?> +<center>#{postarea_extra}</center> +<hr /> +<?py #endif ?> +<form id="delform" action="#{cgi_url}delete" method="post"> + <input type="hidden" name="board" value="#{board}" /> + <?py if threads: ?> + <?py for thread in threads: ?> + <?py if not replythread: ?> + <span id="unhide#{thread['id']}#{board}" style="display:none;">Hilo <a href="#{boards_url}#{board}/res/#{thread['id']}.html">#{thread['id']}</a> oculto. <a class="tt" href="#">Ver hilo</a></span> + <?py #endif ?> + <div id="thread#{thread['id']}#{board}" class="thread" data-length="#{thread['length']}"> + <?py for post in thread['posts']: ?> + <?py if int(post['parentid']) != 0: ?> + <table><tr><td class="ell">…</td> + <td class="reply" id="reply#{post['id']}"> + <?py elif post['file']: ?> + <?py if post['image_width'] != '0': ?> + <div class="fs"><span>Nombre de archivo:</span><a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + <?py else: ?> + <div class="fs"><span>Nombre de archivo:</span><a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B) + <?py #endif ?> + <?py if not replythread: ?> + [<a href="#" title="Ocultar hilo" class="tt">Ocultar hilo</a>] + <?py #endif ?> + </div> + <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}"> + <?py if post['thumb'].startswith('mime'): ?> + <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py elif post['file'][-3:] == 'gif': ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py else: ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py #endif ?> + </a> + <?py #endif ?> + <a name="#{post['id']}"></a> + <?py if post['IS_DELETED'] == '1': ?> + <span class="deleted">No.#{post['id']} eliminado por usuario.</span> + <?py elif post['IS_DELETED'] == '2': ?> + <span class="deleted">No.#{post['id']} eliminado por miembro del staff.</span> + <?py else: ?> + <div class="info"><label><input type="checkbox" name="delete" value="#{post['id']}" /> + <?py if post['subject'] : ?> + <span class="subj">#{post['subject']}</span> + <?py #endif ?></label> + <?py if post['email']: ?> + <?py if post['tripcode']: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a> + <?py else: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a> + <?py #endif ?> + <?py else: ?> + <?py if post['tripcode']: ?> + <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span> + <?py else: ?> + <span class="name"><b>#{post['name']}</b></span> + <?py #endif ?> + <?py #endif ?> + <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span> + <?py if replythread: ?> + <span class="reflink"><a href="##{post['id']}">No.</a><a href="#" class="postid">#{post['id']}</a></span> + <?py else: ?> + <span class="reflink"><a href="#{boards_url}#{board}/res/#{post['parentid'] if post['parentid'] != "0" else post['id']}.html##{post['id']}">No.</a><a href="#{boards_url}#{board}/res/#{post['parentid'] if post['parentid'] != "0" else post['id']}.html#i#{post['id']}">#{post['id']}</a></span> + <?py #endif ?> + <a class="rep" href="#{cgi_url}report/#{board}/#{post['id']}" rel="nofollow">rep</a> + <?py if int(post['expires']): ?> + <small>Expira el ${post['expires_formatted']}</small> + <?py #endif ?> + <?py if int(post['parentid']) != 0: ?> + <?py if post['file']: ?> + <div class="fs"> + <?py if post['image_width'] != '0': ?> + <a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + <?py else: ?> + <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B) + <?py #endif ?> + </div> + <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}"> + <?py if post['thumb'].startswith('mime'): ?> + <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py elif post['file'][-3:] == 'gif': ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py else: ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py #endif ?> + </a> + <?py #endif ?> + <?py #endif ?> + <?py if int(post['parentid']) == 0 and not replythread: ?> + [<a href="#{boards_url}#{board}/res/#{post['id']}.html" class="hsbn">Responder</a>] + <?py if post['file'] == '': ?> + [<a href="#" title="Ocultar Hilo" class="tt">Ocultar</a>] + <?py #endif ?> + <?py #endif ?> + </div> + <?py if post['thumb_width'] != '0' and post['parentid'] != '0': ?> + <blockquote style="margin-left:#{int(post['thumb_width'])+40}px;"> + <?py else: ?> + <blockquote> + <?py #endif ?> + #{post['message']} + </blockquote> + <?py if not replythread and post['shortened']: ?> + <blockquote class="abbrev">(Post muy largo... Presiona <a href="#{boards_url}#{board}/res/#{post['id'] if post['parentid'] == "0" else post['parentid']}.html##{post['id']}">aquí</a> para verlo completo.)</blockquote> + <?py #endif ?> + <?py if int(post['expires_alert']): ?> + <div style="color:red;font-weight:bold;">Este hilo es viejo y desaparecerá pronto.</div> + <?py #endif ?> + <?py #endif ?> + <?py if post['parentid'] == "0": ?> + <?py if not replythread: ?> + <?py if int(thread['omitted']) == 1: ?> + <div class="omitted">Un post omitido. Haz clic en Responder para ver.</div> + <?py elif int(thread['omitted']) > 1: ?> + <div class="omitted">#{thread['omitted']} posts omitidos. Haz clic en Responder para ver.</div> + <?py #endif ?> + <?py #endif ?> + <?py else: ?> + </td></tr></table> + <?py #endif ?> + <?py #endfor ?> + <div class="cut"></div></div> + <?py if replythread: ?> + <hr /> + <div class="nav">[<a href="#{boards_url}#{board}/">Volver al IB</a>] + [<a href="/cgi/catalog/${board}">Catálogo</a>] + [<a href="#top" name="bottom">Subir</a>]</div> + <?py #endif ?> + <hr /> + <?py #endfor ?> + <div class="userdel">Eliminar post <label>[<input type="checkbox" name="imageonly" id="imageonly" />Sólo imagen]</label><br /> + Clave <input type="password" name="password" size="8" /> <input name="deletepost" value="Eliminar" type="submit" /></div> + <?py #endif ?> +</form> +<?py if pagenav: ?> + <div class="pg">#{pagenav}</div> +<?py #endif ?> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py if replythread or oek_finish: ?> + [<a href="#{boards_url}#{board}/">掲示板に戻る</a>] +<?py #endif ?> +<?py if replythread: ?> + [<a href="/cgi/catalog/${board}">カタログ</a>] + [<a href="#bottom" name="top">ボトムへ行く</a>] + <div class="replymode">レス送信モード</div> +<?py #endif ?> +<a name="postbox"></a> +<div class="postarea"> +<?py if allow_oekaki and not oek_finish: ?> + <center><form class="oekform" action="#{cgi_url}oekaki/paint" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /> + <?py if replythread: ?> + <input type="hidden" name="parent" value="#{replythread}" /> + <?py #endif ?> + <select name="oek_applet"> + <option value="neo">PaintBBS NEO</option> + <option value="tegaki">Tegaki</option> + <option value="wpaint">wPaint</option> + <option value="shipainter|n|n">Shi-Painter</option> + <option value="shipainter|y|n">Shi-Painter Pro</option> + </select> + <span id="oek_size"><input type="text" name="oek_x" size="4" maxlength="4" value="300" /> x <input type="text" name="oek_y" size="4" maxlength="4" value="300" /></span> + <input type="submit" value="お絵かきする" /><br /><a href="#{cgi_url}oekaki/finish/#{board}/#{replythread}">アップロード途中の画像</a> + </form></center> +<?py #endif ?> +<?py if oek_finish: ?> +<center style="margin-bottom:0.5em;"><table border=""><tr><td> + <?py if oek_finish == "no": ?> + <font size="+3">画像が見当たりません</font> + <?py else: ?> + <img src="#{boards_url}oek_temp/#{oek_finish}.png?ts=#{ts}" /> + <?py #endif ?> +</td></tr></table></center> +<?py #endif ?> +<form name="postform" id="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /> + <?py if replythread: ?> + <input type="hidden" name="parent" value="#{replythread}" /> + <input type="hidden" name="default_subject" value="#{default_subject}" /> + <?py #endif ?> + <div style="display:none;">Trampa: <input type="text" name="name" size="25" /> <input type="text" name="email" size="25" /></div> + <table class="postform"> + <?py if not disable_name: ?> + <tr> + <td class="postblock">おなまえ</td> + <td><input type="text" name="fielda" size="25" accesskey="n" /></td> + </tr> + <?py #endif ?> + <tr> + <td class="postblock">E-mail</td> + <td> + <input type="text" name="fieldb" size="25" accesskey="e" /> + <?py if disable_subject: ?> + <?py if replythread: ?> + <input type="submit" value="返信" accesskey="z" /> + <?py else: ?> + <input type="submit" value="スレッドを立てる" accesskey="z" /> + <?py #endif ?> + <?py #endif ?> + </td> + </tr> + <?py if not disable_subject: ?> + <tr> + <td class="postblock">題 名</td> + <td> + <input type="text" name="subject" size="35" maxlength="100" accesskey="s" /> + <?py if replythread: ?> + <input type="submit" value="返信" accesskey="z" /> + <?py else: ?> + <input type="submit" value="スレッドを立てる" accesskey="z" /> + <?py #endif ?> + </td> + </tr> + <?py #endif ?> + <tr> + <td class="postblock">コメント</td> + <td><textarea name="message" cols="50" rows="6" accesskey="m"></textarea></td> + </tr> + <?py if not oek_finish: ?> + <?py if (replythread and allow_image_replies) or (not replythread and allow_images): ?> + <tr> + <td class="postblock">添付File</td> + <td> + <input type="file" name="file" id="file" accesskey="f" /> + <span id="filepreview" style="display:none;"></span> + <?py if allow_spoilers: ?> + <label>[<input type="checkbox" name="spoil" id="spoil" />ネタバレ]</label> + <?py #endif ?> + <?py if allow_noimage and not replythread: ?> + <label>[<input type="checkbox" name="noimage" id="noimage" />画像なし]</label> + <?py #endif ?> + </td> + </tr> + <?py #endif ?> + <?py elif oek_finish != 'no': ?> + <input type="hidden" name="oek_file" value="#{oek_finish}" /> + <?py #endif ?> + <tr class="pass"> + <td class="postblock">削除キー</td> + <td><input type="password" name="password" size="8" accesskey="p" /> (削除用)</td> + </tr> + <tr> + <td colspan="2" class="rules"> + <ul> + #{postarea_desc} + <?py if supported_filetypes: ?> + <li>添付可能:<span id="filetypes">#{', '.join(supported_filetypes).upper()}</span>. <span id="maxsize">#{maxsize}</span>KBまで. #{maxdimensions}x#{maxdimensions}以上は縮小.</li> + <?py #endif ?> + <?py if not replythread: ?> + <li><a href="#{cgi_url}catalog/${board}">カタログ</a> (ソート:<a href="/cgi/catalog/${board}?sort=1">新順</a>/<a href="/cgi/catalog/${board}?sort=2">古順</a>/<a href="/cgi/catalog/${board}?sort=3">多順</a>/<a href="/cgi/catalog/${board}?sort=4">少順</a>)</li> + <?py #endif ?> + <?py if int(maxage) != 0: ?> + <li>スレは<b>#{maxage}</b>日間経つと自動的に消されられます.</li> + <?py #endif ?> + <li><a href="/guia.html">使い方</a> · <a href="/faq.html">よくある質問</a> · <a href="/bai/">管理人への連絡</a></li> + </ul> + </td> + </tr> + </table> +</form> +</div> +<hr /> +<?py if postarea_extra: ?> +<center>#{postarea_extra}</center> +<hr /> +<?py #endif ?> +<form id="delform" action="#{cgi_url}delete" method="post"> + <input type="hidden" name="board" value="#{board}" /> + <?py if threads: ?> + <?py for thread in threads: ?> + <?py if not replythread: ?> + <span id="unhide#{thread['id']}#{board}" style="display:none">スレ<a href="#{boards_url}#{board}/res/#{thread['id']}.html">#{thread['id']}</a>は隠しました. <a class="tt" href="#">スレを表示</a></span> + <?py #endif ?> + <div id="thread#{thread['id']}#{board}" class="thread" data-length="#{thread['length']}"> + <?py for post in thread['posts']: ?> + <?py if int(post['parentid']) != 0: ?> + <table><tr><td class="ell">…</td> + <td class="reply" id="reply#{post['id']}"> + <?py elif post['file']: ?> + <?py if post['image_width'] != '0': ?> + <div class="fs"><span>画像ファイル名:</span><a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + <?py else: ?> + <div class="fs"><span>画像ファイル名:</span><a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B) + <?py #endif ?> + <?py if post['file'][-3:] == 'gif': ?> + <small>アニメGIF</small> + <?py elif not post['thumb'].startswith('mime'): ?> + <small>サムネ表示</small> + <?py #endif ?> + <?py if not replythread: ?> + [<a href="#" title="スレを隠す" class="tt">隠す</a>] + <?py #endif ?> + </div> + <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}"> + <?py if post['thumb'].startswith('mime'): ?> + <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py elif post['file'][-3:] == 'gif': ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py else: ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py #endif ?> + </a> + <?py #endif ?> + <a name="#{post['id']}"></a> + <?py if post['IS_DELETED'] == '1': ?> + <span class="deleted">No.#{post['id']}はユーザーに削除されました.</span> + <?py elif post['IS_DELETED'] == '2': ?> + <span class="deleted">No.#{post['id']}は管理人に削除されました.</span> + <?py else: ?> + <div class="info"><label><input type="checkbox" name="delete" value="#{post['id']}" /><span class="subj">#{post['subject'] if post['subject'] else default_subject}</span></label> + <?py if post['email']: ?> + <?py if post['tripcode']: ?> + Name <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a> + <?py else: ?> + Name <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a> + <?py #endif ?> + <?py else: ?> + <?py if post['tripcode']: ?> + Name <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span> + <?py else: ?> + Name <span class="name"><b>#{post['name']}</b></span> + <?py #endif ?> + <?py #endif ?> + <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span> + <?py if replythread: ?> + <span class="reflink"><a href="##{post['id']}">No.</a><a href="#" class="postid">#{post['id']}</a></span> + <?py else: ?> + <span class="reflink"><a href="#{boards_url}#{board}/res/#{post['parentid'] if post['parentid'] != "0" else post['id']}.html##{post['id']}">No.</a><a href="#{boards_url}#{board}/res/#{post['parentid'] if post['parentid'] != "0" else post['id']}.html#i#{post['id']}">#{post['id']}</a></span> + <?py #endif ?> + <a class="rep" href="#{cgi_url}report/#{board}/#{post['id']}" rel="nofollow">rep</a> + <?py if int(post['expires']): ?> + <small>${post['expires_formatted']}頃消えます</small> + <?py #endif ?> + <?py if int(post['parentid']) != 0: ?> + <?py if post['file']: ?> + <div class="fs"> + <?py if post['image_width'] != '0': ?> + <a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + <?py else: ?> + <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B) + <?py #endif ?> + <?py if post['file'][-3:] == 'gif': ?> + <small>アニメGIF</small> + <?py elif not post['thumb'].startswith('mime'): ?> + <small>サムネ表示</small> + <?py #endif ?> + </div> + <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}"> + <?py if post['thumb'].startswith('mime'): ?> + <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py elif post['file'][-3:] == 'gif': ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py else: ?> + <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /> + <?py #endif ?> + </a> + <?py #endif ?> + <?py #endif ?> + <?py if int(post['parentid']) == 0 and not replythread: ?> + [<a href="#{boards_url}#{board}/res/#{post['id']}.html" class="hsbn">返信</a>] + <?py if post['file'] == '': ?> + [<a href="#" title="スレを隠す" class="tt">隠す</a>] + <?py #endif ?> + <?py #endif ?> + </div> + <?py if post['thumb_width'] != '0' and post['parentid'] != '0': ?> + <blockquote style="margin-left:#{int(post['thumb_width'])+40}px;"> + <?py else: ?> + <blockquote> + <?py #endif ?> + #{post['message']} + </blockquote> + <?py if not replythread and post['shortened']: ?> + <blockquote class="abbrev">(投稿は長すぎ... 全部読むには<a href="#{boards_url}#{board}/res/#{post['id'] if post['parentid'] == "0" else post['parentid']}.html##{post['id']}">こっちら</a>へ)</blockquote> + <?py #endif ?> + <?py if int(post['expires_alert']): ?> + <div style="color:red;font-weight:bold">このスレは古いので、もうすぐ消えます。</div> + <?py #endif ?> + <?py #endif ?> + <?py if int(post['parentid']) == 0: ?> + <?py if not replythread: ?> + <?py if int(thread['omitted']) > 0: ?> + <span class="omitted">レス${thread['omitted']}件省略。全て読むには返信ボタンを押してください。</span> + <?py #endif ?> + <?py #endif ?> + <?py else: ?> + </td></tr></table> + <?py #endif ?> + <?py #endfor ?> + <div class="cut"></div></div> + <?py if replythread: ?> + <hr /> + <div class="nav">[<a href="#{boards_url}#{board}/">掲示板に戻る</a>] + [<a href="/cgi/catalog/${board}">カタログ</a>] + [<a href="#top" name="bottom">トップへ戻る</a>]</div> + <?py #endif ?> + <hr /> + <?py #endfor ?> + <div class="userdel"> + 【記事削除】<label>[<input type="checkbox" name="imageonly" id="imageonly" />画像だけ消す]</label><br /> + 削除キー<input type="password" name="password" size="8" /> <input name="deletepost" value="削除" type="submit" /> + </div> + <?py #endif ?> +</form> +<?py if pagenav: ?> + <div class="pg">#{pagenav}</div> +<?py #endif ?> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<div id="ctrl"> + [<a href="#{boards_url}#{board}/">Volver al IB</a>] + [Orden: + <a class="cat_sort" data-sort="0" href="?sort=0">#{"<b>Normal</b>" if i_sort == "" else "Normal"}</a> + <a class="cat_sort" data-sort="1" href="?sort=1">#{"<b>Nuevo</b>" if i_sort == "1" else "Nuevo"}</a> + <a class="cat_sort" data-sort="2" href="?sort=2">#{"<b>Viejo</b>" if i_sort == "2" else "Viejo"}</a> + <a class="cat_sort" data-sort="3" href="?sort=3">#{"<b>Más</b>" if i_sort == "3" else "Más"}</a> + <a class="cat_sort" data-sort="4" href="?sort=4">#{"<b>Menos</b>" if i_sort == "4" else "Menos"}</a>] + [Tamaño: <a id="cat_size" href="#">Pequeño</a>] + [Texto: <a id="cat_hide" href="#">Ocultar</a>] + [Buscar: <input id="cat_search" type="text"><input type="hidden" name="board" value="#{board}" /> +</div> +<div class="extramode">Modo Catálogo</div> +<div id="catalog" style="margin:1em auto;"> + <?py i = 1 ?> + <?py for thread in threads: ?><div id="cat#{thread['id']}#{board}" class="thread" data-num="${i}" data-id="#{thread['id']}" data-res="${thread['length']}"> + <?py if thread['thumb'] != '': ?> + <a href="#{boards_url}#{board}/res/#{thread['id']}.html" rel="nofollow"><img src="#{images_url}#{board}/cat/#{thread['thumb']}" alt="#{thread['id']}" /></a><br /> + <?py #endif ?> + <div class="replies">Respuestas: ${thread['length']}</div> + <?py if thread['thumb'] != '': ?> + <p><span class="subj">${thread['subject']}</span><br />${thread['message']}</p> + <?py else: ?> + <p><a href="#{boards_url}#{board}/res/#{thread['id']}.html" rel="nofollow" class="subj">${thread['subject']}</a><br />${thread['message']}</p> + <?py #endif ?> + <?py i += 1 ?> + </div><?py #endfor ?> +</div> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<br /><br /><hr size="1"> +<br /><br /><div style="text-align:center;color:red;font-size:x-large;font-weight:bold;">#{error} +<br /><br /><a href="#{boards_url}#{board}/">Volver</a></div> +<br /><br /><hr size="1"> +</body> +</html>
\ 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 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<title>Error@Bienvenido a Internet</title> +<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<style type="text/css">.error{color:red;font-weight:bold;font-size:16pt} .sub{font-weight:bold}</style> +</head> +<body> +<?py if exception: ?> +<p class="error">ERROR : Ha ocurrido un error inesperado.</p> +<p class="sub">Esto no es normal y te pedimos que reportes el problema en +<a href="/bai/">Discusión de B.a.I.</a> o a través de +<a href="mailto:burocracia@bienvenidoainternet.org">nuestro e-mail</a>, +presentando los siguientes datos y ojalá indicando qué hacer para reproducirlo:</p> +<p>Versión: weabot +<?py include('templates/revision.html') ?><br /> +Tipo: ${exception}<br /> +Detalle: ${error}<br /> +Traceback:<br /> +<blockquote> + <?py for line in detail: ?> + ${line[0]} ${line[1]} ${line[2]} ${line[3]}<br /> + <?py #endfor ?> +</blockquote></p> +<p class="sub">Te recordamos que el software está en desarrollo y estamos siempre haciendo lo posible para arreglar los problemas lo antes posible.<br />Te pedimos las disculpas por cualquier inconveniente.</p> +<hr /> +<p>weabot dijo "Perdón."<br /><a href="/bai.html">Bienvenido a Internet BBS/IB</a></p> +<?py else: ?> +<p class="error">ERROR : #{error}</p> +<p class="sub">Por favor presiona Atrás y soluciona el problema.</p> +<hr /> +<p>La página principal está <a href="/bai.html">aquí</a>.<br />Si esto es inusual intenta <a href="/bai/">contactarnos</a>.</p><?py #endif ?> +</body> +</html>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8" ?>
+<rss version="2.0">
+ <channel>
+ <title>Bienvenido a Internet BBS/IB</title>
+ <link>https://bienvenidoainternet.org/</link>
+ <description>El BBS/IB más activo de la esfera hispana.</description>
+ <language>es</language>
+ <webMaster>burocracia@bienvenidoainternet.org (Staff ★)</webMaster>
+ <image>
+ <url>https://bienvenidoainternet.org/rss_logo.png</url>
+ <title>Bienvenido a Internet BBS/IB</title>
+ <link>https://bienvenidoainternet.org/</link>
+ <width>144</width>
+ <height>144</height>
+ </image>
+<?py for post in posts: ?>
+ <item>
+ <title>${post['board_name']}: #{post['content']}</title>
+ <pubDate>${post['timestamp_formatted']}</pubDate>
+ <link>https://bienvenidoainternet.org#{post['url']}</link>
+ </item>
+<?py #endfor ?>
+ </channel>
+</rss>
\ 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 +<?py if dir == 'clusterfuck': ?> + +AuthName "BAI" +AuthType Basic +AuthUserFile "/home/z411/.htpasswds/public_html/wiki/passwd" +<Limit GET> +require valid-user +</Limit> + +<?py #endif ?> +<?py if dir == 'anarkia': ?> +ExpiresByType text/css "access plus 0 seconds" +<?py #endif ?> + +ErrorDocument 403 https://bienvenidoainternet.org/cgi/banned/#{dir} +<?py if ips: ?> + +order allow,deny + <?py for ip in ips: ?> +deny from #{ip} + <?py #endfor ?> +allow from all +<?py #endif ?> 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 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Archivo de #{board_name}@Bienvenido a Internet BBS</title> + <meta http-equiv="Content-Type" content="application/xhtml+xml;charset=utf-8" /> + <meta name="robots" content="index, follow" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel="shortcut icon" href="#{static_url}img/favicon.ico" /> + <style type="text/css"> + body {margin:8px} + h1 {margin:0 0 20px} + pre {margin:0} + .fake {color:#0000EE;text-decoration:underline;cursor:pointer} + .fake:active {color:#FF0000} + img {width:20px;height:22px;margin-right:4px} + td {text-align:left;vertical-align:bottom;padding-right:14px} + .r {text-align:right} + a:link, a:hover {color:#0000EE} + a:active {color:#FF0000} + a:visited {color:#551A8B} + </style> +</head> +<body> +<h1>Índice de /#{board}/kako/</h1> +<pre> + <table style="border-collapse:collapse;"> + <tr> + <th><img src="/blank.png" /></th> + <td><span class="fake">Nombre</span></td> + <td><span class="fake">Tamaño</span></td> + <td><span class="fake">Descripción</span></td> + </tr> + <tr> + <td colspan="4" style="padding:0"><hr /></td> + </tr> + <tr> + <th><img src="/back.png" /></th> + <td><a href="/#{board}/">..</a></td> + <td class="r">-</td> + <td></td> + </tr> + <?py for thread in threads: ?> + <tr> + <th><img src="/text.png" /></th> + <td><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">${thread['timestamp']}.json</a></td> + <?py if int(thread['length']) > 1000: ?> + <td class="r">1KR</td> + <?py else: ?> + <td class="r">${thread['length']}R</td> + <?py #endif ?> + <td>${thread['subject']}</td> + </tr> + <?py #endfor ?> + </table> + <hr /> +</pre> +<address>weabot/0.8.4 (CentOS) Servidor ubicado en bienvenidoainternet.org Puerto 443</address> +</body> +</html>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> +<div class="replymode">Nuevo board</div> +<form action="#{cgi_url}manage/addboard" method="post"> + <table> + <tr> + <td class="postblock">Directorio</td> + <td><input type="text" name="dir" maxlength="16" style="width:100%;" /></td> + </tr> + <tr> + <td class="postblock">Nombre</td> + <td><input type="text" name="name" maxlength="64" style="width:100%;" /></td> + </tr> + <tr> + <td colspan="2"><input type="submit" name="submit" style="width:100%;" value="Agregar board" /></td> + </table> +</form> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<?py from tenjin.helpers.html import * ?> +<center> +<div class="replymode">Bans</div> +<?py if mode == 0: ?> +<form action="#{cgi_url}manage/ban/" name="banform" method="post"> +<table> +<tr> + <td class="postblock">Dirección IP</td> + <td><input type="text" name="ip" size="20" /></td> +</tr> +<tr><td colspan="2"><input type="submit" value="Ir a formulario de ban" style="width:100%;" /></td></tr> +</table> +</form> +<hr /> +<table class="managertable"> +<tr> + <th>Dirección IP</th> + <th>Máscara de red</th> + <th>Boards</th> + <th>Agregado</th> + <th>Expira</th> + <th>Ciego</th> + <th>Puesto por</th> + <th>Razón</th> + <th>Nota</th> + <th>Acción</th> +</tr> +<?py for ban in bans: ?> +<tr> + <td>${ban['ip']}</td> + <td>${ban['netmask']}</td> + <td>${ban['boards']}</td> + <td>${ban['added']}</td> + <td>${ban['until']}</td> + <td>${ban['blind']}</td> + <td>${ban['staff']}</td> + <td>${ban['reason']}</td> + <td>${ban['note']}</td> + <td> + [<a href="#{cgi_url}manage/ipshow?ip=#{ban['ip']}">Ver posts</a>] + [<a href="#{cgi_url}manage/ban?ip=#{ban['ip']}&edit=#{ban['id']}">Editar</a>] + [<a href="#{cgi_url}manage/bans/delete/#{ban['id']}">Eliminar</a>] + </td> +</tr> +<?py #endfor ?> +</table> +<?py elif mode == 1: ?> +<form action="#{cgi_url}manage/ban" name="banform" method="post"> +<table> + <tr><td class="postblock">IP</td><td><input type="text" name="ip" value="${ip}" size="20" style="width:100%;" /></td></tr> + <tr><td class="postblock">Máscara de red</td><td><input type="text" name="netmask" value="${startvalues['netmask']}" style="width:100%;" /></td></tr> + <tr> + <td class="postblock">Board(s)</td> + <td> + <input type="checkbox" name="board_all" id="b_all" value="1"#{checked(startvalues['where'] == '')} /><label for="b_all" style="font-weight:bold">Todos los boards</label><hr /> + <?py for board in boards: ?> + <input type="checkbox" name="board_#{board['dir']}" id="b#{board['dir']}" value="1"#{checked(board['dir'] in startvalues['where'])} /><label for="b#{board['dir']}">${board['name']}</label><br /> + <?py #endfor ?> + <?py if edit_id > 0: ?> + <input type="hidden" name="edit" value="${edit_id}" /> + <?py #endif ?> + </td> + </tr> + <tr><td class="postblock">Mensaje</td><td><textarea name="reason" style="width:100%;">${startvalues['reason']}</textarea></td></tr> + <tr><td class="postblock">Nota para staff</td><td><input type="text" name="note" value="${startvalues['note']}" style="width:100%;" /></td></tr> + <tr><td class="postblock">Ciego</td><td><input type="checkbox" name="blind" id="blind" value="1"#{checked(startvalues['blind'] == '1')} /><label for="blind"></label></td></tr> + <tr><td class="postblock">Expira en <span style="font-weight:normal;">(segundos)</span></td><td><input type="text" id="seconds" name="seconds" value="#{startvalues['seconds']}" style="width:100%;" /> + <br /> + <div id="timelist"> + <a href="#" data-secs="0">Nunca</a> + <a href="#" data-secs="3600">1h</a> + <a href="#" data-secs="21600">6h</a> + <a href="#" data-secs="43200">12h</a> + <a href="#" data-secs="86400">1d</a> + <a href="#" data-secs="259200">3d</a> + <a href="#" data-secs="604800">1w</a> + <a href="#" data-secs="2592000">30d</a> + <a href="#" data-secs="31536000">1y</a> + </div> + </td> + </tr> + <tr> + <td colspan="2"><input type="submit" value="Colocar ban" style="width:100%;" /></td> + </tr> +</table> +</form> +<?py #endif ?> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<?py from tenjin.helpers.html import * ?> +<center> +<div class="replymode">Opciones de Board</div> +<?py if mode == 0: ?> +<table class="managertable"> + <tr><th colspan="2">Sección</th><th>Accion</th></tr> + <?py for board in boards: ?> + <tr><td>/#{board['dir']}/</td><td>#{board['name']}</td><td>[<a href="#{cgi_url}manage/board/#{board['dir']}">Configurar</a>]</td></tr> + <?py #endfor ?> + </table> +<?py elif mode == 1: ?> +<form action="#{cgi_url}manage/board/${boardopts['dir']}" method="post"> +<table> +<tr> + <td class="postblock">ID</td> + <td><input type="text" name="name" value="${boardopts['id']}" maxlength="16" style="width:100%;" disabled="disabled" /></td> +</tr> +<tr> + <td class="postblock">Directorio</td> + <td><input type="text" name="name" value="${boardopts['dir']}" maxlength="32" style="width:100%;" disabled="disabled" /></td> +</tr> +<tr> + <td class="postblock">Nombre</td> + <td><input type="text" name="name" value="${boardopts['name']}" maxlength="64" style="width:100%;" /></td> +</tr> +<tr> +<td class="postblock">Nombre largo</td> +<td><input type="text" name="longname" size="50" value="${boardopts['longname']}" maxlength="128" style="width:100%;" /></td> +</tr> +<tr> +<td class="postblock">Sub-nombre</td> +<td><input type="text" name="subname" value="${boardopts['subname']}" maxlength="3" style="width:100%;" /></td> +</tr> +<tr> +<td class="postblock">Tipo</td> +<td> + <select style="width:100%;" name="type"> + <option value="0">Imageboard</option> + <option value="1"#{selected(boardopts['board_type'] == '1')}>Textboard</option> + </select> +</td> +</tr> +<tr> +<td class="postblock">Descripción / Reglas</td> +<td> + <textarea id="brd_desc" name="postarea_desc" rows="10" cols="50" style="width:100%;">${boardopts['postarea_desc']}</textarea> + <div id="prev_desc" style="border:1px dotted gray;display:none;padding:4px;width:100%;" contenteditable="true"></div> +</td> +</tr> +<tr> +<td class="postblock">Caja extra</td> +<td><textarea name="postarea_extra" rows="5" cols="50" style="width:100%;">${boardopts['postarea_extra']}</textarea></td> +</tr> +<tr> +<td class="postblock">Forzar CSS <span style="font-weight:normal;">("" = default)</span></td> +<td><input type="text" name="force_css" size="50" value="#{boardopts['force_css']}" maxlength="255" style="width:100%;" /></td> +</tr> +<tr> +<td class="postblock">Nombre por defecto</td> +<td><input type="text" name="anonymous" size="50" maxlength="128" value="${boardopts['anonymous']}" style="width:100%;" /></td> +</tr> +<tr> +<td class="postblock">Título por defecto</td> +<td><input type="text" name="subject" size="50" maxlength="64" value="${boardopts['subject']}" style="width:100%;" /></td> +</tr> +<tr> +<td class="postblock">Mensaje por defecto</td> +<td><input type="text" name="message" size="50" maxlength="128" value="${boardopts['message']}" style="width:100%;" /></td> +</tr> +<tr> +<td class="postblock">ID</td> +<td> + <select name="useid" style="width:100%;"> + <option value="0">Desactivado</option> + <option value="1"#{selected(boardopts['useid'] == '1')}>Activado</option> + <option value="2"#{selected(boardopts['useid'] == '2')}>Activado siempre</option> + <option value="3"#{selected(boardopts['useid'] == '3')}>Activado siempre, detallado</option> + </select> +</td> +</tr> +<tr> +<td class="postblock">Slip</td> +<td> + <select name="slip" style="width:100%;"> + <option value="0">Desactivado</option> + <option value="1"#{selected(boardopts['slip'] == '1')}>Activado</option> + <option value="2"#{selected(boardopts['slip'] == '2')}>Sólo dominio</option> + <option value="3"#{selected(boardopts['slip'] == '3')}>Todo</option> + </select> +</td> +</tr> +<tr> +<td class="postblock">Código de país</td> +<td> + <select name="countrycode" style="width:100%;"> + <option value="0">Desactivado</option> + <option value="1"#{selected(boardopts['countrycode'] == '1')}>Activado</option> + </select> +</td> +</tr> +<tr> +<td class="postblock">Desactivar nombre</td> +<td><input type="checkbox" name="disable_name" id="noname" value="1"#{checked(boardopts['disable_name'] == '1')} /><label for="noname"></label></td> +</tr> +<tr> +<td class="postblock">Desactivar asunto</td> +<td><input type="checkbox" name="disable_subject" id="nosub" value="1"#{checked(boardopts['disable_subject'] == '1')} /><label for="nosub"></label></td> +</tr> +<tr> +<td class="postblock">Papelera de reciclaje</td> +<td><input type="checkbox" name="recyclebin" id="bin" value="1"#{checked(boardopts['recyclebin'] == '1')} /><label for="bin"></label></td> +</tr> +<tr> +<td class="postblock">Cerrado</td> +<td><input type="checkbox" name="locked" id="locked" value="1"#{checked(boardopts['locked'] == '1')} /><label for="locked"></label></td> +</tr> +<tr> +<td class="postblock">Secreto</td> +<td><input type="checkbox" name="secret" id="secret" value="1"#{checked(boardopts['secret'] == '1')} /><label for="secret"></label></td> +</tr> +<tr> +<td class="postblock">Permitir spoilers</td> +<td><input type="checkbox" name="allow_spoilers" id="spoil" value="1"#{checked(boardopts['allow_spoilers'] == '1')} /><label for="spoil"></label></td> +</tr> +<tr> +<td class="postblock">Permitir oekaki</td> +<td><input type="checkbox" name="allow_oekaki" id="oek" value="1"#{checked(boardopts['allow_oekaki'] == '1')} /><label for="oek"></label></td> +</tr> +<tr> +<td class="postblock">Permitir crear hilos sin imagen</td> +<td><input type="checkbox" name="allow_noimage" id="noimgallow" value="1"#{checked(boardopts['allow_noimage'] == '1')} /><label for="noimgallow"></label></td> +</tr> +<tr> +<td class="postblock">Permitir subida</td> +<td><input type="checkbox" name="allow_images" id="img" value="1"#{checked(boardopts['allow_images'] == '1')} /><label for="img">Al crear un hilo</label><br /><input type="checkbox" name="allow_image_replies" id="imgres" value="1"#{checked(boardopts['allow_image_replies'] == '1')} /><label for="imgres">Al responder</label></td> +</tr> +<tr> +<td class="postblock">Tipos de archivo</td> +<td> + <?py for filetype in filetypes: ?> + <input type="checkbox" name="filetype#{filetype['ext']}" id="#{filetype['ext']}" value="1"#{checked(filetype['ext'] in supported_filetypes)} /><label for="#{filetype['ext']}">${filetype['ext'].upper()}</label><br /> + <?py #endfor ?> +</td> +</tr> +<tr> +<td class="postblock">Tamaño máximo <span style="font-weight:normal;">(KB)</span></td> +<td><input type="text" name="maxsize" value="#{boardopts['maxsize']}" maxlength="5" size="11" /></td> +</tr> +<tr> +<td class="postblock">Dimensión de miniatura <span style="font-weight:normal;">(px)</span></td> +<td><input type="text" name="thumb_px" value="#{boardopts['thumb_px']}" maxlength="3" size="11" /></td> +</tr> +<tr> +<td class="postblock">Hilos en página frontal</td> +<td><input type="text" name="numthreads" value="#{boardopts['numthreads']}" maxlength="2" size="11" /></td> +</tr> +<tr> +<td class="postblock">Respuestas a mostrar</td> +<td><input type="text" name="numcont" value="#{boardopts['numcont']}" maxlength="2" size="11" /></td> +</tr> +<tr> +<td class="postblock">Máximo de líneas <span style="font-weight:normal;">(frontal)</span></td> +<td><input type="text" name="numline" value="#{boardopts['numline']}" maxlength="3" size="11" /></td> +</tr> +<tr> +<td class="postblock">Edad máxima de un hilo</td> +<td><input type="text" name="maxage" value="#{boardopts['maxage']}" maxlength="3" size="11" /> (días; 0 = desactivar)</td> +</tr> +<tr> +<td class="postblock">Inactividad máxima de un hilo</td> +<td><input type="text" name="maxinactive" value="#{boardopts['maxinactive']}" maxlength="3" size="11" /> (días; 0 = desactivar)</td> +</tr> +<tr> +<td class="postblock">Archivar hilos</td> +<td><input type="checkbox" name="archive" id="arch" value="1"#{checked(boardopts['archive'] == '1')} /><label for="arch"></label></td> +</tr> +<tr> +<td class="postblock">Espera para crear nuevo hilo</td> +<td><input type="text" name="threadsecs" value="#{boardopts['threadsecs']}" maxlength="4" size="11" /> (segundos)</td> +</tr> +<tr> +<td class="postblock">Espera entre respuestas</td> +<td><input type="text" name="postsecs" value="#{boardopts['postsecs']}" maxlength="3" size="11" /> (segundos)</td> +</tr> +</table> +<br /> +<hr /> +<input type="submit" value="Guardar cambios" /> +</form> +<?py #endif ?> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> +<div class="replymode">Cambiar contraseña</div> +<form action="#{cgi_url}manage/changepassword" method="post"> +<table> + <tr> + <td class="postblock">Clave actual</td> + <td><input type="password" name="oldpassword" style="width:100%;" /></td> + </tr> + <tr> + <td class="postblock">Nueva clave</td> + <td><input type="password" name="newpassword" style="width:100%;" /></td> + </tr> + <tr> + <td class="postblock">Confirmar nueva clave</td> + <td><input type="password" name="newpassword2" style="width:100%;" /></td> + </tr> + <tr><td colspan="2"><input type="submit" style="width:100%;" value="Cambiar" /></td></tr> +</table> +</form> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> +<div class="replymode">Eliminar Post</div> +<form action="#{cgi_url}manage/delete_confirmed/#{curboard}/#{postid}" method="get"> +<?py if do_ban: ?> + <input type="hidden" name="ban" value="true" /> +<?py #endif ?> +<p> + <b>Post #${postid} de /${curboard}/</b><br /> + <input id="a" type="checkbox" name="imageonly" value="true" /><label for="a">Eliminar sólo archivo</label><br /> + <input id="b" type="checkbox" name="perma" value="true" /><label for="b" style="font-weight:bold">Eliminar permanentemente</label><br /> + <br /> + <i>Nota: Por favor evitar eliminar <b>permanentemente</b> el post al menos que sea estrictamente necesario. + <br />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.</i> + <br /><br /> + <input type="submit" value="Eliminar" /> +</p> +</form> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<?py from tenjin.helpers.html import * ?> +<center> +<div class="replymode">Filtros</div> +<?py if mode == 0: ?> +<table class="managertable"> + <tr> + <th>ID</th> + <th>Boards</th> + <th>Tipo</th> + <th>Acción</th> + <th>Mensaje</th> + <th>Modificado</th> + <th>Por</th> + <th>Acción</th> + </tr> + <?py for filter in filters: ?> + <tr> + <td style="text-align:center">#{filter['id']}</td> + <td style="text-align:center">${filter['boards']}</td> + <td>#{filter['type_formatted']}</td> + <td>#{filter['action_formatted']}</td> + <td>${filter['reason']}</td> + <td style="text-align:center">${filter['added']}</td> + <td style="text-align:center">${filter['staff']}</td> + <td style="text-align:center">[<a href="#{cgi_url}manage/filters/add?edit=#{filter['id']}">Editar</a>]<br /> + [<a href="#{cgi_url}manage/filters/delete/#{filter['id']}">Eliminar</a>]</td> + </tr> + <?py #endfor ?> + <tr><td colspan="9" style="text-align:center"> + <form action="#{cgi_url}manage/filters/add" method="get"> + <input type="submit" value="Agregar filtro" /> + </form></td> + </tr> +</table> +<?py elif mode == 1: ?> +<form name="banform" method="post"> +<table> + <tr><th colspan="3" class="postblock">Tipo de filtro</th></tr> + <tr> + <td class="postblock"><input type="radio" name="type" id="type1" value="0"#{checked(startvalues['type'] == '0')} /><label for="type1">Palabra</label></td> + <td style="text-align:right">Regex:</td> + <td><input type="text" name="word" value="${startvalues['word']}" /></td> + </tr> + <tr> + <td rowspan="2" class="postblock"><input type="radio" name="type" id="type2" value="1"#{checked(startvalues['type'] == '1')} /><label for="type2">Nombre/Tripcode</label></td> + <td style="text-align:right">Nombre:</td> + <td><input type="text" name="name" value="${startvalues['name']}" /> (regex)</td> + </tr> + <tr> + <td style="text-align:right">Tripcode:</td> + <td><input type="text" name="trip" value="${startvalues['trip']}" /> (incluir separador)</td> + </tr> +</table> +<br /> +<div style="text-align:left;display:inline-block;"> + <div class="postblock" style="display:block;text-align:center;margin-bottom:0.5em;">Aplicar a</div> + <div style="padding:0 10px"> + <input type="checkbox" name="board_all" id="board_all" value="1"#{checked(startvalues['where'] == '')} /><label for="board_all" style="font-weight:bold">Todos los boards</label> + <hr /> + <?py for board in boards: ?> + <input type="checkbox" name="board_#{board['dir']}" id="board_#{board['dir']}" value="1"#{checked(board['dir'] in startvalues['where'])} /><label for="board_#{board['dir']}">${board['name']} <span style="opacity:0.5">(/#{board['dir']}/)</span></label><br /> + <?py #endfor ?> + </div> +</div> +<br /><br /> +<table> + <tr> + <th colspan="3" class="postblock">Acción</th> + </tr> + <tr> + <td class="postblock"><input type="radio" name="action" id="act0" value="0"#{checked(startvalues['action'] == '0')} /><label for="act0">Abortar post</label></td> + <td colspan="2"></td> + </tr> + <tr> + <td class="postblock"><input type="radio" name="action" id="act1" value="1"#{checked(startvalues['action'] == '1')} /><label for="act1">Reemplazar</label></td> + <td colspan="2"><input type="text" name="changeto" value="#{startvalues['changeto']}" size="40" /></td> + </tr> + <tr> + <td rowspan="2" class="postblock"><input type="radio" name="action" id="act2" value="2"#{checked(startvalues['action'] == '2')} /><label for="act2">Autoban</label></td> + <td style="text-align:right">Expira en:</td> + <td><input type="text" name="seconds" id="seconds" size="6" value="#{startvalues['seconds']}" /> (segundos)<div style="float:right"><input type="checkbox" name="blind" id="blind" value="1"#{checked(startvalues['blind'] == '1')} /><label for="blind">Ban ciego</label></div></td> + </tr> + <tr> + <td style="text-align:right">Preset:</td> + <td id="timelist"> + <a href="#" data-secs="0">Nunca</a> + <a href="#" data-secs="3600">1h</a> + <a href="#" data-secs="21600">6h</a> + <a href="#" data-secs="43200">12h</a> + <a href="#" data-secs="86400">1d</a> + <a href="#" data-secs="259200">3d</a> + <a href="#" data-secs="604800">1w</a> + <a href="#" data-secs="2592000">30d</a> + <a href="#" data-secs="31536000">1y</a> + </td> + </tr> + <tr> + <td rowspan="2" class="postblock"><input type="radio" name="action" id="act3" value="3"#{checked(startvalues['action'] == '3')} /><label for="act3">Redireccionar</label></td> + <td colspan="2"><input type="text" name="redirect_url" value="#{startvalues['redirect_url']}" size="40" /></td> + </tr> + <tr> + <td style="text-align:right">Tardar:</td> + <td><input type="text" name="redirect_time" size="6" value="#{startvalues['redirect_time']}" /> (segundos)</td> + </tr> +</table> +<br /> +<table> + <tr><th class="postblock" style="padding:2px">Mensaje a mostrar</th></tr> + <tr><td><input type="text" size="50" name="reason" value="#{startvalues['reason']}" /></td></tr> +</table> +<br /> +<input type="submit" name="add" value="#{submit}" /> +</form> +<?py #endif ?> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> + <div class="replymode">Eliminar por IP</div> + <form action="#{cgi_url}manage/ipdelete" name="ipdeleteform" method="post"> + <table> + <tr> + <td class="postblock">Board(s)</td> + <td> + <input type="checkbox" name="board_all" id="all" value="1" /><label for="all" style="font-weight:bold">Todos los boards</label><hr /> + <?py for board in boards: ?> + <input type="checkbox" name="board_#{board['dir']}" id="#{board['dir']}" value="1" /><label for="#{board['dir']}">#{board['name']} <span style="opacity:0.5">(/#{board['dir']}/)</span></label><br /> + <?py #endfor ?> + </td> + </tr> + <tr> + <td class="postblock">Dirección IP</td> + <td><input type="text" name="ip" style="width:100%;" /></td> + </tr> + <tr><td colspan="2"><input type="submit" style="width:100%;" value="Eliminar posts" /></td></tr> + </table> + </form> +</center><hr /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> + <div class="replymode">Mostrar por IP</div> + <?py if mode == 0: ?> + <form action="#{cgi_url}manage/ipshow" method="post"> + <table> + <tr><td class="postblock">Dirección IP</td><td><input type="text" name="ip" /></td></tr> + <tr><td colspan="2"><input type="submit" style="width:100%;" value="Mostrar posts" /></td></tr> + </table> + </form> + <?py else: ?> + <style>td img{max-width:150px;height:auto;}td.z{padding:0}</style> + <div class="logo" style="margin:0;">Actividad IP #{ip} (#{len(posts)})</div> + <center> + Hostname: #{host if host else "Desconocido"} [#{country if country else "??"}]#{" (Nodo Tor)" if tor else ""}<br /> + <br /> + <form action="#{cgi_url}manage/ban/" name="banform" method="post"><input type="hidden" name="ip" value="${ip}" /><input type="submit" value="Ir a formulario de ban" /></form> + <hr /> + <?py if posts: ?> + <table class="managertable"> + <tr> + <th>Sección</th> + <th>Padre</th> + <th>ID</th> + <th>Fecha</th> + <th>Nombre</th> + <th>Asunto</th> + <th>Mensaje</th> + <th>Archivo</th> + <th>Acción</th> + </tr> + <?py for post in posts: ?> + <tr> + <td>#{post['dir']}</td> + <td>#{post['parentid']}</td> + <td>#{post['id']}</td> + <td class="date" data-unix="${post['timestamp']}">#{post['timestamp_formatted']}</td> + <?py if post['tripcode']: ?> + <td class="name"><b>#{post['name']}</b> #{post['tripcode']}</td> + <?py else: ?> + <td class="name"><b>#{post['name']}</b></td> + <?py #endif ?> + <td>#{post['subject']}</td> + <td>#{post['message']}</td> + <?py if post['file']: ?> + <td class="z"><img src="#{images_url}#{post['dir']}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /></td> + <?py else: ?> + <td></td> + <?py #endif ?> + <td> + <?py if post['IS_DELETED'] == '0': ?> + <a href="#{cgi_url}manage/delete/#{post['dir']}/#{post['id']}">Eliminar</a> + <?py elif post['IS_DELETED'] == '1': ?> + <a href="#{cgi_url}manage/recyclebin/0/restore/#{post['dir']}/#{post['id']}">Rec</a> + <abbr title="Eliminado por usuario">[1]</abbr> + <?py else: ?> + <a href="#{cgi_url}manage/recyclebin/0/restore/#{post['dir']}/#{post['id']}">Rec</a> + <abbr title="Eliminado por staff">[2]</abbr> + <?py #endif ?> + </td> + </tr> + <?py #endfor ?> + </table> + <hr /> + <?py else: ?> + <b>Error:</b> No hay posts<br /><br /> + <?py #endif ?> + [<a href="#{cgi_url}manage/ipshow">Volver al panel</a>] + <?py #endif ?> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> +<div class="replymode">Cerrar o abrir board</div> +<table class="managertable"> + <tr><th colspan="2">Sección</th><th>Acción</th></tr> + <?py for board in boards: ?> + <tr> + <td>/#{board['dir']}/</td><td>#{board['name']}</td> + <?py if board['locked'] == '0': ?> + <td style="text-align:center;">[<a href="#{cgi_url}manage/boardlock/#{board['dir']}">Cerrar</a>]</td> + <?py elif board['locked'] == '1': ?> + <td style="text-align:center;">[<a href="#{cgi_url}manage/boardlock/#{board['dir']}">Abrir</a>]</td> + <?py #endif ?> + </tr> + <?py #endfor ?> +</table> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<?py include('templates/base_top.html') ?> +<center> + #{page} + <form action="#{cgi_url}manage" method="post"> + <table> + <tr> + <td class="postblock">Usuario</td> + <td><input type="text" name="username" /></td> + </tr> + <tr> + <td class="postblock">Contraseña</td> + <td><input type="password" name="password" /></td> + </tr> + <tr> + <td colspan="2"><input id="submit" type="submit" name="submit" style="width:100%;" value="Entrar" /></td> + </tr> + </table> + </form> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> +<div class="replymode">Registro</div> +<table class="managertable"> + <tr><th>Fecha</th><th>Staff</th><th>Acción</th></tr> +<?py for log in logs: ?> + <tr> + <td class="date" data-unix="${log['timestamp']}" style="white-space:nowrap;">${log['timestamp_formatted']}</td> + <td>${log['staff']}</td> + <td>${log['action']}</td> + </tr> +<?py #endfor ?> +</table> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> + <div style="margin:0.5em 0;"><strong>BANDEJA DE ENTRADA</strong> + <br /> + Denuncias: + <?py if int(reports) > 0: ?> + <a href="#{cgi_url}manage/reports" style="color:red;font-weight:bold;">#{reports}</a> + <?py else: ?> + 0 + <?py #endif ?></div> + <hr /> + <strong>NOTICIAS DEL STAFF</strong> +</center> +<dl style="margin:0 2.5%"> +<?py for post in posts: ?> + <dt><strong>#{post['title'] if post['title'] else "Sin asunto"}</strong><br />#{post['id']} : <b class="name">${post['name']}</b> : <span class="date" data-unix="${post['timestamp']}"}>${post['timestamp_formatted']}</span></dt> + <dd style="margin-bottom:1em;">#{post['message']}</dd> +<?py #endfor ?> +</dl> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<style>#adminmenu {text-align:center;}#adminmenu table {display:inline-block;font-size:10pt;margin-top:2px;text-align:left;} +#adminmenu a {font-weight:bold;}label {vertical-align:top;}dd p {margin:0;}</style> +<script type="text/javascript" src="/static/js/manage.js"></script> +<input type="hidden" name="board" value="" /> +<?py if int(rights) < 4: ?> +<div id="adminmenu">¡Bienvenido, <b><acronym title="Cuenta creada el #{added}">#{username}</acronym></b>! ¡Eres +<?py if rights == '0': ?><b>Accionista</b> +<?py elif rights == '1': ?><b>Accionista</b> +<?py elif rights == '2': ?><span class="developer">Developer</span> +<?py elif rights == '3': ?><span class="moderator">Moderador</span> +<?py #endif ?> de #{site_title}!<br /> +<center> +<table class="reply"> +<tr><td>Principal:</td> +<td>- <a href="#{cgi_url}manage">Inicio</a> - <a href="#{cgi_url}manage/changepassword">Cambiar contraseña</a> - <a href="#{cgi_url}manage/newschannel">News Channel</a> - <a href="//webmail.bienvenidoainternet.org">Correo</a> - <a href="#{cgi_url}manage/logout">Cerrar sesión</a> -</td></tr> +<tr><td>Posts:</td> +<td>- <a href="#{cgi_url}manage/mod">Modbrowse</a> - <a href="#{cgi_url}manage/ipshow">Ver por IP</a> - <a href="#{cgi_url}manage/recyclebin">Papelera de reciclaje</a> - <a href="#{cgi_url}manage/recent_images">Imágenes recientes</a> -</td></tr> +<tr><td>Moderación:</td> +<td>- <a href="#{cgi_url}manage/reports">Denuncias</a> - <a href="#{cgi_url}manage/ipdelete">Eliminar por IP</a> - <a href="#{cgi_url}manage/bans">Lista de bans</a> - <a href="#{cgi_url}manage/move">Mover hilo</a> - <a href="#{cgi_url}manage/filters">Filtros</a> - <a href="#{cgi_url}manage/quotes">Frases</a> -</td></tr> +<?py if int(rights) < 3: ?> +<tr><td>Administración:</td> +<td>- <a href="#{cgi_url}manage/rebuild">Reconstruir</a> - <a href="#{cgi_url}manage/news?type=1">Noticias</a> - <a href="#{cgi_url}manage/news?type=2">Twitter</a> - <a href="#{cgi_url}manage/board">Opciones de board</a> - <a href="#{cgi_url}manage/addboard">Agregar board</a> - <a href="#{cgi_url}manage/lockboard">Cerrar board</a> -</td></tr> +<?py if int(rights) in [0,2]: ?> +<tr><td>Staff:</td> +<td>- <a href="#{cgi_url}manage/staff">Miembros</a> - <a href="#{cgi_url}manage/logs">Registro de acciones</a> -</td></tr> +<?py #endif ?> +<?py #endif ?> +</table></center></div> +<hr /> +<?py #endif ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> + <div class="replymode">#{title if title else "Mensaje"}</div> + <p>#{message}</p> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> +<div class="replymode">Modbrowse</div> +<?py if mode == 1: ?> +<table class="managertable"> + <tr><th colspan="2">Sección</th><th>Acción</th></tr> + <?py for board in boards: ?> + <tr><td>/#{board['dir']}/</td><td>#{board['name']}</td><td>[<a href="#{cgi_url}manage/mod/#{board['dir']}">Navegar</a>]</td></tr> + <?py #endfor ?> +</table> +<?py elif mode == 2: ?> +<table class="managertable"> +<tr> + <th>#</th> + <th>ID</th> + <th style="width:20%;">Asunto</th> + <th>Fecha</th> + <th style="width:80%;">Mensaje</th> + <th>Resp.</th> + <th>Acciones</th> +</tr> +<?py i = 1 ?> +<?py for thread in threads: ?> +<tr> + <td>#{i}</td> + <td>#{thread['id']}</td> + <td><a href="?thread=#{thread['id']}"><b>#{thread['subject']}</b></a></td> + <td class="date" data-unix="${thread['timestamp']}">#{thread['timestamp_formatted'][:21]}</td> + <td>${thread['message'][:200]}</td> + <td>#{thread['length']}</td> + <td style="white-space:nowrap;"> + <a href="#{cgi_url}manage/lock/#{dir}/#{thread['id']}">L#{"-" if thread['locked'] == "1" else "+"}</a> + <a href="#{cgi_url}manage/permasage/#{dir}/#{thread['id']}">PS#{"-" if thread['locked'] == "2" else "+"}</a> + <a href="#{cgi_url}manage/move/#{dir}/#{thread['id']}">M</a> + <a href="#{cgi_url}manage/delete/#{dir}/#{thread['id']}">D</a> + <a href="#{cgi_url}manage/delete/#{dir}/#{thread['id']}?ban=true">&</a> + <a href="#{cgi_url}manage/ban/#{dir}/#{thread['id']}">B</a> + </td> +</tr> +<?py i += 1 ?> +<?py #endfor ?> +</table> +<hr /> +[<a href="#{cgi_url}manage/mod" class="return">Volver</a>] +<?py elif mode == 3: ?> +<table class="managertable"> +<tr><th colspan="8" style="font-size:16pt;">Hilo: ${posts[0]['subject']} (#{posts[0]['length']})</th></tr> +<tr><td colspan="8" style="font-size:14pt;text-align:center;"><a href="#{cgi_url}manage/lock/#{dir}/#{posts[0]['id']}">#{"Abrir hilo" if posts[0]['locked'] == "1" else "Cerrar hilo"}</a> / +<a href="#{cgi_url}manage/permasage/#{dir}/#{posts[0]['id']}">#{"Quitar permasage" if posts[0]['locked'] == "2" else "Permasage"}</a> / +<a href="#{cgi_url}manage/move/#{dir}/#{posts[0]['id']}">Mover hilo</a></td></tr> +<tr> + <th>#</th> + <th>ID</th> + <th>Fecha</th> + <th>Nombre</th> + <th>Mensaje</th> + <th>Archivo</th> + <th>IP</th> + <th>Acción</th> +</tr> +<?py i = 1 ?> +<?py for p in posts: ?> +<tr> + <td>#{i}</td> + <td>#{p['id']}</td> + <td class="date" data-unix="${p['timestamp']}">${p['timestamp_formatted']}</td> + <td><span class="postername">${p['name']}</span></td> + <td>${p['message']}</td> + <td> + <?py if p['file']: ?><a href="/${dir}/src/#{p['file']}" target="_blank"><img src="/${dir}/mobile/${p['thumb']}" /></a><?py #endif ?> + </td> + <td><a href="#{cgi_url}manage/ipshow?ip=#{p['ip']}">#{p['ip']}</a></td> + <td style="white-space:nowrap;"> + <?py if p['IS_DELETED'] == '0': ?> + <a href="#{cgi_url}manage/delete/#{dir}/#{p['id']}">Eliminar</a> + <a href="#{cgi_url}manage/delete/#{dir}/#{p['id']}?ban=true">&</a> + <a href="/cgi/manage/ban?ip=#{p['ip']}">Ban</a> + <?py elif p['IS_DELETED'] == '1': ?> + <a href="#{cgi_url}manage/recyclebin/0/restore/#{dir}/#{p['id']}">Recuperar</a> + <abbr title="Eliminado por usuario">[1]</abbr> + <?py elif p['IS_DELETED'] == '2': ?> + <a href="#{cgi_url}manage/recyclebin/0/restore/#{dir}/#{p['id']}">Recuperar</a> + <abbr title="Eliminado por staff">[2]</abbr> + <?py #endif ?> + </td> +</tr> +<?py i += 1 ?> +<?py #endfor ?> +</table> +<hr /> +[<a href="#{cgi_url}manage/mod/#{dir}">Volver al panel</a>] +<?py #endif ?> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> +<div class="replymode">Mover hilo</div> +<?py if oldboardid and oldthread: ?> +<form action="#{cgi_url}manage/move/#{oldboardid}/#{oldthread}" method="post"> +<?py else: ?> +<form action="#{cgi_url}manage/move" method="post"> +<?py #endif ?> +<table> + <tr> + <td class="postblock">Board actual</td> + <td> + <?py if oldboardid and oldthread: ?> + <select name="oldboardid" style="width:100%;"> + <?py for board in boards: ?> + <option value="#{board['dir']}"#{' selected="selected"' if oldboardid == board['dir'] else ''}>#{board['dir']} - #{board['name']}</option> + <?py #endfor ?> + </select> + <?py else: ?> + <select name="oldboardid" style="width:100%;"> + <?py for board in boards: ?> + <option value="#{board['dir']}">#{board['dir']} - #{board['name']}</option> + <?py #endfor ?> + </select> + <?py #endif ?> + </td> + </tr> + <tr> + <td class="postblock">ID de hilo</td> + <td> + <?py if oldboardid and oldthread: ?> + <input type="text" name="oldthread" style="width:100%;" value="#{oldthread}" /> + <?py else: ?> + <input type="text" name="oldthread" style="width:100%;" /> + <?py #endif ?> + </td> + </tr> + <tr> + <td class="postblock">Mover a</td> + <td> + <select name="newboardid" style="width:100%;"> + <?py for board in boards: ?> + <option value="#{board['dir']}">#{board['dir']} - #{board['name']}</option> + <?py #endfor ?> + </select> + </td> + </tr> + <tr> + <td class="postblock">Insertar mensaje</td> + <td> + <input type="checkbox" name="msg" value="1" /> + </td> + </tr> + <tr><td colspan="2"><input type="submit" name="submit" style="width:100%;" value="Mover" /></td></tr> +</table> +</form> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> + <div class="replymode">Quotes</div> + <p>Ingresa un mensaje a mostrar por cada linea:</p> + <form method="post" action=""> + <textarea name="data" cols="80" rows="15" style="width:500px;height:250px;">${data}</textarea><br /> + <input type="submit" name="save" style="width:500px;" value="Guardar" /> + </form> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> +<div class="replymode">Reconstruir board</div> +<table class="managertable"> + <tr><th colspan="2">Sección</th><th colspan="2">Acción</th></tr> + <tr><td colspan="2"><b>Home</b></td><td colspan="2" style="text-align:center;">[<a href="#{cgi_url}manage/rebuild/!HOME">Reconstruir</a>]</td></tr> + <tr><td colspan="2"><b>Noticias</b></td><td colspan="2" style="text-align:center;">[<a href="#{cgi_url}manage/rebuild/!NEWS">Reconstruir</a>]</td></tr> + <tr><td colspan="2"><b>Índices de archivos</b></td><td colspan="2" style="text-align:center;">[<a href="#{cgi_url}manage/rebuild/!KAKO">Reconstruir</a>]</td></tr> + <tr><td colspan="2"><b>.htaccess</b></td><td colspan="2" style="text-align:center;">[<a href="#{cgi_url}manage/rebuild/!HTACCESS">Reconstruir</a>]</td></tr> + <?py for board in boards: ?> + <tr><td>/#{board['dir']}/</td><td>#{board['name']}</td><td>[<a href="#{cgi_url}manage/rebuild/#{board['dir']}">Reconstruir frontales</a>]</td><td>[<a href="#{cgi_url}manage/rebuild/#{board['dir']}?everything=1">Reconstruir todo</a>]</td></tr> + <?py #endfor ?> + <tr><td colspan="4" align="center"><form action="#{cgi_url}manage/rebuild/!ALL" method="get"><input type="submit" style="width:100%" value="Reconstruir todos (frontales)" /></form></td></tr> + <tr><td colspan="4" align="center"><form action="#{cgi_url}manage/rebuild/!BBS" method="get"><input type="submit" style="width:100%" value="Reconstruir todos (BBS)" /></form></td></tr> + <tr><td colspan="4" align="center"><form action="#{cgi_url}manage/rebuild/!IB" method="get"><input type="submit" style="width:100%" value="Reconstruir todos (IB)" /></form></td></tr> +</table> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<style>.imgs{font-size:0;}.imgs img{vertical-align:top;margin:2px;height:150px;width:auto;}</style> +<center> +<div class="replymode">Imágenes recientes</div> +<form action="#{cgi_url}manage/recent_images" name="recent_images" method="post"> + <table> + <tr><td class="postblock">Número a mostrar</td><td><input type="text" name="images" size="4" /></td></tr> + <tr><td colspan="2"><input type="submit" style="width:100%;" value="Enviar" /></td></tr> + </table> +</form> +<hr /> +<div class="imgs"> +<?py for post in posts: ?> + <?py if post['parentid'] != '0': ?> + <a href="/#{post['dir']}/res/#{post['parentid']}.html##{post['id']}"><img src="#{boards_url}#{post['dir']}/thumb/#{post['thumb']}" /></a> + <?py else: ?> + <a href="/#{post['dir']}/res/#{post['id']}.html##{post['id']}"><img src="#{boards_url}#{post['dir']}/thumb/#{post['thumb']}" /></a> + <?py #endif ?> +<?py #endfor ?> +</div> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<?py from tenjin.helpers.html import * ?> +<center> +<div class="replymode">Papelera de Reciclaje</div> +<form name="boardForm" method="get" action="#{cgi_url}manage/recyclebin/0"> +<table> +<tr> + <td class="postblock">Eliminado por</td> + <td> + <input type="radio" id="type1" name="type" value="1"#{checked(type == 1)} /><label for="type1">Usuario</label> + <input type="radio" id="type2" name="type" value="2"#{checked(type == 2)} /><label for="type2">Staff</label> + <input type="radio" id="type0" name="type" value="0"#{checked(type == 0)} /><label for="type0">Ambos</label> + </td> +</tr> +<tr> + <td class="postblock">Board</td><td> + <select name="board" style="width:100%;"> + <option value="all">Todos los boards</option> +<?py for board in boards: ?> + <option value="#{board['dir']}"#{selected(board['checked'])}>#{board['dir']} - ${board['name']}</option> +<?py #endfor ?> + </select> + </td> +</tr> +<tr><td colspan="2"><input type="submit" style="width:100%;" value="Mostrar" /></td></tr> +</table> +</form> +<hr /> +<?py if message: ?> +${message} +<hr /> +<?py #endif ?> +<?py if not skip: ?> +<form name="deleteForm" method="post" action="#{cgi_url}manage/recyclebin/#{currentpage}"> + <?py if curboard: ?> + <input type="hidden" name="board" value="#{curboard}" /> + <?py #endif ?> + <table class="managertable"> + <tr> + <th></th> + <th></th> + <th>ID</th> + <th>Timestamp</th> + <th>Board</th> + <th>Tipo</th> + <th>IP</th> + <th>Mensaje</th> + </tr> + <?py for post in posts: ?> + <tr> + <td><a href="#{cgi_url}manage/recyclebin/#{currentpage}/delete/#{post['dir']}/#{post['id']}">X</a><br /><a href="#{cgi_url}manage/recyclebin/#{currentpage}/restore/#{post['dir']}/#{post['id']}">R</a></td> + <td><input type="checkbox" name="!i#{post['dir']}/#{post['id']}" id="#{post['dir']}#{post['id']}" value="1" /><label for="#{post['dir']}#{post['id']}"></label></td> + <td>#{post['id']}</td> + <td class="date" data-unix="${post['timestamp']}">${post['timestamp_formatted']}</td> + <td>${post['dir']}</td> + <td>${post['IS_DELETED']}</td> + <td>${post['ip']}</td> + <td>#{post['message']}</td> + </tr> + <?py #endfor ?> + <tr><td colspan="8" align="center"><input name="deleteall" type="submit" value="Eliminar seleccionados" /></td></tr> + </table> +</form> +<hr /> +<div style="font-size:larger">#{navigator}</div> +<?py else: ?> + No hay posts. +<?py #endif ?> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<?py from tenjin.helpers.html import * ?> +<center> +<div class="replymode">Reportes</div> +<?py if message: ?> +${message} +<?py #endif ?> +<form name="boardForm" method="get" action="#{cgi_url}manage/reports/0"> +<table> + <tr> + <td class="postblock">Board</td> + <td> + <select name="board"> + <option value="all">Todos los boards</option> +<?py for board in boards: ?> + <option value="#{board['dir']}"#{selected(board['checked'])}>#{board['dir']} - #{board['name']}</option> +<?py #endfor ?> + </select> + <td><input type="submit" value="Mostrar" /></td> + </td></tr> +</table> +</form> + +<form name="ignoreForm" method="post" action="#{cgi_url}manage/reports/#{currentpage}"> +<?py if curboard: ?> +<input type="hidden" name="board" value="#{board}" /> +<?py #endif ?> +<hr /> +<table class="managertable"> +<tr> + <th></th> + <th></th> + <th>Fecha</th> + <th>Post</th> + <th>IP Post</th> + <th>Razón</th> + <th>IP Denuncia</th> +</tr> +<?py for report in reports: ?> +<tr> + <td> <a href="#{cgi_url}manage/reports/#{currentpage}/ignore/#{report['id']}">X</a> </td> + <td><input type="checkbox" name="i#{report['id']}" id="i#{report['id']}" value="1" /><label for="i#{report['id']}"></label></td> + <td class="date" data-unix="${report['timestamp']}">${report['timestamp_formatted']}</td> + <td><a href="#{report['link']}">${report['link']}</a></td> + <td><a href="#{cgi_url}manage/ipshow?ip=${report['ip']}">${report['ip']}</a></td> + <td>${report['reason']}</td> + <td><a href="#{cgi_url}manage/ipshow?ip=${report['reporterip']}">${report['reporterip']}</a></td> +</tr> +<?py #endfor ?> +<tr> + <td colspan="8" style="text-align:center;"><input name="ignore" type="submit" value="Ignorar seleccionados" /></td> +</tr> +</table> +</form> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<center> +<div class="replymode">Registro de búsqueda</div> +<table class="managertable"> + <tr> + <th>ID</th> + <th>Fecha</th> + <th>Búsqueda</th> + <th>En</th> + <th>Resultados</th> + <th>Por</th> + </tr> +<?py for log in search: ?> + <tr> + <td>${log['id']}</td> + <td class="date" data-unix="${log['timestamp']}">${log['timestamp_formatted']}</td> + <td>${log['keyword']}</td> + <td>${"[A] " if log['archive'] else ""}${"Global" if log["ita"] == "" else log["ita"]}</td> + <td>${log['res']}</td> + <td><a href="#{cgi_url}manage/ipshow?ip=${log['ip']}">${log['ip']}</a></td> + </tr> +<?py #endfor ?> +</table> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py include('templates/manage/menu.html') ?> +<?py from tenjin.helpers.html import * ?> +<center> +<div class="replymode">Staff</div> +<?py if mode == 0: ?> + <table class="managertable"> + <tr> + <th>ID</th> + <th>Nombre</th> + <th>Nivel</th> + <th>Última actividad</th> + <th>Acciones</th> + </tr> + <?py for member in staff: ?> + <tr> + <td>${member['id']}</td> + <td><b>${member['username']}</b></td> + <td>${member['rights']}</td> + <td class="date" data-unix="${member['lastactivestamp']}">${member['lastactive']}</td> + <td> + [<a href="#{cgi_url}manage/staff/edit/#{member['id']}">Editar</a>] + [<a href="#{cgi_url}manage/staff/delete/#{member['id']}">Eliminar</a>] + </td> + </tr> + <?py #endfor ?> + <tr> + <td colspan="5"><form action="#{cgi_url}manage/staff/add" method="get"><input type="submit" style="width:100%;" value="Agregar miembro" /></form></td> + </tr> + </table> +<?py elif mode == 1: ?> +<form action="#{cgi_url}manage/staff/#{action}" method="post"> +<table> + <tr> + <td class="postblock">Nombre</td> + <td><input type="text" name="username" value="${member_username}" style="width:100%;" /></td> + </tr> + <?py if not member: ?> + <tr> + <td class="postblock">Contraseña</td> + <td><input type="password" name="password" style="width:100%;"/></td> + </tr> + <?py #endif ?> + <tr> + <td class="postblock">Nivel</td> + <td> + <select name="rights" style="width:100%;"> + <option value="3"#{selected(member_rights == '3')}>Moderador</option> + <option value="2"#{selected(member_rights == '2')}>Developer</option> + <option value="1"#{selected(member_rights == '1')}>Administrador</option> + <option value="0"#{selected(member_rights == '0')}>Super-Administrador</option> + </select> + </td> + </tr> + <tr> + <td colspan="2"><input type="submit" name="submit" style="width:100%;" value="${submit}"/></td> + </tr> +</table> +</form> +<?py #endif ?> +</center> +<hr /> +<?py include('templates/base_bottom.html') ?>
\ 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 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<?py if (replythread and threads) or board: ?> + <title>#{board_name}@Bienvenido a Internet Móvil</title> +<?py else: ?> + <title>Bienvenido a Internet Móvil</title> +<?py #endif ?> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel="shortcut icon" href="#{static_url}img/favicon.ico" /> + <link rel="stylesheet" type="text/css" href="#{static_url}css/mobile.css?v=8" /> + <script type="text/javascript" src="#{static_url}js/mobile.js?v=9"></script> +</head> 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 @@ +<?py include('templates/mobile/base_top.html') ?> +<body class="img"><a name="top"></a> +<div class="nav"><div><a href="//m.bienvenidoainternet.org">Home</a><a href="#{cgi_url}mobile/#{board}/">Volver</a><a href="#form">▼</a></div></div> +<?py for thread in threads: ?> +<div id="thread"> +<?py for post in thread['posts']: ?> + <?py if post['IS_DELETED'] == "1": ?> + <div class="pst"><h3 class="del"><a name="#{post['id']}"></a>No.#{post['id']} eliminado por el usuario.</h3></div> + <?py elif post['IS_DELETED'] == "2": ?> + <div class="pst"><h3 class="del"><a name="#{post['id']}"></a>No.#{post['id']} eliminado por miembro del staff.</h3></div> + <?py else: ?> + <?py if post['parentid'] == "0": ?> + <div class="first"><h1>#{post["subject"]} <span>(#{thread['length']})</span></h1> + <?py else: ?> + <div class="pst"> + <?py if post['subject']: ?> + <h2>#{post["subject"]}</h2> + <?py #endif ?> + <?py #endif ?><h3><a href="#" class="num" name="#{post['id']}">#{post['id']}</a>#{post['name']} #{post['tripcode']} #{post['timestamp_formatted']}</h3> + <?py if post['file']: ?><a href="/#{board}/src/#{post['file']}" target="_blank" class="thm"><img src="/#{board}/mobile/#{post['thumb']}" /><br />#{int(post['file_size'])//1024}KB #{post['file'].split(".")[1].upper()}</a><?py #endif ?> + <div class="msg">#{post['message']}</div></div> + <?py #endif ?> +<?py #endfor ?> +<?py if threads[0]['posts'][0]['locked'] != "1": ?> +<a href="./#{thread['id']}" id="n">Recargar</a><span id="n2"></span> +<?py #endif ?> +<div class="nav"><div><a href="//m.bienvenidoainternet.org">Home</a><a href="#{cgi_url}mobile/#{board}/">Volver</a><a href="#top">▲</a></div></div> +<?py if threads[0]['posts'][0]['locked'] == "1": ?> + <div class="warn red" style="text-align:center;">El hilo ha sido cerrado. Ya no se puede postear en él.</div> +<?py else: ?> +<form name="postform" id="postform" action="/cgi/post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{replythread}" /><input type="hidden" name="mobile" value="true" /><input type="hidden" name="password" value="" /> + <div style="display:none;"><input type="text" name="name" /><input type="text" name="email" /></div> + <?py if not disable_subject: ?> + <input class="fld" type="text" name="subject" placeholder="Asunto (opcional)" /> + <?py #endif ?> + <?py if not disable_name: ?> + <input class="fld" type="text" name="fielda" placeholder="Nombre (opcional)" /> + <?py #endif ?> + <input class="fld" type="text" name="fieldb" placeholder="E-mail (opcional)" /> + <textarea name="message" rows="6"></textarea> +<?py if allow_image_replies: ?> + <div class="file"><input type="file" name="file" class="fld" /> + <?py if allow_spoilers: ?> + <label class="fld"><input type="checkbox" name="spoil" /> Spoiler</label> + <?py #endif ?></div> +<?py #endif ?> + <input id="post" type="submit" value="Responder" /> +</form> +<?py #endif ?> +</div> +<?py #endfor ?> +<a name="form"></a> +</body> +</html>
\ 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 @@ +<?py include('templates/mobile/base_top.html') ?> +<body class="img"> +<div class="top"><a href="//m.bienvenidoainternet.org"><img src="#{static_url}css/img/0back.png" /><br />Home</a>Error</div><br /> +<hr size="1"><br /><br /><div style="color:red;font-size:x-large;font-weight:bold;text-align:center;">#{error}</div><br /><br /><hr size="1"> +</body> +</html>
\ 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 @@ +<?py include('templates/mobile/base_top.html') ?> +<body class="txt"> +<div class="top"> + <a href="/movil.html"><img src="#{static_url}css/img/0info.png" /><br />Info</a> + Bienvenido a Internet Móvil +</div> +<div class="bar"><a href="//m.bienvenidoainternet.org">Secciones</a><a href="/cgi/mobilehome" class="sel">Hilos activos</a><a href="/cgi/mobilenewest">Nuevos hilos</a></div> +<div class="list"> + <?py for thread in latest_age: ?> + <a href="/cgi/mobileread${thread['url']}">#{thread['content']}<div>${thread['board_fulln']} <span>R:<span>#{int(thread['length'])-1}</span></span></div></a> + <?py #endfor ?> +</div> +</body> +</html>
\ 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 @@ +<?py include('templates/mobile/base_top.html') ?> +<body class="txt"> +<div class="top"> + <a href="/movil.html"><img src="#{static_url}css/img/0info.png" /><br />Info</a> + Bienvenido a Internet Móvil +</div> +<div class="bar"><a href="//m.bienvenidoainternet.org">Secciones</a><a href="/cgi/mobilehome">Hilos activos</a><a href="/cgi/mobilenewest" class="sel">Nuevos hilos</a></div> +<div class="list"> + <?py for thread in newthreads: ?> + <a href="/cgi/mobileread${thread['url']}">#{thread['content']}<div>${thread['board_fulln']}</div></a> + <?py #endfor ?> +</div> +</body> +</html>
\ 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 @@ +<?py include('templates/mobile/base_top.html') ?> +<body class="img"> +<div class="top"> + <a href="//m.bienvenidoainternet.org"><img src="#{static_url}css/img/0back.png" /><br />Home</a> + #{board_name} +</div> +<?py if mode == 1: ?> + <div class="bar"><a href="#{cgi_url}mobile/#{board}" class="sel">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Lista</a><a href="#{cgi_url}mobilecat/#{board}">Catálogo</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div> + <?py for thread in more_threads: ?> + <div class="prev"> + <a href="#{cgi_url}mobileread/#{board}/#{thread['id']}"><img class="thm" src="/#{board}/mobile/#{thread['thumb']}" /> + <b>#{thread["subject"]}</b> (R:#{int(thread["length"])-1})</a> + <h3>#{thread['name']} #{thread['tripcode']} #{thread['timestamp_formatted']}</h3> + #{thread['message']}#{" [...]" if thread['shortened'] else ""} + <?py if thread['lastreply']: ?> + <div class="pst"><h3>#{thread['lastreply']['name']} #{thread['lastreply']['tripcode']} #{thread['lastreply']['timestamp_formatted']}</h3> + #{thread['lastreply']['message']}#{" [...]" if thread['lastreply']['shortened'] else ""}</div> + <?py #endif ?> + </div> + <?py #endfor ?> +<?py elif mode == 2: ?> + <div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}" class="sel">Lista</a><a href="#{cgi_url}mobilecat/#{board}">Catálogo</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div> + <div class="search"><input id="search" placeholder="Buscar en asuntos" style="padding:7px;" type="text"></div> + <div class="ord"><span>Orden:</span><a data-sort="0" class="sel" href="#">Normal</a><a data-sort="1" href="#">Nuevo</a><a data-sort="2" href="#">Viejo</a><a data-sort="3" href="#">Más</a><a data-sort="4" href="#">Menos</a></div> + <div id="to_sort" class="list"> + <?py i = 1 ?> + <?py for thread in more_threads: ?> + <a data-num="${i}" data-res="#{thread['length']}" data-id="#{thread['id']}" href="#{cgi_url}mobileread/#{board}/#{thread['id']}"><strong>#{thread["subject"]}</strong>: #{thread['message']}#{" [...]" if thread['shortened'] else ""}<br /> + <span class="info"><span>Última: #{thread['lastreply']['timestamp_formatted'] if thread['lastreply'] else thread['timestamp_formatted']}</span> Respuestas: <b>#{int(thread["length"])-1}</b></span></a> + <?py i += 1 ?> + <?py #endfor ?> + </div> +<?py else: ?> + <div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Lista</a><a href="#{cgi_url}mobilecat/#{board}" class="sel">Catálogo</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div> + <div class="search"><input id="catsearch" placeholder="Buscar en catálogo" style="padding:7px;" type="text"></div> + <div class="ord"><span>Orden:</span><a data-sort="0" class="sel" href="#">Normal</a><a data-sort="1" href="#">Nuevo</a><a data-sort="2" href="#">Viejo</a><a data-sort="3" href="#">Más</a><a data-sort="4" href="#">Menos</a></div> + <div id="to_sort" style="text-align:center;margin-top:0.5em;"> + <?py i = 1 ?> + <?py for thread in more_threads: ?><a data-num="${i}" data-res="#{thread['length']}" data-id="#{thread['id']}" class="cat" href="#{cgi_url}mobileread/#{board}/#{thread['id']}"><img src="/#{board}/mobile/#{thread['thumb']}" /><br />(#{int(thread["length"])-1}R) <strong>#{thread["subject"]}</strong>: #{thread['message']}#{" [...]" if thread['shortened'] else ""}</a><?py i += 1 ?><?py #endfor ?> + </div> +<?py #endif ?> +</body> +</html>
\ 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 @@ +<?py include('templates/mobile/base_top.html') ?> +<body class="#{"txt" if board_type == '1' else "img"}"> +<div class="top"> + <a href="//m.bienvenidoainternet.org"><img src="#{static_url}css/img/0back.png" /><br />Home</a> + #{board_name} +</div> +<?py if board_type == '1': ?> +<div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Todos los hilos</a><a href="#{cgi_url}mobilenew/#{board}" class="sel">Nuevo hilo</a></div> +<?py else: ?> +<div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Lista</a><a href="#{cgi_url}mobilecat/#{board}">Catálogo</a><a href="#{cgi_url}mobilenew/#{board}" class="sel">Nuevo hilo</a></div> +<?py #endif ?> +<form name="postform" id="postform" action="/cgi/post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /> <input type="hidden" name="mobile" value="true" /><input type="hidden" name="password" value="" /> + <div style="display:none;"><input type="text" name="name" maxlength="50" /><input type="text" name="email" maxlength="50" /></div> + <?py if not disable_subject: ?> + <input class="fld imp" type="text" name="subject" placeholder="Asunto#{" (opcional)" if board_type == '0' else ""}" maxlength="100" /> + <?py #endif ?> + <?py if not disable_name: ?> + <input class="fld" type="text" name="fielda" placeholder="Nombre (opcional)" maxlength="50" /> + <?py #endif ?> + <input class="fld" type="text" name="fieldb" placeholder="E-mail (opcional)" maxlength="50" /> + <textarea name="message" rows="#{"8" if board_type == '1' else "6"}"></textarea> +<?py if allow_images: ?> + <div class="file"><input type="file" name="file" class="fld" /> + <?py if allow_spoilers: ?> + <label class="fld"><input type="checkbox" name="spoil" /> Spoiler</label> + <?py #endif ?></div> +<?py #endif ?> + <input id="post" type="submit" value="Crear nuevo hilo" /> +</form> +<?py if allow_images: ?> + <div class="rules">Formatos permitidos: #{', '.join(supported_filetypes).upper()}<br />Tamaño máximo: #{maxsize} KB</div> +<?py #endif ?> +</body> +</html>
\ 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 @@ +<?py include('templates/mobile/base_top.html') ?> +<body class="txt"> +<a name="top"></a> +<?py for thread in threads: ?> +<div class="nav"><div><a href="//m.bienvenidoainternet.org">Home</a><a href="#{cgi_url}mobile/#{board}/">Volver</a><a href="#form">▼</a></div></div> +<div id="nav2"> + <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}">Ver hilo completo</a> +<?py if thread['length'] > 51: ?> + <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/l25" rel="nofollow">Últimos 25</a> +<?py #endif ?> +<?py if thread['length'] > 50: ?> + <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/-50" rel="nofollow">Primeros 50</a> +<?py #endif ?> +<?py r = range(thread['length'] / 50) ?> +<?py for i in r[:-1]: ?> + <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/#{(i+1)*50+1}-#{(i+2)*50}" rel="nofollow">#{(i+1)*50+1}-#{(i+2)*50}</a> +<?py #endfor ?> +<?py if r: ?> + <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/#{(r[-1]+1)*50+1}-#{(r[-1]+2)*50}" rel="nofollow">#{(r[-1]+1)*50+1}-</a> +<?py #endif ?> +</div> +<?py if thread['length'] > 1000: ?> + <div class="stop red">■ El hilo superó los 1000 mensajes y ha sido cerrado.</div> +<?py elif thread['length'] > 950: ?> + <div class="warn red">■ El hilo ha recibido más de 950 mensajes. Límite: 1000</div> +<?py elif thread['length'] > 900: ?> + <div class="warn yellow">■ El hilo ha recibido más de 900 mensajes. Límite: 1000</div> +<?py #endif ?> +<div id="thread"> +<h1>#{thread['subject']} <span>(#{thread['length']})</span></h1> +<?py for post in thread['posts']: ?> +<?py if post['IS_DELETED'] == '1': ?> +<div class="pst"><h3 class="del"><a href="#" class="num">#{str(post['num']).zfill(4)}</a> Eliminado por el usuario.</h3></div> +<?py elif post['IS_DELETED'] == '2': ?> +<div class="pst"><h3 class="del"><a href="#" class="num">#{str(post['num']).zfill(4)}</a> Eliminado por miembro del staff.</h3></div> +<?py else: ?> +<div id="p#{post['id']}" class="pst"> + <h3><a href="#" class="num">#{str(post['num']).zfill(4)}</a> #{post['name']} #{post['tripcode']}</h3> + <?py if post['file']: ?><a href="/#{board}/src/#{post['file']}" target="_blank" class="thm"><img src="/#{board}/mobile/#{post['thumb']}" /><br />#{int(post['file_size'])//1024}KB #{post['file'].split(".")[1].upper()}</a><?py #endif ?> + <div class="msg">#{post['message']}</div> + <h4>#{post['timestamp_formatted']}</h4> +</div> +<?py #endif ?> +<?py #endfor ?> +<?py if thread['locked'] != '1': ?> +<a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/#{thread['length']}-n" id="n">Ver nuevos posts</a><span id="n2"></span> +<?py #endif ?> +<div class="nav"> + <div><a href="//m.bienvenidoainternet.org">Home</a><a href="#{cgi_url}mobile/#{board}/">Volver</a><a href="#top">▲</a></div> + <?py if nextrange: ?> + <div><a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}">Hilo completo</a><a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/-50">Primeros 50</a><a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/l10">Últimos 25</a></div> + <?py #endif ?> +</div> +<?py if thread['locked'] != '1': ?> + <form name="postform" id="postform" action="/cgi/post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="mobile" value="true" /><input type="hidden" name="password" value="" /> + <div style="display:none"><input type="text" name="name" /><input type="text" name="email" /></div> + <input class="fld" type="text" name="fielda" placeholder="Nombre (opcional)" /> + <input class="fld" type="text" name="fieldb" placeholder="E-mail (opcional)" /> + <textarea name="message" rows="6"></textarea> +<?py if allow_image_replies: ?> + <div class="file"><input type="file" name="file" class="fld" /> + <?py if allow_spoilers: ?> + <label class="fld"><input type="checkbox" name="spoil" /> Spoiler</label> + <?py #endif ?></div> +<?py #endif ?> + <input id="post" type="submit" value="Responder" /> + </form> +<?py #endif ?> +</div> +<a name="form"></a> +<?py #endfor ?> +</body> +</html>
\ 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 @@ +<?py include('templates/mobile/base_top.html') ?> +<body class="txt"> +<div class="top"> + <a href="//m.bienvenidoainternet.org"><img src="#{static_url}css/img/0back.png" /><br />Home</a> + #{board_name} +</div> +<?py if mode == 1: ?> +<div class="bar"><a href="#{cgi_url}mobile/#{board}" class="sel">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Todos los hilos</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div> +<?py else: ?> +<div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}" class="sel">Todos los hilos</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div> +<div class="search"><input id="search" placeholder="Buscar en asuntos" type="text"></div> +<div class="ord"><span>Orden:</span><a data-sort="0" class="sel" href="#">Normal</a><a data-sort="1" href="#">Nuevo</a><a data-sort="2" href="#">Viejo</a><a data-sort="3" href="#">Más</a><a data-sort="4" href="#">Menos</a></div> +<?py #endif ?> +<div id="to_sort" class="list"> + <?py i = 1 ?> + <?py for thread in more_threads: ?> + <?py if int(thread["length"]) > 10: ?> + <a data-num="${i}" data-res="${thread['length']}" data-id="${thread['id']}" href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/l10">#{thread['subject']}<br /><span class="info"><span>Última: #{timestamps[i-1][1]}</span> Respuestas: <b>#{thread['length']}</b></span></a> + <?py else: ?> + <a data-num="${i}" data-res="#{thread['length']}" data-id="#{thread['id']}" href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/l10">#{thread['subject']}<br /><span class="info"><span>Última: #{timestamps[i-1][1]}</span> Respuestas: <b>#{thread['length']}</b></span></a> + <?py #endif ?> + <?py i += 1 ?> + <?py #endfor ?> +</div> +</body> +</html>
\ 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 (c) 2012-2015 Firehawk/TDA (firehawk@haxor.fi) --> +<!-- Modificado para funcionar con Bienvenido a Internet BBS/IB --> +<html> + <head> + <title>MOD/S3M/XM module player for Web Audio</title> + <meta name="description" content="A MOD/S3M/XM module player in Javascript using the Web Audio API."> + <link rel="stylesheet" href="/firehawk/style.css" type="text/css" media="screen" /> + <script type="text/javascript" src="/firehawk/jquery-2.1.1.js"></script> + <script type="text/javascript" src="/firehawk/utils.js"></script> + <script type="text/javascript" src="/firehawk/player.js"></script> + <script type="text/javascript" src="/firehawk/pt.js"></script> + <script type="text/javascript" src="/firehawk/st3.js"></script> + <script type="text/javascript" src="/firehawk/ft2.js"></script> + <script type="text/javascript" src="/firehawk/ui.js"></script> + </head> + <body data-module="/#{board}/src/#{modfile}"> + <div id="outercontainer"> + <div id="headercontainer"> + <div style="margin-left:8px;float:left">MOD/S3M/XM module player for Web Audio</div> + <div style="margin-right:8px;float:right">(c) 2012-2015 Firehawk/<a class="tdalink" href="http://tda.haxor.fi/" target="_blank">TDA</a></div> + <div style="clear:both;"></div> + </div> + <div id="innercontainer"> + <div id="modsamples"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +</div> + <div style="position:relative;top:8px;margin-bottom:8px;"> + <span id="modtitle"> </span> + <span id="modinfo">(' ')</span> + <span id="modtimer"></span> + <br/><br/> + <a href="#" id="go_back">[<<]</a> + <a href="#" id="play">[reproducir]</a> + <a href="#" id="pause">[pausa]</a> + <a href="#" id="go_fwd">[>>]</a> + <span style="white-space:pre;"> </span> + <a href="#" title="Repeat song" id="modrepeat">[rept]</a> + <a class="down" title="Stereo separation" href="#" id="modpaula">[)oo(]</a> + <a class="down" title="Visualization type" href="#" id="modvis">[trks]</a> + <a title="Amiga A500 lowpass filter" href="#" id="modamiga">[filt]</a> + </div> + <div id="modchannels"><div id="even-channels"></div><div id="odd-channels"></div></div> + <div id="modpattern"></div> + <div style="clear:both"></div> + <div id="infotext"> + Esta es una instancia local del reproductor de MODs por Firehawk - <a href="https://twitter.com/janihalme" style="color:#cce;">Twitter</a> / <a href="mailto:firehawk@haxor.fi" style="color:#cce">firehawk@haxor.fi</a>.<br/>Código fuente disponible en <a style="color:#cce;" target="_blank" href="https://github.com/jhalme/webaudio-mod-player">GitHub</a> bajo licencia MIT. + <!-- + The player has been tested on Chrome 14+, Firefox 24+, Safari 6+ and Edge 20+ so far. <span style="color:#faa">Disable AdBlock if you get cuts or stuttering!</span> + To report bugs, suggest features or request songs, contact me on <a href="https://twitter.com/janihalme" style="color:#cce;">Twitter</a> or + email <a href="mailto:firehawk@haxor.fi" style="color:#cce">firehawk@haxor.fi</a>. + Source code available on .--> + </div> + </div> + + + </div> + </body> +</html> 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 @@ +<a id="noticias" href="/noticias/">Actualidad</a> +<a id="tech" href="/tech/">Tecnología</a> +<a id="juegos" href="/juegos/">Juegos</a> +<a id="musica" href="/musica/">Música</a> +<a id="tv" href="/tv/">TV y Cine</a> +<a id="letras" href="/letras/">Humanidades</a> +<a id="zonavip" href="/zonavip/">Club VIP</a> +<a id="world" href="/world/">World Lobby</a> +| +<a id="img" href="/img/">Imágenes</a> +<a id="2d" href="/2d/">二次元画像</a> +<a id="n" href="/n/">Naturaleza</a> +<a id="o" href="/o/">Oekaki</a> +<a id="0" href="/0/">Cero</a> +| +<a id="bai" href="/bai/">Meta</a>
\ 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 @@ +<?py include('templates/base_top.html') ?> +<?py if selfy: ?> +<script type="text/javascript" src="#{static_url}js/palette_selfy.js"></script> +<?py #endif ?> +<center> +<?py if applet == 'shipainter': ?> +<applet id="oekaki" code="c.ShiPainter.class" archive="#{boards_url}oek_temp/spainter_all.jar" width="#{width+250}" height="#{height+280}" mayscript=""> + <?py for key, value in params.iteritems(): ?> + <param name="#{key}" value="#{value}" /> + <?py #endfor ?> +</applet> +<?py if selfy: ?> +<script type="text/javascript">palette_selfy();</script> +<?py #endif ?> +<?py elif applet == 'neo': ?> +<link rel="stylesheet" href="#{static_url}js/paintbbs/PaintBBS-1.3.4.css" type="text/css" /> +<script src="#{static_url}js/paintbbs/PaintBBS-1.3.4.js" charset="UTF-8"></script> +<applet-dummy id="oekaki" name="paintbbs" width="#{width+250}" height="#{height+280}"> +<param name="image_width" value="#{width}"> +<param name="image_height" value="#{height}"> +<param name="image_bkcolor" value="#FFFFFF"> +<param name="image_size" value="0"> +<param name="undo" value="90"> +<param name="undo_in_mg" value="15"> +<param name="color_text" value="#EFEFFF"> +<param name="color_bk" value="#E8EFFF"> +<param name="color_bk2" value="#D5D8EF"> +<param name="color_icon" value="#A1B8D8"> +<param name="color_iconselect" value="#000000"> +<param name="url_save" value="/oek_temp/save.php?applet=paintbbs"> +<param name="url_exit" value="#{cgi_url}oekaki/finish/#{board}/#{replythread}"> +<param name="poo" value="false"> +<param name="send_advance" value="true"> +<param name="thumbnail_width" value="100%"> +<param name="thumbnail_height" value="100%"> +<param name="tool_advance" value="true"> +<param name="tool_color_button" value="#D2D8FF"> +<param name="tool_color_button2" value="#D2D8FF"> +<param name="tool_color_text" value="#5A5781"> +<param name="tool_color_bar" value="#D2D8F0"> +<param name="tool_color_frame" value="#7474AB"> +<?py if edit: ?> +<param name="image_canvas" value="#{edit}"> +<?py #endif ?> +</applet-dummy> +<?py elif applet == 'wpaint': ?> +<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.1.10.2.min.js"></script> +<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.ui.core.1.10.3.min.js"></script> +<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.ui.widget.1.10.3.min.js"></script> +<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.ui.mouse.1.10.3.min.js"></script> +<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.ui.draggable.1.10.3.min.js"></script> +<link rel="Stylesheet" type="text/css" href="#{static_url}js/wpaint/lib/wColorPicker.min.css" /> +<script type="text/javascript" src="#{static_url}js/wpaint/lib/wColorPicker.min.js"></script> +<link rel="Stylesheet" type="text/css" href="#{static_url}js/wpaint/wPaint.min.css" /> +<script type="text/javascript" src="#{static_url}js/wpaint/wPaint.min.js"></script> +<script type="text/javascript" src="#{static_url}js/wpaint/plugins/main/wPaint.menu.main.min.js"></script> +<script type="text/javascript" src="#{static_url}js/wpaint/plugins/text/wPaint.menu.text.min.js"></script> +<script type="text/javascript" src="#{static_url}js/wpaint/plugins/shapes/wPaint.menu.main.shapes.min.js"></script> +<script type="text/javascript" src="#{static_url}js/wpaint/plugins/file/wPaint.menu.main.file.min.js"></script> +<div id="wPaint" style="position:relative; width:#{width}px; height:#{height}px; background-color:#7a7a7a; margin:70px auto 20px auto;"></div> +<script type="text/javascript" src="#{static_url}js/wpaint/bai.js"></script> +<?py elif applet == 'tegaki': ?> +<form id="imgform" data-w="#{width}" data-h="#{height}" action="#{cgi_url}oekaki/finish/#{board}/#{replythread}" method="post"> +<input type="hidden" name="filebase" id="filebase" /> +</form> +<link rel="Stylesheet" type="text/css" href="#{static_url}js/tegaki/tegaki.css" /> +<script type="text/javascript" src="#{static_url}js/tegaki/tegaki.js"></script> +<div id="buttons"><button id="topen">Abrir Tegaki</button></div> +<div style="font-size:20pt" id="status"></div> +<?py #endif ?> + +<br /><br /><br /> +<div id="links"> +<a href="#{boards_url}#{board}">Volver</a><br /> +<a id="finish" href="#{cgi_url}oekaki/finish/#{board}/#{replythread}">Recuperar dibujo guardado</a> +</div> +</center> +<br /> +<?py include('templates/base_bottom.html') ?> 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 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<title>Has posteado en Bienvenido a Internet BBS/IB</title> +<meta http-equiv="content-type" content="application/xhtml+xml; charset=UTF-8" /> +<meta http-equiv="refresh" content="0;url=#{url}" /> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> +<body style="text-align:center;"> +<h1>Gracias por tu post</h1><h3>${message}</h3><em>(por favor espera)</em> +<?py if timetaken: ?><p style="font-size:small">Tiempo usado: #{timetaken}</p><?py #endif ?> +</body> +</html>
\ 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 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<?py if finished: ?><title>Post denunciado</title> +<?py else: ?><title>Denunciar post ${postshow}</title> +<?py #endif ?> +<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<style>*{box-sizing:border-box} body{max-width:350px;margin:0 auto;text-align:justify} h1{text-align:left} a{color:#06C} a:active{color:#F30} +input{border:1px solid #BBB;width:100%} .long{display:block}</style> +</head> +<body> +<?py if finished: ?> +<hr /><h1>Post denunciado.</h1> +<hr /><a href="javascript:void(0)" onclick="history.go(-2);return(false);" class="long">Volver</a><hr /> +<?py else: ?> +<hr /><h1>Formulario de denuncia</h1> +<hr />Para pedir que el post <b>${postshow}</b> sea eliminado, indica una razón y presiona el botón [Enviar denuncia]. +<hr /><a href="javascript:void(0)" onclick="history.go(-1);return(false);" class="long">Volver</a> +<hr /><form method="post" action=""><input type="text" name="reason" placeholder="Razón" maxlength="100" style="margin-bottom:0.5em;" /><input type="submit" value="Enviar denuncia" /></form> +<hr />Este formulario no es para eliminar tu propio post. +<?py if txt: ?>Para eliminar tu propio post debes presionar el botón <u>del</u> que aparece a la derecha de tu post cuando le pones el cursor encima. [<a href="/faq.html#del">info</a>] +<?py else: ?>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. [<a href="/faq.html#del">info</a>]<?py #endif ?> +<hr />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 <a href="/bai/">meta</a>. +<hr /><a href="mailto:burocracia@bienvenidoainternet.org" class="long">Contacto</a><hr /> +<?py #endif ?> +</body> +</html>
\ 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 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<title>Estadísticas@Bienvenido a Internet</title> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1.0" /> +<style> +body{font-family:arial,sans-serif;background:#090909;color:#fdfdfd;margin:0;text-align:center} +a,a:visited{color:#fdfdfd;text-decoration:none} +a:hover{text-decoration:underline} +hr{margin:1em 0} +span{display:inline-block} +#title{margin-top:1em} +#title a{text-decoration:none} +.t1{font-size:1.8em;display:inline-block;line-height:1em} +.t2{font-size:1em;margin-top:2px} +h2{font-size:1.5em;margin:0 0 .2em 0} +table{font-size:1.4em;margin:0 auto 1em} +th,td{padding:10px;border-top:1px solid #222;border-left:1px solid #222} +.boards{padding:0} +td a{display:block;padding:10px} +.desc tr{text-align:right} +.desc td{text-align:left} +.r{text-align:right} +.l{text-align:left} +.pos{line-height:0em;text-align:center} +.uno{font-size:2em;color:red} +.dos{font-size:1.6em;color:orange} +.tres{font-size:1.3em;color:yellow} +.etc{color:grey} +#foot{margin:1em;font-size:.9em} +#foot a{color:#999} +@media (max-width:600px){ +.t1{font-size:1.6em} +.t2{font-size:.9em} +h2{font-size:1.2em} +table{font-size:1.1em} +table.desc{font-size:.9em} +th,td{padding:5px} +td a{display:block;padding:5px} +.uno{font-size:1.6em} +.dos{font-size:1.4em} +.tres{font-size:1.2em} +#boards th{font-size:.8em} +.long{word-break:break-all} +#foot{font-size:12px} +} +</style> +</head> +<body> +<div id="title"> + <div class="t1"><a href="/" style="font-weight:900">Bienvenido a Internet</a> Estadísticas</div> + <div class="t2"> + <span>Última actualización:</span> <span>${timestamp} GMT${tz}</span> + <?py if not regenerated: ?> + <span>(en caché)</span> + <?py #endif ?> + </div> +</div> + +<hr /> + +<h2 class="rot">Mensajes totales <span>(última semana)</span></h2> + <table> + <tr> + <th>Día</th> + <th class="r">Hilos</th> + <th class="r">Resp.</th> + <th class="r">Total</th> + </tr> + <?py allthreads, allposts = 0, 0 ?> + <?py for day, posts, threads in reversed(days): ?> + <tr> + <td>${day}</td> + <td class="r">${threads}</td> + <td class="r">${int(posts)-int(threads)}</td> + <td class="r">${posts}</td> + </tr> + <?py allthreads += int(threads) ?> + <?py allposts += int(posts) ?> + <?py #endfor ?> + <tr style="font-weight:bold;"> + <td class="l">Total</td> + <td class="r">${allthreads}</td> + <td class="r">${allposts-allthreads}</td> + <td class="r">${allposts}</td> + </tr> + </table> + +<hr /> + +<h2 class="rot">Volumen de mensajes por sección <span>(últimos 30 días)</span></h2> + <table id="boards"> + <tr> + <th class="pos">#</th> + <th class="l">Sección</th> + <th>Mensajes</th> + <th>Porcentaje</th> + </tr> + <?py iter = 1 ?> + <?py for dir, board, percent, num in boards_percent: ?> + <tr> + <td class="pos"> + <?py if iter == 1: ?><span class="uno">${iter}</span> + <?py elif iter == 2: ?><span class="dos">${iter}</span> + <?py elif iter == 3: ?><span class="tres">${iter}</span> + <?py else: ?>${iter}<?py #endif ?> + </td> + <td class="l boards"><a href="/${dir}/" target="_blank">${board}</a></td> + <td class="r">${num}</td> + <td class="r">${percent}%</td> + </tr> + <?py iter += 1 ?> + <?py #endfor ?> + </table> + +<hr /> + +<h2>Sistema</h2> + <table class="desc"> + <tr><th>Máquina</th> + <td>maria (Debian GNU/Linux)</td></tr> + <tr><th>OS</th> + <td class="long">${uname[0]} ${uname[2]}</td></tr> + <tr><th>Release/Arq.</th> + <td>${uname[3]} ${uname[4]}</td></tr> + <tr><th>Motor BBS/IB</th> + <td>weabot ${weabot_ver}</td></tr> + <tr><th>Templating</th> + <td>tenjin ${tenjin_ver}</td></tr> + <tr><th>Versión de Python</th> + <td>${python_ver}</td></tr> + <tr><th>Implementación</th> + <td>${python_impl}</td></tr> + <tr><th>Build</th> + <td>${python_build}</td></tr> + <tr><th>Compilado en</th> + <td>${python_compiler}</td></tr> + </table> + +<hr /> + +<h2>Base de datos</h2> + <table class="desc"> + <tr><th>Base de Datos</th> + <td>MariaDB</td></tr> + <tr><th>Versión</th> + <td>${mysql_ver} Linux x86_64</td></tr> + <tr><th>Mensajes totales activos</th> + <td>${total}</td></tr> + <tr><th>Archivos totales activos</th> + <td>${total_files}</td></tr> + <tr><th>Mensajes totales archivados (BBS)</th> + <td>${total_archived}</td></tr> + <tr><th>Mensajes totales archivados (IB)</th> + <td>0 (QEPD)</td></tr> + </table> + +<hr /> + +<div id="foot">B.a.I. - 2010-2019 · Contacto: <a href="mailto:burocracia@bienvenidoainternet.org">burocracia@bienvenidoainternet.org</a></div> +</body> +</html> 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 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>#{threads[0]['subject']} - Archivo de #{board_name}@Bienvenido a Internet BBS</title> + <meta http-equiv="Content-Type" content="application/xhtml+xml;charset=utf-8" /> +<?py if threads: ?> + <meta property="og:site_name" content="Bienvenido a Internet BBS" /> + <meta property="twitter:site" content="Bienvenido a Internet BBS" /> + <meta name="description" content="${preview}" /> + <meta property="og:title" content="${threads[0]['subject']} - ${board_name}" /> + <meta property="og:description" content="${preview}" /> + <meta property="twitter:title" content="${threads[0]['subject']} - ${board_name}" /> + <meta name="twitter:description" content="${preview}" /> +<?py #endif ?> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel="shortcut icon" href="#{static_url}img/favicon.ico" /> + <link rel="stylesheet" href="/static/css/txt/bbs.css" /> +<?py if not force_css: ?> + <link rel="stylesheet" id="css" href="#{static_url}css/txt/#{txt_styles[txt_styles_default].lower()}.css" /> +<?py else: ?> + <link rel="stylesheet" type="text/css" href="#{force_css}" /> +<?py #endif ?> +<?py if board in ['zonavip', 'world']: ?> + <link rel="stylesheet" href="/static/css/txt/sjis.css" /> +<?py #endif ?> + <script type="text/javascript" src="#{static_url}js/weabotxt.js"></script> + <script type="text/javascript" src="#{static_url}js/aquiencitas.js"></script> +</head> +<body class="threadpage archived" data-brd="#{board}"> +<?py if threads: ?> +<?py for thread in threads: ?> +<div id="thread_nav"> + <a href="/" name="top" target="_top">Bienvenido a Internet</a> + <a href="#{boards_url}#{board}/">■Volver al BBS■</a> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Hilo completo</a> + <?py if thread['length'] > 100: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100">1-</a> + <?py #endif ?> + <?py for i in range(thread['length'] / 100): ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{(i+1)*100+1}-#{(i+2)*100}">#{(i+1)*100+1}-</a> + <?py #endfor ?> + <?py if thread['length'] > 51: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Últimos 50</a> + <?py #endif ?> + <a href="#bottom">▼Bajar▼</a> +</div> +<hr /><div class="stop red">■ Este hilo se encuentra guardado en el archivo</div><hr /> +<div class="thread" data-length="#{thread['length']}"> + <h3>#{thread['subject']} <span>(${(str(thread['length'])+" respuestas") if thread['length'] > 1 else "Una respuesta"})</span></h3> + <?py for post in thread['posts']: ?> + <?py if post['IS_DELETED'] == '1': ?> + <h4 class="deleted">#{post['num']} : Mensaje eliminado por el usuario.</h4> + <?py elif post['IS_DELETED'] == '2': ?> + <h4 class="deleted">#{post['num']} : Mensaje eliminado por miembro del staff.</h4> + <?py else: ?> + <?py if post['num'] == 1: ?> + <div class="reply first" data-n="#{post['num']}"> + <?py else: ?> + <div class="reply" data-n="#{post['num']}"> + <?py #endif ?> + <h4>#{post['num']} : + <?py if post['email']: ?> + <?py if post['tripcode']: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a> + <?py else: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a> + <?py #endif ?> + <?py else: ?> + <?py if post['tripcode']: ?> + <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span> + <?py else: ?> + <span class="name"><b>#{post['name']}</b></span> + <?py #endif ?> + <?py #endif ?> : <span class="date">#{post['timestamp_formatted']}</span></h4> + <div class="msg">#{post['message']}</div> + </div> + <?py #endif ?> + <?py #endfor ?> + <?py if 'size' in thread: ?> + <div class="size">#{thread['size']}</div> + <?py #endif ?> +</div> +<hr /><div class="stop red">■ Este hilo se encuentra guardado en el archivo</div><hr /> +<form class="threadlinks"> + <a href="#{boards_url}#{board}/">■Volver al BBS■</a> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Hilo completo</a> + <?py if prevrange: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{prevrange}">Anteriores 100</a> + <?py #endif ?> + <?py if nextrange: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{nextrange}">Próximos 100</a> + <?py #endif ?> + <?py if thread['length'] > 51: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Últimos 50</a> + <?py #endif ?> + <a href="#top">▲Subir▲</a> +</form> +<?py #endfor ?> +<?py #endif ?> +<div class="end">weabot.py ver <?py include('templates/revision.html') ?> Bienvenido a Internet BBS/IB</div> +<a name="bottom"></a> +</body> +</html>
\ 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 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<?py if replythread and threads: ?> + <title>#{threads[0]['subject']} - #{board_name}@Bienvenido a Internet BBS</title> +<?py elif board: ?> + <title>#{board_long}</title> +<?py else: ?> + <title>#{title}</title> +<?py #endif ?> + <meta http-equiv="Content-Type" content="application/xhtml+xml;charset=utf-8" /> +<?py if replythread and threads: ?> + <meta property="og:site_name" content="Bienvenido a Internet BBS" /> + <meta property="twitter:site" content="Bienvenido a Internet BBS" /> + <meta name="description" content="${preview}" /> + <meta property="og:title" content="${threads[0]['subject']} - ${board_name}" /> + <meta property="og:description" content="${preview}" /> + <meta property="twitter:title" content="${threads[0]['subject']} - ${board_name}" /> + <meta name="twitter:description" content="${preview}" /> +<?py #endif ?> + <meta name="robots" content="#{"noindex" if noindex else "index, follow"}" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel="shortcut icon" href="/favicon.ico" /> + <link rel="stylesheet" href="/static/css/txt/bbs.css" /> +<?py if not force_css: ?> + <link rel="stylesheet" id="css" href="#{static_url}css/txt/#{txt_styles[txt_styles_default].lower()}.css" /> +<?py else: ?> + <link rel="stylesheet" type="text/css" href="#{force_css}" /> +<?py #endif ?> +<?py if board in ['zonavip', 'world']: ?> + <link rel="stylesheet" href="/static/css/txt/sjis.css" /> +<?py #endif ?> +<?py if board == 'polka': ?> + <script type="text/javascript" src="#{static_url}js/weabotxt.test.js"></script> +<?py else: ?> + <script type="text/javascript" src="#{static_url}js/weabotxt.js"></script> +<?py #endif ?> + <script type="text/javascript" src="#{static_url}js/aquiencitas.js"></script> + <script type="text/javascript" src="#{static_url}js/shobon.js"></script> +<?py if replythread and board != 'polka': ?> + <script type="text/javascript" src="#{static_url}js/autorefresh.js"></script> +<?py #endif ?> +</head> 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 @@ +<?py include('templates/txt_base_top.html') ?> +<body class="mainpage" data-brd="#{board}"> +<div id="main_nav"><a href="/" target="_top">Bienvenido a Internet</a> | <?py include('templates/navbar.html') ?></div> +<?py if banner_url: ?> + <img class="banner" src="#{banner_url}" style="width:#{banner_width}px;height:#{banner_height}px;" /> +<?py #endif ?> +<div id="titlebox" class="outerbox"> + <div class="innerbox"> + <div class="threadnav"><a href="#menu" title="Thread list">■</a><a href="#1" title="Next thread">▼</a></div> + <h1>#{board_long}</h1> + <?py if postarea_desc: ?> + <div id="rules">#{postarea_desc}</div> + <?py #endif ?> + <form method="get" action="/tools/search.py" id="search"><input type="text" name="q" value="" /><input type="hidden" name="board" value="#{board}" /><input type="submit" value="Search active posts" /><input type="submit" value="Search archives" formaction="/tools/search_kako.py" /></form> + </div> + <div class="innerbox links"><a href="/guia.html"><b>Cómo postear</b></a> | <a href="/faq.html"><b>Preguntas frecuentes</b></a> | <a href="/bai/"><b>Contacto</b></a> + <?py if not force_css: ?>| <b>Styles:</b> + <?py for title in txt_styles: ?><a href="#" class="ss">#{title}</a> <?py #endfor ?> + <?py #endif ?></div> +</div> +<?py if postarea_extra: ?> +<div class="outerbox"><div class="innerbox">#{postarea_extra}</div></div> +<?py #endif ?> +<a name="menu"></a> +<?py if threads: ?> +<div id="threadbox" class="outerbox"><div class="innerbox"> + <div id="threadlinks"><a href="#{cgi_url}threadlist/#{board}"><b>View all threads</b></a> <a href="kako/"><b>View archive</b></a> <a href="#newthread"><b>Create new thread</b></a></div> + <div id="threadlist"> + <?py iter = 1 ?> + <?py for thread in threads: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{iter}: </a><a href="##{iter}"> <b>#{thread['posts'][0]['subject']}</b> (#{thread['length']})</a><br /> + <?py iter += 1 ?> + <?py #endfor ?> + <?py for thread in more_threads: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{iter}: <b>#{thread["subject"]}</b> (#{thread["length"]})</a><br /> + <?py iter += 1 ?> + <?py #endfor ?> + </div> +</div></div> +<?py titer = 1 ?> +<?py for thread in threads: ?> +<a name="#{titer}"></a> +<div class="thread"><div class="innerbox"> +<div class="threadnav"><a href="#menu" title="Thread list">■</a><a href="##{(titer-1) if titer>1 else len(threads)}" title="Previous thread">▲</a><a href="##{(titer+1) if titer<len(threads) else '1'}" title="Next thread">▼</a></div> +<h2><small>[#{titer}:#{thread['length']}]</small><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{thread['posts'][0]['subject']}</a></h2> +<?py for post in thread['posts']: ?> +<?py if post['IS_DELETED'] == '1': ?> + <h4 class="deleted">#{post['num']} : Post deleted by user.</h4> +<?py elif post['IS_DELETED'] == '2': ?> + <h4 class="deleted">#{post['num']} : Post deleted by staff.</h4> +<?py else: ?> + <div class="reply#{' first' if post['num'] == 1 else ''}" data-n="#{post['num']}"> + <h4>#{post['num']} : + <?py if post['email']: ?> + <?py if post['tripcode']: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a> + <?py else: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a> + <?py #endif ?> + <?py else: ?> + <?py if post['tripcode']: ?> + <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span> + <?py else: ?> + <span class="name"><b>#{post['name']}</b></span> + <?py #endif ?> + <?py #endif ?> : <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span></h4> + <?py if post['file']: ?> + <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank" class="thumb"><img src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /><div>${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}</div></a> + <?py #endif ?> + <div class="msg"> + #{post['message']} + <?py if post['shortened']: ?> + <div class="abbrev">(Post is too long... Click <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{post['num']}">here</a> to view the whole post.)</div> + <?py #endif ?> + </div> +</div> +<?py #endif ?> +<?py #endfor ?> +<?py if thread['locked'] != '1': ?> +<form id="postform#{thread['id']}" class="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="password" value="" /> + <div style="display:none"><input type="text" name="name" size="15" /> <input type="text" name="email" size="15" /></div> + <span><input type="submit" value="Reply" /></span> <span><span>Name: </span><input type="text" name="fielda" size="15" /><span> E-mail: </span><input type="text" name="fieldb" size="15" /></span><br /> + <div class="formpad"> + <textarea name="message" cols="70" rows="5"></textarea> + <?py if allow_image_replies: ?><br /><input type="file" name="file" /><?py #endif ?> +<?py else: ?> +<form class="postform"><div class="locked">This thread has been closed. You cannot post in it any longer.</div><div class="formpad"> +<?py #endif ?> + <div class="threadlinks"> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/"><b>Entire thread</b></a> + <?py if thread['length'] > 51: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50"><b>Last 50</b></a> + <?py #endif ?> + <?py if thread['length'] > 101: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100"><b>First 100</b></a> + <?py #endif ?> + <a href="#menu"><b>Thread list</b></a> + <a href="#newthread"><b>New thread</b></a> + </div> +</div></form> +</div></div> +<?py titer += 1 ?> +<?py #endfor ?> +<?py #endif ?> +<a name="newthread"></a> +<div id="createbox" class="outerbox"> + <div class="extrabox"></div> + <div class="innerbox"> + <div class="threadnav"><a href="#menu" title="Thread list">■</a></div> + <h5>New thread form</h5> + <form id="postform0" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="password" value="" /> + <table style="max-width:600px"> + <tr> + <td class="pblock">Subject:</td> + <td colspan="3" style="width:100%"><input type="text" name="subject" size="50" maxlength="100" /></td> + <td><input type="submit" value="Create new thread" /></td> + </tr> + <tr> + <td class="pblock">Name:</td><td><input type="text" name="fielda" /></td> + <td class="pblock">E-mail:</td><td><input type="text" name="fieldb" /></td> + <td><input type="button" name="preview" value="Preview" /></td> + </tr> + <tr id="options" style="display:none"><td></td><td colspan="4"><div id="preview0" class="msg"></div></td></tr> + <tr><td class="pblock">Body:</td><td colspan="4"><textarea name="message" cols="70" rows="10"></textarea></td></tr> + <?py if allow_images: ?> + <tr><td class="pblock">File:</td><td colspan="4"><input type="file" name="file" /></td></tr> + <?py #endif ?> + </table> + <div style="display:none">Trampa: <input type="text" name="name" maxlength="50" /> <input type="text" name="email" maxlength="50" /></div> + </form> + </div> +</div> +<center id="footer"><a href="/" target="_top">Bienvenido a Internet BBS/IB</a> weabot.py <?py include('templates/revision.html') ?> + FastCGI + tenjin<br />それがBaIクオリティー!</center> +</body> +</html>
\ 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 @@ +<?py include('templates/txt_base_top.html') ?> +<body class="mainpage" data-brd="#{board}"> +<div id="main_nav"><a href="/" target="_top">Bienvenido a Internet</a> | <?py include('templates/navbar.html') ?></div> +<?py if banner_url: ?> + <img class="banner" src="#{banner_url}" style="width:#{banner_width}px;height:#{banner_height}px;" /> +<?py #endif ?> +<div id="titlebox" class="outerbox"> + <div class="innerbox"> + <div class="threadnav"><a href="#menu" title="Ir a lista de hilos">■</a><a href="#1" title="Ir a primer hilo">▼</a></div> + <h1>#{board_long}</h1> + <?py if postarea_desc: ?> + <div id="rules">#{postarea_desc}</div> + <?py #endif ?> + <form method="get" action="/tools/search.py" id="search"><input type="text" name="q" value="" /><input type="hidden" name="board" value="#{board}" /><input type="submit" value="Buscar en mensajes activos" /><input type="submit" value="Buscar en archivo" formaction="/tools/search_kako.py" /></form> + </div> + <div class="innerbox links"><b>¿Eres nuevo?</b> <a href="/guia.html"><b>Cómo postear</b></a> | <a href="/faq.html"><b>Preguntas frecuentes</b></a> | <a href="/bai/"><b>Contacto</b></a> + <?py if not force_css: ?>| <b>Estilo:</b> + <?py for title in txt_styles: ?><a href="#" class="ss">#{title}</a> <?py #endfor ?> + <?py #endif ?></div> +</div> +<?py if postarea_extra: ?> +<div class="outerbox"><div class="innerbox">#{postarea_extra}</div></div> +<?py #endif ?> +<a name="menu"></a> +<?py if threads: ?> +<div id="threadbox" class="outerbox"><div class="innerbox"> + <div id="threadlinks"><a href="#{cgi_url}threadlist/#{board}"><b>Ver todos los hilos</b></a> <a href="kako/"><b>Ver hilos archivados</b></a> <a href="#newthread"><b>Crear nuevo hilo</b></a></div> + <div id="threadlist"> + <?py iter = 1 ?> + <?py for thread in threads: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{iter}: </a><a href="##{iter}"> <b>#{thread['posts'][0]['subject']}</b> (#{thread['length']})</a><br /> + <?py iter += 1 ?> + <?py #endfor ?> + <?py for thread in more_threads: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{iter}: <b>#{thread["subject"]}</b> (#{thread["length"]})</a><br /> + <?py iter += 1 ?> + <?py #endfor ?> + </div> +</div></div> +<?py titer = 1 ?> +<?py for thread in threads: ?> +<a name="#{titer}"></a> +<div class="thread"><div class="innerbox"> +<div class="threadnav"><a href="#menu" title="Lista de hilos">■</a><a href="##{(titer-1) if titer>1 else len(threads)}" title="Hilo anterior">▲</a><a href="##{(titer+1) if titer<len(threads) else '1'}" title="Hilo siguiente">▼</a></div> +<h2><span>[#{titer}:#{thread['length']}]</span><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{thread['posts'][0]['subject']}</a></h2> +<?py for post in thread['posts']: ?> +<?py if post['IS_DELETED'] == '1': ?> + <h4 class="deleted">#{post['num']} Mensaje eliminado por el usuario.</h4> +<?py elif post['IS_DELETED'] == '2': ?> + <h4 class="deleted">#{post['num']} Mensaje eliminado por miembro del staff.</h4> +<?py else: ?> + <div class="reply#{' first' if post['num'] == 1 else ''}" data-n="#{post['num']}"> + <h4>#{post['num']} : + <?py if post['email']: ?> + <?py if post['tripcode']: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a> + <?py else: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a> + <?py #endif ?> + <?py else: ?> + <?py if post['tripcode']: ?> + <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span> + <?py else: ?> + <span class="name"><b>#{post['name']}</b></span> + <?py #endif ?> + <?py #endif ?> : <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span></h4> + <?py if post['file']: ?> + <a href="/#{board}/src/#{post['file']}" target="_blank" class="thumb"><img src="#{'/static/' if post['thumb'].startswith('mime') else ('/'+board+'/thumb/')}#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /><div>${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}</div></a> + <?py #endif ?> + <div class="msg"> + #{post['message']} + <?py if post['shortened']: ?> + <div class="abbrev">(Post muy largo... Presiona <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{post['num']}">aquí</a> para verlo completo.)</div> + <?py #endif ?> + </div> +</div> +<?py #endif ?> +<?py #endfor ?> +<?py if thread['locked'] != '1': ?> +<form id="postform#{thread['id']}" class="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="password" value="" /> + <div style="display:none"><input type="text" name="name" size="15" /> <input type="text" name="email" size="15" /></div> + <span><input type="submit" value="Responder" /></span> <span><span>Nombre: </span><input type="text" name="fielda" size="15" /><span> E-mail: </span><input type="text" name="fieldb" size="15" /></span> + <div class="formpad"> + <textarea name="message" cols="70" rows="5"></textarea> + <?py if allow_image_replies: ?><br /><input type="file" name="file" /><?py #endif ?> +<?py else: ?> +<form class="postform"><div class="locked">El hilo ha sido cerrado. Ya no se puede postear en él.</div><div class="formpad"> +<?py #endif ?> + <div class="threadlinks"> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/"><b>Hilo completo</b></a> + <?py if thread['length'] > 51: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50"><b>Últimos 50</b></a> + <?py #endif ?> + <?py if thread['length'] > 101: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100"><b>Primeros 100</b></a> + <?py #endif ?> + <a href="#menu"><b>Lista de hilos</b></a> + <a href="#newthread"><b>Nuevo hilo</b></a> + </div> +</div></form> +</div></div> +<?py titer += 1 ?> +<?py #endfor ?> +<?py #endif ?> +<a name="newthread"></a> +<div id="createbox" class="outerbox"> + <div class="extrabox"></div> + <div class="innerbox"> + <div class="threadnav"><a href="#menu" title="Lista de hilos">■</a></div> + <h5>Formulario de nuevo hilo</h5> + <form id="postform0" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="password" value="" /> + <table style="max-width:600px"> + <tr> + <td class="pblock">Asunto:</td> + <td colspan="3" style="width:100%"><input type="text" name="subject" size="50" maxlength="100" /></td> + <td><input type="submit" value="Crear nuevo hilo" /></td> + </tr> + <tr> + <td class="pblock">Nombre:</td><td><input type="text" name="fielda" /></td> + <td class="pblock">E-mail:</td><td><input type="text" name="fieldb" /></td> + <td><input type="button" name="preview" value="Previsualizar" /></td> + </tr> + <tr id="options" style="display:none"><td></td><td colspan="4"><div id="preview0" class="msg"></div></td></tr> + <tr><td class="pblock">Mensaje:</td><td colspan="4"><textarea name="message" cols="70" rows="10"></textarea></td></tr> + <?py if allow_images: ?> + <tr><td class="pblock">Archivo:</td><td colspan="4"><input type="file" name="file" /></td></tr> + <?py #endif ?> + </table> + <div style="display:none">Trampa: <input type="text" name="name" maxlength="50" /> <input type="text" name="email" maxlength="50" /></div> + </form> + </div> +</div> +<center id="footer"><a href="/" target="_top">Bienvenido a Internet BBS/IB</a> weabot.py <?py include('templates/revision.html') ?> + FastCGI + tenjin<br />No se ponga sensible, baisano...</center> +</body> +</html>
\ 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 @@ +<html> +<head> +<title>Error</title> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0" /> +<style type="text/css"> +* {word-wrap:break-word;} +body {margin:8px;} +.h {font-weight:bold; font-size:large;} +.err {color:red;} +.sub1 {color:#00AA00;} .sub2 {color:#DD00DD;} +blockquote {margin-left:40px; margin-right:40px;} +ul {padding-left:40px;} +@media(max-width:650px){ + blockquote {margin-left:20px; margin-right:20px;} + ul {padding-left:20px;} +} +</style> +</head> +<body> +<div class="h err">ERROR: #{error}</div> +<blockquote> + Host <b>${info['host']}</b><br> + <blockquote> + Nombre: <b>${info['name']}</b><br> + E-mail: ${info['email']}<br> + Mensaje: <br> + ${info['message']} + </blockquote> +</blockquote> +<hr> +<ul> + <div class="h sub1">¿No sabes qué sucede?</div> + <ul style="line-height:1.5;"> + ¡Revisemos!<br> + <b> + [<a href="/guia.html">¿Eres nuevo?</a>]<br /> + [<a href="/faq.html">Preguntas frecuentes</a>]<br /> + [<a href="#{boards_url}#{board}">Ir a la sección</a>]<br /> + [<a href="#{cgi_url}threadlist/#{board}">Ir a la lista de hilos</a>]<br /> + </b> + </ul><br> + <div class="h sub2">Contacto</div> + <ul style="line-height:1.5;"> + Cualquier problema con el sitio por favor hacerlo llegar al staff de BaI.<br /> + Para ello contáctanos en la <a href="/bai/">sección de discusión</a>. + </ul> +</ul> +</body> +</html>
\ 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 @@ +<?py include('templates/txt_base_top.html') ?> +<body class="threadpage" data-brd="#{board}"> +<?py if threads: ?> +<?py for thread in threads: ?> +<div id="thread_nav"> + <a href="/" name="top" target="_top">Bienvenido a Internet</a> + <a href="#{boards_url}#{board}/">■Return to BBS■</a> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Entire thread</a> + <?py if thread['length'] > 100: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100">First 100</a> + <?py #endif ?> + <?py if thread['length'] > 51: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Last 50</a> + <?py #endif ?> + <a href="#bottom">▼Bottom▼</a> +</div> +<hr /> +<?py if thread['length'] > 1000: ?> + <div class="stop red">The thread got over 1000 posts and has been closed.</div> +<?py elif thread['length'] > 900: ?> + <div class="warn yellow">The thread has reached 900 posts. When it reaches 1000 posts it will be closed.</div> +<?py #endif ?> +<div class="thread" data-length="#{thread['length']}"> + <h3>#{thread['subject']} <span>(${(str(thread['length'])+" replies") if thread['length']>1 else "1 reply"})</span></h3> + <?py for post in thread['posts']: ?> + <?py if post['IS_DELETED'] == '1': ?> + <h4 class="deleted">#{post['num']} : Post deleted by user.</h4> + <?py elif post['IS_DELETED'] == '2': ?> + <h4 class="deleted">#{post['num']} : Post deleted by staff.</h4> + <?py else: ?> + <?py if post['num'] == 1: ?> + <div class="reply first" data-n="#{post['num']}"> + <?py else: ?> + <div class="reply" data-n="#{post['num']}"> + <?py #endif ?> + <h4><a href="#" class="num">#{post['num']}</a> : + <?py if post['email']: ?> + <?py if post['tripcode']: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a> + <?py else: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a> + <?py #endif ?> + <?py else: ?> + <?py if post['tripcode']: ?> + <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span> + <?py else: ?> + <span class="name"><b>#{post['name']}</b></span> + <?py #endif ?> + <?py #endif ?> : <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span> + <span class="del"><a href="#{cgi_url}report/#{board}/#{post['id']}/#{post['num']}">rep</a> <a href="#">del</a></span></h4> + <?py if post['file']: ?> + <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank" class="thumb"><img src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /><div>${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}</div></a> + <?py #endif ?> + <div class="msg"> + #{post['message']} + </div> + </div> + <?py #endif ?> + <?py #endfor ?> + <div class="size">#{thread['size']}</div> +</div> +<hr /> +<?py if thread['locked'] != '1': ?> + <div class="lastposts"><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{thread['length']}-n" id="n">Show new posts</a></div> + <hr /> +<?py #endif ?> +<?py if thread['length'] > 1000: ?> + <div class="stop red">The thread got over 1000 posts and has been closed.</div> +<?py elif thread['length'] > 950: ?> + <div class="warn red">The thread has reached 950 posts. When it reaches 1000 posts it will be closed.</div> +<?py elif thread['length'] > 900: ?> + <div class="warn yellow">The thread has reached 900 posts. When it reaches 1000 posts it will be closed.</div> +<?py #endif ?> +<form id="postform#{thread['id']}" class="postform" name="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <div class="threadlinks"> + <a href="#{boards_url}#{board}">■Return to BBS■</a> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Entire thread</a> + <?py if prevrange: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{prevrange}">Previous 100</a> + <?py #endif ?> + <?py if nextrange: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{nextrange}">Next 100</a> + <?py #endif ?> + <?py if thread['length'] > 51: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Last 50</a> + <?py #endif ?> + <a href="#top">▲Top▲</a> + </div> + <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="password" value="" /> + <?py if thread['locked'] != '1': ?> + <div style="display:none"><input type="text" name="name" size="13" /> <input type="text" name="email" size="13" /></div> + <span><input type="submit" value="Responder" accesskey="z" /> <input type="button" name="preview" value="Previsualizar" /></span> <span><span>Name: </span><input type="text" name="fielda" size="13" accesskey="n" /><span> E-mail: </span><input type="text" name="fieldb" size="13" accesskey="e" /></span><br /> + <textarea name="message" cols="80" rows="7" accesskey="m"></textarea><br /> + <div id="preview#{thread['id']}" class="msg" style="display:none"></div> + <?py if allow_image_replies: ?> + <input type="file" name="file" /> + <?py #endif ?> + <?py #endif ?> +</form> +<?py #endfor ?> +<?py #endif ?> +<div class="end">weabot.py ver <?py include('templates/revision.html') ?> Bienvenido a Internet BBS/IB</div> +<a name="bottom"></a> +</body> +</html>
\ 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 @@ +<?py include('templates/txt_base_top.html') ?> +<body class="threadpage" data-brd="#{board}"> +<?py if threads: ?> +<?py for thread in threads: ?> +<div id="thread_nav"> + <a href="/" name="top" target="_top">Bienvenido a Internet</a> + <a href="#{boards_url}#{board}/">■Volver al BBS■</a> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Hilo completo</a> + <?py if thread['length'] > 100: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100">Primeros 100</a> + <?py #endif ?> + <?py if thread['length'] > 51: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Últimos 50</a> + <?py #endif ?> + <a href="#bottom">▼Bajar▼</a> +</div> +<hr /> +<?py if thread['length'] > 1000: ?> + <div class="stop red">El hilo superó los 1000 mensajes y ha sido cerrado. Ya no se puede postear en él.</div> +<?py elif thread['length'] > 900: ?> + <div class="warn yellow">El hilo ha recibido más de 900 mensajes. Cuando llegue a 1000 será cerrado.</div> +<?py #endif ?> +<div class="thread" data-length="#{thread['length']}"> + <h3>#{thread['subject']} <span>(${(str(thread['length'])+" respuestas") if thread['length']>1 else "Una respuesta"})</span></h3> + <?py for post in thread['posts']: ?> + <?py if post['IS_DELETED'] == '1': ?> + <h4 class="deleted">#{post['num']} : Mensaje eliminado por el usuario.</h4> + <?py elif post['IS_DELETED'] == '2': ?> + <h4 class="deleted">#{post['num']} : Mensaje eliminado por miembro del staff.</h4> + <?py else: ?> + <?py if post['num'] == 1: ?> + <div class="reply first" data-n="#{post['num']}"> + <?py else: ?> + <div class="reply" data-n="#{post['num']}"> + <?py #endif ?> + <h4><a href="#" class="num">#{post['num']}</a> : + <?py if post['email']: ?> + <?py if post['tripcode']: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a> + <?py else: ?> + <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a> + <?py #endif ?> + <?py else: ?> + <?py if post['tripcode']: ?> + <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span> + <?py else: ?> + <span class="name"><b>#{post['name']}</b></span> + <?py #endif ?> + <?py #endif ?> : <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span> + <span class="del"><a href="#{cgi_url}report/#{board}/#{post['id']}/#{post['num']}">rep</a> <a href="#">del</a></span></h4> + <?py if post['file']: ?> + <a href="/#{board}/src/#{post['file']}" target="_blank" class="thumb"><img src="#{'/static/' if post['thumb'].startswith('mime') else ('/'+board+'/thumb/')}#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /><div>${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}</div></a> + <?py #endif ?> + <div class="msg">#{post['message']}</div> + </div> + <?py #endif ?> + <?py #endfor ?> + <div class="size">#{thread['size']}</div> +</div> +<hr /> +<?py if thread['locked'] != '1': ?> + <div class="lastposts"><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{thread['length']}-n" id="n">Ver nuevos posts</a></div> + <hr /> +<?py #endif ?> +<?py if thread['length'] > 1000: ?> + <div class="stop red">El hilo superó los 1000 mensajes y ha sido cerrado. Ya no se puede postear en él.</div> +<?py elif thread['length'] > 900: ?> + <div class="warn yellow">El hilo ha recibido más de 900 mensajes. Cuando llegue a 1000 será cerrado.</div> +<?py #endif ?> +<form id="postform#{thread['id']}" class="postform" name="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <div class="threadlinks"> + <a href="#{boards_url}#{board}/">■Volver al BBS■</a> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Hilo completo</a> + <?py if prevrange: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{prevrange}">Anteriores 100</a> + <?py #endif ?> + <?py if nextrange: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{nextrange}">Próximos 100</a> + <?py #endif ?> + <?py if thread['length'] > 51: ?> + <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Últimos 50</a> + <?py #endif ?> + <a href="#top">▲Subir▲</a> + </div> + <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="password" value="" /> + <?py if thread['locked'] != '1': ?> + <div style="display:none"><input type="text" name="name" size="13" /> <input type="text" name="email" size="13" /></div> + <span><input type="submit" value="Responder" accesskey="z" /> <input type="button" name="preview" value="Previsualizar" /></span> <span><span>Nombre: </span><input type="text" name="fielda" size="13" accesskey="n" /><span> E-mail: </span><input type="text" name="fieldb" size="13" accesskey="e" /></span><br /> + <textarea name="message" cols="80" rows="7" accesskey="m"></textarea><br /> + <div id="preview#{thread['id']}" class="msg" style="display:none"></div> + <?py if allow_image_replies: ?> + <input type="file" name="file" /> + <?py #endif ?> + <?py #endif ?> +</form> +<?py #endfor ?> +<?py #endif ?> +<div class="end">weabot.py ver <?py include('templates/revision.html') ?> Bienvenido a Internet BBS/IB</div> +<a name="bottom"></a> +</body> +</html>
\ 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 @@ +<?py include('templates/txt_base_top.html') ?> +<body class="threads" data-brd="#{board}"> +<div id="main_nav"><a href="/" target="_top">Bienvenido a Internet</a> | <?py include('templates/navbar.html') ?></div> +<?py if banner_url: ?> + <img class="banner" src="#{banner_url}" style="width:#{banner_width}px;height:#{banner_height}px;" /> +<?py #endif ?> +<div id="titlebox" class="outerbox"> + <div class="innerbox"><h1>#{board_long}</h1></div> + <div class="innerbox links"><b>¿Eres nuevo?</b> <a href="/guia.html"><b>Cómo postear</b></a> | <a href="/faq.html"><b>Preguntas frecuentes</b></a> | <a href="/bai/"><b>Contacto</b></a> + <?py if not force_css: ?>| <b>Apariencia:</b> + <?py for title in txt_styles: ?><a href="#" class="ss">#{title}</a> <?py #endfor ?> + <?py #endif ?></div> +</div> +<a name="menu"></a> +<div id="threadbox" class="outerbox"><div class="innerbox"> + <div id="threadlinks"><a href="#{boards_url}#{board}/"><b>Volver al BBS</b></a> <a href="/#{board}/kako/"><b>Ver hilos archivados</b></a> <a href="#newthread"><b>Crear nuevo hilo</b></a></div> + <div id="listmenu">Orden: <a class="l_s" href="#">Normal</a> <a class="l_s" href="#">Edad</a> <a class="l_s" href="#">Largo</a> <a class="l_s" href="#">Rapidez</a> <a class="l_s" href="#">Aleatorio</a> / Modo: <a class="l_d" href="#">Lista</a> <a class="l_d" href="#">Malla</a> / Buscar: <input id="l_sr" style="padding:0px;width:100px;" type="text"></div> +</div></div> +<div id="content" class="list"> +<div id="header" class="row"> + <div>#</div> + <div style="width:100%;">Asunto</div> + <div>Resp.</div> + <div class="hdate">Última respuesta</div> +</div> +<?py iter = 1 ?> +<?py for thread in more_threads: ?> +<div class="row"> + <div class="pos">#{iter}:</div> + <div class="thread"><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/${'l50' if int(thread['length']) > 50 else ''}">#{thread["subject"]}</a></div> + <div class="com">#{thread["length"]}</div> + <div class="date" data-unix="#{timestamps[iter-1][0]}">#{timestamps[iter-1][1]}</div> +</div> +<?py iter += 1 ?> +<?py #endfor ?> +</div> +<a name="newthread"></a> +<div id="createbox" class="outerbox"> + <div class="extrabox"></div> + <div class="innerbox"> + <h5>Formulario de nuevo hilo</h5> + <form id="postform0" action="#{cgi_url}post" method="post" enctype="multipart/form-data"> + <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="password" value="" /> + <table style="max-width:600px;"> + <tr> + <td style="text-align:right;">Asunto:</td> + <td colspan="3" style="width:100%;"><input type="text" name="subject" size="50" maxlength="100" /></td> + <td><input type="submit" value="Crear nuevo hilo" /></td> + </tr> + <tr> + <td style="text-align:right;">Nombre:</td><td><input type="text" name="fielda" /></td> + <td style="text-align:right;">E-mail:</td><td><input type="text" name="fieldb" /></td> + <td><input type="button" name="preview" value="Previsualizar" /></td> + </tr> + <tr id="options" style="display:none;"><td></td><td colspan="4"><div id="preview0" class="msg"></div></td></tr> + <tr><td style="text-align:right;">Mensaje:</td><td colspan="4"><textarea name="message" cols="70" rows="10"></textarea></td></tr> + <?py if allow_images: ?> + <tr><td style="text-align:right;">Archivo:</td><td colspan="4"><input type="file" name="file" /></td></tr> + <?py #endif ?> + </table> + <div style="display:none;">Trampa: <input type="text" name="name" maxlength="50" /> <input type="text" name="email" maxlength="50" /></div> + </form> + </div> +</div> +<center id="footer"><a href="/" target="_top">Bienvenido a Internet BBS/IB</a> weabot.py <?py include('templates/revision.html') ?> + FastCGI + tenjin<br />No se ponga sensible, baisano...</center> +</body> +</html>
\ 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. '<b>%s</b>' % _P("item['id']") => "<b>${item['id']}</b>" """ + 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 "<br />\n" and return it.""" + if not text: + return _escaped.as_escaped('') + return _escaped.as_escaped(text.replace('\n', '<br />\n')) + + def text2html(text, use_nbsp=True): + """(experimental) escape xml characters, replace "\n" to "<br />\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', '<br />\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 = '<a href="javascript:undefined" onclick="%s;return false"%s>%s</a>' % \ + (_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 '<!-- begin: file -->' and '<!-- end: file -->' 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 "<div>\\n#{_context}\\n</div>" is parsed as + "<div>\\n#{_context}</div>". + """ + 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('<?py') and "\n" or "; " + buf.append(self.preamble + eol) + + def after_convert(self, buf): + if self.postamble: + if buf and not buf[-1].endswith("\n"): + buf.append("\n") + buf.append(self.postamble + "\n") + + def convert_file(self, filename): + """Convert file into python script and return it. + This is equivarent to convert(open(filename).read(), filename). + """ + input = _read_template_file(filename) + return self.convert(input, filename) + + def convert(self, input, filename=None): + """Convert string in which python code is embedded into python script and return it. + + input:str + Input string to convert into python code. + filename:str (=None) + Filename of input. this is optional but recommended to report errors. + """ + if self.encoding and isinstance(input, str): + input = input.decode(self.encoding) + self._reset(input, filename) + buf = [] + self.before_convert(buf) + self.parse_stmts(buf, input) + self.after_convert(buf) + script = ''.join(buf) + self.script = script + return script + + STMT_PATTERN = (r'<\?py( |\t|\r?\n)(.*?) ?\?>([ \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(('<ul>\n', )) + ## i = 0 + ## for item in items: + ## i += 1 + ## _buf.extend(('<li>', to_str(item), '</li>\n', )) + ## #endfor + ## _buf.extend(('</ul>\n', )) + ## #endif + ## """[1:] + ## lines = input.splitlines(True) + ## block = self.parse_lines(lines) + ## #=> [ "if items:\n", + ## [ "_buf.extend(('<ul>\n', ))\n", + ## "i = 0\n", + ## "for item in items:\n", + ## [ "i += 1\n", + ## "_buf.extend(('<li>', to_str(item), '</li>\n', ))\n", + ## ], + ## "#endfor\n", + ## "_buf.extend(('</ul>\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("<!-- ***** begin: %s ***** -->\n" % self.filename) + exec(self.bytecode, globals, locals) + _buf.append("<!-- ***** end: %s ***** -->\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<?py%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 '<!-- #/JS -->'. (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 '<!-- #/JS -->'. (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 = '<script' + if self._attrs: + for k in self._attrs: + stag = "".join((stag, ' ', k, '="', self._attrs[k], '"')) + stag += '>' + etag = '</script>' + for text, lspace, funcdecl, rspace, end_p in self._scan_chunks(input, filename): + if end_p: break + if funcdecl: + buf.append(text) + if re.match(r'^\$?\w+\(', funcdecl): + buf.extend((lspace or '', stag, 'function ', funcdecl, "{var _buf='';", rspace or '')) + else: + m = re.match(r'(.+?)\((.*)\)', funcdecl) + buf.extend((lspace or '', stag, m.group(1), '=function(', m.group(2), "){var _buf='';", rspace or '')) + else: + self._parse_stmts(text, buf) + buf.extend((lspace or '', "return _buf;};", etag, rspace or '')) + # + buf.append(text) + + STMT_REXP = re.compile(r'(?:^( *)<|<)\?js(\s.*?) ?\?>([ \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. + <?py include('file.pyhtml') ?> + #{include('file.pyhtml', False)} + <?py val = 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 += '<html xmlns="http://www.w3.org/1999/xhtml"><meta http-equiv="refresh" content="0;url=%s" /><body><p>...</p></body></html>' % 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 += '<html xmlns="http://www.w3.org/1999/xhtml"><meta http-equiv="refresh" content="0;url=%s" /><body><p>...</p></body></html>' % 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 += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s" /><p>%s</p></body></html>' % (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 += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s" /><p>--> --> --></p></body></html>' % 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, '<meta http-equiv="refresh" content="0; url=/cgi/banned/%s">' % 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"] += '<hr />' + extend_str + ' configurado.' + if dice: + post["message"] += '<hr />' + throw_dice(dice) + if ball: + post["message"] += '<hr />' + 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"] = ('[<span style="color:red">%s</span>]<br />' % 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"<hr />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"] += " <em>[%s]</em>" % 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 "<a href" not in post["message"]: + raise UserError, "Al momento de crear un hilo en esta sección necesitas incluír al menos 1 link como fuente en tu mensaje." + + # insert icon if needed + img_src = '<img src="%s" alt="ico" /><br />' % getRandomIco() + post["message"] = img_src + post["message"] + + # insert post, then run timThreads to make sure the board doesn't exceed the page limit + postid = post.insert() + + # delete threads that have crossed last page + trimThreads() + + # fix null references when creating thread + if board["board_type"] == '1' and not post["parentid"]: + post["message"] = re.compile(r'<a href="/(\w+)/res/0.html/(.+)"').sub(r'<a href="/\1/res/'+str(postid)+r'.html/\2"', post["message"]) + UpdateDb("UPDATE `posts` SET message = '%s' WHERE boardid = '%s' AND id = '%s'" % (_mysql.escape_string(post["message"]), _mysql.escape_string(board["id"]), _mysql.escape_string(str(postid)))) + + # do operations if replying to a thread (bump, autoclose, update cache) + logTime("Updating thread") + thread_length = None + if post["parentid"]: + # get length of the thread + thread_length = threadNumReplies(post["parentid"]) + + # bump if not saged + if 'sage' not in post["email"].lower() and parent_post['locked'] != '2': + UpdateDb("UPDATE `posts` SET bumped = %d WHERE (`id` = '%s' OR `parentid` = '%s') AND `boardid` = '%s'" % (post["timestamp"], post["parentid"], post["parentid"], board["id"])) + + # check if thread must be closed + autoclose_thread(post["parentid"], t, thread_length) + + # update final attributes (length and last post) + UpdateDb("UPDATE `posts` SET length = %d, last = %d WHERE `id` = '%s' AND `boardid` = '%s'" % (thread_length, post["timestamp"], post["parentid"], board["id"])) + + # update cache + threadUpdated(post["parentid"]) + else: + # create cache for new thread + threadUpdated(postid) + + regenerateHome() + + # make page redirect + ttaken = timeTaken(_STARTTIME, time.clock()) + noko = 'noko' in email.lower() or (board["board_type"] == '1') + + # get new post url + post_url = make_url(postid, post, parent_post or post, noko, mobile) + + if board['secret'] == '0': + # add to recent posts + if Settings.ENABLE_RSS: + latestAdd(post, thread_length, postid, parent_post) + # call discord hook + if Settings.ENABLE_DISCORD_HOOK and not post["parentid"]: + hook_url = make_url(postid, post, parent_post or post, True, False) + discord_hook(post, hook_url) + + return (post_url, ttaken) + + def delete_post(self, boarddir, postid, imageonly, password, mobile=False): + # open database + OpenDb() + + # set the board + board = setBoard(boarddir) + + if board["dir"] == '0': + raise UserError, "No se pueden eliminar mensajes en esta sección." + + # check if we have a post id and check it's numeric + if not postid: + raise UserError, "Selecciona uno o más mensajes a eliminar." + + # make sure we have a password + if not password: + raise UserError, _("Please enter a password.") + + to_delete = [] + if isinstance(postid, list): + to_delete = [n.value for n in postid] + else: + to_delete = [postid] + + # delete posts + if board['board_type'] == '1' and len(to_delete) == 1: + # we should be deleting only one (textboard) + # check if it's the last post and delete it completely if it is + deltype = '0' + post = FetchOne("SELECT `id`, `timestamp`, `parentid` FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(to_delete[0]))) + if post['parentid'] != '0': + op = get_parent_post(post['parentid'], board['id']) + if op['last'] != post['timestamp']: + deltype = '1' + + deletePost(to_delete[0], password, deltype, imageonly) + latestRemove(post['id']) + regenerateHome() + else: + # delete all checked posts (IB) + deleted = 0 + errors = 0 + msgs = [] + + for pid in to_delete: + try: + deletePost(pid, password, board['recyclebin'], imageonly) + latestRemove(pid) + deleted += 1 + msgs.append('No.%s: Eliminado' % pid) + except UserError, message: + errors += 1 + msgs.append('No.%s: %s' % (pid, message)) + + # regenerate home + if deleted: + regenerateHome() + + # show errors, if any + if errors: + raise UserError, 'No todos los mensajes pudieron ser eliminados.<br />' + '<br />'.join(msgs) + + # redirect + if imageonly: + self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s/" /><p>%s</p></body></html>' % (("/cgi/mobile/" if mobile else Settings.BOARDS_URL) + board["dir"], _("File deleted successfully.")) + else: + self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s/" /><p>%s</p></body></html>' % (("/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", "<br />") + + 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() + |