Files
@ 0383ed91d4ed
Branch filter:
Location: kallithea/kallithea/lib/webutils.py
0383ed91d4ed
11.0 KiB
text/x-python
lib: move url_re to webutils
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 | # -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
kallithea.lib.webutils
~~~~~~~~~~~~~~~~~~~~~~
Helper functions that may rely on the current WSGI request, exposed in the TG2
thread-local "global" variables. It should have few dependencies so it can be
imported anywhere - just like the global variables can be used everywhere.
"""
import json
import logging
import random
import re
from tg import request, session
from webhelpers2.html import HTML, escape, literal
from webhelpers2.html.tags import NotGiven, Option, Options, _input
from webhelpers2.html.tags import _make_safe_id_component as safeid
from webhelpers2.html.tags import checkbox, end_form
from webhelpers2.html.tags import form as insecure_form
from webhelpers2.html.tags import hidden, link_to, password, radio
from webhelpers2.html.tags import select as webhelpers2_select
from webhelpers2.html.tags import submit, text, textarea
from webhelpers2.number import format_byte_size
from webhelpers2.text import chop_at, truncate, wrap_paragraphs
import kallithea
log = logging.getLogger(__name__)
# mute pyflakes "imported but unused"
assert Option
assert checkbox
assert chop_at
assert end_form
assert escape
assert format_byte_size
assert link_to
assert literal
assert password
assert radio
assert safeid
assert submit
assert text
assert textarea
assert truncate
assert wrap_paragraphs
#
# General Kallithea URL handling
#
class UrlGenerator(object):
"""Emulate pylons.url in providing a wrapper around routes.url
This code was added during migration from Pylons to Turbogears2. Pylons
already provided a wrapper like this, but Turbogears2 does not.
When the routing of Kallithea is changed to use less Routes and more
Turbogears2-style routing, this class may disappear or change.
url() (the __call__ method) returns the URL based on a route name and
arguments.
url.current() returns the URL of the current page with arguments applied.
Refer to documentation of Routes for details:
https://routes.readthedocs.io/en/latest/generating.html#generation
"""
def __call__(self, *args, **kwargs):
return request.environ['routes.url'](*args, **kwargs)
def current(self, *args, **kwargs):
return request.environ['routes.url'].current(*args, **kwargs)
url = UrlGenerator()
def canonical_url(*args, **kargs):
'''Like url(x, qualified=True), but returns url that not only is qualified
but also canonical, as configured in canonical_url'''
try:
parts = kallithea.CONFIG.get('canonical_url', '').split('://', 1)
kargs['host'] = parts[1]
kargs['protocol'] = parts[0]
except IndexError:
kargs['qualified'] = True
return url(*args, **kargs)
def canonical_hostname():
'''Return canonical hostname of system'''
try:
parts = kallithea.CONFIG.get('canonical_url', '').split('://', 1)
return parts[1].split('/', 1)[0]
except IndexError:
parts = url('home', qualified=True).split('://', 1)
return parts[1].split('/', 1)[0]
#
# Custom Webhelpers2 stuff
#
def html_escape(s):
"""Return string with all html escaped.
This is also safe for javascript in html but not necessarily correct.
"""
return (s
.replace('&', '&')
.replace(">", ">")
.replace("<", "<")
.replace('"', """)
.replace("'", "'") # Note: this is HTML5 not HTML4 and might not work in mails
)
def reset(name, value, id=NotGiven, **attrs):
"""Create a reset button, similar to webhelpers2.html.tags.submit ."""
return _input("reset", name, value, id, attrs)
def select(name, selected_values, options, id=NotGiven, **attrs):
"""Convenient wrapper of webhelpers2 to let it accept options as a tuple list"""
if isinstance(options, list):
option_list = options
# Handle old value,label lists ... where value also can be value,label lists
options = Options()
for x in option_list:
if isinstance(x, tuple) and len(x) == 2:
value, label = x
elif isinstance(x, str):
value = label = x
else:
log.error('invalid select option %r', x)
raise
if isinstance(value, list):
og = options.add_optgroup(label)
for x in value:
if isinstance(x, tuple) and len(x) == 2:
group_value, group_label = x
elif isinstance(x, str):
group_value = group_label = x
else:
log.error('invalid select option %r', x)
raise
og.add_option(group_label, group_value)
else:
options.add_option(label, value)
return webhelpers2_select(name, selected_values, options, id=id, **attrs)
session_csrf_secret_name = "_session_csrf_secret_token"
def session_csrf_secret_token():
"""Return (and create) the current session's CSRF protection token."""
if not session_csrf_secret_name in session:
session[session_csrf_secret_name] = str(random.getrandbits(128))
session.save()
return session[session_csrf_secret_name]
def form(url, method="post", **attrs):
"""Like webhelpers.html.tags.form , but automatically adding
session_csrf_secret_token for POST. The secret is thus never leaked in GET
URLs.
"""
form = insecure_form(url, method, **attrs)
if method.lower() == 'get':
return form
return form + HTML.div(hidden(session_csrf_secret_name, session_csrf_secret_token()), style="display: none;")
#
# Flash messages, stored in cookie
#
class _Message(object):
"""A message returned by ``pop_flash_messages()``.
Converting the message to a string returns the message text. Instances
also have the following attributes:
* ``category``: the category specified when the message was created.
* ``message``: the html-safe message text.
"""
def __init__(self, category, message):
self.category = category
self.message = message
def _session_flash_messages(append=None, clear=False):
"""Manage a message queue in tg.session: return the current message queue
after appending the given message, and possibly clearing the queue."""
key = 'flash'
if key in session:
flash_messages = session[key]
else:
if append is None: # common fast path - also used for clearing empty queue
return [] # don't bother saving
flash_messages = []
session[key] = flash_messages
if append is not None and append not in flash_messages:
flash_messages.append(append)
if clear:
session.pop(key, None)
session.save()
return flash_messages
def flash(message, category, logf=None):
"""
Show a message to the user _and_ log it through the specified function
category: notice (default), warning, error, success
logf: a custom log function - such as log.debug
logf defaults to log.info, unless category equals 'success', in which
case logf defaults to log.debug.
"""
assert category in ('error', 'success', 'warning'), category
if hasattr(message, '__html__'):
# render to HTML for storing in cookie
safe_message = str(message)
else:
# Apply str - the message might be an exception with __str__
# Escape, so we can trust the result without further escaping, without any risk of injection
safe_message = html_escape(str(message))
if logf is None:
logf = log.info
if category == 'success':
logf = log.debug
logf('Flash %s: %s', category, safe_message)
_session_flash_messages(append=(category, safe_message))
def pop_flash_messages():
"""Return all accumulated messages and delete them from the session.
The return value is a list of ``Message`` objects.
"""
return [_Message(category, message) for category, message in _session_flash_messages(clear=True)]
#
# Generic-ish formatting and markup
#
def js(value):
"""Convert Python value to the corresponding JavaScript representation.
This is necessary to safely insert arbitrary values into HTML <script>
sections e.g. using Mako template expression substitution.
Note: Rather than using this function, it's preferable to avoid the
insertion of values into HTML <script> sections altogether. Instead,
data should (to the extent possible) be passed to JavaScript using
data attributes or AJAX calls, eliminating the need for JS specific
escaping.
Note: This is not safe for use in attributes (e.g. onclick), because
quotes are not escaped.
Because the rules for parsing <script> varies between XHTML (where
normal rules apply for any special characters) and HTML (where
entities are not interpreted, but the literal string "</script>"
is forbidden), the function ensures that the result never contains
'&', '<' and '>', thus making it safe in both those contexts (but
not in attributes).
"""
return literal(
('(' + json.dumps(value) + ')')
# In JSON, the following can only appear in string literals.
.replace('&', r'\x26')
.replace('<', r'\x3c')
.replace('>', r'\x3e')
)
def jshtml(val):
"""HTML escapes a string value, then converts the resulting string
to its corresponding JavaScript representation (see `js`).
This is used when a plain-text string (possibly containing special
HTML characters) will be used by a script in an HTML context (e.g.
element.innerHTML or jQuery's 'html' method).
If in doubt, err on the side of using `jshtml` over `js`, since it's
better to escape too much than too little.
"""
return js(escape(val))
url_re = re.compile(r'''\bhttps?://(?:[\da-zA-Z0-9@:.-]+)'''
r'''(?:[/a-zA-Z0-9_=@#~&+%.,:;?!*()-]*[/a-zA-Z0-9_=@#~])?''')
# Must match regexp in kallithea/public/js/base.js MentionsAutoComplete()
# Check char before @ - it must not look like we are in an email addresses.
# Matching is greedy so we don't have to look beyond the end.
MENTIONS_REGEX = re.compile(r'(?:^|(?<=[^a-zA-Z0-9]))@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])')
def extract_mentioned_usernames(text):
r"""
Returns list of (possible) usernames @mentioned in given text.
>>> extract_mentioned_usernames('@1-2.a_X,@1234 not@not @ddd@not @n @ee @ff @gg, @gg;@hh @n\n@zz,')
['1-2.a_X', '1234', 'ddd', 'ee', 'ff', 'gg', 'gg', 'hh', 'zz']
"""
return MENTIONS_REGEX.findall(text)
|