Skip to content

Commit 52f45f3

Browse files
author
SM_SAYEED
committed
homepage issue resolved.
1 parent df3291a commit 52f45f3

File tree

2 files changed

+225
-11
lines changed

2 files changed

+225
-11
lines changed

app.py

Lines changed: 182 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def ensure_uploads_log_schema():
7070
""")
7171
conn.commit()
7272

73+
#==================================================#
74+
7375
def auto_log_material_files():
7476
"""
7577
Walk UPLOAD_FOLDER and upsert one row per (property, tab, filename).
@@ -121,7 +123,9 @@ def auto_log_material_files():
121123
conn.commit()
122124

123125
return {"status": "ok", "added_or_updated": added_or_updated}
124-
126+
127+
#==================================================#
128+
125129
# Automation of import to sqlite3 database
126130
def auto_import_uploads():
127131
"""
@@ -213,8 +217,9 @@ def tableize(name: str) -> str:
213217
print(f"auto_import_uploads: done, {imported} table(s) updated.")
214218
return imported
215219

216-
# Run-once warm-up
220+
#==================================================#
217221

222+
# Run-once warm-up
218223
from threading import Lock
219224

220225
_startup_done = False
@@ -245,12 +250,185 @@ def _startup_once():
245250
_run_startup_tasks()
246251

247252

248-
# ========== ROUTES ==========
253+
####################################### ========== ROUTES ==========#####################################
249254

250255
#########################################################
251256

252-
# Admin only rescanning for duplicates and re-importing
257+
# --- Public home + Admin SQL Query Tool (CRUD, multi-statement) ---
258+
def _list_user_tables():
259+
"""List non-internal SQLite tables for display in the SQL tool and home page."""
260+
with sqlite3.connect(DB_NAME) as conn:
261+
cur = conn.cursor()
262+
cur.execute("""
263+
SELECT name
264+
FROM sqlite_master
265+
WHERE type='table'
266+
AND name NOT LIKE 'sqlite_%'
267+
ORDER BY 1
268+
""")
269+
return [r[0] for r in cur.fetchall()]
270+
271+
#########################################################
272+
273+
# Public home used by multiple templates (and health check lands here )
274+
@app.route("/", methods=["GET"])
275+
@app.route("/home", methods=["GET"])
276+
def public_home():
277+
tables = _list_user_tables()
278+
return render_template("public_home.html", tables=tables)
279+
280+
#########################################################
281+
282+
@app.route("/materials", methods=["GET"])
283+
def materials_portal():
284+
root = app.config.get("UPLOAD_FOLDER", UPLOAD_FOLDER)
285+
props = []
286+
try:
287+
for d in os.listdir(root):
288+
pdir = os.path.join(root, d)
289+
if not os.path.isdir(pdir):
290+
continue
291+
# Only show properties that have at least one allowed file in dataset/ or results/
292+
has_any = False
293+
for tab in ("dataset", "results"):
294+
sub = os.path.join(pdir, tab)
295+
if not os.path.isdir(sub):
296+
continue
297+
for _r, _ds, files in os.walk(sub):
298+
if any(
299+
(f.rsplit(".", 1)[-1].lower() in (ALLOWED_DATASET_EXTENSIONS | ALLOWED_RESULTS_EXTENSIONS))
300+
for f in files
301+
):
302+
has_any = True
303+
break
304+
if has_any:
305+
break
306+
if has_any:
307+
props.append(d)
308+
except Exception as e:
309+
app.logger.warning("materials_portal: %s", e)
310+
311+
props.sort()
312+
items = []
313+
for p in props:
314+
pretty = p.replace("_", " ").title()
315+
items.append(
316+
f"<li><b>{pretty}</b> — "
317+
f"<a href='/materials/{p}/dataset'>Dataset</a> · "
318+
f"<a href='/materials/{p}/results'>Results</a></li>"
319+
)
320+
321+
html = (
322+
"<!doctype html><meta charset='utf-8'>"
323+
"<title>Materials</title>"
324+
"<style>body{font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif;max-width:760px;margin:2rem auto;padding:0 1rem;line-height:1.5}</style>"
325+
"<h1>Materials</h1>"
326+
"<ul>" + "".join(items) + "</ul>"
327+
"<p><a href='/'>← Back to home</a></p>"
328+
)
329+
return html, 200, {"Content-Type": "text/html; charset=utf-8"}
330+
331+
#########################################################
332+
333+
# SQL Query Tool (admin only, CRUD, multi-statement)
334+
DESTRUCTIVE_REGEX = re.compile(r"\b(drop|delete|update|alter|truncate)\b", re.IGNORECASE)
253335

336+
def _list_user_tables():
337+
with sqlite3.connect(DB_NAME) as conn:
338+
cur = conn.cursor()
339+
cur.execute("""
340+
SELECT name
341+
FROM sqlite_master
342+
WHERE type='table'
343+
AND name NOT LIKE 'sqlite_%'
344+
ORDER BY 1
345+
""")
346+
return [r[0] for r in cur.fetchall()]
347+
348+
def _strip_sql_comments(sql: str) -> str:
349+
# -- inline comments
350+
sql = re.sub(r"--.*?$", "", sql, flags=re.MULTILINE)
351+
# /* block comments */
352+
sql = re.sub(r"/\*.*?\*/", "", sql, flags=re.DOTALL)
353+
return sql
354+
355+
def _is_destructive(sql: str) -> bool:
356+
plain = _strip_sql_comments(sql)
357+
return bool(DESTRUCTIVE_REGEX.search(plain))
358+
359+
@app.route("/admin/sql", methods=["GET", "POST"])
360+
@app.route("/query_sql", methods=["GET", "POST"])
361+
def query_sql():
362+
if not session.get("admin"):
363+
return redirect(url_for("login"))
364+
365+
tables = _list_user_tables()
366+
sql = ""
367+
result_html = ""
368+
error_msg = ""
369+
needs_confirm = False
370+
371+
if request.method == "POST":
372+
sql = (request.form.get("sql") or "").strip()
373+
user_confirmed = (request.form.get("confirm") in ("on", "1", "true", "yes"))
374+
375+
if not sql:
376+
error_msg = "Please enter SQL."
377+
elif re.search(r"\bsqlite_\w+", sql, re.IGNORECASE):
378+
error_msg = "Queries that reference internal tables (sqlite_*) are blocked."
379+
else:
380+
try:
381+
# If destructive, require explicit confirmation
382+
if _is_destructive(sql) and not user_confirmed:
383+
needs_confirm = True
384+
error_msg = (
385+
"This query contains destructive statements "
386+
"(DROP/DELETE/UPDATE/ALTER/TRUNCATE). Check the box below to confirm and resubmit."
387+
)
388+
else:
389+
statements = [s.strip() for s in sql.split(";") if s.strip()]
390+
total_changed = 0
391+
last_select_html = None
392+
393+
with sqlite3.connect(DB_NAME) as conn:
394+
conn.execute("PRAGMA foreign_keys=ON;")
395+
cur = conn.cursor()
396+
397+
for stmt in statements:
398+
if re.match(r"^\s*(with\s+.*?select|select)\b", stmt, re.IGNORECASE | re.DOTALL):
399+
cur.execute(stmt)
400+
rows = cur.fetchall()
401+
cols = [d[0] for d in cur.description] if cur.description else []
402+
df = pd.DataFrame(rows, columns=cols)
403+
last_select_html = df.to_html(classes="data", index=False)
404+
else:
405+
cur.execute(stmt)
406+
total_changed = conn.total_changes
407+
408+
conn.commit()
409+
410+
result_html = (
411+
last_select_html
412+
if last_select_html is not None
413+
else f"<p><b>OK.</b> Executed {len(statements)} statement(s). "
414+
f"Total changed rows: {total_changed}.</p>"
415+
)
416+
417+
except Exception as e:
418+
error_msg = str(e)
419+
420+
return render_template(
421+
"sql_query.html", # <-- matches your existing template filename
422+
tables=tables,
423+
sql=sql,
424+
result_html=result_html,
425+
error_msg=error_msg,
426+
needs_confirm=needs_confirm,
427+
)
428+
429+
#########################################################
430+
431+
# Admin only rescanning for duplicates and re-importing
254432
@app.route('/admin/rescan_uploads')
255433
def rescan_uploads():
256434
"""

