PageRenderTime 50ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/r2/r2/lib/cssfilter.py

https://github.com/wangmxf/lesswrong
Python | 424 lines | 322 code | 51 blank | 51 comment | 53 complexity | 9068220a3212c1e8d90fcf117ab3d5ed MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1
  1. # The contents of this file are subject to the Common Public Attribution
  2. # License Version 1.0. (the "License"); you may not use this file except in
  3. # compliance with the License. You may obtain a copy of the License at
  4. # http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
  5. # License Version 1.1, but Sections 14 and 15 have been added to cover use of
  6. # software over a computer network and provide for limited attribution for the
  7. # Original Developer. In addition, Exhibit A has been modified to be consistent
  8. # with Exhibit B.
  9. #
  10. # Software distributed under the License is distributed on an "AS IS" basis,
  11. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
  12. # the specific language governing rights and limitations under the License.
  13. #
  14. # The Original Code is Reddit.
  15. #
  16. # The Original Developer is the Initial Developer. The Initial Developer of the
  17. # Original Code is CondeNet, Inc.
  18. #
  19. # All portions of the code written by CondeNet are Copyright (c) 2006-2008
  20. # CondeNet, Inc. All Rights Reserved.
  21. ################################################################################
  22. from __future__ import with_statement
  23. from r2.models import *
  24. from r2.lib.utils import sanitize_url, domain, randstr
  25. from r2.lib.strings import string_dict
  26. from pylons import g, c
  27. from pylons.i18n import _
  28. import re
  29. import cssutils
  30. from cssutils import CSSParser
  31. from cssutils.css import CSSStyleRule
  32. from cssutils.css import CSSValue, CSSValueList
  33. from cssutils.css import CSSPrimitiveValue
  34. from cssutils.css import cssproperties
  35. from xml.dom import DOMException
  36. msgs = string_dict['css_validator_messages']
  37. custom_macros = {
  38. 'num': r'[-]?\d+|[-]?\d*\.\d+',
  39. 'percentage': r'{num}%',
  40. 'length': r'0|{num}(em|ex|px|in|cm|mm|pt|pc)',
  41. 'int': r'[-]?\d+',
  42. 'w': r'\s*',
  43. # From: http://www.w3.org/TR/2008/WD-css3-color-20080721/#svg-color
  44. 'x11color': r'aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen',
  45. 'csscolor': r'(maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray|ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)|#[0-9a-f]{3}|#[0-9a-f]{6}|rgb\({w}{int}{w},{w}{int}{w},{w}{int}{w}\)|rgb\({w}{num}%{w},{w}{num}%{w},{w}{num}%{w}\)',
  46. 'color': '{x11color}|{csscolor}',
  47. 'single-text-shadow': r'({color}\s+)?{length}\s+{length}(\s+{length})?|{length}\s+{length}(\s+{length})?(\s+{color})?',
  48. 'box-shadow-pos': r'{length}\s+{length}(\s+{length})?',
  49. }
  50. custom_values = {
  51. '_height': r'{length}|{percentage}|auto|inherit',
  52. '_width': r'{length}|{percentage}|auto|inherit',
  53. '_overflow': r'visible|hidden|scroll|auto|inherit',
  54. 'color': r'{color}',
  55. 'background-color': r'{color}',
  56. 'border-color': r'{color}',
  57. 'background-position': r'(({percentage}|{length}){0,3})?\s*(top|center|left)?\s*(left|center|right)?',
  58. 'opacity': r'{num}',
  59. 'filter': r'alpha\(opacity={num}\)',
  60. }
  61. nonstandard_values = {
  62. # http://www.w3.org/TR/css3-background/#border-top-right-radius
  63. '-moz-border-radius': r'(({length}|{percentage}){w}){1,2}',
  64. '-moz-border-radius-topleft': r'(({length}|{percentage}){w}){1,2}',
  65. '-moz-border-radius-topright': r'(({length}|{percentage}){w}){1,2}',
  66. '-moz-border-radius-bottomleft': r'(({length}|{percentage}){w}){1,2}',
  67. '-moz-border-radius-bottomright': r'(({length}|{percentage}){w}){1,2}',
  68. '-webkit-border-radius': r'(({length}|{percentage}){w}){1,2}',
  69. '-webkit-border-top-left-radius': r'(({length}|{percentage}){w}){1,2}',
  70. '-webkit-border-top-right-radius': r'(({length}|{percentage}){w}){1,2}',
  71. '-webkit-border-bottom-left-radius': r'(({length}|{percentage}){w}){1,2}',
  72. '-webkit-border-bottom-right-radius': r'(({length}|{percentage}){w}){1,2}',
  73. # http://www.w3.org/TR/css3-text/#text-shadow
  74. 'text-shadow': r'none|({single-text-shadow}{w},{w})*{single-text-shadow}',
  75. # http://www.w3.org/TR/css3-background/#the-box-shadow
  76. # (This description doesn't support multiple shadows)
  77. 'box-shadow': 'none|(?:({box-shadow-pos}\s+)?{color}|({color}\s+?){box-shadow-pos})',
  78. }
  79. custom_values.update(nonstandard_values);
  80. def _expand_macros(tokdict,macrodict):
  81. """ Expand macros in token dictionary """
  82. def macro_value(m):
  83. return '(?:%s)' % macrodict[m.groupdict()['macro']]
  84. for key, value in tokdict.items():
  85. while re.search(r'{[a-z][a-z0-9-]*}', value):
  86. value = re.sub(r'{(?P<macro>[a-z][a-z0-9-]*)}',
  87. macro_value, value)
  88. tokdict[key] = value
  89. return tokdict
  90. def _compile_regexes(tokdict):
  91. """ Compile all regular expressions into callable objects """
  92. for key, value in tokdict.items():
  93. tokdict[key] = re.compile('^(?:%s)$' % value, re.I).match
  94. return tokdict
  95. _compile_regexes(_expand_macros(custom_values,custom_macros))
  96. class ValidationReport(object):
  97. def __init__(self, original_text=''):
  98. self.errors = []
  99. self.original_text = original_text.split('\n') if original_text else ''
  100. def __str__(self):
  101. "only for debugging"
  102. return "Report:\n" + '\n'.join(['\t' + str(x) for x in self.errors])
  103. def append(self,error):
  104. if hasattr(error,'line'):
  105. error.offending_line = self.original_text[error.line-1]
  106. self.errors.append(error)
  107. class ValidationError(Exception):
  108. def __init__(self, message, obj = None, line = None):
  109. self.message = message
  110. if obj is not None:
  111. self.obj = obj
  112. # self.offending_line is the text of the actual line that
  113. # caused the problem; it's set by the ValidationReport that
  114. # owns this ValidationError
  115. if obj is not None and line is None and hasattr(self.obj,'_linetoken'):
  116. (_type1,_type2,self.line,_char) = obj._linetoken
  117. elif line is not None:
  118. self.line = line
  119. def __cmp__(self, other):
  120. if hasattr(self,'line') and not hasattr(other,'line'):
  121. return -1
  122. elif hasattr(other,'line') and not hasattr(self,'line'):
  123. return 1
  124. else:
  125. return cmp(self.line,other.line)
  126. def __str__(self):
  127. "only for debugging"
  128. line = (("(%d)" % self.line)
  129. if hasattr(self,'line') else '')
  130. obj = str(self.obj) if hasattr(self,'obj') else ''
  131. return "ValidationError%s: %s (%s)" % (line, self.message, obj)
  132. # local urls should be in the static directory
  133. local_urls = re.compile(r'^/static/[a-z./-]+$')
  134. # substitutable urls will be css-valid labels surrounded by "%%"
  135. custom_img_urls = re.compile(r'%%([a-zA-Z0-9\-]+)%%')
  136. def valid_url(prop,value,report):
  137. """
  138. checks url(...) arguments in CSS, ensuring that the contents are
  139. officially sanctioned. Sanctioned urls include:
  140. * anything in /static/
  141. * image labels %%..%% for images uploaded on /about/stylesheet
  142. * urls with domains in g.allowed_css_linked_domains
  143. """
  144. url = value.getStringValue()
  145. # local urls are allowed
  146. if local_urls.match(url):
  147. pass
  148. # custom urls are allowed, but need to be transformed into a real path
  149. elif custom_img_urls.match(url):
  150. name = custom_img_urls.match(url).group(1)
  151. # the label -> image number lookup is stored on the subreddit
  152. if c.site.images.has_key(name):
  153. num = c.site.images[name]
  154. value._setCssText("url(http:/%s%s_%d.png?v=%s)"
  155. % (g.s3_thumb_bucket, c.site._fullname, num,
  156. randstr(36)))
  157. else:
  158. # unknown image label -> error
  159. report.append(ValidationError(msgs['broken_url']
  160. % dict(brokenurl = value.cssText),
  161. value))
  162. # allowed domains are ok
  163. elif domain(url) in g.allowed_css_linked_domains:
  164. pass
  165. else:
  166. report.append(ValidationError(msgs['broken_url']
  167. % dict(brokenurl = value.cssText),
  168. value))
  169. #elif sanitize_url(url) != url:
  170. # report.append(ValidationError(msgs['broken_url']
  171. # % dict(brokenurl = value.cssText),
  172. # value))
  173. def valid_value(prop,value,report):
  174. if not (value.valid and value.wellformed):
  175. if (value.wellformed
  176. and prop.name in cssproperties.cssvalues
  177. and cssproperties.cssvalues[prop.name](prop.value)):
  178. # it's actually valid. cssutils bug.
  179. pass
  180. elif (not value.valid
  181. and value.wellformed
  182. and prop.name in custom_values
  183. and custom_values[prop.name](prop.value)):
  184. # we're allowing it via our own custom validator
  185. value.valid = True
  186. # see if this suddenly validates the entire property
  187. prop.valid = True
  188. prop.cssValue.valid = True
  189. if prop.cssValue.cssValueType == CSSValue.CSS_VALUE_LIST:
  190. for i in range(prop.cssValue.length):
  191. if not prop.cssValue.item(i).valid:
  192. prop.cssValue.valid = False
  193. prop.valid = False
  194. break
  195. elif not (prop.name in cssproperties.cssvalues or prop.name in custom_values):
  196. error = (msgs['invalid_property']
  197. % dict(cssprop = prop.name))
  198. report.append(ValidationError(error,value))
  199. else:
  200. error = (msgs['invalid_val_for_prop']
  201. % dict(cssvalue = value.cssText,
  202. cssprop = prop.name))
  203. report.append(ValidationError(error,value))
  204. if value.primitiveType == CSSPrimitiveValue.CSS_URI:
  205. valid_url(prop,value,report)
  206. error_message_extract_re = re.compile('.*\\[([0-9]+):[0-9]*:.*\\]$')
  207. only_whitespace = re.compile('^\s*$')
  208. def validate_css(string):
  209. p = CSSParser(raiseExceptions = True)
  210. if not string or only_whitespace.match(string):
  211. return ('',ValidationReport())
  212. report = ValidationReport(string)
  213. # avoid a very expensive parse
  214. max_size_kb = 100;
  215. if len(string) > max_size_kb * 1024:
  216. report.append(ValidationError((msgs['too_big']
  217. % dict (max_size = max_size_kb))))
  218. return (string, report)
  219. try:
  220. parsed = p.parseString(string)
  221. except DOMException,e:
  222. # yuck; xml.dom.DOMException can't give us line-information
  223. # directly, so we have to parse its error message string to
  224. # get it
  225. line = None
  226. line_match = error_message_extract_re.match(e.message)
  227. if line_match:
  228. line = line_match.group(1)
  229. if line:
  230. line = int(line)
  231. error_message= (msgs['syntax_error']
  232. % dict(syntaxerror = e.message))
  233. report.append(ValidationError(error_message,e,line))
  234. return (None,report)
  235. for rule in parsed.cssRules:
  236. if rule.type == CSSStyleRule.IMPORT_RULE:
  237. report.append(ValidationError(msgs['no_imports'],rule))
  238. elif rule.type == CSSStyleRule.COMMENT:
  239. pass
  240. elif rule.type == CSSStyleRule.STYLE_RULE:
  241. style = rule.style
  242. for prop in style.getProperties():
  243. if prop.cssValue.cssValueType == CSSValue.CSS_VALUE_LIST:
  244. for i in range(prop.cssValue.length):
  245. valid_value(prop,prop.cssValue.item(i),report)
  246. if not (prop.cssValue.valid and prop.cssValue.wellformed):
  247. report.append(ValidationError(msgs['invalid_property_list']
  248. % dict(proplist = prop.cssText),
  249. prop.cssValue))
  250. elif prop.cssValue.cssValueType == CSSValue.CSS_PRIMITIVE_VALUE:
  251. valid_value(prop,prop.cssValue,report)
  252. # cssutils bug: because valid values might be marked
  253. # as invalid, we can't trust cssutils to properly
  254. # label valid properties, so we're going to rely on
  255. # the value validation (which will fail if the
  256. # property is invalid anyway). If this bug is fixed,
  257. # we should uncomment this 'if'
  258. # a property is not valid if any of its values are
  259. # invalid, or if it is itself invalid. To get the
  260. # best-quality error messages, we only report on
  261. # whether the property is valid after we've checked
  262. # the values
  263. #if not (prop.valid and prop.wellformed):
  264. # report.append(ValidationError(Invalid property'),prop))
  265. else:
  266. report.append(ValidationError(msgs['unknown_rule_type']
  267. % dict(ruletype = rule.cssText),
  268. rule))
  269. return parsed,report
  270. def find_preview_comments(sr):
  271. comments = Comment._query(Comment.c.sr_id == c.site._id,
  272. limit=25, data=True)
  273. comments = list(comments)
  274. if not comments:
  275. comments = Comment._query(limit=25, data=True)
  276. comments = list(comments)
  277. return comments
  278. def find_preview_links(sr):
  279. from r2.lib.normalized_hot import get_hot
  280. # try to find a link to use, otherwise give up and return
  281. links = get_hot(c.site)
  282. if not links:
  283. sr = Subreddit._by_name(g.default_sr)
  284. if sr:
  285. links = get_hot(sr)
  286. return links
  287. def rendered_link(id, res, links, media, compress):
  288. from pylons.controllers.util import abort
  289. from r2.controllers import ListingController
  290. try:
  291. render_style = c.render_style
  292. c.render_style = 'html'
  293. with c.user.safe_set_attr:
  294. c.user.pref_compress = compress
  295. c.user.pref_media = media
  296. b = IDBuilder([l._fullname for l in links],
  297. num = 1, wrap = ListingController.builder_wrapper)
  298. l = LinkListing(b, nextprev=False,
  299. show_nums=True).listing().render(style='html')
  300. res._update(id, innerHTML=l)
  301. finally:
  302. c.render_style = render_style
  303. def rendered_comment(id, res, comments):
  304. try:
  305. render_style = c.render_style
  306. c.render_style = 'html'
  307. b = IDBuilder([x._fullname for x in comments],
  308. num = 1)
  309. l = LinkListing(b, nextprev=False,
  310. show_nums=False).listing().render(style='html')
  311. res._update('preview_comment', innerHTML=l)
  312. finally:
  313. c.render_style = render_style
  314. class BadImage(Exception): pass
  315. def clean_image(data,format):
  316. import Image
  317. from StringIO import StringIO
  318. try:
  319. in_file = StringIO(data)
  320. out_file = StringIO()
  321. im = Image.open(in_file)
  322. im = im.resize(im.size)
  323. im.save(out_file,format)
  324. ret = out_file.getvalue()
  325. except IOError,e:
  326. raise BadImage(e)
  327. finally:
  328. out_file.close()
  329. in_file.close()
  330. return ret
  331. def save_sr_image(sr, data, num = None):
  332. """
  333. uploades image data to s3 as a PNG and returns its new url. Urls
  334. will be of the form:
  335. http:/${g.s3_thumb_bucket}/${sr._fullname}[_${num}].png?v=${md5hash}
  336. [Note: g.s3_thumb_bucket begins with a "/" so the above url is valid.]
  337. """
  338. import tempfile
  339. from r2.lib import s3cp
  340. from md5 import md5
  341. hash = md5(data).hexdigest()
  342. try:
  343. f = tempfile.NamedTemporaryFile(suffix = '.png')
  344. f.write(data)
  345. f.flush()
  346. resource = g.s3_thumb_bucket + sr._fullname
  347. if num is not None:
  348. resource += '_' + str(num)
  349. resource += '.png'
  350. s3cp.send_file(f.name, resource, 'image/png', 'public-read',
  351. None, False)
  352. finally:
  353. f.close()
  354. return 'http:/%s%s?v=%s' % (g.s3_thumb_bucket,
  355. resource.split('/')[-1], hash)