1a107601fa490ca69215facceebe3d0dc5e7f1f86bcef0037bdb2d139a15065c3f31e872bae79df3
 
 
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