~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/request.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mfrom: (0.9.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080622211713-fpo2zrq3s5dfecxg
Tags: 1.7.0-3
Simplify /etc/moin/wikilist format: "USER URL" (drop unneeded middle
CONFIG_DIR that was wrongly advertised as DATA_DIR).  Make
moin-mass-migrate handle both formats and warn about deprecation of
the old one.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: iso-8859-1 -*-
2
 
"""
3
 
    MoinMoin - Data associated with a single Request
4
 
 
5
 
    @copyright: 2001-2003 by J�rgen Hermann <jh@web.de>
6
 
    @copyright: 2003-2006 by Thomas Waldmann
7
 
    @license: GNU GPL, see COPYING for details.
8
 
"""
9
 
 
10
 
import os, re, time, sys, cgi, StringIO
11
 
import copy
12
 
from MoinMoin import config, wikiutil, user, caching
13
 
from MoinMoin import error, multiconfig
14
 
from MoinMoin.util import MoinMoinNoFooter, IsWin9x
15
 
 
16
 
# umask setting --------------------------------------------------------
17
 
def set_umask(new_mask=0777^config.umask):
18
 
    """ Set the OS umask value (and ignore potential failures on OSes where
19
 
        this is not supported).
20
 
        Default: the bitwise inverted value of config.umask
21
 
    """
22
 
    try:
23
 
        old_mask = os.umask(new_mask)
24
 
    except:
25
 
        # maybe we are on win32?
26
 
        pass
27
 
 
28
 
# We do this at least once per Python process, when request is imported.
29
 
# If other software parts (like twistd's daemonize() function) set an
30
 
# unwanted umask, we have to call this again to set the correct one:
31
 
set_umask()
32
 
 
33
 
# Timing ---------------------------------------------------------------
34
 
 
35
 
class Clock:
36
 
    """ Helper class for code profiling
37
 
        we do not use time.clock() as this does not work across threads
38
 
    """
39
 
 
40
 
    def __init__(self):
41
 
        self.timings = {}
42
 
        self.states = {}
43
 
 
44
 
    def start(self, timer):
45
 
        state = self.states.setdefault(timer, 'new')
46
 
        if state == 'new':
47
 
            self.timings[timer] = time.time()
48
 
            self.states[timer] = 'running'
49
 
        elif state == 'running':
50
 
            pass # this timer is already running, do nothing
51
 
        elif state == 'stopped':
52
 
            # if a timer is stopped, timings has the sum of all times it was running
53
 
            self.timings[timer] = time.time() - self.timings[timer]
54
 
            self.states[timer] = 'running'
55
 
 
56
 
    def stop(self, timer):
57
 
        state = self.states.setdefault(timer, 'neverstarted')
58
 
        if state == 'running':
59
 
            self.timings[timer] = time.time() - self.timings[timer]
60
 
            self.states[timer] = 'stopped'
61
 
        elif state == 'stopped':
62
 
            pass # this timer already has been stopped, do nothing
63
 
        elif state == 'neverstarted':
64
 
            pass # this timer never has been started, do nothing
65
 
 
66
 
    def value(self, timer):
67
 
        state = self.states.setdefault(timer, 'nosuchtimer')
68
 
        if state == 'stopped':
69
 
            result = "%.3fs" % self.timings[timer]
70
 
        elif state == 'running':
71
 
            result = "%.3fs (still running)" % (time.time() - self.timings[timer])
72
 
        else:
73
 
            result = "- (%s)" % state
74
 
        return result
75
 
 
76
 
    def dump(self):
77
 
        outlist = []
78
 
        for timer in self.timings.keys():
79
 
            value = self.value(timer)
80
 
            outlist.append("%s = %s" % (timer, value))
81
 
        outlist.sort()
82
 
        return outlist
83
 
 
84
 
 
85
 
# Utilities
86
 
 
87
 
def cgiMetaVariable(header, scheme='http'):
88
 
    """ Return CGI meta variable for header name
89
 
    
90
 
    e.g 'User-Agent' -> 'HTTP_USER_AGENT'    
91
 
    See http://www.faqs.org/rfcs/rfc3875.html section 4.1.18
92
 
    """
93
 
    var = '%s_%s' % (scheme, header)
94
 
    return var.upper().replace('-', '_')
95
 
    
96
 
 
97
 
# Request Base ----------------------------------------------------------
98
 
 
99
 
class RequestBase(object):
100
 
    """ A collection for all data associated with ONE request. """
101
 
 
102
 
    # Defaults (used by sub classes)
103
 
    http_accept_language = 'en'
104
 
    server_name = 'localhost'
105
 
    server_port = '80'
106
 
 
107
 
    # Extra headers we support. Both standalone and twisted store
108
 
    # headers as lowercase.
109
 
    moin_location = 'x-moin-location'
110
 
    proxy_host = 'x-forwarded-host'
111
 
    
112
 
    def __init__(self, properties={}):
113
 
 
114
 
        # twistd's daemonize() overrides our umask, so we reset it here every
115
 
        # request. we do it for all request types to avoid similar problems.
116
 
        set_umask()
117
 
 
118
 
        # Decode values collected by sub classes
119
 
        self.path_info = self.decodePagename(self.path_info)
120
 
 
121
 
        self.opened_logs = 0
122
 
        self.failed = 0
123
 
        self._available_actions = None
124
 
        self._known_actions = None
125
 
 
126
 
        # Pages meta data that we collect in one request
127
 
        self.pages = {}
128
 
              
129
 
        self.sent_headers = 0
130
 
        self.user_headers = []
131
 
        self.cacheable = 0 # may this output get cached by http proxies/caches?
132
 
        self.http_caching_disabled = 0 # see disableHttpCaching()
133
 
        self.page = None
134
 
        self._dicts = None
135
 
        
136
 
        # Fix dircaching problems on Windows 9x
137
 
        if IsWin9x():
138
 
            import dircache
139
 
            dircache.reset()
140
 
 
141
 
        # Check for dumb proxy requests
142
 
        # TODO relying on request_uri will not work on all servers, especially
143
 
        # not on external non-Apache servers
144
 
        self.forbidden = False
145
 
        if self.request_uri.startswith('http://'):
146
 
            self.makeForbidden403()
147
 
 
148
 
        # Init
149
 
        else:
150
 
            self.writestack = []
151
 
            self.clock = Clock()
152
 
            self.clock.start('total')
153
 
            # order is important here!
154
 
            self.__dict__.update(properties)
155
 
            try:
156
 
                self._load_multi_cfg()
157
 
            except error.NoConfigMatchedError:
158
 
                self.makeForbidden(404, 'No wiki configuration matching the URL found!\r\n')
159
 
                return
160
 
 
161
 
            self.open_logs()
162
 
 
163
 
            self.isSpiderAgent = self.check_spider()
164
 
        
165
 
            # Set decode charsets.  Input from the user is always in
166
 
            # config.charset, which is the page charsets. Except
167
 
            # path_info, which may use utf-8, and handled by decodePagename.
168
 
            self.decode_charsets = [config.charset]
169
 
            
170
 
            # hierarchical wiki - set rootpage
171
 
            from MoinMoin.Page import Page
172
 
            #path = self.getPathinfo()
173
 
            #if path.startswith('/'):
174
 
            #    pages = path[1:].split('/')
175
 
            #    if 0: # len(path) > 1:
176
 
            #        ## breaks MainPage/SubPage on flat storage
177
 
            #        rootname = u'/'.join(pages[:-1])
178
 
            #    else:
179
 
            #        # this is the usual case, as it ever was...
180
 
            #        rootname = u""
181
 
            #else:
182
 
            #    # no extra path after script name
183
 
            #    rootname = u""
184
 
 
185
 
            self.args = {}
186
 
            self.form = {}
187
 
 
188
 
            if not self.query_string.startswith('action=xmlrpc'):
189
 
                self.args = self.form = self.setup_args()
190
 
 
191
 
            rootname = u''
192
 
            self.rootpage = Page(self, rootname, is_rootpage=1)
193
 
 
194
 
            self.user = self.get_user_from_form()
195
 
            
196
 
            if not self.query_string.startswith('action=xmlrpc'):
197
 
                if not self.forbidden and self.isForbidden():
198
 
                    self.makeForbidden403()
199
 
                if not self.forbidden and self.surge_protect():
200
 
                    self.makeUnavailable503()
201
 
 
202
 
            self.logger = None
203
 
            self.pragma = {}
204
 
            self.mode_getpagelinks = 0 # is > 0 as long as we are in a getPageLinks call
205
 
            self.parsePageLinks_running = {} # avoid infinite recursion by remembering what we are already running
206
 
 
207
 
            from MoinMoin import i18n
208
 
            self.i18n = i18n
209
 
            self.lang = i18n.requestLanguage(self) 
210
 
            # Language for content. Page content should use the wiki default lang,
211
 
            # but generated content like search results should use the user language.
212
 
            self.content_lang = self.cfg.language_default
213
 
            self.getText = lambda text, i18n=self.i18n, request=self, lang=self.lang, **kv: i18n.getText(text, request, lang, kv.get('formatted', True))
214
 
 
215
 
            self.reset()
216
 
        
217
 
    def surge_protect(self, kick_him=False):
218
 
        """ check if someone requesting too much from us,
219
 
            if kick_him is True, we unconditionally blacklist the current user/ip
220
 
        """
221
 
        limits = self.cfg.surge_action_limits
222
 
        if not limits:
223
 
            return False
224
 
        validuser = self.user.valid
225
 
        current_id = validuser and self.user.name or self.remote_addr
226
 
        if not validuser and current_id.startswith('127.'): # localnet
227
 
            return False
228
 
        current_action = self.form.get('action', ['show'])[0]
229
 
        
230
 
        default_limit = self.cfg.surge_action_limits.get('default', (30, 60))
231
 
        
232
 
        now = int(time.time())
233
 
        surgedict = {}
234
 
        surge_detected = False
235
 
        
236
 
        try:
237
 
            cache = caching.CacheEntry(self, 'surgeprotect', 'surge-log')
238
 
            if cache.exists():
239
 
                data = cache.content(decode=True)
240
 
                data = data.split("\n")
241
 
                for line in data:
242
 
                    try:
243
 
                        id, t, action, surge_indicator = line.split("\t")
244
 
                        t = int(t)
245
 
                        maxnum, dt = limits.get(action, default_limit)
246
 
                        if t >= now - dt:
247
 
                            events = surgedict.setdefault(id, copy.copy({}))
248
 
                            timestamps = events.setdefault(action, copy.copy([]))
249
 
                            timestamps.append((t, surge_indicator))
250
 
                    except StandardError, err:
251
 
                        pass
252
 
                
253
 
            maxnum, dt = limits.get(current_action, default_limit)
254
 
            events = surgedict.setdefault(current_id, copy.copy({}))
255
 
            timestamps = events.setdefault(current_action, copy.copy([]))
256
 
            surge_detected = len(timestamps) > maxnum
257
 
 
258
 
            surge_indicator = surge_detected and "!" or ""
259
 
            timestamps.append((now, surge_indicator))
260
 
            if surge_detected:
261
 
                if len(timestamps) < maxnum * 2:
262
 
                    timestamps.append((now + self.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
263
 
        
264
 
            if current_action != 'AttachFile': # don't add AttachFile accesses to all or picture galleries will trigger SP
265
 
                current_action = 'all' # put a total limit on user's requests
266
 
                maxnum, dt = limits.get(current_action, default_limit)
267
 
                events = surgedict.setdefault(current_id, copy.copy({}))
268
 
                timestamps = events.setdefault(current_action, copy.copy([]))
269
 
 
270
 
                if kick_him: # ban this guy, NOW
271
 
                    timestamps.extend([(now + self.cfg.surge_lockout_time, "!")] * (2*maxnum))
272
 
 
273
 
                surge_detected = surge_detected or len(timestamps) > maxnum
274
 
            
275
 
                surge_indicator = surge_detected and "!" or ""
276
 
                timestamps.append((now, surge_indicator))
277
 
                if surge_detected:
278
 
                    if len(timestamps) < maxnum * 2:
279
 
                        timestamps.append((now + self.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
280
 
        
281
 
            data = []
282
 
            for id, events in surgedict.items():
283
 
                for action, timestamps in events.items():
284
 
                    for t, surge_indicator in timestamps:
285
 
                        data.append("%s\t%d\t%s\t%s" % (id, t, action, surge_indicator))
286
 
            data = "\n".join(data)
287
 
            cache.update(data, encode=True)
288
 
        except StandardError, err:
289
 
            self.log('%s: %s' % (err.__class__.__name__, str(err)))
290
 
 
291
 
        return surge_detected   
292
 
        
293
 
    def getDicts(self):
294
 
        """ Lazy initialize the dicts on the first access """
295
 
        if self._dicts is None:
296
 
            from MoinMoin import wikidicts
297
 
            dicts = wikidicts.GroupDict(self)
298
 
            dicts.scandicts()
299
 
            self._dicts = dicts
300
 
        return self._dicts
301
 
        
302
 
    def delDicts(self):
303
 
        """ Delete the dicts, used by some tests """
304
 
        del self._dicts
305
 
        self._dicts = None
306
 
 
307
 
    dicts = property(getDicts, None, delDicts)
308
 
  
309
 
    def _load_multi_cfg(self):
310
 
        # protect against calling multiple times
311
 
        if not hasattr(self, 'cfg'):
312
 
            self.clock.start('load_multi_cfg')
313
 
            self.cfg = multiconfig.getConfig(self.url)
314
 
            self.clock.stop('load_multi_cfg')
315
 
            
316
 
    def setAcceptedCharsets(self, accept_charset):
317
 
        """ Set accepted_charsets by parsing accept-charset header
318
 
 
319
 
        Set self.accepted_charsets to an ordered list based on http_accept_charset. 
320
 
        
321
 
        Reference: http://www.w3.org/Protocols/rfc2616/rfc2616.txt
322
 
 
323
 
        TODO: currently no code use this value.
324
 
 
325
 
        @param accept_charset: accept-charset header
326
 
        """        
327
 
        charsets = []
328
 
        if accept_charset:
329
 
            accept_charset = accept_charset.lower()
330
 
            # Add iso-8859-1 if needed
331
 
            if (not '*' in accept_charset and
332
 
                accept_charset.find('iso-8859-1') < 0):
333
 
                accept_charset += ',iso-8859-1'
334
 
 
335
 
            # Make a list, sorted by quality value, using Schwartzian Transform
336
 
            # Create list of tuples (value, name) , sort, extract names  
337
 
            for item in accept_charset.split(','):
338
 
                if ';' in item:
339
 
                    name, qval = item.split(';')
340
 
                    qval = 1.0 - float(qval.split('=')[1])
341
 
                else:
342
 
                    name, qval = item, 0
343
 
                charsets.append((qval, name))                 
344
 
            charsets.sort()
345
 
            # Remove *, its not clear what we should do with it later
346
 
            charsets = [name for qval, name in charsets if name != '*']
347
 
 
348
 
        self.accepted_charsets = charsets
349
 
          
350
 
    def _setup_vars_from_std_env(self, env):
351
 
        """ Set common request variables from CGI environment
352
 
        
353
 
        Parse a standard CGI environment as created by common web servers.
354
 
        Reference: http://www.faqs.org/rfcs/rfc3875.html
355
 
 
356
 
        @param env: dict like object containing cgi meta variables
357
 
        """
358
 
        # Values we can just copy
359
 
        self.env = env
360
 
        self.http_accept_language = env.get('HTTP_ACCEPT_LANGUAGE', self.http_accept_language)
361
 
        self.server_name = env.get('SERVER_NAME', self.server_name)
362
 
        self.server_port = env.get('SERVER_PORT', self.server_port)
363
 
        self.saved_cookie = env.get('HTTP_COOKIE', '')
364
 
        self.script_name = env.get('SCRIPT_NAME', '')
365
 
        self.path_info = env.get('PATH_INFO', '')
366
 
        self.query_string = env.get('QUERY_STRING', '')
367
 
        self.request_method = env.get('REQUEST_METHOD', None)
368
 
        self.remote_addr = env.get('REMOTE_ADDR', '')
369
 
        self.http_user_agent = env.get('HTTP_USER_AGENT', '')
370
 
        self.if_modified_since = env.get('If-modified-since') or env.get(cgiMetaVariable('If-modified-since'))
371
 
        self.if_none_match = env.get('If-none-match') or env.get(cgiMetaVariable('If-none-match'))
372
 
 
373
 
        # REQUEST_URI is not part of CGI spec, but an addition of Apache.
374
 
        self.request_uri = env.get('REQUEST_URI', '')
375
 
        
376
 
        # Values that need more work
377
 
        self.setHttpReferer(env.get('HTTP_REFERER'))
378
 
        self.setIsSSL(env)
379
 
        self.setHost(env.get('HTTP_HOST'))
380
 
        self.fixURI(env)
381
 
        self.setURL(env)
382
 
        
383
 
        ##self.debugEnvironment(env)
384
 
 
385
 
    def setHttpReferer(self, referer):
386
 
        """ Set http_referer, making sure its ascii
387
 
        
388
 
        IE might send non-ascii value.
389
 
        """
390
 
        value = ''
391
 
        if referer:
392
 
            value = unicode(referer, 'ascii', 'replace')
393
 
            value = value.encode('ascii', 'replace')
394
 
        self.http_referer = value
395
 
 
396
 
    def setIsSSL(self, env):
397
 
        """ Set is_ssl 
398
 
        
399
 
        @param env: dict like object containing cgi meta variables
400
 
        """
401
 
        self.is_ssl = bool(env.get('SSL_PROTOCOL') or
402
 
                           env.get('SSL_PROTOCOL_VERSION') or
403
 
                           env.get('HTTPS') == 'on')
404
 
 
405
 
    def setHost(self, host=None):
406
 
        """ Set http_host 
407
 
        
408
 
        Create from server name and port if missing. Previous code
409
 
        default to localhost.
410
 
        """
411
 
        if not host:
412
 
            port = ''
413
 
            standardPort = ('80', '443')[self.is_ssl]
414
 
            if self.server_port != standardPort:
415
 
                port = ':' + self.server_port
416
 
            host = self.server_name + port
417
 
        self.http_host = host
418
 
        
419
 
    def fixURI(self, env):
420
 
        """ Fix problems with script_name and path_info
421
 
        
422
 
        Handle the strange charset semantics on Windows and other non
423
 
        posix systems. path_info is transformed into the system code
424
 
        page by the web server. Additionally, paths containing dots let
425
 
        most webservers choke.
426
 
        
427
 
        Broken environment variables in different environments:
428
 
                path_info script_name
429
 
        Apache1     X          X      PI does not contain dots
430
 
        Apache2     X          X      PI is not encoded correctly
431
 
        IIS         X          X      path_info include script_name
432
 
        Other       ?          -      ? := Possible and even RFC-compatible.
433
 
                                      - := Hopefully not.
434
 
 
435
 
        @param env: dict like object containing cgi meta variables
436
 
        """ 
437
 
        # Fix the script_name when using Apache on Windows.
438
 
        server_software = env.get('SERVER_SOFTWARE', '')
439
 
        if os.name == 'nt' and server_software.find('Apache/') != -1:
440
 
            # Removes elements ending in '.' from the path.
441
 
            self.script_name = '/'.join([x for x in self.script_name.split('/') 
442
 
                                         if not x.endswith('.')])
443
 
 
444
 
        # Fix path_info
445
 
        if os.name != 'posix' and self.request_uri != '':
446
 
            # Try to recreate path_info from request_uri.
447
 
            import urlparse
448
 
            scriptAndPath = urlparse.urlparse(self.request_uri)[2]
449
 
            path = scriptAndPath.replace(self.script_name, '', 1)            
450
 
            self.path_info = wikiutil.url_unquote(path, want_unicode=False)
451
 
        elif os.name == 'nt':
452
 
            # Recode path_info to utf-8
453
 
            path = wikiutil.decodeWindowsPath(self.path_info)
454
 
            self.path_info = path.encode("utf-8")
455
 
            
456
 
            # Fix bug in IIS/4.0 when path_info contain script_name
457
 
            if self.path_info.startswith(self.script_name):
458
 
                self.path_info = self.path_info[len(self.script_name):]
459
 
 
460
 
    def setURL(self, env):
461
 
        """ Set url, used to locate wiki config 
462
 
        
463
 
        This is the place to manipulate url parts as needed.
464
 
        
465
 
        @param env: dict like object containing cgi meta variables or http headers.
466
 
        """
467
 
        # If we serve on localhost:8000 and use a proxy on
468
 
        # example.com/wiki, our urls will be example.com/wiki/pagename
469
 
        # Same for the wiki config - they must use the proxy url.
470
 
        self.rewriteHost(env)
471
 
        self.rewriteURI(env)
472
 
        
473
 
        if not self.request_uri:
474
 
            self.request_uri = self.makeURI()
475
 
        self.url = self.http_host + self.request_uri
476
 
 
477
 
    def rewriteHost(self, env):
478
 
        """ Rewrite http_host transparently
479
 
        
480
 
        Get the proxy host using 'X-Forwarded-Host' header, added by
481
 
        Apache 2 and other proxy software.
482
 
        
483
 
        TODO: Will not work for Apache 1 or others that don't add this header.
484
 
        
485
 
        TODO: If we want to add an option to disable this feature it
486
 
        should be in the server script, because the config is not
487
 
        loaded at this point, and must be loaded after url is set.
488
 
        
489
 
        @param env: dict like object containing cgi meta variables or http headers.
490
 
        """
491
 
        proxy_host = (env.get(self.proxy_host) or
492
 
                      env.get(cgiMetaVariable(self.proxy_host)))
493
 
        if proxy_host:
494
 
            self.http_host = proxy_host
495
 
 
496
 
    def rewriteURI(self, env):
497
 
        """ Rewrite request_uri, script_name and path_info transparently
498
 
        
499
 
        Useful when running mod python or when running behind a proxy,
500
 
        e.g run on localhost:8000/ and serve as example.com/wiki/.
501
 
 
502
 
        Uses private 'X-Moin-Location' header to set the script name.
503
 
        This allow setting the script name when using Apache 2
504
 
        <location> directive::
505
 
 
506
 
            <Location /my/wiki/>
507
 
                RequestHeader set X-Moin-Location /my/wiki/
508
 
            </location>
509
 
        
510
 
        TODO: does not work for Apache 1 and others that do not allow
511
 
        setting custom headers per request.
512
 
        
513
 
        @param env: dict like object containing cgi meta variables or http headers.
514
 
        """
515
 
        location = (env.get(self.moin_location) or 
516
 
                    env.get(cgiMetaVariable(self.moin_location)))
517
 
        if location is None:
518
 
            return
519
 
        
520
 
        scriptAndPath = self.script_name + self.path_info
521
 
        location = location.rstrip('/')
522
 
        self.script_name = location
523
 
        
524
 
        # This may happen when using mod_python
525
 
        if scriptAndPath.startswith(location):
526
 
            self.path_info = scriptAndPath[len(location):]
527
 
 
528
 
        # Recreate the URI from the modified parts
529
 
        if self.request_uri:
530
 
            self.request_uri = self.makeURI()
531
 
 
532
 
    def makeURI(self):
533
 
        """ Return uri created from uri parts """
534
 
        uri = self.script_name + wikiutil.url_quote(self.path_info)
535
 
        if self.query_string:
536
 
            uri += '?' + self.query_string
537
 
        return uri
538
 
 
539
 
    def splitURI(self, uri):
540
 
        """ Return path and query splited from uri
541
 
        
542
 
        Just like CGI environment, the path is unquoted, the query is not.
543
 
        """
544
 
        if '?' in uri:
545
 
            path, query = uri.split('?', 1)
546
 
        else:
547
 
            path, query = uri, ''
548
 
        return wikiutil.url_unquote(path, want_unicode=False), query        
549
 
 
550
 
    def get_user_from_form(self):
551
 
        """ read the maybe present UserPreferences form and call get_user with the values """
552
 
        name = self.form.get('name', [None])[0]
553
 
        password = self.form.get('password', [None])[0]
554
 
        login = self.form.has_key('login')
555
 
        logout = self.form.has_key('logout')
556
 
        u = self.get_user_default_unknown(name=name, password=password,
557
 
                                          login=login, logout=logout,
558
 
                                          user_obj=None)
559
 
        return u
560
 
    
561
 
    def get_user_default_unknown(self, **kw):
562
 
        """ call do_auth and if it doesnt return a user object, make some "Unknown User" """
563
 
        user_obj = self.get_user_default_None(**kw)
564
 
        if user_obj is None:
565
 
            user_obj = user.User(self, auth_method="request:427")
566
 
        return user_obj
567
 
 
568
 
    def get_user_default_None(self, **kw):
569
 
        """ loop over auth handlers, return a user obj or None """
570
 
        name = kw.get('name')
571
 
        password = kw.get('password')
572
 
        login = kw.get('login')
573
 
        logout = kw.get('logout')
574
 
        user_obj = kw.get('user_obj')
575
 
        for auth in self.cfg.auth:
576
 
            user_obj, continue_flag = auth(self, name=name, password=password,
577
 
                                           login=login, logout=logout, user_obj=user_obj)
578
 
            if not continue_flag:
579
 
                break
580
 
        return user_obj
581
 
        
582
 
    def reset(self):
583
 
        """ Reset request state.
584
 
 
585
 
        Called after saving a page, before serving the updated
586
 
        page. Solves some practical problems with request state
587
 
        modified during saving.
588
 
 
589
 
        """
590
 
        # This is the content language and has nothing to do with
591
 
        # The user interface language. The content language can change
592
 
        # during the rendering of a page by lang macros
593
 
        self.current_lang = self.cfg.language_default
594
 
 
595
 
        self._all_pages = None
596
 
        # caches unique ids
597
 
        self._page_ids = {}
598
 
        # keeps track of pagename/heading combinations
599
 
        # parsers should use this dict and not a local one, so that
600
 
        # macros like TableOfContents in combination with Include can work
601
 
        self._page_headings = {}
602
 
 
603
 
        if hasattr(self, "_fmt_hd_counters"):
604
 
            del self._fmt_hd_counters
605
 
 
606
 
    def loadTheme(self, theme_name):
607
 
        """ Load the Theme to use for this request.
608
 
 
609
 
        @param theme_name: the name of the theme
610
 
        @type theme_name: str
611
 
        @rtype: int
612
 
        @return: success code
613
 
                 0 on success
614
 
                 1 if user theme could not be loaded,
615
 
                 2 if a hard fallback to modern theme was required.
616
 
        """
617
 
        fallback = 0
618
 
        if theme_name == "<default>":
619
 
            theme_name = self.cfg.theme_default
620
 
        
621
 
        try:
622
 
            Theme = wikiutil.importPlugin(self.cfg, 'theme', theme_name, 'Theme')
623
 
        except wikiutil.PluginMissingError:
624
 
            fallback = 1
625
 
            try:
626
 
                Theme = wikiutil.importPlugin(self.cfg, 'theme', self.cfg.theme_default, 'Theme')
627
 
            except wikiutil.PluginMissingError:
628
 
                fallback = 2
629
 
                from MoinMoin.theme.modern import Theme
630
 
        
631
 
        self.theme = Theme(self)
632
 
        return fallback
633
 
 
634
 
    def setContentLanguage(self, lang):
635
 
        """ Set the content language, used for the content div
636
 
 
637
 
        Actions that generate content in the user language, like search,
638
 
        should set the content direction to the user language before they
639
 
        call send_title!
640
 
        """
641
 
        self.content_lang = lang
642
 
        self.current_lang = lang
643
 
 
644
 
    def getPragma(self, key, defval=None):
645
 
        """ Query a pragma value (#pragma processing instruction)
646
 
 
647
 
            Keys are not case-sensitive.
648
 
        """
649
 
        return self.pragma.get(key.lower(), defval)
650
 
 
651
 
    def setPragma(self, key, value):
652
 
        """ Set a pragma value (#pragma processing instruction)
653
 
 
654
 
            Keys are not case-sensitive.
655
 
        """
656
 
        self.pragma[key.lower()] = value
657
 
 
658
 
    def getPathinfo(self):
659
 
        """ Return the remaining part of the URL. """
660
 
        return self.path_info
661
 
 
662
 
    def getScriptname(self):
663
 
        """ Return the scriptname part of the URL ('/path/to/my.cgi'). """
664
 
        if self.script_name == '/':
665
 
            return ''
666
 
        return self.script_name
667
 
 
668
 
    def getKnownActions(self):
669
 
        """ Create a dict of avaiable actions
670
 
 
671
 
        Return cached version if avaiable.
672
 
       
673
 
        @rtype: dict
674
 
        @return: dict of all known actions
675
 
        """
676
 
        try:
677
 
            self.cfg._known_actions # check
678
 
        except AttributeError:
679
 
            from MoinMoin import wikiaction
680
 
            # Add built in  actions from wikiaction
681
 
            actions = [name[3:] for name in wikiaction.__dict__ if name.startswith('do_')]
682
 
 
683
 
            # Add plugins           
684
 
            dummy, plugins = wikiaction.getPlugins(self)
685
 
            actions.extend(plugins)
686
 
 
687
 
            # Add extensions
688
 
            from MoinMoin.action import extension_actions
689
 
            actions.extend(extension_actions)           
690
 
           
691
 
            # TODO: Use set when we require Python 2.3
692
 
            actions = dict(zip(actions, [''] * len(actions)))            
693
 
            self.cfg._known_actions = actions
694
 
 
695
 
        # Return a copy, so clients will not change the dict.
696
 
        return self.cfg._known_actions.copy()        
697
 
 
698
 
    def getAvailableActions(self, page):
699
 
        """ Get list of avaiable actions for this request
700
 
 
701
 
        The dict does not contain actions that starts with lower case.
702
 
        Themes use this dict to display the actions to the user.
703
 
 
704
 
        @param page: current page, Page object
705
 
        @rtype: dict
706
 
        @return: dict of avaiable actions
707
 
        """
708
 
        if self._available_actions is None:
709
 
            # Add actions for existing pages only, including deleted pages.
710
 
            # Fix *OnNonExistingPage bugs.
711
 
            if not (page.exists(includeDeleted=1) and self.user.may.read(page.page_name)):
712
 
                return []
713
 
 
714
 
            # Filter non ui actions (starts with lower case letter)
715
 
            actions = self.getKnownActions()
716
 
            for key in actions.keys():
717
 
                if key[0].islower():
718
 
                    del actions[key]
719
 
 
720
 
            # Filter wiki excluded actions
721
 
            for key in self.cfg.actions_excluded:
722
 
                if key in actions:
723
 
                    del actions[key]                
724
 
 
725
 
            # Filter actions by page type, acl and user state
726
 
            excluded = []
727
 
            if ((page.isUnderlayPage() and not page.isStandardPage()) or
728
 
                not self.user.may.write(page.page_name) or
729
 
                not self.user.may.delete(page.page_name)):
730
 
                # Prevent modification of underlay only pages, or pages
731
 
                # the user can't write and can't delete
732
 
                excluded = [u'RenamePage', u'DeletePage', ] # AttachFile must NOT be here!
733
 
            for key in excluded:
734
 
                if key in actions:
735
 
                    del actions[key]                
736
 
 
737
 
            self._available_actions = actions
738
 
 
739
 
        # Return a copy, so clients will not change the dict.
740
 
        return self._available_actions.copy()
741
 
 
742
 
    def redirectedOutput(self, function, *args, **kw):
743
 
        """ Redirect output during function, return redirected output """
744
 
        buffer = StringIO.StringIO()
745
 
        self.redirect(buffer)
746
 
        try:
747
 
            function(*args, **kw)
748
 
        finally:
749
 
            self.redirect()
750
 
        text = buffer.getvalue()
751
 
        buffer.close()        
752
 
        return text
753
 
 
754
 
    def redirect(self, file=None):
755
 
        """ Redirect output to file, or restore saved output """
756
 
        if file:
757
 
            self.writestack.append(self.write)
758
 
            self.write = file.write
759
 
        else:
760
 
            self.write = self.writestack.pop()
761
 
 
762
 
    def reset_output(self):
763
 
        """ restore default output method
764
 
            destroy output stack
765
 
            (useful for error messages)
766
 
        """
767
 
        if self.writestack:
768
 
            self.write = self.writestack[0]
769
 
            self.writestack = []
770
 
 
771
 
    def log(self, msg):
772
 
        """ Log to stderr, which may be error.log """
773
 
        msg = msg.strip()
774
 
        # Encode unicode msg
775
 
        if isinstance(msg, unicode):
776
 
            msg = msg.encode(config.charset)
777
 
        # Add time stamp
778
 
        msg = '[%s] %s\n' % (time.asctime(), msg)
779
 
        sys.stderr.write(msg)
780
 
 
781
 
    def timing_log(self, start, action):
782
 
        """ Log to timing log (for performance analysis) """
783
 
        indicator = ''
784
 
        if start:
785
 
            total = "vvv"
786
 
        else:
787
 
            self.clock.stop('total') # make sure it is stopped
788
 
            total_secs = self.clock.timings['total']
789
 
            # we add some stuff that is easy to grep when searching for peformance problems:
790
 
            if total_secs > 50:
791
 
                indicator += '!4!'
792
 
            elif total_secs > 20:
793
 
               indicator += '!3!'
794
 
            elif total_secs > 10:
795
 
                indicator += '!2!'
796
 
            elif total_secs > 2:
797
 
                indicator += '!1!'
798
 
            total = self.clock.value('total')
799
 
            # use + for existing pages, - for non-existing pages
800
 
            indicator += self.page.exists() and '+' or '-'
801
 
            if self.isSpiderAgent:
802
 
                indicator += "B"
803
 
 
804
 
        # Add time stamp and process ID
805
 
        pid = os.getpid()
806
 
        t = time.time()
807
 
        timestr = time.strftime("%Y%m%d %H%M%S", time.gmtime(t))
808
 
        msg = '%s %5d %-6s %4s %-10s %s\n' % (timestr, pid, total, indicator, action, self.url)
809
 
        self.timing_logfile.write(msg)
810
 
        self.timing_logfile.flush()
811
 
 
812
 
    def write(self, *data):
813
 
        """ Write to output stream.
814
 
        """
815
 
        raise NotImplementedError
816
 
 
817
 
    def encode(self, data):
818
 
        """ encode data (can be both unicode strings and strings),
819
 
            preparing for a single write()
820
 
        """
821
 
        wd = []
822
 
        for d in data:
823
 
            try:
824
 
                if isinstance(d, unicode):
825
 
                    # if we are REALLY sure, we can use "strict"
826
 
                    d = d.encode(config.charset, 'replace')
827
 
                elif d is None:
828
 
                    continue
829
 
                wd.append(d)
830
 
            except UnicodeError:
831
 
                print >>sys.stderr, "Unicode error on: %s" % repr(d)
832
 
        return ''.join(wd)
833
 
 
834
 
    def decodePagename(self, name):
835
 
        """ Decode path, possibly using non ascii characters
836
 
 
837
 
        Does not change the name, only decode to Unicode.
838
 
 
839
 
        First split the path to pages, then decode each one. This enables
840
 
        us to decode one page using config.charset and another using
841
 
        utf-8. This situation happens when you try to add to a name of
842
 
        an existing page.
843
 
 
844
 
        See http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.2.1
845
 
        
846
 
        @param name: page name, string
847
 
        @rtype: unicode
848
 
        @return decoded page name
849
 
        """
850
 
        # Split to pages and decode each one
851
 
        pages = name.split('/')
852
 
        decoded = []
853
 
        for page in pages:
854
 
            # Recode from utf-8 into config charset. If the path
855
 
            # contains user typed parts, they are encoded using 'utf-8'.
856
 
            if config.charset != 'utf-8':
857
 
                try:
858
 
                    page = unicode(page, 'utf-8', 'strict')
859
 
                    # Fit data into config.charset, replacing what won't
860
 
                    # fit. Better have few "?" in the name than crash.
861
 
                    page = page.encode(config.charset, 'replace')
862
 
                except UnicodeError:
863
 
                    pass
864
 
                
865
 
            # Decode from config.charset, replacing what can't be decoded.
866
 
            page = unicode(page, config.charset, 'replace')
867
 
            decoded.append(page)
868
 
 
869
 
        # Assemble decoded parts
870
 
        name = u'/'.join(decoded)
871
 
        return name
872
 
 
873
 
    def normalizePagename(self, name):
874
 
        """ Normalize page name 
875
 
 
876
 
        Convert '_' to spaces - allows using nice URLs with spaces, with no
877
 
        need to quote.
878
 
 
879
 
        Prevent creating page names with invisible characters or funny
880
 
        whitespace that might confuse the users or abuse the wiki, or
881
 
        just does not make sense.
882
 
 
883
 
        Restrict even more group pages, so they can be used inside acl lines.
884
 
        
885
 
        @param name: page name, unicode
886
 
        @rtype: unicode
887
 
        @return: decoded and sanitized page name
888
 
        """
889
 
        # Replace underscores with spaces
890
 
        name = name.replace(u'_', u' ')
891
 
 
892
 
        # Strip invalid characters
893
 
        name = config.page_invalid_chars_regex.sub(u'', name)
894
 
 
895
 
        # Split to pages and normalize each one
896
 
        pages = name.split(u'/')
897
 
        normalized = []
898
 
        for page in pages:            
899
 
            # Ignore empty or whitespace only pages
900
 
            if not page or page.isspace():
901
 
                continue
902
 
 
903
 
            # Cleanup group pages.
904
 
            # Strip non alpha numeric characters, keep white space
905
 
            if wikiutil.isGroupPage(self, page):
906
 
                page = u''.join([c for c in page
907
 
                                 if c.isalnum() or c.isspace()])
908
 
 
909
 
            # Normalize white space. Each name can contain multiple 
910
 
            # words separated with only one space. Split handle all
911
 
            # 30 unicode spaces (isspace() == True)
912
 
            page = u' '.join(page.split())
913
 
            
914
 
            normalized.append(page)            
915
 
        
916
 
        # Assemble components into full pagename
917
 
        name = u'/'.join(normalized)
918
 
        return name
919
 
        
920
 
    def read(self, n):
921
 
        """ Read n bytes from input stream.
922
 
        """
923
 
        raise NotImplementedError
924
 
 
925
 
    def flush(self):
926
 
        """ Flush output stream.
927
 
        """
928
 
        raise NotImplementedError
929
 
 
930
 
    def check_spider(self):
931
 
        """ check if the user agent for current request is a spider/bot """
932
 
        isSpider = False
933
 
        spiders = self.cfg.ua_spiders
934
 
        if spiders:
935
 
            ua = self.getUserAgent()
936
 
            if ua:
937
 
                isSpider = re.search(spiders, ua, re.I) is not None
938
 
        return isSpider
939
 
 
940
 
    def isForbidden(self):
941
 
        """ check for web spiders and refuse anything except viewing """
942
 
        forbidden = 0
943
 
        # we do not have a parsed query string here, so we can just do simple matching
944
 
        qs = self.query_string
945
 
        if ((qs != '' or self.request_method != 'GET') and
946
 
            not 'action=rss_rc' in qs and
947
 
            # allow spiders to get attachments and do 'show'
948
 
            not ('action=AttachFile' in qs and 'do=get' in qs) and
949
 
            not 'action=show' in qs and
950
 
            not 'action=sitemap' in qs
951
 
            ):
952
 
            forbidden = self.isSpiderAgent
953
 
 
954
 
        if not forbidden and self.cfg.hosts_deny:
955
 
            ip = self.remote_addr
956
 
            for host in self.cfg.hosts_deny:
957
 
                if host[-1] == '.' and ip.startswith(host):
958
 
                    forbidden = 1
959
 
                    #self.log("hosts_deny (net): %s" % str(forbidden))
960
 
                    break
961
 
                if ip == host:
962
 
                    forbidden = 1
963
 
                    #self.log("hosts_deny (ip): %s" % str(forbidden))
964
 
                    break
965
 
        return forbidden
966
 
 
967
 
    def setup_args(self, form=None):
968
 
        """ Return args dict 
969
 
        
970
 
        In POST request, invoke _setup_args_from_cgi_form to handle possible
971
 
        file uploads. For other request simply parse the query string.
972
 
        
973
 
        Warning: calling with a form might fail, depending on the type of the
974
 
        request! Only the request know which kind of form it can handle.
975
 
        
976
 
        TODO: The form argument should be removed in 1.5.
977
 
        """
978
 
        if form is not None or self.request_method == 'POST':
979
 
            return self._setup_args_from_cgi_form(form)
980
 
        args = cgi.parse_qs(self.query_string, keep_blank_values=1)
981
 
        return self.decodeArgs(args)
982
 
 
983
 
    def _setup_args_from_cgi_form(self, form=None):
984
 
        """ Return args dict from a FieldStorage
985
 
        
986
 
        Create the args from a standard cgi.FieldStorage or from given form.
987
 
        Each key contain a list of values.
988
 
 
989
 
        @param form: a cgi.FieldStorage
990
 
        @rtype: dict
991
 
        @return: dict with form keys, each contains a list of values
992
 
        """
993
 
        if form is None:
994
 
            form = cgi.FieldStorage()
995
 
 
996
 
        args = {}
997
 
        for key in form:
998
 
            values = form[key]
999
 
            if not isinstance(values, list):
1000
 
                values = [values]
1001
 
            fixedResult = []
1002
 
            for item in values:
1003
 
                fixedResult.append(item.value)
1004
 
                if isinstance(item, cgi.FieldStorage) and item.filename:
1005
 
                    # Save upload file name in a separate key
1006
 
                    args[key + '__filename__'] = item.filename            
1007
 
            args[key] = fixedResult
1008
 
            
1009
 
        return self.decodeArgs(args)
1010
 
 
1011
 
    def decodeArgs(self, args):
1012
 
        """ Decode args dict 
1013
 
        
1014
 
        Decoding is done in a separate path because it is reused by
1015
 
        other methods and sub classes.
1016
 
        """
1017
 
        decode = wikiutil.decodeUserInput
1018
 
        result = {}
1019
 
        for key in args:
1020
 
            if key + '__filename__' in args:
1021
 
                # Copy file data as is
1022
 
                result[key] = args[key]
1023
 
            elif key.endswith('__filename__'):
1024
 
                result[key] = decode(args[key], self.decode_charsets)
1025
 
            else:
1026
 
                result[key] = [decode(value, self.decode_charsets) for value in args[key]]
1027
 
        return result
1028
 
 
1029
 
    def getBaseURL(self):
1030
 
        """ Return a fully qualified URL to this script. """
1031
 
        return self.getQualifiedURL(self.getScriptname())
1032
 
 
1033
 
    def getQualifiedURL(self, uri=''):
1034
 
        """ Return an absolute URL starting with schema and host.
1035
 
 
1036
 
        Already qualified urls are returned unchanged.
1037
 
 
1038
 
        @param uri: server rooted uri e.g /scriptname/pagename.
1039
 
                    It must start with a slash. Must be ascii and url encoded.
1040
 
        """
1041
 
        import urlparse
1042
 
        scheme = urlparse.urlparse(uri)[0]
1043
 
        if scheme:
1044
 
            return uri
1045
 
 
1046
 
        scheme = ('http', 'https')[self.is_ssl]
1047
 
        result = "%s://%s%s" % (scheme, self.http_host, uri)
1048
 
 
1049
 
        # This might break qualified urls in redirects!
1050
 
        # e.g. mapping 'http://netloc' -> '/'
1051
 
        return wikiutil.mapURL(self, result)
1052
 
 
1053
 
    def getUserAgent(self):
1054
 
        """ Get the user agent. """
1055
 
        return self.http_user_agent
1056
 
 
1057
 
    def makeForbidden(self, resultcode, msg):
1058
 
        statusmsg = {
1059
 
            403: 'FORBIDDEN',
1060
 
            404: 'Not found',
1061
 
            503: 'Service unavailable',
1062
 
        }
1063
 
        self.http_headers([
1064
 
            'Status: %d %s' % (resultcode, statusmsg[resultcode]),
1065
 
            'Content-Type: text/plain'
1066
 
        ])
1067
 
        self.write(msg)
1068
 
        self.setResponseCode(resultcode)
1069
 
        self.forbidden = True
1070
 
 
1071
 
    def makeForbidden403(self):
1072
 
        self.makeForbidden(403, 'You are not allowed to access this!\r\n')
1073
 
 
1074
 
    def makeUnavailable503(self):
1075
 
        self.makeForbidden(503, "Warning:\r\n"
1076
 
                   "You triggered the wiki's surge protection by doing too many requests in a short time.\r\n"
1077
 
                   "Please make a short break reading the stuff you already got.\r\n"
1078
 
                   "When you restart doing requests AFTER that, slow down or you might get locked out for a longer time!\r\n")
1079
 
 
1080
 
    def initTheme(self):
1081
 
        """ Set theme - forced theme, user theme or wiki default """
1082
 
        if self.cfg.theme_force:
1083
 
            theme_name = self.cfg.theme_default
1084
 
        else:
1085
 
            theme_name = self.user.theme_name
1086
 
        self.loadTheme(theme_name)
1087
 
        
1088
 
    def run(self):
1089
 
        # Exit now if __init__ failed or request is forbidden
1090
 
        if self.failed or self.forbidden:
1091
 
            # Don't sleep() here, it binds too much of our resources!
1092
 
            return self.finish()
1093
 
 
1094
 
        _ = self.getText
1095
 
        self.clock.start('run')
1096
 
 
1097
 
        from MoinMoin.Page import Page
1098
 
 
1099
 
        if self.query_string == 'action=xmlrpc':
1100
 
            from MoinMoin.wikirpc import xmlrpc
1101
 
            xmlrpc(self)
1102
 
            return self.finish()
1103
 
        
1104
 
        if self.query_string == 'action=xmlrpc2':
1105
 
            from MoinMoin.wikirpc import xmlrpc2
1106
 
            xmlrpc2(self)
1107
 
            return self.finish()
1108
 
 
1109
 
        # parse request data
1110
 
        try:
1111
 
            self.initTheme()
1112
 
            
1113
 
            action = self.form.get('action', [None])[0]
1114
 
 
1115
 
            if self.cfg.log_timing:
1116
 
                self.timing_log(True, action)
1117
 
 
1118
 
            # The last component in path_info is the page name, if any
1119
 
            path = self.getPathinfo()
1120
 
            if path.startswith('/'):
1121
 
                pagename = self.normalizePagename(path)
1122
 
            else:
1123
 
                pagename = None
1124
 
 
1125
 
            # need to inform caches that content changes based on:
1126
 
            # * cookie (even if we aren't sending one now)
1127
 
            # * User-Agent (because a bot might be denied and get no content)
1128
 
            # * Accept-Language (except if moin is told to ignore browser language)
1129
 
            if self.cfg.language_ignore_browser:
1130
 
                self.setHttpHeader("Vary: Cookie,User-Agent")
1131
 
            else:
1132
 
                self.setHttpHeader("Vary: Cookie,User-Agent,Accept-Language")
1133
 
 
1134
 
            # Handle request. We have these options:
1135
 
            
1136
 
            # 1. If user has a bad user name, delete its bad cookie and
1137
 
            # send him to UserPreferences to make a new account.
1138
 
            if not user.isValidName(self, self.user.name):
1139
 
                msg = _("""Invalid user name {{{'%s'}}}.
1140
 
Name may contain any Unicode alpha numeric character, with optional one
1141
 
space between words. Group page name is not allowed.""") % self.user.name
1142
 
                self.user = self.get_user_default_unknown(name=self.user.name, logout=True)
1143
 
                page = wikiutil.getSysPage(self, 'UserPreferences')
1144
 
                page.send_page(self, msg=msg)
1145
 
 
1146
 
            # 2. Or jump to page where user left off
1147
 
            elif not pagename and not action and self.user.remember_last_visit:
1148
 
                pagetrail = self.user.getTrail()
1149
 
                if pagetrail:
1150
 
                    # Redirect to last page visited
1151
 
                    if ":" in pagetrail[-1]:
1152
 
                        wikitag, wikiurl, wikitail, error = wikiutil.resolve_wiki(self, pagetrail[-1]) 
1153
 
                        url = wikiurl + wikiutil.quoteWikinameURL(wikitail)
1154
 
                    else:
1155
 
                        url = Page(self, pagetrail[-1]).url(self)
1156
 
                else:
1157
 
                    # Or to localized FrontPage
1158
 
                    url = wikiutil.getFrontPage(self).url(self)
1159
 
                self.http_redirect(url)
1160
 
                return self.finish()
1161
 
            
1162
 
            # 3. Or save drawing
1163
 
            elif self.form.has_key('filepath') and self.form.has_key('noredirect'):
1164
 
                # looks like user wants to save a drawing
1165
 
                from MoinMoin.action.AttachFile import execute
1166
 
                # TODO: what if pagename is None?
1167
 
                execute(pagename, self)
1168
 
                raise MoinMoinNoFooter           
1169
 
 
1170
 
            # 4. Or handle action
1171
 
            else:
1172
 
                if action is None:
1173
 
                    action = 'show'
1174
 
                # pagename could be empty after normalization e.g. '///' -> ''
1175
 
                # Use localized FrontPage if pagename is empty
1176
 
                if not pagename:
1177
 
                    self.page = wikiutil.getFrontPage(self)
1178
 
                else:
1179
 
                    self.page = Page(self, pagename)
1180
 
 
1181
 
                # Complain about unknown actions
1182
 
                if not action in self.getKnownActions():
1183
 
                    self.http_headers()
1184
 
                    self.write(u'<html><body><h1>Unknown action %s</h1></body>' % wikiutil.escape(action))
1185
 
 
1186
 
                # Disallow non available actions
1187
 
                elif action[0].isupper() and not action in self.getAvailableActions(self.page):
1188
 
                    # Send page with error
1189
 
                    msg = _("You are not allowed to do %s on this page.") % wikiutil.escape(action)
1190
 
                    if not self.user.valid:
1191
 
                        # Suggest non valid user to login
1192
 
                        msg += " " + _("Login and try again.", formatted=0)
1193
 
                    self.page.send_page(self, msg=msg)
1194
 
 
1195
 
                # Try action
1196
 
                else:
1197
 
                    from MoinMoin.wikiaction import getHandler
1198
 
                    handler = getHandler(self, action)
1199
 
                    if handler is None:
1200
 
                        # Send page with error
1201
 
                        msg = _("You are not allowed to do %s on this page.") % wikiutil.escape(action)
1202
 
                        if not self.user.valid:
1203
 
                            # Suggest non valid user to login
1204
 
                            msg += " " + _("Login and try again.", formatted=0)
1205
 
                        self.page.send_page(self, msg=msg)
1206
 
                    else:
1207
 
                        handler(self.page.page_name, self)
1208
 
 
1209
 
            # generate page footer (actions that do not want this footer use
1210
 
            # raise util.MoinMoinNoFooter to break out of the default execution
1211
 
            # path, see the "except MoinMoinNoFooter" below)
1212
 
 
1213
 
            self.clock.stop('run')
1214
 
            self.clock.stop('total')
1215
 
 
1216
 
            # Close html code
1217
 
            if self.cfg.show_timings and action != 'print':
1218
 
                self.write('<ul id="timings">\n')
1219
 
                for t in self.clock.dump():
1220
 
                    self.write('<li>%s</li>\n' % t)
1221
 
                self.write('</ul>\n')
1222
 
            #self.write('<!-- auth_method == %s -->' % repr(self.user.auth_method))
1223
 
            self.write('</body>\n</html>\n\n')
1224
 
            
1225
 
        except MoinMoinNoFooter:
1226
 
            pass
1227
 
        except Exception, err:
1228
 
            self.fail(err)
1229
 
 
1230
 
        if self.cfg.log_timing:
1231
 
            self.timing_log(False, action)
1232
 
 
1233
 
        return self.finish()
1234
 
 
1235
 
    def http_redirect(self, url):
1236
 
        """ Redirect to a fully qualified, or server-rooted URL
1237
 
        
1238
 
        @param url: relative or absolute url, ascii using url encoding.
1239
 
        """
1240
 
        url = self.getQualifiedURL(url)
1241
 
        self.http_headers(["Status: 302 Found", "Location: %s" % url])
1242
 
        self.setResponseCode(302)
1243
 
 
1244
 
    def setHttpHeader(self, header):
1245
 
        """ Save header for later send.
1246
 
        
1247
 
            Attention: although we use a list here, some implementations use a dict,
1248
 
            thus multiple calls with the same header type do NOT work in the end!
1249
 
        """
1250
 
        self.user_headers.append(header)
1251
 
 
1252
 
    def setResponseCode(self, code, message=None):
1253
 
        pass
1254
 
 
1255
 
    def fail(self, err):
1256
 
        """ Fail when we can't continue
1257
 
 
1258
 
        Send 500 status code with the error name. Reference: 
1259
 
        http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
1260
 
 
1261
 
        Log the error, then let failure module handle it. 
1262
 
 
1263
 
        @param err: Exception instance or subclass.
1264
 
        """
1265
 
        self.failed = 1 # save state for self.run()            
1266
 
        self.http_headers(['Status: 500 MoinMoin Internal Error'])
1267
 
        self.setResponseCode(500)
1268
 
        self.log('%s: %s' % (err.__class__.__name__, str(err)))        
1269
 
        from MoinMoin import failure
1270
 
        failure.handle(self)             
1271
 
 
1272
 
    def open_logs(self):
1273
 
        pass
1274
 
 
1275
 
    def makeUniqueID(self, base):
1276
 
        """
1277
 
        Generates a unique ID using a given base name. Appends a running count to the base.
1278
 
 
1279
 
        @param base: the base of the id
1280
 
        @type base: unicode
1281
 
 
1282
 
        @returns: an unique id
1283
 
        @rtype: unicode
1284
 
        """
1285
 
        if not isinstance(base, unicode):
1286
 
            base = unicode(str(base), 'ascii', 'ignore')
1287
 
        count = self._page_ids.get(base, -1) + 1
1288
 
        self._page_ids[base] = count
1289
 
        if count == 0:
1290
 
            return base
1291
 
        return u'%s_%04d' % (base, count)
1292
 
 
1293
 
    def httpDate(self, when=None, rfc='1123'):
1294
 
        """ Returns http date string, according to rfc2068
1295
 
 
1296
 
        See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-3.3
1297
 
 
1298
 
        A http 1.1 server should use only rfc1123 date, but cookie's
1299
 
        "expires" field should use the older obsolete rfc850 date.
1300
 
 
1301
 
        Note: we can not use strftime() because that honors the locale
1302
 
        and rfc2822 requires english day and month names.
1303
 
 
1304
 
        We can not use email.Utils.formatdate because it formats the
1305
 
        zone as '-0000' instead of 'GMT', and creates only rfc1123
1306
 
        dates. This is a modified version of email.Utils.formatdate
1307
 
        from Python 2.4.
1308
 
 
1309
 
        @param when: seconds from epoch, as returned by time.time()
1310
 
        @param rfc: conform to rfc ('1123' or '850')
1311
 
        @rtype: string
1312
 
        @return: http date conforming to rfc1123 or rfc850
1313
 
        """
1314
 
        if when is None:
1315
 
            when = time.time()
1316
 
        now = time.gmtime(when)
1317
 
        month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
1318
 
                 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.tm_mon - 1]
1319
 
        if rfc == '1123':
1320
 
            day = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now.tm_wday]
1321
 
            date = '%02d %s %04d' % (now.tm_mday, month, now.tm_year)
1322
 
        elif rfc == '850':
1323
 
            day = ["Monday", "Tuesday", "Wednesday", "Thursday",
1324
 
                    "Friday", "Saturday", "Sunday"][now.tm_wday]
1325
 
            date = '%02d-%s-%s' % (now.tm_mday, month, str(now.tm_year)[-2:])
1326
 
        else:
1327
 
            raise ValueError("Invalid rfc value: %s" % rfc)
1328
 
        
1329
 
        return '%s, %s %02d:%02d:%02d GMT' % (day, date, now.tm_hour,
1330
 
                                              now.tm_min, now.tm_sec)
1331
 
 
1332
 
    def disableHttpCaching(self, level=1):
1333
 
        """ Prevent caching of pages that should not be cached.
1334
 
 
1335
 
        level == 1 means disabling caching when we have a cookie set
1336
 
        level == 2 means completely disabling caching (used by Page*Editor)
1337
 
 
1338
 
        This is important to prevent caches break acl by providing one
1339
 
        user pages meant to be seen only by another user, when both users
1340
 
        share the same caching proxy.
1341
 
 
1342
 
        AVOID using no-cache and no-store for attachments as it is completely broken on IE!
1343
 
        
1344
 
        Details: http://support.microsoft.com/support/kb/articles/Q234/0/67.ASP
1345
 
        """
1346
 
        if level <= self.http_caching_disabled:
1347
 
            return # only make caching stricter
1348
 
 
1349
 
        if level == 1:
1350
 
            # Set Cache control header for http 1.1 caches
1351
 
            # See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.3
1352
 
            # and http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.9
1353
 
            #self.setHttpHeader('Cache-Control: no-cache="set-cookie", private, max-age=0')
1354
 
            self.setHttpHeader('Cache-Control: private, must-revalidate, max-age=10')
1355
 
        elif level == 2:
1356
 
            self.setHttpHeader('Cache-Control: no-cache')
1357
 
 
1358
 
        # Set Expires for http 1.0 caches (does not support Cache-Control)
1359
 
        when = time.time() - (3600 * 24 * 365)
1360
 
        self.setHttpHeader('Expires: %s' % self.httpDate(when=when))
1361
 
 
1362
 
        # Set Pragma for http 1.0 caches
1363
 
        # See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.32
1364
 
        # DISABLED for level == 1 to fix IE https file attachment downloading trouble.
1365
 
        if level == 2:
1366
 
            self.setHttpHeader('Pragma: no-cache')
1367
 
 
1368
 
        self.http_caching_disabled = level
1369
 
 
1370
 
    def finish(self):
1371
 
        """ General cleanup on end of request
1372
 
        
1373
 
        Delete circular references - all object that we create using self.name = class(self).
1374
 
        This helps Python to collect these objects and keep our memory footprint lower.
1375
 
        """
1376
 
        try:
1377
 
            del self.user
1378
 
            del self.theme
1379
 
            del self.dicts
1380
 
        except:
1381
 
            pass
1382
 
 
1383
 
    # Debug ------------------------------------------------------------
1384
 
 
1385
 
    def debugEnvironment(self, env):
1386
 
        """ Environment debugging aid """
1387
 
        # Keep this one name per line so its easy to comment stuff
1388
 
        names = [
1389
 
#             'http_accept_language',
1390
 
#             'http_host',
1391
 
#             'http_referer',
1392
 
#             'http_user_agent',
1393
 
#             'is_ssl',
1394
 
            'path_info',
1395
 
            'query_string',
1396
 
#             'remote_addr',
1397
 
            'request_method',
1398
 
#             'request_uri',
1399
 
#             'saved_cookie',
1400
 
            'script_name',
1401
 
#             'server_name',
1402
 
#             'server_port',
1403
 
            ]
1404
 
        names.sort()
1405
 
        attributes = []
1406
 
        for name in names:
1407
 
            attributes.append('  %s = %r\n' % (name, getattr(self, name, None)))
1408
 
        attributes = ''.join(attributes)
1409
 
        
1410
 
        environment = []
1411
 
        names = env.keys()
1412
 
        names.sort()
1413
 
        for key in names:
1414
 
            environment.append('  %s = %r\n' % (key, env[key]))
1415
 
        environment = ''.join(environment)
1416
 
        
1417
 
        data = '\nRequest Attributes\n%s\nEnviroment\n%s' % (attributes, environment)        
1418
 
        f = open('/tmp/env.log', 'a')
1419
 
        try:
1420
 
            f.write(data)
1421
 
        finally:
1422
 
            f.close()
1423
 
  
1424
 
 
1425
 
# CGI ---------------------------------------------------------------
1426
 
 
1427
 
class RequestCGI(RequestBase):
1428
 
    """ specialized on CGI requests """
1429
 
 
1430
 
    def __init__(self, properties={}):
1431
 
        try:
1432
 
            # force input/output to binary
1433
 
            if sys.platform == "win32":
1434
 
                import msvcrt
1435
 
                msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
1436
 
                msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1437
 
 
1438
 
            self._setup_vars_from_std_env(os.environ)
1439
 
            RequestBase.__init__(self, properties)
1440
 
 
1441
 
        except Exception, err:
1442
 
            self.fail(err)
1443
 
            
1444
 
    def open_logs(self):
1445
 
        # create log file for catching stderr output
1446
 
        if not self.opened_logs:
1447
 
            sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at', 0)
1448
 
            self.opened_logs = 1
1449
 
 
1450
 
    def read(self, n=None):
1451
 
        """ Read from input stream. """
1452
 
        if n is None:
1453
 
            return sys.stdin.read()
1454
 
        else:
1455
 
            return sys.stdin.read(n)
1456
 
 
1457
 
    def write(self, *data):
1458
 
        """ Write to output stream. """
1459
 
        sys.stdout.write(self.encode(data))
1460
 
 
1461
 
    def flush(self):
1462
 
        sys.stdout.flush()
1463
 
        
1464
 
    def finish(self):
1465
 
        RequestBase.finish(self)
1466
 
        # flush the output, ignore errors caused by the user closing the socket
1467
 
        try:
1468
 
            sys.stdout.flush()
1469
 
        except IOError, ex:
1470
 
            import errno
1471
 
            if ex.errno != errno.EPIPE: raise
1472
 
 
1473
 
    # Headers ----------------------------------------------------------
1474
 
    
1475
 
    def http_headers(self, more_headers=[]):
1476
 
        # Send only once
1477
 
        if getattr(self, 'sent_headers', None):
1478
 
            return
1479
 
        
1480
 
        self.sent_headers = 1
1481
 
        have_ct = 0
1482
 
 
1483
 
        # send http headers
1484
 
        for header in more_headers + getattr(self, 'user_headers', []):
1485
 
            if header.lower().startswith("content-type:"):
1486
 
                # don't send content-type multiple times!
1487
 
                if have_ct: continue
1488
 
                have_ct = 1
1489
 
            if type(header) is unicode:
1490
 
                header = header.encode('ascii')
1491
 
            self.write("%s\r\n" % header)
1492
 
 
1493
 
        if not have_ct:
1494
 
            self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
1495
 
 
1496
 
        self.write('\r\n')
1497
 
 
1498
 
        #from pprint import pformat
1499
 
        #sys.stderr.write(pformat(more_headers))
1500
 
        #sys.stderr.write(pformat(self.user_headers))
1501
 
 
1502
 
 
1503
 
# Twisted -----------------------------------------------------------
1504
 
 
1505
 
class RequestTwisted(RequestBase):
1506
 
    """ specialized on Twisted requests """
1507
 
 
1508
 
    def __init__(self, twistedRequest, pagename, reactor, properties={}):
1509
 
        try:
1510
 
            self.twistd = twistedRequest
1511
 
            self.reactor = reactor
1512
 
            
1513
 
            # Copy headers
1514
 
            self.http_accept_language = self.twistd.getHeader('Accept-Language')
1515
 
            self.saved_cookie = self.twistd.getHeader('Cookie')
1516
 
            self.http_user_agent = self.twistd.getHeader('User-Agent')
1517
 
            self.if_modified_since = self.twistd.getHeader('If-Modified-Since')
1518
 
            self.if_none_match = self.twistd.getHeader('If-None-Match')
1519
 
           
1520
 
            # Copy values from twisted request
1521
 
            self.server_protocol = self.twistd.clientproto
1522
 
            self.server_name = self.twistd.getRequestHostname().split(':')[0]
1523
 
            self.server_port = str(self.twistd.getHost()[2])
1524
 
            self.is_ssl = self.twistd.isSecure()
1525
 
            self.path_info = '/' + '/'.join([pagename] + self.twistd.postpath)
1526
 
            self.request_method = self.twistd.method
1527
 
            self.remote_addr = self.twistd.getClientIP()
1528
 
            self.request_uri = self.twistd.uri
1529
 
            self.script_name = "/" + '/'.join(self.twistd.prepath[:-1])
1530
 
 
1531
 
            # Values that need more work
1532
 
            self.query_string = self.splitURI(self.twistd.uri)[1]
1533
 
            self.setHttpReferer(self.twistd.getHeader('Referer'))
1534
 
            self.setHost()
1535
 
            self.setURL(self.twistd.getAllHeaders())
1536
 
 
1537
 
            ##self.debugEnvironment(twistedRequest.getAllHeaders())
1538
 
            
1539
 
            RequestBase.__init__(self, properties)
1540
 
 
1541
 
        except MoinMoinNoFooter: # might be triggered by http_redirect
1542
 
            self.http_headers() # send headers (important for sending MOIN_ID cookie)
1543
 
            self.finish()
1544
 
 
1545
 
        except Exception, err:
1546
 
            # Wrap err inside an internal error if needed
1547
 
            from MoinMoin import error
1548
 
            if isinstance(err, error.FatalError):
1549
 
                self.delayedError = err
1550
 
            else:
1551
 
                self.delayedError = error.InternalError(str(err))
1552
 
 
1553
 
    def run(self):
1554
 
        """ Handle delayed errors then invoke base class run """
1555
 
        if hasattr(self, 'delayedError'):
1556
 
            self.fail(self.delayedError)
1557
 
            return self.finish()
1558
 
        RequestBase.run(self)
1559
 
            
1560
 
    def setup_args(self, form=None):
1561
 
        """ Return args dict 
1562
 
        
1563
 
        Twisted already parsed args, including __filename__ hacking,
1564
 
        but did not decoded the values.
1565
 
        """
1566
 
        return self.decodeArgs(self.twistd.args)
1567
 
        
1568
 
    def read(self, n=None):
1569
 
        """ Read from input stream. """
1570
 
        # XXX why is that wrong?:
1571
 
        #rd = self.reactor.callFromThread(self.twistd.read)
1572
 
        
1573
 
        # XXX do we need self.reactor.callFromThread with that?
1574
 
        # XXX if yes, why doesn't it work?
1575
 
        self.twistd.content.seek(0, 0)
1576
 
        if n is None:
1577
 
            rd = self.twistd.content.read()
1578
 
        else:
1579
 
            rd = self.twistd.content.read(n)
1580
 
        #print "request.RequestTwisted.read: data=\n" + str(rd)
1581
 
        return rd
1582
 
    
1583
 
    def write(self, *data):
1584
 
        """ Write to output stream. """
1585
 
        #print "request.RequestTwisted.write: data=\n" + wd
1586
 
        self.reactor.callFromThread(self.twistd.write, self.encode(data))
1587
 
 
1588
 
    def flush(self):
1589
 
        pass # XXX is there a flush in twisted?
1590
 
 
1591
 
    def finish(self):
1592
 
        RequestBase.finish(self)
1593
 
        self.reactor.callFromThread(self.twistd.finish)
1594
 
 
1595
 
    def open_logs(self):
1596
 
        return
1597
 
        # create log file for catching stderr output
1598
 
        if not self.opened_logs:
1599
 
            sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at', 0)
1600
 
            self.opened_logs = 1
1601
 
 
1602
 
    # Headers ----------------------------------------------------------
1603
 
 
1604
 
    def __setHttpHeader(self, header):
1605
 
        if type(header) is unicode:
1606
 
            header = header.encode('ascii')
1607
 
        key, value = header.split(':', 1)
1608
 
        value = value.lstrip()
1609
 
        if key.lower() == 'set-cookie':
1610
 
            key, value = value.split('=', 1)
1611
 
            self.twistd.addCookie(key, value)
1612
 
        else:
1613
 
            self.twistd.setHeader(key, value)
1614
 
        #print "request.RequestTwisted.setHttpHeader: %s" % header
1615
 
 
1616
 
    def http_headers(self, more_headers=[]):
1617
 
        if getattr(self, 'sent_headers', None):
1618
 
            return
1619
 
        self.sent_headers = 1
1620
 
        have_ct = 0
1621
 
 
1622
 
        # set http headers
1623
 
        for header in more_headers + getattr(self, 'user_headers', []):
1624
 
            if header.lower().startswith("content-type:"):
1625
 
                # don't send content-type multiple times!
1626
 
                if have_ct: continue
1627
 
                have_ct = 1
1628
 
            self.__setHttpHeader(header)
1629
 
 
1630
 
        if not have_ct:
1631
 
            self.__setHttpHeader("Content-type: text/html;charset=%s" % config.charset)
1632
 
 
1633
 
    def http_redirect(self, url):
1634
 
        """ Redirect to a fully qualified, or server-rooted URL 
1635
 
        
1636
 
        @param url: relative or absolute url, ascii using url encoding.
1637
 
        """
1638
 
        url = self.getQualifiedURL(url)
1639
 
        self.twistd.redirect(url)
1640
 
        # calling finish here will send the rest of the data to the next
1641
 
        # request. leave the finish call to run()
1642
 
        #self.twistd.finish()
1643
 
        raise MoinMoinNoFooter
1644
 
 
1645
 
    def setResponseCode(self, code, message=None):
1646
 
        self.twistd.setResponseCode(code, message)
1647
 
        
1648
 
# CLI ------------------------------------------------------------------
1649
 
 
1650
 
class RequestCLI(RequestBase):
1651
 
    """ specialized on command line interface and script requests """
1652
 
 
1653
 
    def __init__(self, url='CLI', pagename='', properties={}):
1654
 
        self.saved_cookie = ''
1655
 
        self.path_info = '/' + pagename
1656
 
        self.query_string = ''
1657
 
        self.remote_addr = '127.0.0.1'
1658
 
        self.is_ssl = 0
1659
 
        self.http_user_agent = 'CLI/Script'
1660
 
        self.url = url
1661
 
        self.request_method = 'GET'
1662
 
        self.request_uri = '/' + pagename # TODO check
1663
 
        self.http_host = 'localhost'
1664
 
        self.http_referer = ''
1665
 
        self.script_name = '.'
1666
 
        self.if_modified_since = None
1667
 
        self.if_none_match = None
1668
 
        RequestBase.__init__(self, properties)
1669
 
        self.cfg.caching_formats = [] # don't spoil the cache
1670
 
        self.initTheme() # usually request.run() does this, but we don't use it
1671
 
  
1672
 
    def read(self, n=None):
1673
 
        """ Read from input stream. """
1674
 
        if n is None:
1675
 
            return sys.stdin.read()
1676
 
        else:
1677
 
            return sys.stdin.read(n)
1678
 
 
1679
 
    def write(self, *data):
1680
 
        """ Write to output stream. """
1681
 
        sys.stdout.write(self.encode(data))
1682
 
 
1683
 
    def flush(self):
1684
 
        sys.stdout.flush()
1685
 
        
1686
 
    def finish(self):
1687
 
        RequestBase.finish(self)
1688
 
        # flush the output, ignore errors caused by the user closing the socket
1689
 
        try:
1690
 
            sys.stdout.flush()
1691
 
        except IOError, ex:
1692
 
            import errno
1693
 
            if ex.errno != errno.EPIPE: raise
1694
 
 
1695
 
    def isForbidden(self):
1696
 
        """ Nothing is forbidden """
1697
 
        return 0
1698
 
 
1699
 
    # Accessors --------------------------------------------------------
1700
 
 
1701
 
    def getQualifiedURL(self, uri=None):
1702
 
        """ Return a full URL starting with schema and host
1703
 
        
1704
 
        TODO: does this create correct pages when you render wiki pages
1705
 
              within a cli request?!
1706
 
        """
1707
 
        return uri
1708
 
 
1709
 
    # Headers ----------------------------------------------------------
1710
 
 
1711
 
    def setHttpHeader(self, header):
1712
 
        pass
1713
 
 
1714
 
    def http_headers(self, more_headers=[]):
1715
 
        pass
1716
 
 
1717
 
    def http_redirect(self, url):
1718
 
        """ Redirect to a fully qualified, or server-rooted URL 
1719
 
        
1720
 
        TODO: Does this work for rendering redirect pages?
1721
 
        """
1722
 
        raise Exception("Redirect not supported for command line tools!")
1723
 
 
1724
 
 
1725
 
# StandAlone Server ----------------------------------------------------
1726
 
 
1727
 
class RequestStandAlone(RequestBase):
1728
 
    """ specialized on StandAlone Server (MoinMoin.server.standalone) requests """
1729
 
    script_name = ''
1730
 
    
1731
 
    def __init__(self, sa, properties={}):
1732
 
        """
1733
 
        @param sa: stand alone server object
1734
 
        @param properties: ...
1735
 
        """
1736
 
        try:
1737
 
            self.sareq = sa
1738
 
            self.wfile = sa.wfile
1739
 
            self.rfile = sa.rfile
1740
 
            self.headers = sa.headers
1741
 
            self.is_ssl = 0
1742
 
            
1743
 
            # Copy headers
1744
 
            self.http_accept_language = (sa.headers.getheader('accept-language') 
1745
 
                                         or self.http_accept_language)
1746
 
            self.http_user_agent = sa.headers.getheader('user-agent', '')            
1747
 
            co = filter(None, sa.headers.getheaders('cookie'))
1748
 
            self.saved_cookie = ', '.join(co) or ''
1749
 
            self.if_modified_since = sa.headers.getheader('if-modified-since')
1750
 
            self.if_none_match = sa.headers.getheader('if-none-match')
1751
 
            
1752
 
            # Copy rest from standalone request   
1753
 
            self.server_name = sa.server.server_name
1754
 
            self.server_port = str(sa.server.server_port)
1755
 
            self.request_method = sa.command
1756
 
            self.request_uri = sa.path
1757
 
            self.remote_addr = sa.client_address[0]
1758
 
 
1759
 
            # Values that need more work                        
1760
 
            self.path_info, self.query_string = self.splitURI(sa.path)
1761
 
            self.setHttpReferer(sa.headers.getheader('referer'))
1762
 
            self.setHost(sa.headers.getheader('host'))
1763
 
            self.setURL(sa.headers)
1764
 
 
1765
 
            ##self.debugEnvironment(sa.headers)
1766
 
            
1767
 
            RequestBase.__init__(self, properties)
1768
 
 
1769
 
        except Exception, err:
1770
 
            self.fail(err)
1771
 
 
1772
 
    def _setup_args_from_cgi_form(self, form=None):
1773
 
        """ Override to create standlone form """
1774
 
        form = cgi.FieldStorage(self.rfile, headers=self.headers, environ={'REQUEST_METHOD': 'POST'})
1775
 
        return RequestBase._setup_args_from_cgi_form(self, form)
1776
 
        
1777
 
    def read(self, n=None):
1778
 
        """ Read from input stream
1779
 
        
1780
 
        Since self.rfile.read() will block, content-length will be used instead.
1781
 
        
1782
 
        TODO: test with n > content length, or when calling several times
1783
 
        with smaller n but total over content length.
1784
 
        """
1785
 
        if n is None:
1786
 
            try:
1787
 
                n = int(self.headers.get('content-length'))
1788
 
            except (TypeError, ValueError):
1789
 
                import warnings
1790
 
                warnings.warn("calling request.read() when content-length is "
1791
 
                              "not available will block")
1792
 
                return self.rfile.read()
1793
 
        return self.rfile.read(n)
1794
 
 
1795
 
    def write(self, *data):
1796
 
        """ Write to output stream. """
1797
 
        self.wfile.write(self.encode(data))
1798
 
 
1799
 
    def flush(self):
1800
 
        self.wfile.flush()
1801
 
        
1802
 
    def finish(self):
1803
 
        RequestBase.finish(self)
1804
 
        self.wfile.flush()
1805
 
 
1806
 
    # Headers ----------------------------------------------------------
1807
 
 
1808
 
    def http_headers(self, more_headers=[]):
1809
 
        if getattr(self, 'sent_headers', None):
1810
 
            return
1811
 
        
1812
 
        self.sent_headers = 1
1813
 
        user_headers = getattr(self, 'user_headers', [])
1814
 
        
1815
 
        # check for status header and send it
1816
 
        our_status = 200
1817
 
        for header in more_headers + user_headers:
1818
 
            if header.lower().startswith("status:"):
1819
 
                try:
1820
 
                    our_status = int(header.split(':', 1)[1].strip().split(" ", 1)[0]) 
1821
 
                except:
1822
 
                    pass
1823
 
                # there should be only one!
1824
 
                break
1825
 
        # send response
1826
 
        self.sareq.send_response(our_status)
1827
 
 
1828
 
        # send http headers
1829
 
        have_ct = 0
1830
 
        for header in more_headers + user_headers:
1831
 
            if type(header) is unicode:
1832
 
                header = header.encode('ascii')
1833
 
            if header.lower().startswith("content-type:"):
1834
 
                # don't send content-type multiple times!
1835
 
                if have_ct: continue
1836
 
                have_ct = 1
1837
 
 
1838
 
            self.write("%s\r\n" % header)
1839
 
 
1840
 
        if not have_ct:
1841
 
            self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
1842
 
 
1843
 
        self.write('\r\n')
1844
 
 
1845
 
        #from pprint import pformat
1846
 
        #sys.stderr.write(pformat(more_headers))
1847
 
        #sys.stderr.write(pformat(self.user_headers))
1848
 
 
1849
 
# mod_python/Apache ----------------------------------------------------
1850
 
 
1851
 
class RequestModPy(RequestBase):
1852
 
    """ specialized on mod_python requests """
1853
 
 
1854
 
    def __init__(self, req):
1855
 
        """ Saves mod_pythons request and sets basic variables using
1856
 
            the req.subprocess_env, cause this provides a standard
1857
 
            way to access the values we need here.
1858
 
 
1859
 
            @param req: the mod_python request instance
1860
 
        """
1861
 
        try:
1862
 
            # flags if headers sent out contained content-type or status
1863
 
            self._have_ct = 0
1864
 
            self._have_status = 0
1865
 
 
1866
 
            req.add_common_vars()
1867
 
            self.mpyreq = req
1868
 
            # some mod_python 2.7.X has no get method for table objects,
1869
 
            # so we make a real dict out of it first.
1870
 
            if not hasattr(req.subprocess_env, 'get'):
1871
 
                env=dict(req.subprocess_env)
1872
 
            else:
1873
 
                env=req.subprocess_env
1874
 
            self._setup_vars_from_std_env(env)
1875
 
            RequestBase.__init__(self)
1876
 
 
1877
 
        except Exception, err:
1878
 
            self.fail(err)
1879
 
            
1880
 
    def fixURI(self, env):
1881
 
        """ Fix problems with script_name and path_info using
1882
 
        PythonOption directive to rewrite URI.
1883
 
        
1884
 
        This is needed when using Apache 1 or other server which does
1885
 
        not support adding custom headers per request. With mod_python we
1886
 
        can use the PythonOption directive:
1887
 
        
1888
 
            <Location /url/to/mywiki/>
1889
 
                PythonOption X-Moin-Location /url/to/mywiki/
1890
 
            </location>
1891
 
 
1892
 
        Note that *neither* script_name *nor* path_info can be trusted
1893
 
        when Moin is invoked as a mod_python handler with apache1, so we
1894
 
        must build both using request_uri and the provided PythonOption.
1895
 
        """
1896
 
        # Be compatible with release 1.3.5 "Location" option 
1897
 
        # TODO: Remove in later release, we should have one option only.
1898
 
        old_location = 'Location'
1899
 
        options_table = self.mpyreq.get_options()
1900
 
        if not hasattr(options_table, 'get'):
1901
 
            options = dict(options_table)
1902
 
        else:
1903
 
            options = options_table
1904
 
        location = options.get(self.moin_location) or options.get(old_location)
1905
 
        if location:
1906
 
            env[self.moin_location] = location
1907
 
            # Try to recreate script_name and path_info from request_uri.
1908
 
            import urlparse
1909
 
            scriptAndPath = urlparse.urlparse(self.request_uri)[2]
1910
 
            self.script_name = location.rstrip('/')
1911
 
            path = scriptAndPath.replace(self.script_name, '', 1)            
1912
 
            self.path_info = wikiutil.url_unquote(path, want_unicode=False)
1913
 
 
1914
 
        RequestBase.fixURI(self, env)
1915
 
 
1916
 
    def _setup_args_from_cgi_form(self, form=None):
1917
 
        """ Override to use mod_python.util.FieldStorage 
1918
 
        
1919
 
        Its little different from cgi.FieldStorage, so we need to
1920
 
        duplicate the conversion code.
1921
 
        """
1922
 
        from mod_python import util
1923
 
        if form is None:
1924
 
            form = util.FieldStorage(self.mpyreq)
1925
 
 
1926
 
        args = {}
1927
 
        for key in form.keys():
1928
 
            if key is None:
1929
 
                continue                
1930
 
            values = form[key]
1931
 
            if not isinstance(values, list):
1932
 
                values = [values]
1933
 
            fixedResult = []
1934
 
 
1935
 
            for item in values:
1936
 
                # Remember filenames with a name hack
1937
 
                if hasattr(item, 'filename') and item.filename:
1938
 
                    args[key + '__filename__'] = item.filename
1939
 
                # mod_python 2.7 might return strings instead of Field
1940
 
                # objects.
1941
 
                if hasattr(item, 'value'):
1942
 
                    item = item.value
1943
 
                fixedResult.append(item)                
1944
 
            args[key] = fixedResult
1945
 
            
1946
 
        return self.decodeArgs(args)
1947
 
 
1948
 
    def run(self, req):
1949
 
        """ mod_python calls this with its request object. We don't
1950
 
            need it cause its already passed to __init__. So ignore
1951
 
            it and just return RequestBase.run.
1952
 
 
1953
 
            @param req: the mod_python request instance
1954
 
        """
1955
 
        return RequestBase.run(self)
1956
 
 
1957
 
    def read(self, n=None):
1958
 
        """ Read from input stream. """
1959
 
        if n is None:
1960
 
            return self.mpyreq.read()
1961
 
        else:
1962
 
            return self.mpyreq.read(n)
1963
 
 
1964
 
    def write(self, *data):
1965
 
        """ Write to output stream. """
1966
 
        self.mpyreq.write(self.encode(data))
1967
 
 
1968
 
    def flush(self):
1969
 
        """ We can't flush it, so do nothing. """
1970
 
        pass
1971
 
        
1972
 
    def finish(self):
1973
 
        """ Just return apache.OK. Status is set in req.status. """
1974
 
        RequestBase.finish(self)
1975
 
        # is it possible that we need to return something else here?
1976
 
        from mod_python import apache
1977
 
        return apache.OK
1978
 
 
1979
 
    # Headers ----------------------------------------------------------
1980
 
 
1981
 
    def setHttpHeader(self, header):
1982
 
        """ Filters out content-type and status to set them directly
1983
 
            in the mod_python request. Rest is put into the headers_out
1984
 
            member of the mod_python request.
1985
 
 
1986
 
            @param header: string, containing valid HTTP header.
1987
 
        """
1988
 
        if type(header) is unicode:
1989
 
            header = header.encode('ascii')
1990
 
        key, value = header.split(':', 1)
1991
 
        value = value.lstrip()
1992
 
        if key.lower() == 'content-type':
1993
 
            # save content-type for http_headers
1994
 
            if not self._have_ct:
1995
 
                # we only use the first content-type!
1996
 
                self.mpyreq.content_type = value
1997
 
                self._have_ct = 1
1998
 
        elif key.lower() == 'status':
1999
 
            # save status for finish
2000
 
            try:
2001
 
                self.mpyreq.status = int(value.split(' ', 1)[0])
2002
 
            except:
2003
 
                pass
2004
 
            else:
2005
 
                self._have_status = 1
2006
 
        else:
2007
 
            # this is a header we sent out
2008
 
            self.mpyreq.headers_out[key]=value
2009
 
 
2010
 
    def http_headers(self, more_headers=[]):
2011
 
        """ Sends out headers and possibly sets default content-type
2012
 
            and status.
2013
 
 
2014
 
            @param more_headers: list of strings, defaults to []
2015
 
        """
2016
 
        for header in more_headers + getattr(self, 'user_headers', []):
2017
 
            self.setHttpHeader(header)
2018
 
        # if we don't had an content-type header, set text/html
2019
 
        if self._have_ct == 0:
2020
 
            self.mpyreq.content_type = "text/html;charset=%s" % config.charset
2021
 
        # if we don't had a status header, set 200
2022
 
        if self._have_status == 0:
2023
 
            self.mpyreq.status = 200
2024
 
        # this is for mod_python 2.7.X, for 3.X it's a NOP
2025
 
        self.mpyreq.send_http_header()
2026
 
 
2027
 
# FastCGI -----------------------------------------------------------
2028
 
 
2029
 
class RequestFastCGI(RequestBase):
2030
 
    """ specialized on FastCGI requests """
2031
 
 
2032
 
    def __init__(self, fcgRequest, env, form, properties={}):
2033
 
        """ Initializes variables from FastCGI environment and saves
2034
 
            FastCGI request and form for further use.
2035
 
 
2036
 
            @param fcgRequest: the FastCGI request instance.
2037
 
            @param env: environment passed by FastCGI.
2038
 
            @param form: FieldStorage passed by FastCGI.
2039
 
        """
2040
 
        try:
2041
 
            self.fcgreq = fcgRequest
2042
 
            self.fcgenv = env
2043
 
            self.fcgform = form
2044
 
            self._setup_vars_from_std_env(env)
2045
 
            RequestBase.__init__(self, properties)
2046
 
 
2047
 
        except Exception, err:
2048
 
            self.fail(err)
2049
 
 
2050
 
    def _setup_args_from_cgi_form(self, form=None):
2051
 
        """ Override to use FastCGI form """
2052
 
        if form is None:
2053
 
            form = self.fcgform
2054
 
        return RequestBase._setup_args_from_cgi_form(self, form)
2055
 
 
2056
 
    def open_logs(self):
2057
 
        # create log file for catching stderr output
2058
 
        if not self.opened_logs:
2059
 
            sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at', 0)
2060
 
            self.opened_logs = 1
2061
 
 
2062
 
    def read(self, n=None):
2063
 
        """ Read from input stream. """
2064
 
        if n is None:
2065
 
            return self.fcgreq.stdin.read()
2066
 
        else:
2067
 
            return self.fcgreq.stdin.read(n)
2068
 
 
2069
 
    def write(self, *data):
2070
 
        """ Write to output stream. """
2071
 
        self.fcgreq.out.write(self.encode(data))
2072
 
 
2073
 
    def flush(self):
2074
 
        """ Flush output stream. """
2075
 
        self.fcgreq.flush_out()
2076
 
 
2077
 
    def finish(self):
2078
 
        """ Call finish method of FastCGI request to finish handling of this request. """
2079
 
        RequestBase.finish(self)
2080
 
        self.fcgreq.finish()
2081
 
 
2082
 
    # Headers ----------------------------------------------------------
2083
 
 
2084
 
    def http_headers(self, more_headers=[]):
2085
 
        """ Send out HTTP headers. Possibly set a default content-type. """
2086
 
        if getattr(self, 'sent_headers', None):
2087
 
            return
2088
 
        self.sent_headers = 1
2089
 
        have_ct = 0
2090
 
 
2091
 
        # send http headers
2092
 
        for header in more_headers + getattr(self, 'user_headers', []):
2093
 
            if type(header) is unicode:
2094
 
                header = header.encode('ascii')
2095
 
            if header.lower().startswith("content-type:"):
2096
 
                # don't send content-type multiple times!
2097
 
                if have_ct: continue
2098
 
                have_ct = 1
2099
 
            self.write("%s\r\n" % header)
2100
 
 
2101
 
        if not have_ct:
2102
 
            self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
2103
 
 
2104
 
        self.write('\r\n')
2105
 
 
2106
 
        #from pprint import pformat
2107
 
        #sys.stderr.write(pformat(more_headers))
2108
 
        #sys.stderr.write(pformat(self.user_headers))
2109
 
 
2110
 
    def open_logs(self):
2111
 
        # create log file for catching stderr output
2112
 
        if not self.opened_logs:
2113
 
            sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at')
2114
 
            if self.cfg.log_timing:
2115
 
                self.timing_logfile = open(os.path.join(self.cfg.data_dir, 'timing.log'), 'at')
2116
 
            self.opened_logs = 1
2117
 
 
2118
 
# WSGI --------------------------------------------------------------
2119
 
 
2120
 
class RequestWSGI(RequestBase):
2121
 
    def __init__(self, env):
2122
 
        try:
2123
 
            self.env = env
2124
 
            self.hasContentType = False
2125
 
            
2126
 
            self.stdin = env['wsgi.input']
2127
 
            self.stdout = StringIO.StringIO()
2128
 
            
2129
 
            self.status = '200 OK'
2130
 
            self.headers = []
2131
 
            
2132
 
            self._setup_vars_from_std_env(env)
2133
 
            RequestBase.__init__(self, {})
2134
 
        
2135
 
        except Exception, err:
2136
 
            self.fail(err)
2137
 
    
2138
 
    def setup_args(self, form=None):
2139
 
        if form is None:
2140
 
            form = cgi.FieldStorage(fp=self.stdin, environ=self.env, keep_blank_values=1)
2141
 
        return self._setup_args_from_cgi_form(form)
2142
 
    
2143
 
    def read(self, n=None):
2144
 
        if n is None:
2145
 
            return self.stdin.read()
2146
 
        else:
2147
 
            return self.stdin.read(n)
2148
 
    
2149
 
    def write(self, *data):
2150
 
        self.stdout.write(self.encode(data))
2151
 
    
2152
 
    def reset_output(self):
2153
 
        self.stdout = StringIO.StringIO()
2154
 
    
2155
 
    def setHttpHeader(self, header):
2156
 
        if type(header) is unicode:
2157
 
            header = header.encode('ascii')
2158
 
        
2159
 
        key, value = header.split(':', 1)
2160
 
        value = value.lstrip()
2161
 
        if key.lower() == 'content-type':
2162
 
            # save content-type for http_headers
2163
 
            if self.hasContentType:
2164
 
                # we only use the first content-type!
2165
 
                return
2166
 
            else:
2167
 
                self.hasContentType = True
2168
 
        
2169
 
        elif key.lower() == 'status':
2170
 
            # save status for finish
2171
 
            self.status = value
2172
 
            return
2173
 
            
2174
 
        self.headers.append((key, value))
2175
 
    
2176
 
    def http_headers(self, more_headers=[]):
2177
 
        for header in more_headers:
2178
 
            self.setHttpHeader(header)
2179
 
        
2180
 
        if not self.hasContentType:
2181
 
            self.headers.insert(0, ('Content-Type', 'text/html;charset=%s' % config.charset))
2182
 
    
2183
 
    def flush(self):
2184
 
        pass
2185
 
    
2186
 
    def finish(self):
2187
 
        pass
2188
 
    
2189
 
    def output(self):
2190
 
        return self.stdout.getvalue()
2191
 
 
2192