2001-12-19 08:09:21 +03:00
|
|
|
# -*-python-*-
|
|
|
|
#
|
2013-01-04 23:01:54 +04:00
|
|
|
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
|
2001-12-19 08:09:21 +03:00
|
|
|
#
|
|
|
|
# By using this file, you agree to the terms and conditions set forth in
|
2006-03-18 05:07:36 +03:00
|
|
|
# the LICENSE.html file which can be found at the top level of the ViewVC
|
2005-12-17 20:19:28 +03:00
|
|
|
# distribution or at http://viewvc.org/license-1.html.
|
2001-12-19 08:09:21 +03:00
|
|
|
#
|
2006-03-18 05:07:36 +03:00
|
|
|
# For more information, visit http://viewvc.org/
|
2001-12-19 08:09:21 +03:00
|
|
|
#
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
#
|
|
|
|
# accept.py: parse/handle the various Accept headers from the client
|
|
|
|
#
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
def language(hdr):
|
|
|
|
"Parse an Accept-Language header."
|
|
|
|
|
|
|
|
# parse the header, storing results in a _LanguageSelector object
|
|
|
|
return _parse(hdr, _LanguageSelector())
|
|
|
|
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
_re_token = re.compile(r'\s*([^\s;,"]+|"[^"]*")+\s*')
|
|
|
|
_re_param = re.compile(r';\s*([^;,"]+|"[^"]*")+\s*')
|
|
|
|
_re_split_param = re.compile(r'([^\s=])\s*=\s*(.*)')
|
|
|
|
|
|
|
|
def _parse(hdr, result):
|
|
|
|
# quick exit for empty or not-supplied header
|
|
|
|
if not hdr:
|
|
|
|
return result
|
|
|
|
|
|
|
|
pos = 0
|
|
|
|
while pos < len(hdr):
|
|
|
|
name = _re_token.match(hdr, pos)
|
|
|
|
if not name:
|
2009-02-26 19:12:15 +03:00
|
|
|
raise AcceptLanguageParseError()
|
Wow. Drop a "general code cleanup" kind of bomb on the codebase. All
of this is aimed at not paying the maintenance price of supporting
Python versions prior to 2.4 any longer, plus a little bit of just
getting dead code out of the way.
* lib/compat.py
Remove as unused.
* bin/cvsdbadmin,
* bin/loginfo-handler,
* bin/make-database,
* bin/svndbadmin,
* lib/accept.py,
* lib/blame.py,
* lib/cvsdb.py,
* lib/popen.py,
* lib/query.py,
* lib/sapi.py,
* lib/vcauth/forbidden/__init__.py
* lib/vcauth/forbiddenre/__init__.py,
* lib/vcauth/svnauthz/__init__.py,
* lib/vclib/__init__.py,
* lib/vclib/ccvs/blame.py,
* lib/win32popen.py,
* tests/timelog.py
Replace explicit import and use of the 'string' module with newer constructs.
* bin/standalone.py,
* lib/viewvc.py
No longer use 'compat' module. Replace explicit import and use of
the 'string' module with newer constructs.
* lib/dbi.py
Use calender.timegm() instead of compat.timegm().
* lib/vcauth/__init__.py
Lose unused module imports.
* lib/config.py,
Replace explicit import and use of the 'string' module with newer
constructs where possible. Lose old ConfigParser patch-up code for
Python 1.5.1.
* lib/vclib/ccvs/ccvs.py
Replace explicit import and use of the 'string' module with newer
constructs where possible. Import _path_join() from bincvs, and use
it instead of a bunch of copy-and-pasted string join() statements
throughout.
* lib/vclib/ccvs/__init__.py
(cvs_strptime): Moved here from the 'compat' module.
* lib/vclib/ccvs/bincvs.py
(): No longer use 'compat' module. Replace explicit import and use
of the 'string' module with newer constructs.
(_path_join): New, used now instead of a bunch of copy-and-pasted
string join() statements throughout.
* viewvc-install
Don't use the 'compat' module any more.
Also, so some rearranging of non-critical bits.
* misc/: New directory.
* misc/py2html.py: Moved from 'lib/py2html.py'.
* misc/PyFontify.py: Moved from 'lib/PyFontify.py'.
* misc/elemx/: Moved from 'elemx/'.
* misc/tparse/: Moved from 'tparse/'.
* tools/make-release
Omit 'misc' directory from releases, too.
git-svn-id: http://viewvc.tigris.org/svn/viewvc/trunk@2437 8cb11bc2-c004-0410-86c3-e597b4017df7
2010-09-03 20:49:52 +04:00
|
|
|
a = result.item_class(name.group(1).lower())
|
2001-12-19 08:09:21 +03:00
|
|
|
pos = name.end()
|
|
|
|
while 1:
|
|
|
|
# are we looking at a parameter?
|
|
|
|
match = _re_param.match(hdr, pos)
|
|
|
|
if not match:
|
|
|
|
break
|
|
|
|
param = match.group(1)
|
|
|
|
pos = match.end()
|
|
|
|
|
|
|
|
# split up the pieces of the parameter
|
|
|
|
match = _re_split_param.match(param)
|
|
|
|
if not match:
|
|
|
|
# the "=" was probably missing
|
|
|
|
continue
|
|
|
|
|
Wow. Drop a "general code cleanup" kind of bomb on the codebase. All
of this is aimed at not paying the maintenance price of supporting
Python versions prior to 2.4 any longer, plus a little bit of just
getting dead code out of the way.
* lib/compat.py
Remove as unused.
* bin/cvsdbadmin,
* bin/loginfo-handler,
* bin/make-database,
* bin/svndbadmin,
* lib/accept.py,
* lib/blame.py,
* lib/cvsdb.py,
* lib/popen.py,
* lib/query.py,
* lib/sapi.py,
* lib/vcauth/forbidden/__init__.py
* lib/vcauth/forbiddenre/__init__.py,
* lib/vcauth/svnauthz/__init__.py,
* lib/vclib/__init__.py,
* lib/vclib/ccvs/blame.py,
* lib/win32popen.py,
* tests/timelog.py
Replace explicit import and use of the 'string' module with newer constructs.
* bin/standalone.py,
* lib/viewvc.py
No longer use 'compat' module. Replace explicit import and use of
the 'string' module with newer constructs.
* lib/dbi.py
Use calender.timegm() instead of compat.timegm().
* lib/vcauth/__init__.py
Lose unused module imports.
* lib/config.py,
Replace explicit import and use of the 'string' module with newer
constructs where possible. Lose old ConfigParser patch-up code for
Python 1.5.1.
* lib/vclib/ccvs/ccvs.py
Replace explicit import and use of the 'string' module with newer
constructs where possible. Import _path_join() from bincvs, and use
it instead of a bunch of copy-and-pasted string join() statements
throughout.
* lib/vclib/ccvs/__init__.py
(cvs_strptime): Moved here from the 'compat' module.
* lib/vclib/ccvs/bincvs.py
(): No longer use 'compat' module. Replace explicit import and use
of the 'string' module with newer constructs.
(_path_join): New, used now instead of a bunch of copy-and-pasted
string join() statements throughout.
* viewvc-install
Don't use the 'compat' module any more.
Also, so some rearranging of non-critical bits.
* misc/: New directory.
* misc/py2html.py: Moved from 'lib/py2html.py'.
* misc/PyFontify.py: Moved from 'lib/PyFontify.py'.
* misc/elemx/: Moved from 'elemx/'.
* misc/tparse/: Moved from 'tparse/'.
* tools/make-release
Omit 'misc' directory from releases, too.
git-svn-id: http://viewvc.tigris.org/svn/viewvc/trunk@2437 8cb11bc2-c004-0410-86c3-e597b4017df7
2010-09-03 20:49:52 +04:00
|
|
|
pname = match.group(1).lower()
|
2001-12-19 08:09:21 +03:00
|
|
|
if pname == 'q' or pname == 'qs':
|
|
|
|
try:
|
|
|
|
a.quality = float(match.group(2))
|
|
|
|
except ValueError:
|
|
|
|
# bad float literal
|
|
|
|
pass
|
|
|
|
elif pname == 'level':
|
|
|
|
try:
|
|
|
|
a.level = float(match.group(2))
|
|
|
|
except ValueError:
|
|
|
|
# bad float literal
|
|
|
|
pass
|
|
|
|
elif pname == 'charset':
|
Wow. Drop a "general code cleanup" kind of bomb on the codebase. All
of this is aimed at not paying the maintenance price of supporting
Python versions prior to 2.4 any longer, plus a little bit of just
getting dead code out of the way.
* lib/compat.py
Remove as unused.
* bin/cvsdbadmin,
* bin/loginfo-handler,
* bin/make-database,
* bin/svndbadmin,
* lib/accept.py,
* lib/blame.py,
* lib/cvsdb.py,
* lib/popen.py,
* lib/query.py,
* lib/sapi.py,
* lib/vcauth/forbidden/__init__.py
* lib/vcauth/forbiddenre/__init__.py,
* lib/vcauth/svnauthz/__init__.py,
* lib/vclib/__init__.py,
* lib/vclib/ccvs/blame.py,
* lib/win32popen.py,
* tests/timelog.py
Replace explicit import and use of the 'string' module with newer constructs.
* bin/standalone.py,
* lib/viewvc.py
No longer use 'compat' module. Replace explicit import and use of
the 'string' module with newer constructs.
* lib/dbi.py
Use calender.timegm() instead of compat.timegm().
* lib/vcauth/__init__.py
Lose unused module imports.
* lib/config.py,
Replace explicit import and use of the 'string' module with newer
constructs where possible. Lose old ConfigParser patch-up code for
Python 1.5.1.
* lib/vclib/ccvs/ccvs.py
Replace explicit import and use of the 'string' module with newer
constructs where possible. Import _path_join() from bincvs, and use
it instead of a bunch of copy-and-pasted string join() statements
throughout.
* lib/vclib/ccvs/__init__.py
(cvs_strptime): Moved here from the 'compat' module.
* lib/vclib/ccvs/bincvs.py
(): No longer use 'compat' module. Replace explicit import and use
of the 'string' module with newer constructs.
(_path_join): New, used now instead of a bunch of copy-and-pasted
string join() statements throughout.
* viewvc-install
Don't use the 'compat' module any more.
Also, so some rearranging of non-critical bits.
* misc/: New directory.
* misc/py2html.py: Moved from 'lib/py2html.py'.
* misc/PyFontify.py: Moved from 'lib/PyFontify.py'.
* misc/elemx/: Moved from 'elemx/'.
* misc/tparse/: Moved from 'tparse/'.
* tools/make-release
Omit 'misc' directory from releases, too.
git-svn-id: http://viewvc.tigris.org/svn/viewvc/trunk@2437 8cb11bc2-c004-0410-86c3-e597b4017df7
2010-09-03 20:49:52 +04:00
|
|
|
a.charset = match.group(2).lower()
|
2001-12-19 08:09:21 +03:00
|
|
|
|
|
|
|
result.append(a)
|
|
|
|
if hdr[pos:pos+1] == ',':
|
|
|
|
pos = pos + 1
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
class _AcceptItem:
|
|
|
|
def __init__(self, name):
|
|
|
|
self.name = name
|
|
|
|
self.quality = 1.0
|
|
|
|
self.level = 0.0
|
|
|
|
self.charset = ''
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
s = self.name
|
|
|
|
if self.quality != 1.0:
|
|
|
|
s = '%s;q=%.3f' % (s, self.quality)
|
|
|
|
if self.level != 0.0:
|
|
|
|
s = '%s;level=%.3f' % (s, self.level)
|
|
|
|
if self.charset:
|
|
|
|
s = '%s;charset=%s' % (s, self.charset)
|
|
|
|
return s
|
|
|
|
|
|
|
|
class _LanguageRange(_AcceptItem):
|
|
|
|
def matches(self, tag):
|
|
|
|
"Match the tag against self. Returns the qvalue, or None if non-matching."
|
|
|
|
if tag == self.name:
|
|
|
|
return self.quality
|
|
|
|
|
|
|
|
# are we a prefix of the available language-tag
|
|
|
|
name = self.name + '-'
|
|
|
|
if tag[:len(name)] == name:
|
|
|
|
return self.quality
|
|
|
|
return None
|
|
|
|
|
|
|
|
class _LanguageSelector:
|
2002-05-23 23:13:40 +04:00
|
|
|
"""Instances select an available language based on the user's request.
|
|
|
|
|
|
|
|
Languages found in the user's request are added to this object with the
|
|
|
|
append() method (they should be instances of _LanguageRange). After the
|
|
|
|
languages have been added, then the caller can use select_from() to
|
|
|
|
determine which user-request language(s) best matches the set of
|
|
|
|
available languages.
|
|
|
|
|
|
|
|
Strictly speaking, this class is pretty close for more than just
|
|
|
|
language matching. It has been implemented to enable q-value based
|
|
|
|
matching between requests and availability. Some minor tweaks may be
|
|
|
|
necessary, but simply using a new 'item_class' should be sufficient
|
|
|
|
to allow the _parse() function to construct a selector which holds
|
|
|
|
the appropriate item implementations (e.g. _LanguageRange is the
|
|
|
|
concrete _AcceptItem class that handles matching of language tags).
|
|
|
|
"""
|
|
|
|
|
2001-12-19 08:09:21 +03:00
|
|
|
item_class = _LanguageRange
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.requested = [ ]
|
|
|
|
|
|
|
|
def select_from(self, avail):
|
|
|
|
"""Select one of the available choices based on the request.
|
|
|
|
|
|
|
|
Note: if there isn't a match, then the first available choice is
|
2002-05-23 23:13:40 +04:00
|
|
|
considered the default. Also, if a number of matches are equally
|
|
|
|
relevant, then the first-requested will be used.
|
2001-12-19 08:09:21 +03:00
|
|
|
|
|
|
|
avail is a list of language-tag strings of available languages
|
|
|
|
"""
|
|
|
|
|
|
|
|
# tuples of (qvalue, language-tag)
|
|
|
|
matches = [ ]
|
|
|
|
|
|
|
|
# try matching all pairs of desired vs available, recording the
|
|
|
|
# resulting qvalues. we also need to record the longest language-range
|
|
|
|
# that matches since the most specific range "wins"
|
|
|
|
for tag in avail:
|
|
|
|
longest = 0
|
|
|
|
final = 0.0
|
|
|
|
|
|
|
|
# check this tag against the requests from the user
|
|
|
|
for want in self.requested:
|
|
|
|
qvalue = want.matches(tag)
|
2001-12-20 03:25:45 +03:00
|
|
|
#print 'have %s. want %s. qvalue=%s' % (tag, want.name, qvalue)
|
2001-12-19 08:09:21 +03:00
|
|
|
if qvalue is not None and len(want.name) > longest:
|
2002-05-23 23:13:40 +04:00
|
|
|
# we have a match and it is longer than any we may have had.
|
|
|
|
# the final qvalue should be from this tag.
|
2001-12-19 08:09:21 +03:00
|
|
|
final = qvalue
|
|
|
|
longest = len(want.name)
|
|
|
|
|
|
|
|
# a non-zero qvalue is a potential match
|
|
|
|
if final:
|
|
|
|
matches.append((final, tag))
|
|
|
|
|
2002-05-23 23:13:40 +04:00
|
|
|
# if there are no matches, then return the default language tag
|
|
|
|
if not matches:
|
|
|
|
return avail[0]
|
|
|
|
|
|
|
|
# get the highest qvalue and its corresponding tag
|
|
|
|
matches.sort()
|
|
|
|
qvalue, tag = matches[-1]
|
|
|
|
|
|
|
|
# if the qvalue is zero, then we have no valid matches. return the
|
|
|
|
# default language tag.
|
|
|
|
if not qvalue:
|
|
|
|
return avail[0]
|
|
|
|
|
|
|
|
# if there are two or more matches, and the second-highest has a
|
|
|
|
# qvalue equal to the best, then we have multiple "best" options.
|
|
|
|
# select the one that occurs first in self.requested
|
|
|
|
if len(matches) >= 2 and matches[-2][0] == qvalue:
|
|
|
|
# remove non-best matches
|
|
|
|
while matches[0][0] != qvalue:
|
|
|
|
del matches[0]
|
|
|
|
#print "non-deterministic choice", matches
|
|
|
|
|
|
|
|
# sequence through self.requested, in order
|
|
|
|
for want in self.requested:
|
|
|
|
# try to find this one in our best matches
|
|
|
|
for qvalue, tag in matches:
|
|
|
|
if want.matches(tag):
|
|
|
|
# this requested item is one of the "best" options
|
|
|
|
### note: this request item could match *other* "best" options,
|
|
|
|
### so returning *this* one is rather non-deterministic.
|
|
|
|
### theoretically, we could go further here, and do another
|
|
|
|
### search based on the ordering in 'avail'. however, note
|
|
|
|
### that this generally means that we are picking from multiple
|
|
|
|
### *SUB* languages, so I'm all right with the non-determinism
|
|
|
|
### at this point. stupid client should send a qvalue if they
|
|
|
|
### want to refine.
|
|
|
|
return tag
|
|
|
|
|
|
|
|
# NOTREACHED
|
|
|
|
|
|
|
|
# return the best match
|
|
|
|
return tag
|
2001-12-19 08:09:21 +03:00
|
|
|
|
|
|
|
def append(self, item):
|
|
|
|
self.requested.append(item)
|
|
|
|
|
2009-02-26 19:12:15 +03:00
|
|
|
class AcceptLanguageParseError(Exception):
|
2001-12-19 08:09:21 +03:00
|
|
|
pass
|
|
|
|
|
|
|
|
def _test():
|
|
|
|
s = language('en')
|
|
|
|
assert s.select_from(['en']) == 'en'
|
|
|
|
assert s.select_from(['en', 'de']) == 'en'
|
|
|
|
assert s.select_from(['de', 'en']) == 'en'
|
|
|
|
|
2002-05-23 23:13:40 +04:00
|
|
|
# Netscape 4.x and early version of Mozilla may not send a q value
|
|
|
|
s = language('en, ja')
|
|
|
|
assert s.select_from(['en', 'ja']) == 'en'
|
|
|
|
|
2001-12-19 08:09:21 +03:00
|
|
|
s = language('fr, de;q=0.9, en-gb;q=0.7, en;q=0.6, en-gb-foo;q=0.8')
|
|
|
|
assert s.select_from(['en']) == 'en'
|
|
|
|
assert s.select_from(['en-gb-foo']) == 'en-gb-foo'
|
|
|
|
assert s.select_from(['de', 'fr']) == 'fr'
|
|
|
|
assert s.select_from(['de', 'en-gb']) == 'de'
|
|
|
|
assert s.select_from(['en-gb', 'en-gb-foo']) == 'en-gb-foo'
|
|
|
|
assert s.select_from(['en-bar']) == 'en-bar'
|
|
|
|
assert s.select_from(['en-gb-bar', 'en-gb-foo']) == 'en-gb-foo'
|
|
|
|
|
|
|
|
# non-deterministic. en-gb;q=0.7 matches both avail tags.
|
|
|
|
#assert s.select_from(['en-gb-bar', 'en-gb']) == 'en-gb'
|