Add missing files
This commit is contained in:
parent
3e968eeb0d
commit
3ce38e48ef
4
.gitreview
Normal file
4
.gitreview
Normal file
@ -0,0 +1,4 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack-ci/lodgeit.git
|
48
lodgeit/lib/antispam.py
Normal file
48
lodgeit/lib/antispam.py
Normal file
@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
lodgeit.lib.antispam
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Fight stupid spammers.
|
||||
|
||||
:copyright: 2007-2008 by Armin Ronacher, Christopher Grebs.
|
||||
:license: BSD
|
||||
"""
|
||||
from __future__ import division
|
||||
import re
|
||||
from operator import sub
|
||||
from itertools import starmap
|
||||
|
||||
|
||||
_url_pattern = (
|
||||
r'(?:(?:https?|ftps?|file|ssh|mms|irc|rsync|smb)://|'
|
||||
r'(?:mailto|telnet|s?news|sips?|skype):)'
|
||||
)
|
||||
|
||||
_link_re = re.compile(r'''(?xi)
|
||||
(%(url)s[^\s\'"]+)|
|
||||
(<a(?:\s+\w+\s*=\s*
|
||||
(?:"\s*%(url)s.*?"|'\s*%(url)s.*?'|%(url)s.*?)
|
||||
)+\s*>.*?</a>)
|
||||
''' % dict(url=_url_pattern))
|
||||
|
||||
|
||||
# maximum number of links in percent
|
||||
MAX_LINK_PERCENTAGE = 30
|
||||
|
||||
# maximum number of links in the text (hard limit)
|
||||
MAX_LINK_NUMBER = 15
|
||||
|
||||
|
||||
def check_for_link_spam(code):
|
||||
"""It's spam if more than 30% of the text are links."""
|
||||
spans = [x.span() for x in _link_re.finditer(code)]
|
||||
if len(spans) > MAX_LINK_PERCENTAGE:
|
||||
return True
|
||||
return (sum(starmap(sub, spans)) * -100) / (len(code) or 1) \
|
||||
> MAX_LINK_PERCENTAGE
|
||||
|
||||
|
||||
def is_spam(code):
|
||||
"""Check if the code provided contains spam."""
|
||||
return check_for_link_spam(code)
|
412
lodgeit/lib/captcha.py
Normal file
412
lodgeit/lib/captcha.py
Normal file
@ -0,0 +1,412 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
lodgeit.captcha
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
A module that produces image and audio captchas. Uses some code of
|
||||
PyCAPTCHA by Micah Dowty and was originally used in inyoka.
|
||||
|
||||
|
||||
:copyright: Copyright 2007-2008 by Armin Ronacher, Micah Dowty.
|
||||
:license: BSD
|
||||
"""
|
||||
import random
|
||||
import colorsys
|
||||
import math
|
||||
from os import listdir
|
||||
from os.path import abspath, join, dirname, pardir
|
||||
from PIL import ImageFont, ImageDraw, Image, ImageChops, ImageColor
|
||||
from werkzeug import Response
|
||||
from lodgeit import local
|
||||
|
||||
try:
|
||||
from hashlib import sha1
|
||||
except ImportError:
|
||||
from sha import new as sha1
|
||||
|
||||
resource_path = abspath(join(dirname(__file__), pardir, 'res'))
|
||||
|
||||
|
||||
def check_hashed_solution(solution, hashed_solution=None, secret_key=None):
|
||||
"""Check a solution against the hashed solution from the first
|
||||
request by using the secret key.
|
||||
"""
|
||||
if hashed_solution is None:
|
||||
hashed_solution = local.request.session.get('captcha_id')
|
||||
if hashed_solution is None:
|
||||
return False
|
||||
return hashed_solution == calculate_hash(solution, secret_key)
|
||||
|
||||
|
||||
def calculate_hash(solution, secret_key=None):
|
||||
"""Calculate the hash."""
|
||||
if secret_key is None:
|
||||
secret_key = local.application.secret_key
|
||||
return sha1('%s|%s' % (
|
||||
secret_key,
|
||||
solution.encode('utf-8')
|
||||
)).hexdigest()
|
||||
|
||||
|
||||
def generate_word():
|
||||
"""This function returns a pronounceable word."""
|
||||
consonants = 'bcdfghjklmnprstvwz'
|
||||
vowels = 'aeiou'
|
||||
both = consonants + vowels
|
||||
length = random.randrange(8, 12)
|
||||
return ''.join(
|
||||
random.choice(consonants) +
|
||||
random.choice(vowels) +
|
||||
random.choice(both) for x in xrange(length // 3)
|
||||
)[:length]
|
||||
|
||||
|
||||
def get_random_resource(type, prefix=None):
|
||||
"""Return a random resource of a given type."""
|
||||
path = join(resource_path, type)
|
||||
choices = (x for x in listdir(path) if not x.startswith('.'))
|
||||
if prefix is not None:
|
||||
choices = (x for x in choices if x.startswith(prefix))
|
||||
return join(path, random.choice(tuple(choices)))
|
||||
|
||||
|
||||
def random_color(saturation=0.5, lumination=None):
|
||||
"""Return a random number with the given saturation."""
|
||||
hue = random.random()
|
||||
if lumination is None:
|
||||
lumination = random.random()
|
||||
r, g, b = colorsys.hls_to_rgb(hue, lumination, saturation)
|
||||
return '#%02x%02x%02x' % (
|
||||
int(r * 255) & 0xff,
|
||||
int(g * 255) & 0xff,
|
||||
int(b * 255) & 0xff
|
||||
)
|
||||
|
||||
|
||||
class Captcha(object):
|
||||
"""Represents a captcha."""
|
||||
default_size = (300, 100)
|
||||
|
||||
def __init__(self, solution=None):
|
||||
if solution is None:
|
||||
solution = generate_word()
|
||||
self.solution = solution
|
||||
self.layers = [
|
||||
RandomBackground(),
|
||||
RandomDistortion()
|
||||
]
|
||||
text_layer = TextLayer(self.solution, bg=self.layers[0].bg)
|
||||
self.layers.extend((text_layer, SineWarp()))
|
||||
|
||||
def hash_solution(self, secret_key=None):
|
||||
"""Return the solution as hashed value."""
|
||||
return calculate_hash(self.solution, secret_key)
|
||||
|
||||
def render_image(self, size=None):
|
||||
if size is None:
|
||||
size = self.default_size
|
||||
image = Image.new('RGBA', size)
|
||||
for layer in self.layers:
|
||||
image = layer.render(image)
|
||||
return image
|
||||
|
||||
def get_response(self, size=None, set_cookie=False):
|
||||
response = Response(mimetype='image/png')
|
||||
self.render_image(size=None).save(response.stream, 'PNG')
|
||||
if set_cookie:
|
||||
local.request.session['captcha_id'] = self.hash_solution()
|
||||
return response
|
||||
|
||||
|
||||
class Layer(object):
|
||||
"""Baseclass for a captcha layer."""
|
||||
bg = 'dark'
|
||||
|
||||
def render(self, image):
|
||||
return image
|
||||
|
||||
|
||||
class TextLayer(Layer):
|
||||
"""Add text to the captcha."""
|
||||
bg = 'transparent'
|
||||
|
||||
def __init__(self, text, min_size=32, max_size=48, bg='dark'):
|
||||
self.text = text
|
||||
self.alignment = (random.random(), random.random())
|
||||
if bg == 'dark':
|
||||
color = random_color(saturation=0.3, lumination=0.8)
|
||||
else:
|
||||
color = random_color(saturation=0.1, lumination=0.1)
|
||||
self.text_color = color
|
||||
self.transparency = random.randint(20, 60)
|
||||
self.font = ImageFont.truetype(get_random_resource('fonts'),
|
||||
random.randrange(min_size, max_size))
|
||||
|
||||
def render(self, image):
|
||||
text_layer = Image.new('RGB', image.size, (0, 0, 0))
|
||||
alpha = Image.new('L', image.size, 0)
|
||||
|
||||
# draw grayscale image white on black
|
||||
text_image = Image.new('L', image.size, 0)
|
||||
draw = ImageDraw.Draw(text_image)
|
||||
text_size = self.font.getsize(self.text)
|
||||
x = int((image.size[0] - text_size[0]) * self.alignment[0] + 0.5)
|
||||
y = int((image.size[1] - text_size[1]) * self.alignment[1] + 0.5)
|
||||
draw.text((x, y), self.text, font=self.font,
|
||||
fill=255 - self.transparency)
|
||||
|
||||
# colorize the text and calculate the alpha channel
|
||||
alpha = ImageChops.lighter(alpha, text_image)
|
||||
color_layer = Image.new('RGBA', image.size, self.text_color)
|
||||
mask = Image.eval(text_image, lambda x: 255 * (x != 0))
|
||||
text_layer = Image.composite(color_layer, text_layer, mask)
|
||||
|
||||
# paste the text on the image with the correct alphachannel
|
||||
image.paste(text_layer, alpha)
|
||||
return image
|
||||
|
||||
|
||||
class CombinedLayer(Layer):
|
||||
"""Combines multiple layers."""
|
||||
|
||||
def __init__(self, layers):
|
||||
self.layers = layers
|
||||
if layers:
|
||||
self.bg = layers[0].bg
|
||||
|
||||
def render(self, image):
|
||||
for layer in self.layers:
|
||||
image = layer.render(image)
|
||||
return image
|
||||
|
||||
|
||||
class RandomBackground(CombinedLayer):
|
||||
"""Selects a random background."""
|
||||
|
||||
def __init__(self):
|
||||
layers = [random.choice([SolidColor, DarkBackground,
|
||||
LightBackground])()]
|
||||
for x in xrange(random.randrange(1, 4)):
|
||||
layers.append(random.choice([
|
||||
NoiseBackground,
|
||||
GridBackground
|
||||
])())
|
||||
CombinedLayer.__init__(self, layers)
|
||||
self.bg = layers[0].bg
|
||||
|
||||
|
||||
class RandomDistortion(CombinedLayer):
|
||||
"""Selects a random distortion."""
|
||||
background = 'transparent'
|
||||
|
||||
def __init__(self):
|
||||
layers = []
|
||||
for x in xrange(random.randrange(1, 3)):
|
||||
layers.append(random.choice((
|
||||
WigglyBlocks,
|
||||
SineWarp
|
||||
))())
|
||||
CombinedLayer.__init__(self, layers)
|
||||
|
||||
|
||||
class Picture(Layer):
|
||||
"""Add a background to the captcha."""
|
||||
|
||||
def __init__(self, picture):
|
||||
self.image = Image.open(picture)
|
||||
self.offset = (random.random(), random.random())
|
||||
|
||||
def render(self, image):
|
||||
tile = self.image
|
||||
for j in xrange(-1, int(image.size[1] / tile.size[1]) + 1):
|
||||
for i in xrange(-1, int(image.size[0] / tile.size[0]) + 1):
|
||||
dest = (int((self.offset[0] + i) * tile.size[0]),
|
||||
int((self.offset[1] + j) * tile.size[1]))
|
||||
image.paste(tile, dest)
|
||||
return image
|
||||
|
||||
|
||||
class LightBackground(Picture):
|
||||
bg = 'light'
|
||||
|
||||
def __init__(self):
|
||||
Picture.__init__(self, get_random_resource('backgrounds/light'))
|
||||
|
||||
|
||||
class DarkBackground(Picture):
|
||||
|
||||
def __init__(self):
|
||||
Picture.__init__(self, get_random_resource('backgrounds/dark'))
|
||||
|
||||
|
||||
class NoiseBackground(Layer):
|
||||
"""Add some noise as background. You can combine this with another
|
||||
background layer.
|
||||
"""
|
||||
bg = 'transparent'
|
||||
|
||||
def __init__(self, saturation=0.1, num_dots=None):
|
||||
self.saturation = saturation
|
||||
self.num_dots = random.randrange(300, 500)
|
||||
self.seed = random.random()
|
||||
|
||||
def render(self, image):
|
||||
r = random.Random(self.seed)
|
||||
for i in xrange(self.num_dots):
|
||||
dot_size = random.randrange(1, 5)
|
||||
bx = int(r.uniform(0, image.size[0] - dot_size))
|
||||
by = int(r.uniform(0, image.size[1] - dot_size))
|
||||
image.paste(random_color(self.saturation, 0.4),
|
||||
(bx, by, bx + dot_size - 1,
|
||||
by + dot_size - 1))
|
||||
return image
|
||||
|
||||
|
||||
class GridBackground(Layer):
|
||||
"""Add a grid as background. You can combine this with another
|
||||
background layer.
|
||||
"""
|
||||
bg = 'transparent'
|
||||
|
||||
def __init__(self, size=None, color=None):
|
||||
if size is None:
|
||||
size = random.randrange(10, 50)
|
||||
if color is None:
|
||||
color = random_color(0, 0.4)
|
||||
self.size = size
|
||||
self.color = color
|
||||
self.offset = (random.uniform(0, self.size),
|
||||
random.uniform(0, self.size))
|
||||
|
||||
def render(self, image):
|
||||
draw = ImageDraw.Draw(image)
|
||||
for i in xrange(image.size[0] / self.size + 1):
|
||||
draw.line((i * self.size + self.offset[0], 0,
|
||||
i * self.size + self.offset[0], image.size[1]),
|
||||
fill=self.color)
|
||||
for i in xrange(image.size[0] / self.size + 1):
|
||||
draw.line((0, i * self.size + self.offset[1],
|
||||
image.size[0], i * self.size+self.offset[1]),
|
||||
fill=self.color)
|
||||
return image
|
||||
|
||||
|
||||
class SolidColor(Layer):
|
||||
"""A solid color background. Very weak on its own, but good
|
||||
to combine with other backgrounds.
|
||||
"""
|
||||
|
||||
def __init__(self, color=None):
|
||||
if color is None:
|
||||
color = random_color(0.2, random.random() > 0.5 and 0.3 or 0.7)
|
||||
self.color = ImageColor.getrgb(color)
|
||||
if colorsys.rgb_to_hls(*[x / 255.0 for x in self.color])[1] > 0.5:
|
||||
self.bg = 'light'
|
||||
|
||||
def render(self, image):
|
||||
image.paste(self.color)
|
||||
return image
|
||||
|
||||
|
||||
class WigglyBlocks(Layer):
|
||||
"""Randomly select and shift blocks of the image"""
|
||||
bg = 'transparent'
|
||||
|
||||
def __init__(self, block_size=None, sigma=0.01, iterations=None):
|
||||
if block_size is None:
|
||||
block_size = random.randrange(15, 25)
|
||||
if iterations is None:
|
||||
iterations = random.randrange(250, 350)
|
||||
self.block_size = block_size
|
||||
self.sigma = sigma
|
||||
self.iterations = iterations
|
||||
self.seed = random.random()
|
||||
|
||||
def render(self, image):
|
||||
r = random.Random(self.seed)
|
||||
for i in xrange(self.iterations):
|
||||
# Select a block
|
||||
bx = int(r.uniform(0, image.size[0] - self.block_size))
|
||||
by = int(r.uniform(0, image.size[1] - self.block_size))
|
||||
block = image.crop((bx, by, bx + self.block_size - 1,
|
||||
by + self.block_size - 1))
|
||||
|
||||
# Figure out how much to move it.
|
||||
# The call to floor() is important so we always round toward
|
||||
# 0 rather than to -inf. Just int() would bias the block motion.
|
||||
mx = int(math.floor(r.normalvariate(0, self.sigma)))
|
||||
my = int(math.floor(r.normalvariate(0, self.sigma)))
|
||||
|
||||
# Now actually move the block
|
||||
image.paste(block, (bx+mx, by+my))
|
||||
return image
|
||||
|
||||
|
||||
class WarpBase(Layer):
|
||||
"""Abstract base class for image warping. Subclasses define a function
|
||||
that maps points in the output image to points in the input image. This
|
||||
warping engine runs a grid of points through this transform and uses PIL's
|
||||
mesh transform to warp the image.
|
||||
"""
|
||||
bg = 'transparent'
|
||||
filtering = Image.BILINEAR
|
||||
resolution = 10
|
||||
|
||||
def get_transform(self, image):
|
||||
"""Return a transformation function, subclasses should override this"""
|
||||
return lambda x, y: (x, y)
|
||||
|
||||
def render(self, image):
|
||||
r = self.resolution
|
||||
x_points = image.size[0] / r + 2
|
||||
y_points = image.size[1] / r + 2
|
||||
f = self.get_transform(image)
|
||||
|
||||
# Create a list of arrays with transformed points
|
||||
x_rows = []
|
||||
y_rows = []
|
||||
for j in xrange(y_points):
|
||||
x_row = []
|
||||
y_row = []
|
||||
for i in xrange(x_points):
|
||||
x, y = f(i * r, j * r)
|
||||
|
||||
# Clamp the edges so we don't get black undefined areas
|
||||
x = max(0, min(image.size[0] - 1, x))
|
||||
y = max(0, min(image.size[1] - 1, y))
|
||||
|
||||
x_row.append(x)
|
||||
y_row.append(y)
|
||||
x_rows.append(x_row)
|
||||
y_rows.append(y_row)
|
||||
|
||||
# Create the mesh list, with a transformation for
|
||||
# each square between points on the grid
|
||||
mesh = []
|
||||
for j in xrange(y_points - 1):
|
||||
for i in xrange(x_points-1):
|
||||
mesh.append((
|
||||
# Destination rectangle
|
||||
(i * r, j * r, (i + 1) * r, (j + 1) * r),
|
||||
# Source quadrilateral
|
||||
(x_rows[j][i], y_rows[j][i],
|
||||
x_rows[j + 1][i], y_rows[j+1][i],
|
||||
x_rows[j + 1][i + 1], y_rows[j + 1][i + 1],
|
||||
x_rows[j][i+1], y_rows[j][i + 1]),
|
||||
))
|
||||
return image.transform(image.size, Image.MESH, mesh, self.filtering)
|
||||
|
||||
|
||||
class SineWarp(WarpBase):
|
||||
"""Warp the image using a random composition of sine waves"""
|
||||
|
||||
def __init__(self, amplitude_range=(3, 6.5), period_range=(0.04, 0.1)):
|
||||
self.amplitude = random.uniform(*amplitude_range)
|
||||
self.period = random.uniform(*period_range)
|
||||
self.offset = (random.uniform(0, math.pi * 2 / self.period),
|
||||
random.uniform(0, math.pi * 2 / self.period))
|
||||
|
||||
def get_transform(self, image):
|
||||
return (lambda x, y, a=self.amplitude, p=self.period,
|
||||
o=self.offset: (math.sin((y + o[0]) * p) * a + x,
|
||||
math.sin((x + o[1]) * p) * a + y))
|
Loading…
Reference in New Issue
Block a user