Add validation to package API

This commit is contained in:
rubenwardy 2021-02-02 22:34:51 +00:00
parent 551996ca14
commit ca58c70206
5 changed files with 116 additions and 33 deletions

View File

@ -227,13 +227,11 @@ class PackageForm(FlaskForm):
media_license = QuerySelectField("Media License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) media_license = QuerySelectField("Media License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=makeLabel) tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=makeLabel)
content_warnings = QuerySelectMultipleField('Content Warnings', query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=makeLabel) content_warnings = QuerySelectMultipleField('Content Warnings', query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=makeLabel)
# harddep_str = StringField("Hard Dependencies", [Optional()])
# softdep_str = StringField("Soft Dependencies", [Optional()])
repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None]) repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None])
website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None]) website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None]) issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None])
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)]) forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
submit = SubmitField("Save") submit = SubmitField("Save")
@bp.route("/packages/new/", methods=["GET", "POST"]) @bp.route("/packages/new/", methods=["GET", "POST"])

View File

@ -24,7 +24,7 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
* PUT `/api/packages/<author>/<name>/` (Update) * PUT `/api/packages/<author>/<name>/` (Update)
* JSON dictionary with any of these keys (all are optional): * JSON dictionary with any of these keys (all are optional):
* `title`: Human-readable title. * `title`: Human-readable title.
* `short_desc` * `short_description`
* `desc` * `desc`
* `type`: One of `GAME`, `MOD`, `TXP`. * `type`: One of `GAME`, `MOD`, `TXP`.
* `license`: A license name. * `license`: A license name.

View File

@ -15,24 +15,96 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import re, validators
from app.logic.LogicError import LogicError from app.logic.LogicError import LogicError
from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, NotificationType, AuditSeverity from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, NotificationType, AuditSeverity, License
from app.utils import addNotification, addAuditLog from app.utils import addNotification, addAuditLog
def check(cond: bool, msg: str):
if not cond:
raise LogicError(400, msg)
def get_license(name):
if type(name) == License:
return name
license = License.query.filter(License.name.ilike(name)).first()
if license is None:
raise LogicError(400, "Unknown license: " + name)
return license
name_re = re.compile("^[a-z0-9_]+$")
TYPES = {
"name": str,
"title": str,
"short_description": str,
"short_desc": str,
"desc": str,
"tags": list,
"content_warnings": list,
"repo": str,
"website": str,
"issue_tracker": str,
"issueTracker": str,
"forums": int,
}
def is_int(val):
try:
int(val)
return True
except ValueError:
return False
def validate(data: dict):
for key, typ in TYPES.items():
if data.get(key) is not None:
check(isinstance(data[key], typ), key + " must be a " + typ.__name__)
if "name" in data:
name = data["name"]
check(isinstance(name, str), "Name must be a string")
check(bool(name_re.match(name)),
"Name can only contain lower case letters (a-z), digits (0-9), and underscores (_)")
for key in ["repo", "website", "issue_tracker", "issueTracker"]:
value = data.get(key)
if value is not None:
check(value.startswith("http://") or value.startswith("https://"),
key + " must start with http:// or https://")
check(validators.url(value, public=True), key + " must be a valid URL")
def do_edit_package(user: User, package: Package, was_new: bool, data: dict, reason: str = None): def do_edit_package(user: User, package: Package, was_new: bool, data: dict, reason: str = None):
if not package.checkPerm(user, Permission.EDIT_PACKAGE):
raise LogicError(403, "You do not have permission to edit this package")
if "name" in data and package.name != data["name"] and \ if "name" in data and package.name != data["name"] and \
not package.checkPerm(user, Permission.CHANGE_NAME): not package.checkPerm(user, Permission.CHANGE_NAME):
raise LogicError(403, "You do not have permission to change the package name") raise LogicError(403, "You do not have permission to change the package name")
if not package.checkPerm(user, Permission.EDIT_PACKAGE): for alias, to in { "short_description": "short_desc", "issue_tracker": "issueTracker" }.items():
raise LogicError(403, "You do not have permission to edit this package")
for alias, to in { "short_description": "short_desc" }.items():
if alias in data: if alias in data:
data[to] = data[alias] data[to] = data[alias]
validate(data)
if "type" in data:
data["type"] = PackageType.coerce(data["type"])
if "license" in data:
data["license"] = get_license(data["license"])
if "media_license" in data:
data["media_license"] = get_license(data["media_license"])
for key in ["name", "title", "short_desc", "desc", "type", "license", "media_license", for key in ["name", "title", "short_desc", "desc", "type", "license", "media_license",
"repo", "website", "issueTracker", "forums"]: "repo", "website", "issueTracker", "forums"]:
if key in data: if key in data:
@ -45,16 +117,27 @@ def do_edit_package(user: User, package: Package, was_new: bool, data: dict, rea
m = MetaPackage.GetOrCreate(package.name, {}) m = MetaPackage.GetOrCreate(package.name, {})
package.provides.append(m) package.provides.append(m)
package.tags.clear() if "tags" in data:
package.tags.clear()
if "tag" in data: for tag_id in data["tags"]:
for tag in data["tag"]: if is_int(tag_id):
package.tags.append(Tag.query.get(tag)) package.tags.append(Tag.query.get(tag_id))
else:
tag = Tag.query.filter_by(name=tag_id).first()
if tag is None:
raise LogicError(400, "Unknown tag: " + tag_id)
package.tags.append(tag)
if "content_warnings" in data: if "content_warnings" in data:
package.content_warnings.clear() package.content_warnings.clear()
for warning in data["content_warnings"]: for warning_id in data["content_warnings"]:
package.content_warnings.append(ContentWarning.query.get(warning)) if is_int(warning_id):
package.content_warnings.append(ContentWarning.query.get(warning_id))
else:
warning = ContentWarning.query.filter_by(name=warning_id).first()
if warning is None:
raise LogicError(400, "Unknown warning: " + warning_id)
package.content_warnings.append(warning)
if not was_new: if not was_new:
if reason is None: if reason is None:

View File

@ -17,7 +17,6 @@
import datetime import datetime
import enum import enum
from urllib.parse import urlparse
from flask import url_for from flask import url_for
from flask_sqlalchemy import BaseQuery from flask_sqlalchemy import BaseQuery

View File

@ -1,4 +1,4 @@
Flask Flask~=1.1.2
Flask-FlatPages Flask-FlatPages
Flask-Gravatar Flask-Gravatar
Flask-Login Flask-Login
@ -12,24 +12,24 @@ GitHub-Flask
SQLAlchemy-Searchable SQLAlchemy-Searchable
bcrypt bcrypt
markdown markdown~=3.2.2
bleach bleach~=3.1.5
passlib passlib~=1.7.2
pygments pygments
beautifulsoup4 beautifulsoup4~=4.9.1
celery celery~=4.4.6
kombu kombu~=4.6.11
GitPython GitPython
git-archive-all git-archive-all
lxml lxml
pillow pillow~=7.2.0
pyScss pyScss
redis redis~=3.5.3
psycopg2 psycopg2
pytest pytest~=5.4.3
pytest-cov pytest-cov
email_validator email_validator
@ -38,8 +38,11 @@ pyyaml
ua-parser ua-parser
user-agents user-agents
Werkzeug Werkzeug~=1.0.1
WTForms WTForms~=2.3.1
SQLAlchemy SQLAlchemy~=1.3.18
requests requests~=2.24.0
alembic alembic~=1.4.2
validators~=0.16.0
gitdb~=4.0.5