15
17
# You should have received a copy of the GNU General Public License
16
18
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
# Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA
21
from xml.etree import ElementTree as ET
23
from elementtree import ElementTree as ET
22
from __future__ import print_function
34
from xml.etree import ElementTree as ET
37
from breezy import urlutils
35
39
log = logging.getLogger("loggerhead.controllers")
37
42
def fix_year(year):
44
49
# Display of times.
46
51
# date_day -- just the day
47
# date_time -- full date with time
52
# date_time -- full date with time (UTC)
49
# displaydate -- for use in sentences
50
54
# approximatedate -- for use in tables
52
# displaydate and approximatedate return an elementtree <span> Element
53
# with the full date in a tooltip.
56
# approximatedate return an elementtree <span> Element
57
# with the full date (UTC) in a tooltip.
55
60
def date_day(value):
56
61
return value.strftime('%Y-%m-%d')
59
64
def date_time(value):
60
return value.strftime('%Y-%m-%d %T')
63
def _displaydate(date):
64
delta = abs(datetime.datetime.now() - date)
65
if delta > datetime.timedelta(1, 0, 0):
66
# far in the past or future, display the date
67
return 'on ' + date_day(date)
68
return _approximatedate(date)
66
# Note: this assumes that the value is UTC in some fashion.
67
return value.strftime('%Y-%m-%d %H:%M:%S UTC')
71
72
def _approximatedate(date):
72
delta = datetime.datetime.now() - date
73
if abs(delta) > datetime.timedelta(1, 0, 0):
74
# far in the past or future, display the date
75
delta = datetime.datetime.utcnow() - date
76
76
future = delta < datetime.timedelta(0, 0, 0)
78
years = delta.days // 365
79
months = delta.days // 30 # This is approximate.
79
hours = delta.seconds / 3600
81
hours = delta.seconds // 3600
80
82
minutes = (delta.seconds - (3600*hours)) / 60
81
83
seconds = delta.seconds % 60
114
122
return _wrap_with_date_time_title(date, _approximatedate(date))
117
def displaydate(date):
118
return _wrap_with_date_time_title(date, _displaydate(date))
121
class Container (object):
125
class Container(object):
123
127
Convert a dict into an object with attributes.
125
130
def __init__(self, _dict=None, **kw):
131
self._properties = {}
126
132
if _dict is not None:
127
for key, value in _dict.iteritems():
133
for key, value in _dict.items():
128
134
setattr(self, key, value)
129
for key, value in kw.iteritems():
135
for key, value in kw.items():
130
136
setattr(self, key, value)
132
138
def __repr__(self):
134
for key, value in self.__dict__.iteritems():
135
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
140
for key, value in self.__dict__.items():
141
if key.startswith('_') or (getattr(self.__dict__[key],
142
'__call__', None) is not None):
137
144
out += '%r => %r, ' % (key, value)
148
def __getattr__(self, attr):
149
"""Used for handling things that aren't already available."""
150
if attr.startswith('_') or attr not in self._properties:
151
raise AttributeError('No attribute: %s' % (attr,))
152
val = self._properties[attr](self, attr)
153
setattr(self, attr, val)
156
def _set_property(self, attr, prop_func):
157
"""Set a function that will be called when an attribute is desired.
159
We will cache the return value, so the function call should be
160
idempotent. We will pass 'self' and the 'attr' name when triggered.
162
if attr.startswith('_'):
163
raise ValueError("Cannot create properties that start with _")
164
self._properties[attr] = prop_func
142
167
def trunc(text, limit=10):
143
168
if len(text) <= limit:
167
193
return '%s at %s' % (username, domains[-2])
168
194
return '%s at %s' % (username, domains[0])
196
def hide_emails(emails):
198
try to obscure any email address in a list of bazaar committers' names.
202
result.append(hide_email(email))
171
205
# only do this if unicode turns out to be a problem
172
206
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
208
# Can't be a dict; & needs to be done first.
212
("'", "'"), # ' is defined in XML, but not HTML.
219
"""Transform dangerous (X)HTML characters into entities.
221
Like cgi.escape, except also escaping \" and '. This makes it safe to use
222
in both attribute and element content.
224
If you want to safely fill a format string with escaped values, use
227
for char, repl in html_entity_subs:
228
s = s.replace(char, repl)
232
def html_format(template, *args):
233
"""Safely format an HTML template string, escaping the arguments.
235
The template string must not be user-controlled; it will not be escaped.
237
return template % tuple(html_escape(arg) for arg in args)
174
240
# FIXME: get rid of this method; use fixed_width() and avoid XML().
175
242
def html_clean(s):
177
244
clean up a string for html display. expand any tabs, encode any html
178
245
entities, and replace spaces with ' '. this is primarily for use
179
246
in displaying monospace text.
181
s = cgi.escape(s.expandtabs())
248
s = html_escape(s.expandtabs())
182
249
s = s.replace(' ', ' ')
187
253
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
258
CSS is stupid. In some cases we need to replace an empty value with
259
a non breaking space ( ). There has to be a better way of doing this.
261
return: the same value received if not empty, and a ' ' if it is.
265
elif isinstance(s, int):
269
elif isinstance(s, bytes):
271
s = s.decode('utf-8')
272
except UnicodeDecodeError:
273
s = s.decode('iso-8859-15')
275
elif isinstance(s, str):
189
281
def fixed_width(s):
191
283
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
192
284
chop up the string.
194
if not isinstance(s, unicode):
286
if not isinstance(s, str):
195
287
# this kinda sucks. file contents are just binary data, and no
196
288
# encoding metadata is stored, so we need to guess. this is probably
197
289
# okay for most code, but for people using things like KOI-8, this
201
293
s = s.decode('utf-8')
202
294
except UnicodeDecodeError:
203
295
s = s.decode('iso-8859-15')
204
return s.expandtabs().replace(' ', NONBREAKING_SPACE)
297
s = html_escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
299
return bleach.clean(s).replace('\n', '<br/>')
207
302
def fake_permissions(kind, executable):
266
362
if (base < 100) and (dot != 0):
267
363
out += '.%d' % (dot,)
268
364
if divisor == KILO:
270
366
elif divisor == MEG:
272
368
elif divisor == GIG:
373
def local_path_from_url(url):
374
"""Convert Bazaar URL to local path, ignoring readonly+ prefix"""
375
readonly_prefix = 'readonly+'
376
if url.startswith(readonly_prefix):
377
url = url[len(readonly_prefix):]
378
return urlutils.local_path_from_url(url)
277
381
def fill_in_navigation(navigation):
279
383
given a navigation block (used by the template for the page header), fill
285
389
navigation.position = 0
286
390
navigation.count = len(navigation.revid_list)
287
391
navigation.page_position = navigation.position // navigation.pagesize + 1
288
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
392
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
393
- 1)) // navigation.pagesize
290
395
def get_offset(offset):
291
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
396
if (navigation.position + offset < 0) or (
397
navigation.position + offset > navigation.count - 1):
293
399
return navigation.revid_list[navigation.position + offset]
401
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
295
402
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
296
403
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
297
404
prev_page_revno = navigation.history.get_revno(
315
422
[navigation.scan_url, next_page_revno], **params)
425
def directory_breadcrumbs(path, is_root, view):
427
Generate breadcrumb information from the directory path given
429
The path given should be a path up to any branch that is currently being
433
path -- The path to convert into breadcrumbs
434
is_root -- Whether or not loggerhead is serving a branch at its root
435
view -- The type of view we are showing (files, changes etc)
437
# Is our root directory itself a branch?
445
# Create breadcrumb trail for the path leading up to the branch
447
'dir_name': "(root)",
452
dir_parts = path.strip('/').split('/')
453
for index, dir_name in enumerate(dir_parts):
455
'dir_name': dir_name,
456
'path': '/'.join(dir_parts[:index + 1]),
459
# If we are not in the directory view, the last crumb is a branch,
460
# so we need to specify a view
461
if view != 'directory':
462
breadcrumbs[-1]['suffix'] = '/' + view
466
def branch_breadcrumbs(path, tree, view):
468
Generate breadcrumb information from the branch path given
470
The path given should be a path that exists within a branch
473
path -- The path to convert into breadcrumbs
474
tree -- Tree to get file information from
475
view -- The type of view we are showing (files, changes etc)
477
dir_parts = path.strip('/').split('/')
478
inner_breadcrumbs = []
479
for index, dir_name in enumerate(dir_parts):
480
inner_breadcrumbs.append({
481
'dir_name': dir_name,
482
'path': '/'.join(dir_parts[:index + 1]),
483
'suffix': '/' + view,
485
return inner_breadcrumbs
318
488
def decorator(unbound):
319
490
def new_decorator(f):
321
492
g.__name__ = f.__name__
328
499
return new_decorator
331
# common threading-lock decorator
332
def with_lock(lockname, debug_name=None):
333
if debug_name is None:
334
debug_name = lockname
336
def _decorator(unbound):
337
def locked(self, *args, **kw):
338
getattr(self, lockname).acquire()
340
return unbound(self, *args, **kw)
342
getattr(self, lockname).release()
349
506
def _f(*a, **kw):
350
from loggerhead.lsprof import profile
509
from .loggerhead.lsprof import profile
353
511
ret, stats = profile(f, *a, **kw)
354
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
512
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
513
int((time.time() - z) * 1000)))
357
516
now = time.time()
358
517
msec = int(now * 1000) % 1000
359
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
518
timestr = time.strftime('%Y%m%d%H%M%S',
519
time.localtime(now)) + ('%03d' % (msec,))
360
520
filename = f.__name__ + '-' + timestr + '.lsprof'
361
521
cPickle.dump(stats, open(filename, 'w'), 2)
391
549
# for re-ordering an existing page by different sort
393
551
t_context = threading.local()
394
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
395
'compare_revid', 'sort')
553
'start_revid', 'filter_path', 'q', 'remember', 'compare_revid', 'sort')
398
556
def set_context(map):
399
t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
557
t_context.map = dict((k, v) for (k, v) in map.items() if k in _valid)
402
560
def get_context(**overrides):
415
573
map['remember'] = t_context.map.get('remember', None)
417
575
map.update(t_context.map)
418
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
576
overrides = dict((k, v) for (k, v) in overrides.items() if k in _valid)
419
577
map.update(overrides)
581
class Reloader(object):
583
This class wraps all paste.reloader logic. All methods are @classmethod.
586
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
589
def _turn_sigterm_into_systemexit(cls):
591
Attempts to turn a SIGTERM exception into a SystemExit exception.
598
def handle_term(signo, frame):
600
signal.signal(signal.SIGTERM, handle_term)
603
def is_installed(cls):
604
return os.environ.get(cls._reloader_environ_key)
608
from paste import reloader
609
reloader.install(int(1))
612
def restart_with_reloader(cls):
613
"""Based on restart_with_monitor from paste.script.serve."""
614
print('Starting subprocess with file monitor')
616
args = [sys.executable] + sys.argv
617
new_environ = os.environ.copy()
618
new_environ[cls._reloader_environ_key] = 'true'
622
cls._turn_sigterm_into_systemexit()
623
proc = subprocess.Popen(args, env=new_environ)
624
exit_code = proc.wait()
626
except KeyboardInterrupt:
627
print('^C caught in monitor process')
631
and getattr(os, 'kill', None) is not None):
634
os.kill(proc.pid, signal.SIGTERM)
635
except (OSError, IOError):
638
# Reloader always exits with code 3; but if we are
639
# a monitor, any exit code will restart
642
print('-'*20, 'Restarting', '-'*20)
645
def convert_file_errors(application):
646
"""WSGI wrapper to convert some file errors to Paste exceptions"""
647
def new_application(environ, start_response):
649
return application(environ, start_response)
650
except (IOError, OSError) as e:
653
from paste import httpexceptions
654
if e.errno == errno.ENOENT:
655
raise httpexceptions.HTTPNotFound()
656
elif e.errno == errno.EACCES:
657
raise httpexceptions.HTTPForbidden()
660
return new_application
663
def convert_to_json_ready(obj):
664
if isinstance(obj, Container):
665
d = obj.__dict__.copy()
668
elif isinstance(obj, bytes):
669
return obj.decode('UTF-8')
670
elif isinstance(obj, datetime.datetime):
671
return tuple(obj.utctimetuple())
672
raise TypeError(repr(obj) + " is not JSON serializable")