Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
837 changes: 827 additions & 10 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ lettre = { version = "0.11.19", default-features = false, features = [
"tokio1",
"tokio1-rustls",
] }
lingua = "=1.8.0"
linkify = "0.11.0"
lz4_flex = { version = "0.11.5", default-features = false, features = [
"checked-decode",
Expand Down Expand Up @@ -245,7 +246,6 @@ webauthn-rs = "0.5.5"
webauthn-rs-proto = "0.5.5"
webp = { version = "0.3.1", default-features = false }
webview2-com = "0.38.0" # Should be updated in lockstep with wry
whatlang = "0.18.0"
whoami = "1.6.1"
windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62
windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ import {
type MessageDescriptor,
useVIntl,
} from '@modrinth/ui'
import { isStaff } from '@modrinth/utils'
import type { Component } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

Expand Down Expand Up @@ -223,6 +224,7 @@ const messages = defineMessages({

const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const flags = useFeatureFlags()

const props = withDefaults(defineProps<Props>(), {
versions: () => [],
Expand Down Expand Up @@ -420,6 +422,15 @@ function isNagComplete(nag: Nag): boolean {
const visibleNags = computed<Nag[]>(() => {
const finalNags = applicableNags.value.filter((nag) => !isNagComplete(nag))

if (
isProcessing.value &&
isStaff(props.currentMember?.user) &&
!flags.value.alwaysShowPublishingChecklistForStaff &&
!finalNags.some((nag) => nag.status === 'required')
) {
return []
}

if (props.project.status === 'draft') {
finalNags.push({
id: 'submit-for-review',
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/composables/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
alwaysIgnoreErrorBanner: false,
showViewProdRouteBanner: false,
showModeratorProjectMemberUi: false,
alwaysShowPublishingChecklistForStaff: false,
archonApiStaging: false,
showHostingAccessInstanceAuditLog: false,
versionDevInfoCollapsed: true,
Expand Down
2 changes: 1 addition & 1 deletion apps/labrinth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ image = { workspace = true, features = [
itertools = { workspace = true }
json-patch = { workspace = true }
lettre = { workspace = true }
lingua = { workspace = true }
linkify = { workspace = true }
modrinth-content-management = { workspace = true }
modrinth-util = { workspace = true, features = ["decimal", "sentry", "utoipa"] }
Expand Down Expand Up @@ -134,7 +135,6 @@ validator = { workspace = true, features = ["derive"] }
webauthn-rs = { workspace = true, features = ["conditional-ui", "danger-allow-state-serialisation"] }
webauthn-rs-proto = { workspace = true }
webp = { workspace = true }
whatlang = { workspace = true }
woothee = { workspace = true }
xredis = { workspace = true }
yaserde = { workspace = true, features = ["derive"] }
Expand Down
28 changes: 18 additions & 10 deletions apps/labrinth/src/routes/v3/projects/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,17 @@ pub(crate) async fn ensure_project_is_valid_for_review(
.collect::<Vec<_>>();
let project = Project::from(reloaded_project.clone());

if has_required_nags_with_context(
&project,
&versions,
&available_categories,
&disclosures,
) {
let has_required_nags = web::block(move || {
has_required_nags_with_context(
&project,
&versions,
&available_categories,
&disclosures,
)
})
.await
.wrap_internal_err("validating project for review")?;
if has_required_nags {
return Err(ApiError::Request(eyre!(
"project must have no required validation nags before or while under review"
)));
Expand Down Expand Up @@ -157,12 +162,15 @@ pub async fn validate(
.collect::<Vec<_>>();
let project = Project::from(project);

Ok(web::Json(ProjectValidationResponse {
nags: validate_project(
let nags = web::block(move || {
validate_project(
&project,
&versions,
&available_categories,
&disclosures,
),
}))
)
})
.await
.wrap_internal_err("validating project")?;
Ok(web::Json(ProjectValidationResponse { nags }))
}
20 changes: 17 additions & 3 deletions apps/labrinth/src/validate/project/description.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ use super::text::{
ProfanityKind, contains_description_spam, extract_description_blocks,
extract_description_text, find_banned_description_link,
has_image_without_alt_text, has_sufficient_english_blocks,
js_string_length, non_standard_text_ratio, normalize_project_field_text,
profanity_matches, project_requires_english,
is_confidently_non_english_short_text, js_string_length,
non_standard_text_ratio, normalize_project_field_text, profanity_matches,
project_requires_english, project_text_similarity,
};
use super::{ProjectNag, ProjectNagKind, ProjectNagSeverity};

use crate::models::projects::Project;

const MIN_DESCRIPTION_CHARS: usize = 125;
const MIN_DESCRIPTION_LANGUAGE_CHARS: usize = 35;
const MAX_DESCRIPTION_SUMMARY_SIMILARITY: f64 = 0.8;
const MAX_PROFANITY_COUNT: usize = 2;
const NON_STANDARD_TEXT_FAILURE_THRESHOLD: f64 = 0.05;

Expand Down Expand Up @@ -91,6 +94,14 @@ pub(super) fn validate(project: &Project) -> Vec<ProjectNag> {
);
}
}
if project_text_similarity(&normalized_text, &project.summary)
>= MAX_DESCRIPTION_SUMMARY_SIMILARITY
{
nags.push(ProjectNag::new(
ProjectNagKind::ProjectDescriptionMatchesSummary,
ProjectNagSeverity::Required,
));
}
if has_spam {
nags.push(ProjectNag::new(
ProjectNagKind::ProjectDescriptionSpam,
Expand Down Expand Up @@ -151,8 +162,11 @@ fn is_non_english_text(
text: &str,
blocks: &[String],
) -> bool {
let length = js_string_length(text);
project_requires_english(project)
&& js_string_length(text) >= MIN_DESCRIPTION_CHARS
&& length >= MIN_DESCRIPTION_LANGUAGE_CHARS
&& (length >= MIN_DESCRIPTION_CHARS
|| is_confidently_non_english_short_text(&blocks.join(" ")))
&& !has_sufficient_english_blocks(blocks)
}

Expand Down
55 changes: 43 additions & 12 deletions apps/labrinth/src/validate/project/description/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use regex::Regex;
use unicode_segmentation::UnicodeSegmentation;

static HTML_HEADER: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?is)<h[1-3]\b[^>]*>(.*?)</h[1-3]>").unwrap()
Regex::new(r"(?is)<h[1-6]\b[^>]*>(.*?)</h[1-6]>").unwrap()
});
static ADJACENT_HTML_HEADERS: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?is)</h([1-3])>\s*<h([1-3])\b").unwrap());
LazyLock::new(|| Regex::new(r"(?is)</h([1-6])>\s*<h([1-6])\b").unwrap());
static TRAILING_HTML_HEADER: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?is)</h[1-6]>\s*(?:</[a-z][^>]*>\s*)*$").unwrap()
});
Expand Down Expand Up @@ -101,7 +101,6 @@ impl<'a> DescriptionMarkdown<'a> {
let markdown_headers = self
.headings
.iter()
.filter(|heading| is_primary_heading(heading.level))
.filter(|heading| header_is_long(&heading.text))
.count();
let without_code = self.replace_code(" ");
Expand Down Expand Up @@ -130,8 +129,7 @@ impl<'a> DescriptionMarkdown<'a> {
let [previous, current] = headings else {
return false;
};
is_primary_heading(previous.level)
&& previous.level == current.level
previous.level == current.level
&& self.markdown[previous.range.end..current.range.start]
.trim()
.is_empty()
Expand Down Expand Up @@ -180,13 +178,6 @@ fn header_is_long(header: &str) -> bool {
rendered.graphemes(true).count() > 80
}

fn is_primary_heading(level: HeadingLevel) -> bool {
matches!(
level,
HeadingLevel::H1 | HeadingLevel::H2 | HeadingLevel::H3
)
}

#[cfg(test)]
mod tests {
use super::DescriptionMarkdown;
Expand Down Expand Up @@ -250,4 +241,44 @@ image: "![](/missing-alt.png)"
.has_adjacent_same_level_headers()
);
}

#[test]
fn all_heading_levels_are_validated() {
for level in 1..=6 {
let prefix = "#".repeat(level);
let long_text = "heading ".repeat(12);
for (long_header, adjacent_headers, separated_headers) in [
(
format!("{prefix} {long_text}"),
format!("{prefix} First\n\n{prefix} Second"),
format!(
"{prefix} First\n\n```yaml\n# comment\n```\n\n{prefix} Second"
),
),
(
format!("<h{level}>{long_text}</h{level}>"),
format!(
"<h{level}>First</h{level}>\n\n<h{level}>Second</h{level}>"
),
format!(
"<h{level}>First</h{level}>\n\n```yaml\n# comment\n```\n\n<h{level}>Second</h{level}>"
),
),
] {
let markdown = DescriptionMarkdown::parse(&long_header);
assert_eq!(markdown.long_header_count(), 1, "{long_header}");
assert!(markdown.ends_with_header(), "{long_header}");
assert!(
DescriptionMarkdown::parse(&adjacent_headers)
.has_adjacent_same_level_headers(),
"{adjacent_headers}"
);
assert!(
!DescriptionMarkdown::parse(&separated_headers)
.has_adjacent_same_level_headers(),
"{separated_headers}"
);
}
}
}
}
21 changes: 12 additions & 9 deletions apps/labrinth/src/validate/project/disclosures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ pub(super) fn validate(
project: &crate::models::projects::Project,
disclosures: Option<&[ProjectDisclosure]>,
) -> Vec<ProjectNag> {
let (project_type, _) =
LegacyProject::get_project_type(&project.project_types);
let mut nags = vec![
ProjectNag::new(
ProjectNagKind::CheckDisclosures,
ProjectNagSeverity::Suggestion,
)
.with_details(serde_json::json!({ "project_type": project_type })),
];
let mut nags = Vec::new();
if disclosures.is_some_and(|disclosures| disclosures.is_empty()) {
let (project_type, _) =
LegacyProject::get_project_type(&project.project_types);
nags.push(
ProjectNag::new(
ProjectNagKind::CheckDisclosures,
ProjectNagSeverity::Suggestion,
)
.with_details(serde_json::json!({ "project_type": project_type })),
);
}

if disclosures.is_some_and(|disclosures| {
disclosures.iter().any(disclosure_has_paired_html)
Expand Down
Loading
Loading