| | 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 | |
| | 24 | from r2.models import * |
| | 25 | from r2.lib.utils import sanitize_url, domain |
| | 26 | from r2.lib.strings import string_dict |
| | 27 | |
| | 28 | from pylons import g |
| | 29 | from pylons.i18n import _ |
| | 30 | |
| | 31 | import re |
| | 32 | |
| | 33 | import cssutils |
| | 34 | from cssutils import CSSParser |
| | 35 | from cssutils.css import CSSStyleRule |
| | 36 | from cssutils.css import CSSValue, CSSValueList |
| | 37 | from cssutils.css import CSSPrimitiveValue |
| | 38 | from cssutils.css import cssproperties |
| | 39 | from xml.dom import DOMException |
| | 40 | |
| | 41 | msgs = string_dict['css_validator_messages'] |
| | 42 | |
| | 43 | custom_macros = { |
| | 44 | 'num': r'[-]?\d+|[-]?\d*\.\d+', |
| | 45 | 'percentage': r'{num}%', |
| | 46 | 'length': r'0|{num}(em|ex|px|in|cm|mm|pt|pc)', |
| | 47 | 'color': r'orangered|dimgray|lightgray|whitesmoke|pink', |
| | 48 | } |
| | 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 | |
| | 62 | def _expand_macros(tokdict,macrodict): |
| | 63 | """ Expand macros in token dictionary """ |
| | 64 | def macro_value(m): |
| | 65 | return '(?:%s)' % macrodict[m.groupdict()['macro']] |
| | 66 | for key, value in tokdict.items(): |
| | 67 | while re.search(r'{[a-z][a-z0-9-]*}', value): |
| | 68 | value = re.sub(r'{(?P<macro>[a-z][a-z0-9-]*)}', |
| | 69 | macro_value, value) |
| | 70 | tokdict[key] = value |
| | 71 | return tokdict |
| | 72 | def _compile_regexes(tokdict): |
| | 73 | """ Compile all regular expressions into callable objects """ |
| | 74 | for key, value in tokdict.items(): |
| | 75 | tokdict[key] = re.compile('^(?:%s)$' % value, re.I).match |
| | 76 | return tokdict |
| | 77 | _compile_regexes(_expand_macros(custom_values,custom_macros)) |
| | 78 | |
| | 79 | class ValidationReport(object): |
| | 80 | def __init__(self, original_text=''): |
| | 81 | self.errors = [] |
| | 82 | self.original_text = original_text.split('\n') if original_text else '' |
| | 83 | |
| | 84 | def __str__(self): |
| | 85 | "only for debugging" |
| | 86 | return "Report:\n" + '\n'.join(['\t' + str(x) for x in self.errors]) |
| | 87 | |
| | 88 | def append(self,error): |
| | 89 | if hasattr(error,'line'): |
| | 90 | error.offending_line = self.original_text[error.line-1] |
| | 91 | self.errors.append(error) |
| | 92 | |
| | 93 | class ValidationError(Exception): |
| | 94 | def __init__(self, message, obj = None, line = None): |
| | 95 | self.message = message |
| | 96 | if obj is not None: |
| | 97 | self.obj = obj |
| | 98 | # self.offending_line is the text of the actual line that |
| | 99 | # caused the problem; it's set by the ValidationReport that |
| | 100 | # owns this ValidationError |
| | 101 | |
| | 102 | if obj is not None and line is None and hasattr(self.obj,'_linetoken'): |
| | 103 | (_type1,_type2,self.line,_char) = obj._linetoken |
| | 104 | elif line is not None: |
| | 105 | self.line = line |
| | 106 | |
| | 107 | def __cmp__(self, other): |
| | 108 | if hasattr(self,'line') and not hasattr(other,'line'): |
| | 109 | return -1 |
| | 110 | elif hasattr(other,'line') and not hasattr(self,'line'): |
| | 111 | return 1 |
| | 112 | else: |
| | 113 | return cmp(self.line,other.line) |
| | 114 | |
| | 115 | |
| | 116 | def __str__(self): |
| | 117 | "only for debugging" |
| | 118 | line = (("(%d)" % self.line) |
| | 119 | if hasattr(self,'line') else '') |
| | 120 | obj = str(self.obj) if hasattr(self,'obj') else '' |
| | 121 | return "ValidationError%s: %s (%s)" % (line, self.message, obj) |
| | 122 | |
| | 123 | local_urls = re.compile(r'^/static/[a-z./-]+$') |
| | 124 | def valid_url(prop,value,report): |
| | 125 | url = value.getStringValue() |
| | 126 | if local_urls.match(url): |
| | 127 | pass |
| | 128 | elif domain(url) in g.allowed_css_linked_domains: |
| | 129 | pass |
| | 130 | else: |
| | 131 | report.append(ValidationError(msgs['broken_url'] |
| | 132 | % dict(brokenurl = value.cssText), |
| | 133 | value)) |
| | 134 | #elif sanitize_url(url) != url: |
| | 135 | # report.append(ValidationError(msgs['broken_url'] |
| | 136 | # % dict(brokenurl = value.cssText), |
| | 137 | # value)) |
| | 138 | |
| | 139 | |
| | 140 | def valid_value(prop,value,report): |
| | 141 | if not (value.valid and value.wellformed): |
| | 142 | if (value.wellformed |
| | 143 | and prop.name in cssproperties.cssvalues |
| | 144 | and cssproperties.cssvalues[prop.name](prop.value)): |
| | 145 | # it's actually valid. cssutils bug. |
| | 146 | pass |
| | 147 | elif (not value.valid |
| | 148 | and value.wellformed |
| | 149 | and prop.name in custom_values |
| | 150 | and custom_values[prop.name](prop.value)): |
| | 151 | # we're allowing it via our own custom validator |
| | 152 | value.valid = True |
| | 153 | |
| | 154 | # see if this suddenly validates the entire property |
| | 155 | prop.valid = True |
| | 156 | prop.cssValue.valid = True |
| | 157 | if prop.cssValue.cssValueType == CSSValue.CSS_VALUE_LIST: |
| | 158 | for i in range(prop.cssValue.length): |
| | 159 | if not prop.cssValue.item(i).valid: |
| | 160 | prop.cssValue.valid = False |
| | 161 | prop.valid = False |
| | 162 | break |
| | 163 | elif not (prop.name in cssproperties.cssvalues or prop.name in custom_values): |
| | 164 | error = (msgs['invalid_property'] |
| | 165 | % dict(cssprop = prop.name)) |
| | 166 | report.append(ValidationError(error,value)) |
| | 167 | else: |
| | 168 | error = (msgs['invalid_val_for_prop'] |
| | 169 | % dict(cssvalue = value.cssText, |
| | 170 | cssprop = prop.name)) |
| | 171 | report.append(ValidationError(error,value)) |
| | 172 | |
| | 173 | if value.primitiveType == CSSPrimitiveValue.CSS_URI: |
| | 174 | valid_url(prop,value,report) |
| | 175 | |
| | 176 | error_message_extract_re = re.compile('.*\\[([0-9]+):[0-9]*:.*\\]$') |
| | 177 | only_whitespace = re.compile('^\s*$') |
| | 178 | def validate_css(string): |
| | 179 | p = CSSParser(raiseExceptions = True) |
| | 180 | |
| | 181 | if not string or only_whitespace.match(string): |
| | 182 | return ('',ValidationReport()) |
| | 183 | |
| | 184 | report = ValidationReport(string) |
| | 185 | |
| | 186 | # avoid a very expensive parse |
| | 187 | max_size_kb = 100; |
| | 188 | if len(string) > max_size_kb * 1024: |
| | 189 | report.append(ValidationError((msgs['too_big'] |
| | 190 | % dict (max_size = max_size_kb)))) |
| | 191 | return (string, report) |
| | 192 | |
| | 193 | try: |
| | 194 | parsed = p.parseString(string) |
| | 195 | except DOMException,e: |
| | 196 | # yuck; xml.dom.DOMException can't give us line-information |
| | 197 | # directly, so we have to parse its error message string to |
| | 198 | # get it |
| | 199 | line = None |
| | 200 | line_match = error_message_extract_re.match(e.message) |
| | 201 | if line_match: |
| | 202 | line = line_match.group(1) |
| | 203 | if line: |
| | 204 | line = int(line) |
| | 205 | error_message= (msgs['syntax_error'] |
| | 206 | % dict(syntaxerror = e.message)) |
| | 207 | report.append(ValidationError(error_message,e,line)) |
| | 208 | return (None,report) |
| | 209 | |
| | 210 | for rule in parsed.cssRules: |
| | 211 | if rule.type == CSSStyleRule.IMPORT_RULE: |
| | 212 | report.append(ValidationError(msgs['no_imports'],rule)) |
| | 213 | elif rule.type == CSSStyleRule.COMMENT: |
| | 214 | pass |
| | 215 | elif rule.type == CSSStyleRule.STYLE_RULE: |
| | 216 | style = rule.style |
| | 217 | for prop in style.getProperties(): |
| | 218 | |
| | 219 | if prop.cssValue.cssValueType == CSSValue.CSS_VALUE_LIST: |
| | 220 | for i in range(prop.cssValue.length): |
| | 221 | valid_value(prop,prop.cssValue.item(i),report) |
| | 222 | if not (prop.cssValue.valid and prop.cssValue.wellformed): |
| | 223 | report.append(ValidationError(msgs['invalid_property_list'] |
| | 224 | % dict(proplist = prop.cssText), |
| | 225 | prop.cssValue)) |
| | 226 | elif prop.cssValue.cssValueType == CSSValue.CSS_PRIMITIVE_VALUE: |
| | 227 | valid_value(prop,prop.cssValue,report) |
| | 228 | |
| | 229 | # cssutils bug: because valid values might be marked |
| | 230 | # as invalid, we can't trust cssutils to properly |
| | 231 | # label valid properties, so we're going to rely on |
| | 232 | # the value validation (which will fail if the |
| | 233 | # property is invalid anyway). If this bug is fixed, |
| | 234 | # we should uncomment this 'if' |
| | 235 | |
| | 236 | # a property is not valid if any of its values are |
| | 237 | # invalid, or if it is itself invalid. To get the |
| | 238 | # best-quality error messages, we only report on |
| | 239 | # whether the property is valid after we've checked |
| | 240 | # the values |
| | 241 | #if not (prop.valid and prop.wellformed): |
| | 242 | # report.append(ValidationError(_('invalid property'),prop)) |
| | 243 | |
| | 244 | else: |
| | 245 | report.append(ValidationError(msgs['unknown_rule_type'] |
| | 246 | % dict(ruletype = rule.cssText), |
| | 247 | rule)) |
| | 248 | |
| | 249 | return parsed,report |
| | 250 | |
| | 251 | def builder_wrapper(thing): |
| | 252 | if c.user.pref_compress and isinstance(thing, Link): |
| | 253 | thing.__class__ = LinkCompressed |
| | 254 | thing.score_fmt = Score.points |
| | 255 | return Wrapped(thing) |
| | 256 | |
| | 257 | def find_preview_comments(sr): |
| | 258 | comments = Comment._query(Comment.c.sr_id == c.site._id, |
| | 259 | limit=25, data=True) |
| | 260 | comments = list(comments) |
| | 261 | if not comments: |
| | 262 | comments = Comment._query(limit=25, data=True) |
| | 263 | comments = list(comments) |
| | 264 | |
| | 265 | return comments |
| | 266 | |
| | 267 | def find_preview_links(sr): |
| | 268 | from r2.lib.normalized_hot import get_hot |
| | 269 | |
| | 270 | # try to find a link to use, otherwise give up and return |
| | 271 | links = get_hot(c.site) |
| | 272 | if not links: |
| | 273 | sr = Subreddit._by_name(g.default_sr) |
| | 274 | if sr: |
| | 275 | links = get_hot(sr) |
| | 276 | |
| | 277 | return links |
| | 278 | |
| | 279 | def rendered_link(id, res, links, media, compress): |
| | 280 | from pylons.controllers.util import abort |
| | 281 | |
| | 282 | try: |
| | 283 | render_style = c.render_style |
| | 284 | |
| | 285 | c.render_style = 'html' |
| | 286 | |
| | 287 | with c.user.safe_set_attr: |
| | 288 | c.user.pref_compress = compress |
| | 289 | c.user.pref_media = media |
| | 290 | |
| | 291 | b = IDBuilder([l._fullname for l in links], |
| | 292 | num = 1, wrap = builder_wrapper) |
| | 293 | l = LinkListing(b, nextprev=False, |
| | 294 | show_nums=True).listing().render(style='html') |
| | 295 | res._update(id, innerHTML=l) |
| | 296 | |
| | 297 | finally: |
| | 298 | c.render_style = render_style |
| | 299 | |
| | 300 | def rendered_comment(id, res, comments): |
| | 301 | try: |
| | 302 | render_style = c.render_style |
| | 303 | |
| | 304 | c.render_style = 'html' |
| | 305 | |
| | 306 | b = IDBuilder([x._fullname for x in comments], |
| | 307 | num = 1) |
| | 308 | l = LinkListing(b, nextprev=False, |
| | 309 | show_nums=False).listing().render(style='html') |
| | 310 | res._update('preview_comment', innerHTML=l) |
| | 311 | |
| | 312 | finally: |
| | 313 | c.render_style = render_style |
| | 314 | |
| | 315 | class BadImage(Exception): pass |
| | 316 | |
| | 317 | def clean_image(data,format): |
| | 318 | import Image |
| | 319 | from StringIO import StringIO |
| | 320 | |
| | 321 | try: |
| | 322 | in_file = StringIO(data) |
| | 323 | out_file = StringIO() |
| | 324 | |
| | 325 | im = Image.open(in_file) |
| | 326 | im = im.resize(im.size) |
| | 327 | |
| | 328 | im.save(out_file,format) |
| | 329 | ret = out_file.getvalue() |
| | 330 | except IOError,e: |
| | 331 | raise BadImage(e) |
| | 332 | finally: |
| | 333 | out_file.close() |
| | 334 | in_file.close() |
| | 335 | |
| | 336 | return ret |
| | 337 | |
| | 338 | def save_header_image(sr, data): |
| | 339 | import tempfile |
| | 340 | from r2.lib import s3cp |
| | 341 | from md5 import md5 |
| | 342 | |
| | 343 | hash = md5(data).hexdigest() |
| | 344 | |
| | 345 | try: |
| | 346 | f = tempfile.NamedTemporaryFile(suffix = '.png') |
| | 347 | f.write(data) |
| | 348 | f.flush() |
| | 349 | |
| | 350 | resource = g.s3_thumb_bucket + sr._fullname + '.png' |
| | 351 | s3cp.send_file(f.name, resource, 'image/png', 'public-read', None, False) |
| | 352 | finally: |
| | 353 | f.close() |
| | 354 | |
| | 355 | return 'http:/%s%s.png?v=%s' % (g.s3_thumb_bucket, sr._fullname, hash) |
| | 356 | |
| | 357 | |
| | 358 | |
| | 359 | |