Compare commits

...

2 Commits

Author SHA1 Message Date
phuoc d2b061e87e minor fixes by Opencode and remove temp files 2026-09-11 17:44:24 +02:00
phuoc a82704d238 minor fixes by Opencode 2026-09-11 17:43:02 +02:00
2 changed files with 88 additions and 21 deletions
+69
View File
@@ -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
+19 -21
View File
@@ -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 !"