prune_captcha 1.10.0__py3-none-any.whl → 1.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- captcha_prune/utils.py +27 -51
- {prune_captcha-1.10.0.dist-info → prune_captcha-1.12.0.dist-info}/METADATA +10 -41
- {prune_captcha-1.10.0.dist-info → prune_captcha-1.12.0.dist-info}/RECORD +5 -8
- {prune_captcha-1.10.0.dist-info → prune_captcha-1.12.0.dist-info}/WHEEL +1 -1
- captcha_prune/urls.py +0 -10
- captcha_prune/views.py +0 -45
- commons/decorators.py +0 -41
- {prune_captcha-1.10.0.dist-info → prune_captcha-1.12.0.dist-info}/top_level.txt +0 -0
captcha_prune/utils.py
CHANGED
@@ -1,68 +1,44 @@
|
|
1
1
|
import os
|
2
2
|
import random
|
3
|
-
from urllib.parse import urlencode
|
4
3
|
|
5
|
-
import requests
|
6
4
|
from django.conf import settings
|
7
|
-
from django.
|
8
|
-
from django.http import HttpRequest, HttpResponse
|
9
|
-
from django.urls import reverse
|
5
|
+
from django.shortcuts import get_object_or_404
|
10
6
|
|
7
|
+
from captcha_prune.models import Captcha
|
8
|
+
|
9
|
+
|
10
|
+
def create_and_get_captcha() -> dict:
|
11
|
+
captcha = Captcha.objects.create()
|
11
12
|
|
12
|
-
def create_and_get_puzzle(request: HttpRequest) -> HttpResponse | dict:
|
13
13
|
_, _, puzzle_images_path = settings.PUZZLE_IMAGE_STATIC_PATH.rpartition("static/")
|
14
|
-
try:
|
15
|
-
response = requests.post(
|
16
|
-
request.build_absolute_uri(reverse("captcha:create-captcha"))
|
17
|
-
).json()
|
18
|
-
except requests.RequestException:
|
19
|
-
return HttpResponse(status=502)
|
20
|
-
request.session["puzzle_uuid"] = response["uuid"]
|
21
14
|
puzzle_images = [
|
22
15
|
f
|
23
16
|
for f in os.listdir(settings.PUZZLE_IMAGE_STATIC_PATH)
|
24
|
-
if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif"))
|
17
|
+
if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp"))
|
25
18
|
]
|
26
19
|
selected_image = random.choice(puzzle_images)
|
20
|
+
|
27
21
|
return {
|
28
|
-
"uuid":
|
29
|
-
"width":
|
30
|
-
"height":
|
31
|
-
"piece_width":
|
32
|
-
"piece_height":
|
33
|
-
"pos_x_solution":
|
34
|
-
"pos_y_solution":
|
35
|
-
"piece_pos_x":
|
36
|
-
"piece_pos_y":
|
22
|
+
"uuid": captcha.uuid,
|
23
|
+
"width": captcha.width,
|
24
|
+
"height": captcha.height,
|
25
|
+
"piece_width": captcha.piece_width,
|
26
|
+
"piece_height": captcha.piece_height,
|
27
|
+
"pos_x_solution": captcha.pos_x_solution,
|
28
|
+
"pos_y_solution": captcha.pos_y_solution,
|
29
|
+
"piece_pos_x": captcha.piece_pos_x,
|
30
|
+
"piece_pos_y": captcha.piece_pos_y,
|
37
31
|
"image": f"{puzzle_images_path}{selected_image}",
|
38
32
|
}
|
39
33
|
|
40
34
|
|
41
|
-
def verify_captcha(
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
return
|
51
|
-
try:
|
52
|
-
base_url = request.build_absolute_uri(
|
53
|
-
reverse("captcha:verify-captcha", kwargs={"uuid": puzzle_uuid}),
|
54
|
-
)
|
55
|
-
data = {
|
56
|
-
"pos_x_answer": request.POST.get("pos_x_answer"),
|
57
|
-
"pos_y_answer": request.POST.get("pos_y_answer"),
|
58
|
-
}
|
59
|
-
query_string = urlencode(data)
|
60
|
-
full_url = f"{base_url}?{query_string}"
|
61
|
-
response = requests.get(full_url)
|
62
|
-
if response.status_code == 401:
|
63
|
-
messages.error(request, "Captcha incorrect. Veuillez réessayer.")
|
64
|
-
return incorrect_captcha
|
65
|
-
del form.fields["pos_x_answer"]
|
66
|
-
del form.fields["pos_y_answer"]
|
67
|
-
except requests.RequestException:
|
68
|
-
return HttpResponse(status=502)
|
35
|
+
def verify_captcha(puzzle_uuid: str, pos_x_answer: int, pos_y_answer: int) -> bool:
|
36
|
+
captcha = get_object_or_404(Captcha, uuid=puzzle_uuid)
|
37
|
+
|
38
|
+
if (
|
39
|
+
abs(captcha.pos_x_solution - pos_x_answer) <= captcha.precision
|
40
|
+
and abs(captcha.pos_y_solution - pos_y_answer) <= captcha.precision
|
41
|
+
):
|
42
|
+
return True
|
43
|
+
else:
|
44
|
+
return False
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: prune_captcha
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.12.0
|
4
4
|
Summary: A tool to protect formulaire from spam.
|
5
5
|
Author-email: Arnout <bastien@prune.sh>
|
6
6
|
Project-URL: Made_by, https://prune.sh/
|
@@ -61,21 +61,21 @@ Don't hesitate to regularly run `poetry update`, as the captcha evolves with tim
|
|
61
61
|
In `settings.py`, set the path to the images used for the puzzle:
|
62
62
|
|
63
63
|
```python
|
64
|
-
PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/"
|
64
|
+
PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/puzzles/"
|
65
65
|
```
|
66
66
|
|
67
67
|
### Utilisation
|
68
68
|
|
69
69
|
- GET request (form display)
|
70
70
|
|
71
|
-
- Use
|
71
|
+
- Use create_captcha to generate the captcha data:
|
72
72
|
|
73
73
|
```python
|
74
|
-
from captcha_prune.utils import
|
74
|
+
from captcha_prune.utils import create_captcha
|
75
75
|
```
|
76
76
|
|
77
77
|
```python
|
78
|
-
puzzle =
|
78
|
+
puzzle = create_captcha(request)
|
79
79
|
```
|
80
80
|
|
81
81
|
- Passes the data into the context under the puzzle variable:
|
@@ -103,50 +103,19 @@ PUZZLE_IMAGE_STATIC_PATH = "website/static/website/images/"
|
|
103
103
|
```
|
104
104
|
|
105
105
|
```python
|
106
|
-
response = verify_captcha(
|
107
|
-
request, redirect("/"), redirect("website:contact-page"), form
|
108
|
-
)
|
106
|
+
response = verify_captcha(request)
|
109
107
|
```
|
110
108
|
|
111
|
-
-
|
109
|
+
- True if the captcha is correct, else False.
|
112
110
|
|
113
|
-
- Redirects in case of
|
114
|
-
|
115
|
-
```python
|
116
|
-
if response is not None:
|
117
|
-
return response
|
118
|
-
```
|
119
|
-
|
120
|
-
### Example
|
121
|
-
|
122
|
-
```python
|
123
|
-
def contact_view(request):
|
124
|
-
if request.method == "POST":
|
125
|
-
form = ContactForm(request.POST)
|
126
|
-
if form.is_valid():
|
127
|
-
response = verify_captcha(
|
128
|
-
request, redirect("/"), redirect("website:contact-page"), form
|
129
|
-
)
|
130
|
-
if response is not None:
|
131
|
-
return response
|
132
|
-
messages.success(request, "Formulaire soumis avec succès.")
|
133
|
-
return redirect("/")
|
134
|
-
else:
|
135
|
-
puzzle = create_and_get_puzzle(request)
|
136
|
-
else:
|
137
|
-
form = ContactForm()
|
138
|
-
puzzle = create_and_get_puzzle(request)
|
139
|
-
return render(
|
140
|
-
request,
|
141
|
-
"website/pages/contact/page.html",
|
142
|
-
{"form": form, "puzzle": puzzle},
|
143
|
-
)
|
144
|
-
```
|
111
|
+
- Redirects in case of incorrect captcha.
|
145
112
|
|
146
113
|
# Available Versions
|
147
114
|
|
148
115
|
| Version | Date | Notes |
|
149
116
|
| ------- | ---------- | ---------------------------------- |
|
117
|
+
| 1.12.0 | 2025-05-21 | removed views, added utils, ... |
|
118
|
+
| 1.11.0 | 2025-05-21 | removed utils |
|
150
119
|
| 1.10.0 | 2025-05-20 | fix documentation, removed ... |
|
151
120
|
| 1.9.0 | 2025-05-20 | puzzle images path fixed |
|
152
121
|
| 1.8.0 | 2025-05-20 | added migrations |
|
@@ -4,20 +4,17 @@ captcha_prune/asgi.py,sha256=wi2rRLFltXOZyve8mAB_E8udaFyONOf5N42WrWIQX8M,403
|
|
4
4
|
captcha_prune/models.py,sha256=O8nxhVfPat3oaDGZzd88kQHkGayQIWZ_dOO9uu1R6P4,1240
|
5
5
|
captcha_prune/payloads.py,sha256=PwR48xKg9YEgGgoJmGe7ywwFPCqM4gumomyFunn-Ems,115
|
6
6
|
captcha_prune/settings.py,sha256=YEZHEjYYYK-cH3gSLIR3a7g4Bypnuf_5eMTTcm719CE,3395
|
7
|
-
captcha_prune/
|
8
|
-
captcha_prune/utils.py,sha256=3AYKlLxLUdGf9SBGg5Mgzym7LlxBpDhYv4ITFFZ0nAc,2415
|
9
|
-
captcha_prune/views.py,sha256=BzjQfXLwYx2YSWODrnGpokqU8XeC-9QORivzWKxYofY,1539
|
7
|
+
captcha_prune/utils.py,sha256=bEOMAA4jatS9eAgHEHpq0qNHjkCrywDGHAn81JPwbuo,1352
|
10
8
|
captcha_prune/wsgi.py,sha256=fukA_iiCT4FFzcJ0QX2Zr_HNddqdq-ln3ien2UWcCTA,403
|
11
9
|
captcha_prune/migrations/0001_initial.py,sha256=QMTaeSIxPociSrV4r89zfsbxMwMu4qpL16LT3yPgcaw,831
|
12
10
|
captcha_prune/migrations/0002_remove_captcha_created_at_remove_captcha_pos_x_and_more.py,sha256=xyyE_G3t8kJqFvKuNWFRTyTyyD1kzvfeSgbkTQ9DO8A,2099
|
13
11
|
captcha_prune/migrations/0003_captcha_created_at_captcha_updated_at.py,sha256=qT9cTDe3coiWXYbYfBlK4iEYg71E9B2FGuo74Wy_gbA,698
|
14
12
|
captcha_prune/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
13
|
commons/base_model.py,sha256=q1Q7lgtvN_t6ujhEiSsNNXb_ay0qY6FbfK0Zm2Hdgp0,213
|
16
|
-
commons/decorators.py,sha256=nU_3SyxENcWDdxo03VyNYEvoevplC6ig7BFogY8apNs,1306
|
17
14
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
15
|
tests/captcha_prune/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
16
|
tests/captcha_prune/test_views.py,sha256=h-Mrdiprh71s7kIJPYAt6xv4G_S7vp51WCpAzy4CZ6U,2364
|
20
|
-
prune_captcha-1.
|
21
|
-
prune_captcha-1.
|
22
|
-
prune_captcha-1.
|
23
|
-
prune_captcha-1.
|
17
|
+
prune_captcha-1.12.0.dist-info/METADATA,sha256=sU70ylcO8ntgm7y1CZqPQiEmwI7Yf-rXULvoZxn10Dw,3443
|
18
|
+
prune_captcha-1.12.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
19
|
+
prune_captcha-1.12.0.dist-info/top_level.txt,sha256=EB4h0WF_YGF7kAWB0XtqmuloOhkL5pC71CzgEVYam7w,28
|
20
|
+
prune_captcha-1.12.0.dist-info/RECORD,,
|
captcha_prune/urls.py
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
from django.urls import path
|
2
|
-
|
3
|
-
from captcha_prune.views import create_captcha_view, verify_captcha_view
|
4
|
-
|
5
|
-
app_name = "captcha"
|
6
|
-
|
7
|
-
urlpatterns = [
|
8
|
-
path("", create_captcha_view, name="create-captcha"),
|
9
|
-
path("<str:uuid>/", verify_captcha_view, name="verify-captcha"),
|
10
|
-
]
|
captcha_prune/views.py
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
from django.http import HttpRequest, HttpResponse, JsonResponse
|
2
|
-
from django.shortcuts import get_object_or_404
|
3
|
-
from django.views.decorators.csrf import csrf_exempt
|
4
|
-
from django.views.decorators.http import require_GET, require_POST
|
5
|
-
|
6
|
-
from captcha_prune.models import Captcha
|
7
|
-
from captcha_prune.payloads import PuzzleAnswerPayload
|
8
|
-
from commons.decorators import use_payload
|
9
|
-
|
10
|
-
|
11
|
-
@require_POST
|
12
|
-
@csrf_exempt
|
13
|
-
def create_captcha_view(request: HttpRequest) -> HttpResponse:
|
14
|
-
captcha = Captcha.objects.create()
|
15
|
-
return JsonResponse(
|
16
|
-
status=201,
|
17
|
-
data={
|
18
|
-
"uuid": str(captcha.uuid),
|
19
|
-
"width": captcha.width,
|
20
|
-
"height": captcha.height,
|
21
|
-
"piece_width": captcha.piece_width,
|
22
|
-
"piece_height": captcha.piece_height,
|
23
|
-
"pos_x_solution": captcha.pos_x_solution,
|
24
|
-
"pos_y_solution": captcha.pos_y_solution,
|
25
|
-
"piece_pos_x": captcha.piece_pos_x,
|
26
|
-
"piece_pos_y": captcha.piece_pos_y,
|
27
|
-
},
|
28
|
-
)
|
29
|
-
|
30
|
-
|
31
|
-
@require_GET
|
32
|
-
@use_payload(PuzzleAnswerPayload)
|
33
|
-
def verify_captcha_view(
|
34
|
-
request: HttpRequest, uuid: str, payload: PuzzleAnswerPayload
|
35
|
-
) -> HttpResponse:
|
36
|
-
captcha = get_object_or_404(Captcha, uuid=uuid)
|
37
|
-
pos_x_answer = payload.pos_x_answer
|
38
|
-
pos_y_answer = payload.pos_y_answer
|
39
|
-
if (
|
40
|
-
abs(captcha.pos_x_solution - pos_x_answer) <= captcha.precision
|
41
|
-
and abs(captcha.pos_y_solution - pos_y_answer) <= captcha.precision
|
42
|
-
):
|
43
|
-
return HttpResponse(status=304)
|
44
|
-
else:
|
45
|
-
return HttpResponse(status=401)
|
commons/decorators.py
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
import functools
|
2
|
-
from typing import get_origin
|
3
|
-
|
4
|
-
from django.http import HttpResponse
|
5
|
-
from pydantic import ValidationError
|
6
|
-
|
7
|
-
|
8
|
-
def use_payload(payload_class):
|
9
|
-
def decorator(func):
|
10
|
-
@functools.wraps(func)
|
11
|
-
def wrapper(request, *args, **kwargs):
|
12
|
-
data = {}
|
13
|
-
|
14
|
-
if request.method == "POST":
|
15
|
-
body = request.POST
|
16
|
-
elif request.method == "GET":
|
17
|
-
body = request.GET
|
18
|
-
else:
|
19
|
-
raise NotImplementedError
|
20
|
-
|
21
|
-
for field_name, field in payload_class.model_fields.items():
|
22
|
-
url_name = field.alias if field.alias else field_name
|
23
|
-
is_list_field = get_origin(field.annotation) is list
|
24
|
-
url_value = (
|
25
|
-
body.getlist(url_name) if is_list_field else body.get(url_name)
|
26
|
-
)
|
27
|
-
if url_value is not None:
|
28
|
-
data[url_name] = url_value
|
29
|
-
|
30
|
-
try:
|
31
|
-
payload = payload_class.model_validate(data)
|
32
|
-
except ValidationError as e:
|
33
|
-
return HttpResponse(
|
34
|
-
e.json(), headers={"content_type": "application/json"}, status=400
|
35
|
-
)
|
36
|
-
|
37
|
-
return func(request, *args, payload=payload, **kwargs)
|
38
|
-
|
39
|
-
return wrapper
|
40
|
-
|
41
|
-
return decorator
|
File without changes
|