improving safety, Sanitizing input, Validating and sanitizing data, Ensuring proper HTML escaping
This commit is contained in:
parent
bee297bf19
commit
df9085f479
BIN
anime_backlog.db-shm
Normal file
BIN
anime_backlog.db-shm
Normal file
Binary file not shown.
BIN
anime_backlog.db-wal
Normal file
BIN
anime_backlog.db-wal
Normal file
Binary file not shown.
61
backend.py
61
backend.py
@ -1,6 +1,7 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
|
import html
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(filename='anime_tracker.log', level=logging.ERROR,
|
logging.basicConfig(filename='anime_tracker.log', level=logging.ERROR,
|
||||||
@ -8,7 +9,8 @@ logging.basicConfig(filename='anime_tracker.log', level=logging.ERROR,
|
|||||||
|
|
||||||
class AnimeBackend:
|
class AnimeBackend:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.db = sqlite3.connect('anime_backlog.db')
|
self.db = sqlite3.connect('anime_backlog.db', isolation_level=None) # Autocommit mode to prevent locks
|
||||||
|
self.db.execute('PRAGMA journal_mode=WAL') # Use WAL mode for better concurrency
|
||||||
self.create_table()
|
self.create_table()
|
||||||
|
|
||||||
def create_table(self):
|
def create_table(self):
|
||||||
@ -68,25 +70,64 @@ class AnimeBackend:
|
|||||||
|
|
||||||
def add_anime(self, data):
|
def add_anime(self, data):
|
||||||
try:
|
try:
|
||||||
|
# Sanitize string inputs
|
||||||
|
sanitized_data = {
|
||||||
|
'name': html.escape(data['name'].strip()) if data['name'] else '',
|
||||||
|
'year': data['year'],
|
||||||
|
'season': data['season'].strip() if data['season'] else '',
|
||||||
|
'status': data['status'].strip() if data['status'] else 'unwatched',
|
||||||
|
'type': data['type'].strip() if data['type'] else '',
|
||||||
|
'comment': html.escape(data['comment'].strip()) if data['comment'] else '',
|
||||||
|
'url': data['url'].strip() if data['url'] else ''
|
||||||
|
}
|
||||||
cursor = self.db.cursor()
|
cursor = self.db.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO anime (name, year, season, status, type, comment, url) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO anime (name, year, season, status, type, comment, url) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
(data['name'], data['year'], data['season'], data['status'], data['type'], data['comment'], data['url'])
|
(
|
||||||
|
sanitized_data['name'],
|
||||||
|
sanitized_data['year'],
|
||||||
|
sanitized_data['season'],
|
||||||
|
sanitized_data['status'],
|
||||||
|
sanitized_data['type'],
|
||||||
|
sanitized_data['comment'],
|
||||||
|
sanitized_data['url']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error adding anime: {e}")
|
logging.error(f"Error adding anime: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def edit_anime(self, anime_id, data):
|
def edit_anime(self, anime_id, data):
|
||||||
try:
|
try:
|
||||||
|
# Sanitize string inputs
|
||||||
|
sanitized_data = {
|
||||||
|
'name': html.escape(data['name'].strip()) if data['name'] else '',
|
||||||
|
'year': data['year'],
|
||||||
|
'season': data['season'].strip() if data['season'] else '',
|
||||||
|
'status': data['status'].strip() if data['status'] else 'unwatched',
|
||||||
|
'type': data['type'].strip() if data['type'] else '',
|
||||||
|
'comment': html.escape(data['comment'].strip()) if data['comment'] else '',
|
||||||
|
'url': data['url'].strip() if data['url'] else ''
|
||||||
|
}
|
||||||
cursor = self.db.cursor()
|
cursor = self.db.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE anime SET name=?, year=?, season=?, status=?, type=?, comment=?, url=? WHERE id=?",
|
"UPDATE anime SET name=?, year=?, season=?, status=?, type=?, comment=?, url=? WHERE id=?",
|
||||||
(data['name'], data['year'], data['season'], data['status'], data['type'], data['comment'], data['url'], anime_id)
|
(
|
||||||
|
sanitized_data['name'],
|
||||||
|
sanitized_data['year'],
|
||||||
|
sanitized_data['season'],
|
||||||
|
sanitized_data['status'],
|
||||||
|
sanitized_data['type'],
|
||||||
|
sanitized_data['comment'],
|
||||||
|
sanitized_data['url'],
|
||||||
|
anime_id
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error editing anime id {anime_id}: {e}")
|
logging.error(f"Error editing anime id {anime_id}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def delete_anime(self, anime_id):
|
def delete_anime(self, anime_id):
|
||||||
try:
|
try:
|
||||||
@ -95,6 +136,7 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error deleting anime id {anime_id}: {e}")
|
logging.error(f"Error deleting anime id {anime_id}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def change_status(self, anime_id, new_status):
|
def change_status(self, anime_id, new_status):
|
||||||
try:
|
try:
|
||||||
@ -103,6 +145,7 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error changing status for anime id {anime_id}: {e}")
|
logging.error(f"Error changing status for anime id {anime_id}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def add_placeholders_for_year(self, year):
|
def add_placeholders_for_year(self, year):
|
||||||
try:
|
try:
|
||||||
@ -115,6 +158,7 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error adding placeholders for year {year}: {e}")
|
logging.error(f"Error adding placeholders for year {year}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def import_from_csv(self, file_name):
|
def import_from_csv(self, file_name):
|
||||||
try:
|
try:
|
||||||
@ -134,6 +178,13 @@ class AnimeBackend:
|
|||||||
year = int(year_str)
|
year = int(year_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
# Sanitize CSV inputs
|
||||||
|
name = html.escape(name.strip()) if name else ''
|
||||||
|
season = season.strip() if season else ''
|
||||||
|
status = status.strip() if status else 'unwatched'
|
||||||
|
type_ = type_.strip() if type_ else ''
|
||||||
|
comment = html.escape(comment.strip()) if comment else ''
|
||||||
|
url = url.strip() if url else ''
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT id FROM anime WHERE name = ? AND year = ? AND season = ?",
|
"SELECT id FROM anime WHERE name = ? AND year = ? AND season = ?",
|
||||||
(name, year, season)
|
(name, year, season)
|
||||||
@ -146,6 +197,7 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error importing from CSV {file_name}: {e}")
|
logging.error(f"Error importing from CSV {file_name}: {e}")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
def export_to_csv(self, file_name):
|
def export_to_csv(self, file_name):
|
||||||
try:
|
try:
|
||||||
@ -153,7 +205,7 @@ class AnimeBackend:
|
|||||||
cursor.execute("SELECT * FROM anime")
|
cursor.execute("SELECT * FROM anime")
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
with open(file_name, 'w', newline='') as f:
|
with open(file_name, 'w', newline='') as f:
|
||||||
writer = csv.writer(f)
|
writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
|
||||||
writer.writerow(['id', 'name', 'year', 'season', 'status', 'type', 'comment', 'url'])
|
writer.writerow(['id', 'name', 'year', 'season', 'status', 'type', 'comment', 'url'])
|
||||||
writer.writerows(rows)
|
writer.writerows(rows)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -166,3 +218,4 @@ class AnimeBackend:
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error deleting year {year}: {e}")
|
logging.error(f"Error deleting year {year}: {e}")
|
||||||
|
self.db.rollback()
|
30
frontend.py
30
frontend.py
@ -9,7 +9,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QMainWindow, QTabWidget, QScrollArea, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
QMainWindow, QTabWidget, QScrollArea, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
||||||
QLabel, QToolButton, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QSpinBox,
|
QLabel, QToolButton, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QSpinBox,
|
||||||
QComboBox, QTextEdit, QDialogButtonBox, QAction, QFileDialog, QMessageBox,
|
QComboBox, QTextEdit, QDialogButtonBox, QAction, QFileDialog, QMessageBox,
|
||||||
QInputDialog, QApplication, QAbstractItemView, QSizePolicy, QHeaderView, QPushButton
|
QInputDialog, QApplication, QAbstractItemView, QSizePolicy, QHeaderView
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QSettings
|
from PyQt5.QtCore import Qt, QSettings
|
||||||
from PyQt5.QtGui import QColor, QIcon, QFont
|
from PyQt5.QtGui import QColor, QIcon, QFont
|
||||||
@ -25,6 +25,7 @@ class AnimeDialog(QDialog):
|
|||||||
self.setWindowTitle("Add Anime" if entry is None else "Edit Anime")
|
self.setWindowTitle("Add Anime" if entry is None else "Edit Anime")
|
||||||
layout = QFormLayout(self)
|
layout = QFormLayout(self)
|
||||||
self.name_edit = QLineEdit()
|
self.name_edit = QLineEdit()
|
||||||
|
self.name_edit.setMaxLength(255) # Prevent overly long inputs
|
||||||
layout.addRow("Name", self.name_edit)
|
layout.addRow("Name", self.name_edit)
|
||||||
self.year_spin = QSpinBox()
|
self.year_spin = QSpinBox()
|
||||||
self.year_spin.setRange(1900, 2100)
|
self.year_spin.setRange(1900, 2100)
|
||||||
@ -39,20 +40,23 @@ class AnimeDialog(QDialog):
|
|||||||
self.status_combo.addItems(['unwatched', 'watching', 'completed'])
|
self.status_combo.addItems(['unwatched', 'watching', 'completed'])
|
||||||
layout.addRow("Status", self.status_combo)
|
layout.addRow("Status", self.status_combo)
|
||||||
self.comment_edit = QTextEdit()
|
self.comment_edit = QTextEdit()
|
||||||
|
self.comment_edit.setAcceptRichText(False) # Prevent HTML injection
|
||||||
layout.addRow("Comment", self.comment_edit)
|
layout.addRow("Comment", self.comment_edit)
|
||||||
self.url_edit = QLineEdit()
|
self.url_edit = QLineEdit()
|
||||||
|
self.url_edit.setMaxLength(2048) # Reasonable limit for URLs
|
||||||
layout.addRow("MAL URL", self.url_edit)
|
layout.addRow("MAL URL", self.url_edit)
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
layout.addRow(buttons)
|
layout.addRow(buttons)
|
||||||
if entry:
|
if entry:
|
||||||
self.name_edit.setText(entry[1])
|
# Unescape for display in input fields
|
||||||
|
self.name_edit.setText(html.unescape(entry[1]))
|
||||||
self.year_spin.setValue(entry[2])
|
self.year_spin.setValue(entry[2])
|
||||||
self.season_combo.setCurrentText(entry[3])
|
self.season_combo.setCurrentText(entry[3])
|
||||||
self.status_combo.setCurrentText(entry[4])
|
self.status_combo.setCurrentText(entry[4])
|
||||||
self.type_combo.setCurrentText(entry[5] or '')
|
self.type_combo.setCurrentText(entry[5] or '')
|
||||||
self.comment_edit.setText(entry[6] or '')
|
self.comment_edit.setPlainText(html.unescape(entry[6] or ''))
|
||||||
self.url_edit.setText(entry[7] or '')
|
self.url_edit.setText(entry[7] or '')
|
||||||
else:
|
else:
|
||||||
if default_year is not None:
|
if default_year is not None:
|
||||||
@ -70,14 +74,15 @@ class AnimeDialog(QDialog):
|
|||||||
self.season_combo.setEnabled(True)
|
self.season_combo.setEnabled(True)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
|
# Sanitize inputs by escaping special characters
|
||||||
return {
|
return {
|
||||||
'name': self.name_edit.text(),
|
'name': html.escape(self.name_edit.text().strip()),
|
||||||
'year': self.year_spin.value(),
|
'year': self.year_spin.value(),
|
||||||
'season': self.season_combo.currentText(),
|
'season': html.escape(self.season_combo.currentText().strip()),
|
||||||
'status': self.status_combo.currentText(),
|
'status': html.escape(self.status_combo.currentText().strip()),
|
||||||
'type': self.type_combo.currentText(),
|
'type': html.escape(self.type_combo.currentText().strip()),
|
||||||
'comment': self.comment_edit.toPlainText(),
|
'comment': html.escape(self.comment_edit.toPlainText().strip()),
|
||||||
'url': self.url_edit.text()
|
'url': html.escape(self.url_edit.text().strip())
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShortcutsDialog(QDialog):
|
class ShortcutsDialog(QDialog):
|
||||||
@ -514,6 +519,7 @@ class AnimeTracker(QMainWindow):
|
|||||||
index = self.tab_widget.addTab(year_tab, tab_text)
|
index = self.tab_widget.addTab(year_tab, tab_text)
|
||||||
if completed:
|
if completed:
|
||||||
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
||||||
|
self.filter_tables(self.search_text) # Apply search filter after loading tabs
|
||||||
|
|
||||||
def create_actions_widget(self, anime_id, status):
|
def create_actions_widget(self, anime_id, status):
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
@ -572,6 +578,9 @@ class AnimeTracker(QMainWindow):
|
|||||||
if dialog.exec_() == QDialog.Accepted:
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
current_id = self.get_current_tab_identifier()
|
current_id = self.get_current_tab_identifier()
|
||||||
data = dialog.get_data()
|
data = dialog.get_data()
|
||||||
|
if not data['name']:
|
||||||
|
QMessageBox.warning(self, "Error", "Anime name cannot be empty.")
|
||||||
|
return
|
||||||
self.backend.add_anime(data)
|
self.backend.add_anime(data)
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(current_id)
|
||||||
@ -583,6 +592,9 @@ class AnimeTracker(QMainWindow):
|
|||||||
dialog = AnimeDialog(self, entry)
|
dialog = AnimeDialog(self, entry)
|
||||||
if dialog.exec_() == QDialog.Accepted:
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
data = dialog.get_data()
|
data = dialog.get_data()
|
||||||
|
if not data['name']:
|
||||||
|
QMessageBox.warning(self, "Error", "Anime name cannot be empty.")
|
||||||
|
return
|
||||||
self.backend.edit_anime(anime_id, data)
|
self.backend.edit_anime(anime_id, data)
|
||||||
self.load_tabs()
|
self.load_tabs()
|
||||||
self.set_current_tab_by_identifier(current_id)
|
self.set_current_tab_by_identifier(current_id)
|
||||||
|
Loading…
Reference in New Issue
Block a user