templates/sql_query.html

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!DOCTYPE html>
1+
<!DOCTYPE html>
22
<html>
33
<head>
44
<title>SQL Query Tool | Patterns Matter</title>
@@ -15,6 +15,12 @@
1515
margin-bottom: 1em;
1616
}
1717
.return-home:hover { background: #225c44; }
18+
.warn-box {
19+
margin-top: .75em; padding: .75em;
20+
border: 1px solid #c93; background: #fff8e5; border-radius: 6px;
21+
}
22+
.warn-box label { cursor: pointer; }
23+
textarea { width: 90%; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
1824
</style>
1925
</head>
2026
<body>
@@ -43,19 +49,49 @@ <h2>Available Tables:</h2>
4349
{% endfor %}
4450
</ul>
4551

46-
<form method="post">
47-
<label for="sql">Enter SQL SELECT query:</label><br>
48-
<textarea id="sql" name="sql" rows="6" style="width:90%;" required>{{ sql }}</textarea>
49-
<button type="submit">Run Query</button>
52+
<form method="post" id="sql-form">
53+
<label for="sql">Enter SQL (supports multiple statements separated by “;”):</label><br>
54+
<textarea id="sql" name="sql" rows="10" required>{{ sql }}</textarea>
55+
56+
<!-- Destructive confirmation (server-driven + client hint) -->
57+
<div id="confirm-wrap" class="warn-box" style="display: {{ 'block' if needs_confirm else 'none' }};">
58+
<input type="checkbox" id="confirm" name="confirm">
59+
<label for="confirm"><b>Are you sure?</b> Your query includes
60+
<code>DROP</code>/<code>DELETE</code>/<code>UPDATE</code>/<code>ALTER</code>/<code>TRUNCATE</code>.
61+
</label>
62+
</div>
63+
64+
<div style="margin-top: .8em;">
65+
<button type="submit">Run</button>
66+
</div>
5067
</form>
68+
5169
{% if result_html %}
5270
<h2>Result:</h2>
5371
{{ result_html|safe }}
5472
{% endif %}
5573
{% if error_msg %}
56-
<h3 style="color:red;">Error:</h3>
57-
<pre>{{ error_msg }}</pre>
74+
<h3 style="color:#a00;">Notice:</h3>
75+
<pre style="white-space: pre-wrap;">{{ error_msg }}</pre>
5876
{% endif %}
5977
</div>
78+
79+
<script>
80+
// Client-side helper: show confirm box if destructive terms detected.
81+
(function(){
82+
var ta = document.getElementById('sql');
83+
var wrap = document.getElementById('confirm-wrap');
84+
var re = /\b(drop|delete|update|alter|truncate)\b/i;
85+
86+
function update() {
87+
try {
88+
var s = ta.value.replace(/--.*?$/mg, "").replace(/\/\*[\s\S]*?\*\//g, "");
89+
wrap.style.display = re.test(s) ? 'block' : ({{ 'true' if needs_confirm else 'false' }}) ? 'block' : 'none';
90+
} catch (_) {}
91+
}
92+
ta && ta.addEventListener('input', update);
93+
update();
94+
})();
95+
</script>
6096
</body>
6197
</html>

0 commit comments

Comments
 (0)