Compare commits
2 Commits
218c27c726
...
d2b061e87e
| 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.httpexceptions import HTTPFound
|
||||
import os
|
||||
import shutil
|
||||
import magic
|
||||
import sys
|
||||
import datetime
|
||||
import time
|
||||
import charset_normalizer
|
||||
import html
|
||||
import re
|
||||
|
||||
@view_config(route_name='home', renderer='html_cleanup:templates/home.jinja2')
|
||||
@@ -22,6 +19,8 @@ def home(request):
|
||||
if request.POST['uploadfile'] != b'':
|
||||
input_file = request.POST['uploadfile'].file
|
||||
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
|
||||
message, input_encoding, output_name = process_file(request, input_file, input_name)
|
||||
# process without error ?
|
||||
@@ -84,12 +83,13 @@ def process_file(request, input_file, input_name):
|
||||
with open(input_filename, 'rb') as f:
|
||||
raw_bytes = f.read()
|
||||
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':
|
||||
with open(input_filename, 'r', encoding=input_encoding) as f:
|
||||
input_buffer = f.read()
|
||||
# --- 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é."
|
||||
add_error2log(logfile_name, input_name, message)
|
||||
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]
|
||||
|
||||
# controls OK, cleanup input_filename
|
||||
output_file = os.path.join(temp_folder, output_name)
|
||||
message = clean_html(input_filename, output_file)
|
||||
output_path = os.path.join(temp_folder, output_name)
|
||||
message = clean_html(input_filename, output_path)
|
||||
if message:
|
||||
add_error2log(logfile_name, input_name, message)
|
||||
else:
|
||||
@@ -161,11 +161,10 @@ def clean_html(input_file, output_file):
|
||||
line_out = ""
|
||||
|
||||
if not body:
|
||||
init = re.search(r'^\s*<body.*',line_in)
|
||||
if init:
|
||||
body_match = re.search(r'^\s*<body.*', line_in, re.IGNORECASE)
|
||||
if body_match:
|
||||
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))
|
||||
init=0
|
||||
|
||||
if body:
|
||||
nb_lines += 1
|
||||
@@ -176,7 +175,7 @@ def clean_html(input_file, output_file):
|
||||
skip_tag = False
|
||||
start_from = start_from + 1 if start_from < len(line_in) else 0
|
||||
else:
|
||||
start_from = init if nb_lines == 1 else 0
|
||||
start_from = 0
|
||||
|
||||
if start_from >= 0:
|
||||
done = False
|
||||
@@ -210,7 +209,7 @@ def clean_html(input_file, output_file):
|
||||
|
||||
if tag:
|
||||
tag[0] = tag[0].lower()
|
||||
id_match_regex=r'.*(id=\"\w+\").*'
|
||||
id_match_regex=r'.*(id=\"[\w-]+\").*'
|
||||
|
||||
if tag[0] in ["table", "img", "a"]:
|
||||
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",
|
||||
"body", "/body", "/html", "/div", "/img", "/a"]:
|
||||
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])
|
||||
elif tag[0] in ["br", "br/", "hr"]:
|
||||
line_out += "<{}>".format(tag[0])
|
||||
@@ -278,9 +277,8 @@ def view_log(request):
|
||||
if username in users:
|
||||
# lire et affiche le fichier log
|
||||
try:
|
||||
file = open(logfile_name, "r")
|
||||
lines = file.readlines()
|
||||
file.close()
|
||||
with open(logfile_name, "r") as log_fh:
|
||||
lines = log_fh.readlines()
|
||||
except Exception as e:
|
||||
message = "Le fichier Journal est vide !"
|
||||
pass
|
||||
@@ -295,11 +293,11 @@ def view_log(request):
|
||||
nbs = os.path.getctime(f_path) # en nombre de secondes
|
||||
dtLocaltime = time.localtime(nbs) # convertir en local time
|
||||
|
||||
file = []
|
||||
file.append(time.strftime("%d/%m/%Y %H:%M",dtLocaltime))
|
||||
file.append(f.name)
|
||||
file.append('%s Ko' % round(os.path.getsize(f_path) / 1024, 1))
|
||||
files_list.append(file)
|
||||
file_entry = []
|
||||
file_entry.append(time.strftime("%d/%m/%Y %H:%M", dtLocaltime))
|
||||
file_entry.append(f.name)
|
||||
file_entry.append('%s Ko' % round(os.path.getsize(f_path) / 1024, 1))
|
||||
files_list.append(file_entry)
|
||||
else:
|
||||
message = "Identifiant invalide !"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user