Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2b061e87e | |||
| a82704d238 |
@@ -0,0 +1,69 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
Pyramid web app that uploads HTML files, strips unwanted tags, converts encoding to UTF-8, and serves cleaned files for download. UI is in French.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv env
|
||||||
|
env/bin/pip install --upgrade pip setuptools
|
||||||
|
env/bin/pip install -e ".[testing]"
|
||||||
|
env/bin/pserve development.ini # localhost:9480
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
env/bin/pytest # run all tests
|
||||||
|
env/bin/pytest tests/test_views.py # single file
|
||||||
|
env/bin/pserve development.ini # dev server (port 9480, template reload on)
|
||||||
|
env/bin/pserve production.ini # production server (port 9480, HTTPS)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
html_cleanup/
|
||||||
|
├── __init__.py # Pyramid app factory (main())
|
||||||
|
├── routes.py # Two routes: / (home), /view_log
|
||||||
|
├── views/
|
||||||
|
│ ├── default.py # Core logic: home(), process_file(), clean_html()
|
||||||
|
│ └── notfound.py # 404 handler
|
||||||
|
├── templates/ # Jinja2 templates (layout.jinja2, home.jinja2, view_log.jinja2)
|
||||||
|
└── static/
|
||||||
|
└── temp/ # Upload output dir — configured in INI files via temp_folder setting
|
||||||
|
```
|
||||||
|
|
||||||
|
- `clean_html()` in `views/default.py` is the core — it parses HTML tags and keeps only a whitelisted set (table, img, a, div, p, h1–h6, b, em, i, li, ol, ul, strong, sub, sup, u, s, del, ins, br, hr).
|
||||||
|
- The temp folder path is set via `temp_folder` in `.ini` files — **development.ini uses an absolute local path** (`/Users/phuoc/pyramid/html_cleanup/html_cleanup/static/temp/`).
|
||||||
|
- `view_log` view has a **hardcoded user list** for access control.
|
||||||
|
|
||||||
|
## Config Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `development.ini` | Dev config — debugtoolbar, template reload, local temp path |
|
||||||
|
| `production.ini` | Prod config — HTTPS, warns only |
|
||||||
|
| `testing.ini` | Test config — used by pytest via `conftest.py` |
|
||||||
|
| `pytest.ini` | `--strict-markers`, test paths: `html_cleanup` + `tests` |
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
- **Tests are broken**:
|
||||||
|
- `tests/test_views.py` imports `my_view` — function was renamed to `home` and is no longer importable without a proper test request.
|
||||||
|
- `tests/test_functional.py` asserts `b'Pyramid'` in response body, but the page title is "HTML cleanup".
|
||||||
|
- Do not rely on `tests/` passing as-is; fix tests if modifying views.
|
||||||
|
- The `clean_html` regex for id attributes (`id=\"\w+\"`) won't match hyphens despite a comment claiming hyphens are supported — only `[a-zA-Z0-9_]` is matched.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Templates use Jinja2 (not Mako). Extension: `.jinja2`.
|
||||||
|
- All user-facing strings are in French.
|
||||||
|
- Error logging goes to `static/temp/errors_log` (appended, not rotated).
|
||||||
|
- Tag whitelist in `clean_html()` is a series of `if/elif` chains — not a config or data structure.
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
- Analyze @html_cleanup/views/default.py .py for potential issues, bugs, and code quality problems
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
from pyramid.view import view_config
|
from pyramid.view import view_config
|
||||||
from pyramid.httpexceptions import HTTPFound
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import magic
|
import magic
|
||||||
import sys
|
|
||||||
import datetime
|
import datetime
|
||||||
import time
|
import time
|
||||||
import charset_normalizer
|
import charset_normalizer
|
||||||
import html
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@view_config(route_name='home', renderer='html_cleanup:templates/home.jinja2')
|
@view_config(route_name='home', renderer='html_cleanup:templates/home.jinja2')
|
||||||
@@ -22,6 +19,8 @@ def home(request):
|
|||||||
if request.POST['uploadfile'] != b'':
|
if request.POST['uploadfile'] != b'':
|
||||||
input_file = request.POST['uploadfile'].file
|
input_file = request.POST['uploadfile'].file
|
||||||
input_name = request.POST['uploadfile'].filename
|
input_name = request.POST['uploadfile'].filename
|
||||||
|
# Sanitize filename to prevent path traversal
|
||||||
|
input_name = os.path.basename(input_name)
|
||||||
# récupère le fichier download, faire les controles et traiter
|
# récupère le fichier download, faire les controles et traiter
|
||||||
message, input_encoding, output_name = process_file(request, input_file, input_name)
|
message, input_encoding, output_name = process_file(request, input_file, input_name)
|
||||||
# process without error ?
|
# process without error ?
|
||||||
@@ -84,12 +83,13 @@ def process_file(request, input_file, input_name):
|
|||||||
with open(input_filename, 'rb') as f:
|
with open(input_filename, 'rb') as f:
|
||||||
raw_bytes = f.read()
|
raw_bytes = f.read()
|
||||||
detected = charset_normalizer.detect(raw_bytes)
|
detected = charset_normalizer.detect(raw_bytes)
|
||||||
input_encoding = detected['encoding'].lower()
|
input_encoding = (detected.get('encoding') or 'utf-8').lower()
|
||||||
if input_encoding != 'utf-8':
|
if input_encoding != 'utf-8':
|
||||||
with open(input_filename, 'r', encoding=input_encoding) as f:
|
with open(input_filename, 'r', encoding=input_encoding) as f:
|
||||||
input_buffer = f.read()
|
input_buffer = f.read()
|
||||||
# --- check if input_file contains tag <body>
|
# --- check if input_file contains tag <body>
|
||||||
if input_buffer.find('<body') == -1 or input_buffer.find('</body>') == -1:
|
body_lower = input_buffer.lower()
|
||||||
|
if body_lower.find('<body') == -1 or body_lower.find('</body>') == -1:
|
||||||
message = "Le format du fichier n'est pas valide (absence de tag <body> ou </body>). Téléchargement refusé."
|
message = "Le format du fichier n'est pas valide (absence de tag <body> ou </body>). Téléchargement refusé."
|
||||||
add_error2log(logfile_name, input_name, message)
|
add_error2log(logfile_name, input_name, message)
|
||||||
return message, input_encoding, output_name
|
return message, input_encoding, output_name
|
||||||
@@ -109,8 +109,8 @@ def process_file(request, input_file, input_name):
|
|||||||
output_name = slots[0].strip() + "-clean." + slots[1]
|
output_name = slots[0].strip() + "-clean." + slots[1]
|
||||||
|
|
||||||
# controls OK, cleanup input_filename
|
# controls OK, cleanup input_filename
|
||||||
output_file = os.path.join(temp_folder, output_name)
|
output_path = os.path.join(temp_folder, output_name)
|
||||||
message = clean_html(input_filename, output_file)
|
message = clean_html(input_filename, output_path)
|
||||||
if message:
|
if message:
|
||||||
add_error2log(logfile_name, input_name, message)
|
add_error2log(logfile_name, input_name, message)
|
||||||
else:
|
else:
|
||||||
@@ -161,11 +161,10 @@ def clean_html(input_file, output_file):
|
|||||||
line_out = ""
|
line_out = ""
|
||||||
|
|
||||||
if not body:
|
if not body:
|
||||||
init = re.search(r'^\s*<body.*',line_in)
|
body_match = re.search(r'^\s*<body.*', line_in, re.IGNORECASE)
|
||||||
if init:
|
if body_match:
|
||||||
body = True
|
body = True
|
||||||
fo.write("<html>\n\t<head>\n\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset={}\">\n\t\t<title>untitled</title>\n\t</head>\n".format(encoding))
|
fo.write("<html>\n\t<head>\n\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset={}\">\n\t\t<title>untitled</title>\n\t</head>\n".format(encoding))
|
||||||
init=0
|
|
||||||
|
|
||||||
if body:
|
if body:
|
||||||
nb_lines += 1
|
nb_lines += 1
|
||||||
@@ -176,7 +175,7 @@ def clean_html(input_file, output_file):
|
|||||||
skip_tag = False
|
skip_tag = False
|
||||||
start_from = start_from + 1 if start_from < len(line_in) else 0
|
start_from = start_from + 1 if start_from < len(line_in) else 0
|
||||||
else:
|
else:
|
||||||
start_from = init if nb_lines == 1 else 0
|
start_from = 0
|
||||||
|
|
||||||
if start_from >= 0:
|
if start_from >= 0:
|
||||||
done = False
|
done = False
|
||||||
@@ -210,7 +209,7 @@ def clean_html(input_file, output_file):
|
|||||||
|
|
||||||
if tag:
|
if tag:
|
||||||
tag[0] = tag[0].lower()
|
tag[0] = tag[0].lower()
|
||||||
id_match_regex=r'.*(id=\"\w+\").*'
|
id_match_regex=r'.*(id=\"[\w-]+\").*'
|
||||||
|
|
||||||
if tag[0] in ["table", "img", "a"]:
|
if tag[0] in ["table", "img", "a"]:
|
||||||
line_out += "<{}".format(s)
|
line_out += "<{}".format(s)
|
||||||
@@ -240,7 +239,7 @@ def clean_html(input_file, output_file):
|
|||||||
elif tag[0] in ["/h1", "/h2", "/h3", "/h4", "/h5", "/h6", "/p",
|
elif tag[0] in ["/h1", "/h2", "/h3", "/h4", "/h5", "/h6", "/p",
|
||||||
"body", "/body", "/html", "/div", "/img", "/a"]:
|
"body", "/body", "/html", "/div", "/img", "/a"]:
|
||||||
line_out += "<{}>".format(tag[0])
|
line_out += "<{}>".format(tag[0])
|
||||||
elif tag[0] in ["table", "/table", "td", "/td", "th", "/th", "tr", "/tr"]:
|
elif tag[0] in ["/table", "td", "/td", "th", "/th", "tr", "/tr"]:
|
||||||
line_out += "<{}>".format(tag[0])
|
line_out += "<{}>".format(tag[0])
|
||||||
elif tag[0] in ["br", "br/", "hr"]:
|
elif tag[0] in ["br", "br/", "hr"]:
|
||||||
line_out += "<{}>".format(tag[0])
|
line_out += "<{}>".format(tag[0])
|
||||||
@@ -278,9 +277,8 @@ def view_log(request):
|
|||||||
if username in users:
|
if username in users:
|
||||||
# lire et affiche le fichier log
|
# lire et affiche le fichier log
|
||||||
try:
|
try:
|
||||||
file = open(logfile_name, "r")
|
with open(logfile_name, "r") as log_fh:
|
||||||
lines = file.readlines()
|
lines = log_fh.readlines()
|
||||||
file.close()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
message = "Le fichier Journal est vide !"
|
message = "Le fichier Journal est vide !"
|
||||||
pass
|
pass
|
||||||
@@ -295,11 +293,11 @@ def view_log(request):
|
|||||||
nbs = os.path.getctime(f_path) # en nombre de secondes
|
nbs = os.path.getctime(f_path) # en nombre de secondes
|
||||||
dtLocaltime = time.localtime(nbs) # convertir en local time
|
dtLocaltime = time.localtime(nbs) # convertir en local time
|
||||||
|
|
||||||
file = []
|
file_entry = []
|
||||||
file.append(time.strftime("%d/%m/%Y %H:%M",dtLocaltime))
|
file_entry.append(time.strftime("%d/%m/%Y %H:%M", dtLocaltime))
|
||||||
file.append(f.name)
|
file_entry.append(f.name)
|
||||||
file.append('%s Ko' % round(os.path.getsize(f_path) / 1024, 1))
|
file_entry.append('%s Ko' % round(os.path.getsize(f_path) / 1024, 1))
|
||||||
files_list.append(file)
|
files_list.append(file_entry)
|
||||||
else:
|
else:
|
||||||
message = "Identifiant invalide !"
|
message = "Identifiant invalide !"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user