Fix plugin validator, fix version urls, clippy lints, additional categories (#421)

This commit is contained in:
Geometrically
2022-08-16 17:42:04 -07:00
committed by GitHub
parent c76b527b93
commit ac3a17b178
17 changed files with 650 additions and 646 deletions

View File

@@ -74,10 +74,9 @@ impl Category {
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
if !name.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
}
@@ -102,10 +101,9 @@ impl Category {
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
if !name.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
}
@@ -203,10 +201,9 @@ impl<'a> CategoryBuilder<'a> {
self,
name: &'a str,
) -> Result<CategoryBuilder<'a>, DatabaseError> {
if name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
if name.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
Ok(Self {
name: Some(name),
..self
@@ -296,10 +293,9 @@ impl Loader {
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
if !name.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
}
@@ -403,10 +399,9 @@ impl<'a> LoaderBuilder<'a> {
self,
name: &'a str,
) -> Result<LoaderBuilder<'a>, DatabaseError> {
if name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
if name.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
Ok(Self {
name: Some(name),
..self
@@ -501,10 +496,9 @@ impl GameVersion {
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
if !version
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
{
if !version.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
return Err(DatabaseError::InvalidIdentifier(version.to_string()));
}
@@ -676,7 +670,7 @@ impl<'a> GameVersionBuilder<'a> {
) -> Result<GameVersionBuilder<'a>, DatabaseError> {
if version
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
.all(|c| c.is_ascii_alphanumeric() || "-_.+".contains(c))
{
Ok(Self {
version: Some(version),
@@ -693,7 +687,7 @@ impl<'a> GameVersionBuilder<'a> {
) -> Result<GameVersionBuilder<'a>, DatabaseError> {
if version_type
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
.all(|c| c.is_ascii_alphanumeric() || "-_.+".contains(c))
{
Ok(Self {
version_type: Some(version_type),
@@ -851,10 +845,9 @@ impl<'a> LicenseBuilder<'a> {
self,
short: &'a str,
) -> Result<LicenseBuilder<'a>, DatabaseError> {
if short
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
{
if short.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
Ok(Self {
short: Some(short),
..self
@@ -1011,10 +1004,9 @@ impl<'a> DonationPlatformBuilder<'a> {
self,
short: &'a str,
) -> Result<DonationPlatformBuilder<'a>, DatabaseError> {
if short
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
{
if short.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
Ok(Self {
short: Some(short),
..self
@@ -1075,10 +1067,9 @@ impl ReportType {
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
if !name.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
}
@@ -1164,10 +1155,9 @@ impl<'a> ReportTypeBuilder<'a> {
self,
name: &'a str,
) -> Result<ReportTypeBuilder<'a>, DatabaseError> {
if name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
if name.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
Ok(Self { name: Some(name) })
} else {
Err(DatabaseError::InvalidIdentifier(name.to_string()))
@@ -1213,10 +1203,9 @@ impl ProjectType {
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
if !name.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
}
@@ -1330,10 +1319,9 @@ impl<'a> ProjectTypeBuilder<'a> {
self,
name: &'a str,
) -> Result<ProjectTypeBuilder<'a>, DatabaseError> {
if name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
if name.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '+'
}) {
Ok(Self { name: Some(name) })
} else {
Err(DatabaseError::InvalidIdentifier(name.to_string()))

View File

@@ -116,7 +116,7 @@ pub struct TeamId(pub i64);
#[sqlx(transparent)]
pub struct TeamMemberId(pub i64);
#[derive(Copy, Clone, Debug, Type, PartialEq)]
#[derive(Copy, Clone, Debug, Type, PartialEq, Eq)]
#[sqlx(transparent)]
pub struct ProjectId(pub i64);
#[derive(Copy, Clone, Debug, Type)]

View File

@@ -30,7 +30,7 @@ pub enum DatabaseError {
RandomId,
#[error(
"Invalid identifier: Category/version names must contain only ASCII \
alphanumeric characters or '_-'."
alphanumeric characters or '_-+'."
)]
InvalidIdentifier(String),
#[error("Invalid permissions bitflag!")]

View File

@@ -629,8 +629,7 @@ impl Project {
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,
ARRAY_AGG(DISTINCT ca.category) filter (where ca.category is not null) additional_categories,
ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,
ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions,
ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery,
ARRAY_AGG(DISTINCT md.joining_platform_id || ' |||| ' || dp.short || ' |||| ' || dp.name || ' |||| ' || md.url) filter (where md.joining_platform_id is not null) donations
@@ -643,8 +642,7 @@ impl Project {
LEFT JOIN mods_donations md ON md.joining_mod_id = m.id
LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id
LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id
LEFT JOIN categories c ON mc.joining_category_id = c.id AND mc.is_additional = FALSE
LEFT JOIN categories ca ON mc.joining_category_id = c.id AND mc.is_additional = TRUE
LEFT JOIN categories c ON mc.joining_category_id = c.id
LEFT JOIN versions v ON v.mod_id = m.id
LEFT JOIN mods_gallery mg ON mg.mod_id = m.id
WHERE m.id = $1
@@ -656,6 +654,23 @@ impl Project {
.await?;
if let Some(m) = result {
let categories_raw = m.categories.unwrap_or_default();
let mut categories = Vec::new();
let mut additional_categories = Vec::new();
for category in categories_raw {
let category: Vec<&str> = category.split(" |||| ").collect();
if category.len() >= 2 {
if category[1].parse::<bool>().ok().unwrap_or_default() {
additional_categories.push(category[0].to_string());
} else {
categories.push(category[0].to_string());
}
}
}
Ok(Some(QueryProject {
inner: Project {
id: ProjectId(m.id),
@@ -685,10 +700,8 @@ impl Project {
approved: m.approved,
},
project_type: m.project_type_name,
categories: m.categories.unwrap_or_default(),
additional_categories: m
.additional_categories
.unwrap_or_default(),
categories,
additional_categories,
versions: {
let versions = m.versions.unwrap_or_default();
@@ -803,8 +816,7 @@ impl Project {
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,
ARRAY_AGG(DISTINCT ca.category) filter (where ca.category is not null) additional_categories,
ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,
ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions,
ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery,
ARRAY_AGG(DISTINCT md.joining_platform_id || ' |||| ' || dp.short || ' |||| ' || dp.name || ' |||| ' || md.url) filter (where md.joining_platform_id is not null) donations
@@ -817,8 +829,7 @@ impl Project {
LEFT JOIN mods_donations md ON md.joining_mod_id = m.id
LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id
LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id
LEFT JOIN categories c ON mc.joining_category_id = c.id AND mc.is_additional = FALSE
LEFT JOIN categories ca ON mc.joining_category_id = c.id AND mc.is_additional = TRUE
LEFT JOIN categories c ON mc.joining_category_id = c.id
LEFT JOIN versions v ON v.mod_id = m.id
LEFT JOIN mods_gallery mg ON mg.mod_id = m.id
WHERE m.id = ANY($1)
@@ -830,6 +841,25 @@ impl Project {
.try_filter_map(|e| async {
Ok(e.right().map(|m| {
let id = m.id;
let categories_raw = m.categories.unwrap_or_default();
let mut categories = Vec::new();
let mut additional_categories = Vec::new();
for category in categories_raw {
let category: Vec<&str> =
category.split(" |||| ").collect();
if category.len() >= 2 {
if category[1].parse::<bool>().ok().unwrap_or_default() {
additional_categories.push(category[0].to_string());
} else {
categories.push(category[0].to_string());
}
}
}
QueryProject {
inner: Project {
id: ProjectId(id),
@@ -859,8 +889,8 @@ impl Project {
approved: m.approved
},
project_type: m.project_type_name,
categories: m.categories.unwrap_or_default(),
additional_categories: m.additional_categories.unwrap_or_default(),
categories,
additional_categories,
versions: {
let versions = m.versions.unwrap_or_default();

View File

@@ -735,14 +735,14 @@ impl Version {
if dependency.len() >= 4 {
Some(QueryDependency {
project_id: match &*dependency[1] {
project_id: match dependency[1] {
"0" => None,
_ => match dependency[1].parse() {
Ok(x) => Some(ProjectId(x)),
Err(_) => None,
},
},
version_id: match &*dependency[0] {
version_id: match dependency[0] {
"0" => None,
_ => match dependency[0].parse() {
Ok(x) => Some(VersionId(x)),
@@ -896,14 +896,14 @@ impl Version {
if dependency.len() >= 4 {
Some(QueryDependency {
project_id: match &*dependency[1] {
project_id: match dependency[1] {
"0" => None,
_ => match dependency[1].parse() {
Ok(x) => Some(ProjectId(x)),
Err(_) => None,
},
},
version_id: match &*dependency[0] {
version_id: match dependency[0] {
"0" => None,
_ => match dependency[0].parse() {
Ok(x) => Some(VersionId(x)),

View File

@@ -219,7 +219,10 @@ pub struct License {
pub struct DonationLink {
pub id: String,
pub platform: String,
#[validate(url)]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
pub url: String,
}
@@ -482,7 +485,7 @@ impl DependencyType {
}
/// A specific version of Minecraft
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(transparent)]
pub struct GameVersion(pub String);

View File

@@ -25,11 +25,18 @@ pub async fn count_download(
let project_id: crate::database::models::ids::ProjectId =
download_body.hash.into();
let id_option = crate::models::ids::base62_impl::parse_base62(
&download_body.version_name,
)
.ok()
.map(|x| x as i64);
let (version_id, project_id) = if let Some(version) = sqlx::query!(
"SELECT id, mod_id FROM versions
WHERE (version_number = $1 AND mod_id = $2)",
WHERE ((version_number = $1 OR id = $3) AND mod_id = $2)",
download_body.version_name,
project_id as crate::database::models::ids::ProjectId
project_id as crate::database::models::ids::ProjectId,
id_option
)
.fetch_optional(pool.as_ref())
.await?

View File

@@ -118,7 +118,7 @@ pub async fn maven_metadata(
.last()
.unwrap_or(&"release".to_string())
.to_string(),
release: latest_release.unwrap_or_default().to_string(),
release: latest_release.unwrap_or_default(),
versions: Versions {
versions: new_versions,
},

View File

@@ -170,19 +170,34 @@ struct ProjectCreateData {
/// A list of the categories that the project is in.
pub additional_categories: Vec<String>,
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to where to submit bugs or issues with the project.
pub issues_url: Option<String>,
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to the source code for the project.
pub source_url: Option<String>,
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to the project's wiki page or other relevant information.
pub wiki_url: Option<String>,
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to the project's license page
pub license_url: Option<String>,
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
/// An optional link to the project's discord.
pub discord_url: Option<String>,
/// An optional list of all donation links the project has\
@@ -550,7 +565,7 @@ pub async fn project_create_inner(
&cdn_url,
&content_disposition,
project_id,
&version_data.version_number,
created_version.version_id.into(),
&*project_create_data.project_type,
version_data.loaders.clone(),
version_data.game_versions.clone(),

View File

@@ -266,35 +266,50 @@ pub struct EditProject {
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
pub issues_url: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
pub source_url: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
pub wiki_url: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
pub license_url: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(url, length(max = 2048))]
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 2048)
)]
pub discord_url: Option<Option<String>>,
#[validate]
pub donation_urls: Option<Vec<DonationLink>>,

View File

@@ -302,7 +302,7 @@ async fn version_create_inner(
&cdn_url,
&content_disposition,
version.project_id.into(),
&version.version_number,
version.version_id.into(),
&*project_type,
version_data.loaders,
version_data.game_versions,
@@ -520,7 +520,6 @@ async fn upload_file_to_version_inner(
}
let project_id = ProjectId(version.project_id.0 as u64);
let version_number = version.version_number;
let project_type = sqlx::query!(
"
@@ -567,7 +566,7 @@ async fn upload_file_to_version_inner(
let mut dependencies = version
.dependencies
.iter()
.map(|x| models::version_item::DependencyBuilder {
.map(|x| DependencyBuilder {
project_id: x.project_id,
version_id: x.version_id,
file_name: None,
@@ -584,7 +583,7 @@ async fn upload_file_to_version_inner(
&cdn_url,
&content_disposition,
project_id,
&version_number,
version_id.into(),
&*project_type,
version.loaders.clone().into_iter().map(Loader).collect(),
version
@@ -626,7 +625,7 @@ pub async fn upload_file(
cdn_url: &str,
content_disposition: &actix_web::http::header::ContentDisposition,
project_id: ProjectId,
version_number: &str,
version_id: VersionId,
project_type: &str,
loaders: Vec<Loader>,
game_versions: Vec<GameVersion>,
@@ -748,13 +747,11 @@ pub async fn upload_file(
let file_path_encode = format!(
"data/{}/versions/{}/{}",
project_id,
version_number,
version_id,
urlencoding::encode(file_name)
);
let file_path = format!(
"data/{}/versions/{}/{}",
project_id, version_number, &file_name
);
let file_path =
format!("data/{}/versions/{}/{}", project_id, version_id, &file_name);
let upload_data = file_host
.upload_file(content_type, &file_path, data.freeze())

View File

@@ -8,6 +8,7 @@ use meilisearch_sdk::document::Document;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::cmp::min;
use std::fmt::Write;
use thiserror::Error;
pub mod indexing;
@@ -20,6 +21,8 @@ pub enum SearchError {
Serde(#[from] serde_json::Error),
#[error("Error while parsing an integer: {0}")]
IntParsing(#[from] std::num::ParseIntError),
#[error("Error while formatting strings: {0}")]
FormatError(#[from] std::fmt::Error),
#[error("Environment Error")]
Env(#[from] dotenv::Error),
#[error("Invalid index to sort by: {0}")]
@@ -34,6 +37,7 @@ impl actix_web::ResponseError for SearchError {
SearchError::Serde(..) => StatusCode::BAD_REQUEST,
SearchError::IntParsing(..) => StatusCode::BAD_REQUEST,
SearchError::InvalidIndex(..) => StatusCode::BAD_REQUEST,
SearchError::FormatError(..) => StatusCode::BAD_REQUEST,
}
}
@@ -45,6 +49,7 @@ impl actix_web::ResponseError for SearchError {
SearchError::Serde(..) => "invalid_input",
SearchError::IntParsing(..) => "invalid_input",
SearchError::InvalidIndex(..) => "invalid_input",
SearchError::FormatError(..) => "invalid_input",
},
description: &self.to_string(),
})
@@ -215,7 +220,7 @@ pub async fn search_for_project(
filter_string.push(')');
if !filters.is_empty() {
filter_string.push_str(&format!(" AND ({})", filter_string))
write!(filter_string, " AND ({})", filters)?;
}
} else {
filter_string.push_str(&*filters);

View File

@@ -88,3 +88,15 @@ pub fn validate_deps(
Ok(())
}
pub fn validate_url(value: &String) -> Result<(), validator::ValidationError> {
let url = url::Url::parse(value)
.ok()
.ok_or_else(|| validator::ValidationError::new("invalid URL"))?;
if url.scheme() != "https" {
return Err(validator::ValidationError::new("URL must be https"));
}
Ok(())
}

View File

@@ -27,11 +27,11 @@ impl super::Validator for BukkitValidator {
&self,
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
) -> Result<ValidationResult, ValidationError> {
archive.by_name("plugin.yml").map_err(|_| {
ValidationError::InvalidInput(
"No plugin.yml present for plugin file.".into(),
)
})?;
if archive.by_name("plugin.yml").is_err() {
return Ok(ValidationResult::Warning(
"No plugin.yml present for plugin file.",
));
}
Ok(ValidationResult::Pass)
}