Profiles: Add validation errors

This commit is contained in:
asonix 2021-02-13 18:00:30 -06:00
parent dcf5ca3e0a
commit df9a45137d
11 changed files with 407 additions and 120 deletions

View file

@ -47,7 +47,7 @@ impl Action for CreateComment {
changes.body_source(body_source); changes.body_source(body_source);
} }
let comment = changes.save()?; let comment = changes.save()??;
if let Some(apub_id) = &self.note_apub_id { if let Some(apub_id) = &self.note_apub_id {
ctx.apub.comment(apub_id, comment.id())?; ctx.apub.comment(apub_id, comment.id())?;
@ -124,7 +124,7 @@ impl Action for UpdateComment {
changes.body_source(body_source); changes.body_source(body_source);
} }
let comment = if changes.any_changes() { let comment = if changes.any_changes() {
changes.save()? changes.save()??
} else { } else {
comment comment
}; };

View file

@ -14,10 +14,11 @@ impl Action for CreateProfile {
self.owner_source.clone(), self.owner_source.clone(),
self.handle.clone(), self.handle.clone(),
self.domain.clone(), self.domain.clone(),
self.published, )?;
);
changes.login_required(self.login_required); changes
.login_required(self.login_required)
.published(self.published);
if let Some(display_name) = &self.display_name { if let Some(display_name) = &self.display_name {
changes.display_name(display_name); changes.display_name(display_name);
} }
@ -34,7 +35,7 @@ impl Action for CreateProfile {
changes.updated(updated); changes.updated(updated);
} }
let profile = changes.save()?; let profile = changes.save()??;
if let Some(apub) = &self.apub { if let Some(apub) = &self.apub {
ctx.apub.profile(&apub.person_apub_id, profile.id())?; ctx.apub.profile(&apub.person_apub_id, profile.id())?;
@ -110,7 +111,7 @@ impl Action for UpdateProfile {
} }
let profile = if changes.any_changes() { let profile = if changes.any_changes() {
changes.save()? changes.save()??
} else { } else {
profile profile
}; };

View file

@ -17,7 +17,7 @@ impl Action for CreateServer {
if let Some(description) = &self.description { if let Some(description) = &self.description {
changes.description(description); changes.description(description);
} }
let server = changes.save()?; let server = changes.save()??;
if let Some(apub) = &self.apub { if let Some(apub) = &self.apub {
ctx.apub.server(&apub.application_apub_id, server.id())?; ctx.apub.server(&apub.application_apub_id, server.id())?;
@ -66,7 +66,7 @@ impl Action for UpdateServer {
changes.description_source(description_source); changes.description_source(description_source);
} }
let server = if changes.any_changes() { let server = if changes.any_changes() {
changes.save()? changes.save()??
} else { } else {
server server
}; };

View file

@ -32,7 +32,7 @@ impl Action for CreateSubmission {
changes.add_file(*file); changes.add_file(*file);
} }
let submission = changes.save()?; let submission = changes.save()??;
let submission_id = submission.id(); let submission_id = submission.id();
if let Some(apub_id) = &self.note_apub_id { if let Some(apub_id) = &self.note_apub_id {
@ -150,7 +150,7 @@ impl Action for UpdateSubmission {
} }
let submission = if changes.any_changes() { let submission = if changes.any_changes() {
changes.save()? changes.save()??
} else { } else {
submission submission
}; };

View file

@ -132,8 +132,11 @@ impl Outbound for ProfileCreated {
shared_inbox: Some(shared_inbox), shared_inbox: Some(shared_inbox),
..Endpoints::default() ..Endpoints::default()
}) })
.set_preferred_username(profile.handle()) .set_preferred_username(profile.handle());
.set_published(profile.published().into());
if let Some(published) = profile.published() {
person.set_published(published.into());
}
if let Some(display_name) = profile.display_name() { if let Some(display_name) = profile.display_name() {
person.set_name(display_name); person.set_name(display_name);
@ -235,9 +238,11 @@ impl Outbound for ProfileUpdated {
person person
.set_id(person_id.clone()) .set_id(person_id.clone())
.set_preferred_username(profile.handle()) .set_preferred_username(profile.handle());
.set_published(profile.published().into());
if let Some(published) = profile.published() {
person.set_published(published.into());
}
if let Some(outbox) = endpoints.outbox { if let Some(outbox) = endpoints.outbox {
person.set_outbox(outbox); person.set_outbox(outbox);
} }

View file

@ -24,7 +24,7 @@ mod viewer;
use apub::ApubIds; use apub::ApubIds;
use pictrs::ImageInfo; use pictrs::ImageInfo;
use store::OwnerSource; use store::{OwnerSource, ValidationError};
use viewer::Viewer; use viewer::Viewer;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -44,6 +44,9 @@ pub enum Error {
#[error("Blocked")] #[error("Blocked")]
Blocked, Blocked,
#[error("Validations failed")]
Validate(Vec<ValidationError>),
#[error("Error deleting file: {0}")] #[error("Error deleting file: {0}")]
DeleteFile(#[from] pictrs::DeleteError), DeleteFile(#[from] pictrs::DeleteError),
@ -60,6 +63,22 @@ pub enum Error {
Apub(#[from] apub::StoreError), Apub(#[from] apub::StoreError),
} }
impl From<Vec<ValidationError>> for Error {
fn from(v: Vec<ValidationError>) -> Self {
Self::Validate(v)
}
}
impl Error {
pub fn validation_errors(&self) -> Option<&[ValidationError]> {
if let Self::Validate(errors) = &self {
Some(errors.as_slice())
} else {
None
}
}
}
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
pub enum OnBehalfOf { pub enum OnBehalfOf {
Profile(Uuid), Profile(Uuid),
@ -116,6 +135,21 @@ impl<T> Required<T> for Option<T> {
} }
} }
#[derive(Clone, Debug)]
pub struct ContentConfig {
pub max_bio_length: usize,
pub max_display_name_length: usize,
pub max_handle_length: usize,
pub max_domain_length: usize,
pub max_subission_title_length: usize,
pub max_submission_body_length: usize,
pub max_post_title_length: usize,
pub max_post_body_length: usize,
pub max_comment_length: usize,
pub max_server_title_length: usize,
pub max_server_body_length: usize,
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
enum KeyOwner { enum KeyOwner {
Server(Uuid), Server(Uuid),
@ -129,6 +163,7 @@ pub struct State {
pub pictrs: pictrs::State, pub pictrs: pictrs::State,
pub spawner: Arc<dyn Spawner + Send + Sync>, pub spawner: Arc<dyn Spawner + Send + Sync>,
pub url_for: Arc<dyn UrlFor + Send + Sync>, pub url_for: Arc<dyn UrlFor + Send + Sync>,
pub content_config: ContentConfig,
pub arbiter: Arbiter, pub arbiter: Arbiter,
_db: Db, _db: Db,
} }
@ -140,6 +175,7 @@ impl State {
apub_info: impl ApubIds + Send + Sync + 'static, apub_info: impl ApubIds + Send + Sync + 'static,
spawner: impl Spawner + Send + Sync + 'static, spawner: impl Spawner + Send + Sync + 'static,
url_for: impl UrlFor + Send + Sync + 'static, url_for: impl UrlFor + Send + Sync + 'static,
content_config: ContentConfig,
arbiter: Arbiter, arbiter: Arbiter,
db: Db, db: Db,
) -> Result<Arc<Self>, sled::Error> { ) -> Result<Arc<Self>, sled::Error> {
@ -149,6 +185,7 @@ impl State {
pictrs: pictrs::State::new(pictrs_upstream, image_info), pictrs: pictrs::State::new(pictrs_upstream, image_info),
spawner: Arc::new(spawner), spawner: Arc::new(spawner),
url_for: Arc::new(url_for), url_for: Arc::new(url_for),
content_config,
arbiter, arbiter,
_db: db, _db: db,
})) }))

View file

@ -1,4 +1,4 @@
use super::{StoreError, Undo}; use super::{StoreError, Undo, ValidationError, ValidationErrorKind};
use crate::State; use crate::State;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html}; use hyaenidae_content::{bbcode, html};
@ -39,6 +39,7 @@ pub struct CommentChanges<'a> {
body_source: Option<String>, body_source: Option<String>,
published: Option<DateTime<Utc>>, published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>, updated: Option<DateTime<Utc>>,
errors: Vec<ValidationError>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -115,7 +116,8 @@ impl Comment {
body: None, body: None,
body_source: None, body_source: None,
published: self.published, published: self.published,
updated: None, updated: self.updated,
errors: vec![],
} }
} }
@ -142,11 +144,29 @@ impl<'a> CommentChanges<'a> {
body_source: None, body_source: None,
published: None, published: None,
updated: None, updated: None,
errors: vec![],
} }
} }
pub(crate) fn body(&mut self, body: &str) -> &mut Self { pub(crate) fn body(&mut self, body: &str) -> &mut Self {
self.body = Some(html(body)); let body = html(body.trim());
if body.len() > self.state.content_config.max_comment_length {
self.errors.push(ValidationError {
field: String::from("body"),
kind: ValidationErrorKind::TooLong {
maximum: self.state.content_config.max_comment_length,
proposed: body.len(),
},
});
} else if body.is_empty() {
self.errors.push(ValidationError {
field: String::from("body"),
kind: ValidationErrorKind::Empty,
});
} else {
self.body = Some(body);
}
self self
} }
@ -163,9 +183,19 @@ impl<'a> CommentChanges<'a> {
} }
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self { pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
if self.published.is_some() { if let Some(published) = self.published {
if self.updated.unwrap_or(published) > updated {
self.errors.push(ValidationError {
field: String::from("updated"),
kind: ValidationErrorKind::Outdated {
current: self.updated.unwrap_or(published),
proposed: updated,
},
});
} else {
self.updated = Some(updated); self.updated = Some(updated);
} }
}
self self
} }
@ -173,8 +203,12 @@ impl<'a> CommentChanges<'a> {
self.body.is_some() || self.published.is_some() || self.updated.is_some() self.body.is_some() || self.published.is_some() || self.updated.is_some()
} }
pub(crate) fn save(self) -> Result<Comment, StoreError> { pub(crate) fn save(self) -> Result<Result<Comment, Vec<ValidationError>>, StoreError> {
self.state.store.comments.save(&self) if self.errors.is_empty() {
self.state.store.comments.save(&self).map(Ok)
} else {
Ok(Err(self.errors))
}
} }
} }
@ -319,16 +353,6 @@ impl Store {
let mut stored_comment: StoredComment = serde_json::from_slice(&stored_comment_ivec)?; let mut stored_comment: StoredComment = serde_json::from_slice(&stored_comment_ivec)?;
if let Some(updated) = changes.updated {
if let Some(previously_updated) =
stored_comment.updated.or_else(|| stored_comment.published)
{
if updated < previously_updated {
return Err(StoreError::Outdated);
}
}
}
if let Some(body) = &changes.body { if let Some(body) = &changes.body {
stored_comment.body = body.clone(); stored_comment.body = body.clone();
} }

View file

@ -23,6 +23,31 @@ pub use submission::{Store as SubmissionStore, Submission, SubmissionChanges, Vi
pub use term_search::TermSearch; pub use term_search::TermSearch;
pub use view::Store as ViewStore; pub use view::Store as ViewStore;
#[derive(Clone, Debug, thiserror::Error)]
pub enum ValidationErrorKind {
#[error("Provided string is too long, must be shorter than {maximum}")]
TooLong { maximum: usize, proposed: usize },
#[error("Provided string must be present")]
Empty,
#[error("Provided changes {proposed} are older than current data {current}")]
Outdated {
current: DateTime<Utc>,
proposed: DateTime<Utc>,
},
#[error("Tried to save with a file that doesn't exist: {0}")]
MissingFile(Uuid),
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("Failed to validate {field}: {kind}")]
pub struct ValidationError {
field: String,
kind: ValidationErrorKind,
}
#[derive(Clone)] #[derive(Clone)]
pub struct Store { pub struct Store {
pub profiles: profile::Store, pub profiles: profile::Store,

View file

@ -1,4 +1,4 @@
use super::{StoreError, TermSearch, Undo}; use super::{StoreError, TermSearch, Undo, ValidationError, ValidationErrorKind};
use crate::State; use crate::State;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html, strip}; use hyaenidae_content::{bbcode, html, strip};
@ -12,7 +12,6 @@ enum ProfileChangesKind {
source: OwnerSource, source: OwnerSource,
domain: String, domain: String,
handle: String, handle: String,
published: DateTime<Utc>,
}, },
Update { Update {
id: Uuid, id: Uuid,
@ -30,7 +29,9 @@ pub struct ProfileChanges<'a> {
login_required: Option<bool>, login_required: Option<bool>,
icon: Option<Uuid>, icon: Option<Uuid>,
banner: Option<Uuid>, banner: Option<Uuid>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>, updated: Option<DateTime<Utc>>,
errors: Vec<ValidationError>,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -51,11 +52,12 @@ pub struct Profile {
description_source: Option<String>, description_source: Option<String>,
icon: Option<Uuid>, icon: Option<Uuid>,
banner: Option<Uuid>, banner: Option<Uuid>,
published: DateTime<Utc>, published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>, updated: Option<DateTime<Utc>>,
login_required: bool, login_required: bool,
suspended: bool, suspended: bool,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Store { pub struct Store {
profile_tree: Tree, profile_tree: Tree,
@ -78,22 +80,34 @@ struct StoredProfile {
owner_source: StoredOwnerSource, owner_source: StoredOwnerSource,
handle: String, handle: String,
domain: String, domain: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
display_name: Option<String>, display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
display_name_source: Option<String>, display_name_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>, description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
description_source: Option<String>, description_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
icon: Option<Uuid>, icon: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
banner: Option<Uuid>, banner: Option<Uuid>,
published: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
published: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
updated: Option<DateTime<Utc>>, updated: Option<DateTime<Utc>>,
login_required: bool, login_required: bool,
created_at: DateTime<Utc>, created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
suspended_at: Option<DateTime<Utc>>, suspended_at: Option<DateTime<Utc>>,
} }
@ -104,15 +118,49 @@ impl<'a> ProfileChanges<'a> {
source: OwnerSource, source: OwnerSource,
domain: String, domain: String,
handle: String, handle: String,
published: DateTime<Utc>, ) -> Result<Self, Vec<ValidationError>> {
) -> Self { let handle = strip(handle.trim());
ProfileChanges { let domain = strip(domain.trim());
let mut errors = vec![];
if handle.len() > state.content_config.max_handle_length {
errors.push(ValidationError {
field: String::from("handle"),
kind: ValidationErrorKind::TooLong {
maximum: state.content_config.max_handle_length,
proposed: handle.len(),
},
});
} else if handle.is_empty() {
errors.push(ValidationError {
field: String::from("handle"),
kind: ValidationErrorKind::Empty,
});
}
if domain.len() > state.content_config.max_domain_length {
errors.push(ValidationError {
field: String::from("domain"),
kind: ValidationErrorKind::TooLong {
maximum: state.content_config.max_domain_length,
proposed: domain.len(),
},
});
} else if domain.is_empty() {
errors.push(ValidationError {
field: String::from("domain"),
kind: ValidationErrorKind::Empty,
});
}
if errors.is_empty() {
Ok(ProfileChanges {
state, state,
kind: ProfileChangesKind::Create { kind: ProfileChangesKind::Create {
source, source,
domain, domain,
handle, handle,
published,
}, },
display_name: None, display_name: None,
display_name_source: None, display_name_source: None,
@ -121,12 +169,35 @@ impl<'a> ProfileChanges<'a> {
login_required: None, login_required: None,
icon: None, icon: None,
banner: None, banner: None,
published: None,
updated: None, updated: None,
errors: vec![],
})
} else {
Err(errors)
} }
} }
pub(crate) fn display_name(&mut self, display_name: &str) -> &mut Self { pub(crate) fn display_name(&mut self, display_name: &str) -> &mut Self {
self.display_name = Some(strip(display_name)); let display_name = strip(display_name.trim());
if display_name.len() > self.state.content_config.max_display_name_length {
self.errors.push(ValidationError {
field: String::from("display_name"),
kind: ValidationErrorKind::TooLong {
maximum: self.state.content_config.max_display_name_length,
proposed: display_name.len(),
},
});
} else if display_name.is_empty() {
self.errors.push(ValidationError {
field: String::from("display_name"),
kind: ValidationErrorKind::Empty,
});
} else {
self.display_name = Some(display_name);
}
self self
} }
@ -136,7 +207,25 @@ impl<'a> ProfileChanges<'a> {
} }
pub(crate) fn description(&mut self, description: &str) -> &mut Self { pub(crate) fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(html(description)); let description = html(description.trim());
if description.len() > self.state.content_config.max_bio_length {
self.errors.push(ValidationError {
field: String::from("description"),
kind: ValidationErrorKind::TooLong {
maximum: self.state.content_config.max_bio_length,
proposed: description.len(),
},
});
} else if description.is_empty() {
self.errors.push(ValidationError {
field: String::from("description"),
kind: ValidationErrorKind::Empty,
});
} else {
self.description = Some(description);
}
self self
} }
@ -160,8 +249,27 @@ impl<'a> ProfileChanges<'a> {
self self
} }
pub(crate) fn published(&mut self, published: DateTime<Utc>) -> &mut Self {
if self.published.is_none() {
self.published = Some(published);
}
self
}
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self { pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
if let Some(published) = self.published {
if self.updated.unwrap_or(published) > updated {
self.errors.push(ValidationError {
field: String::from("updated"),
kind: ValidationErrorKind::Outdated {
current: self.updated.unwrap_or(published),
proposed: updated,
},
});
} else {
self.updated = Some(updated); self.updated = Some(updated);
}
}
self self
} }
@ -171,11 +279,14 @@ impl<'a> ProfileChanges<'a> {
|| self.login_required.is_some() || self.login_required.is_some()
|| self.icon.is_some() || self.icon.is_some()
|| self.banner.is_some() || self.banner.is_some()
|| self.updated.is_some()
} }
pub(crate) fn save(self) -> Result<Profile, StoreError> { pub(crate) fn save(self) -> Result<Result<Profile, Vec<ValidationError>>, StoreError> {
self.state.store.profiles.save(&self) if self.errors.is_empty() {
self.state.store.profiles.save(&self).map(Ok)
} else {
Ok(Err(self.errors))
}
} }
} }
@ -197,7 +308,9 @@ impl Profile {
login_required: None, login_required: None,
icon: None, icon: None,
banner: None, banner: None,
updated: None, published: self.published,
updated: self.updated,
errors: vec![],
} }
} }
@ -248,7 +361,7 @@ impl Profile {
self.banner self.banner
} }
pub fn published(&self) -> DateTime<Utc> { pub fn published(&self) -> Option<DateTime<Utc>> {
self.published self.published
} }
@ -283,9 +396,8 @@ impl Store {
source: OwnerSource, source: OwnerSource,
handle: String, handle: String,
domain: String, domain: String,
published: DateTime<Utc>, ) -> Result<ProfileChanges<'a>, Vec<ValidationError>> {
) -> ProfileChanges<'a> { ProfileChanges::new(state, source, domain, handle)
ProfileChanges::new(state, source, domain, handle, published)
} }
fn save<'a>(&self, changes: &ProfileChanges<'a>) -> Result<Profile, StoreError> { fn save<'a>(&self, changes: &ProfileChanges<'a>) -> Result<Profile, StoreError> {
@ -294,9 +406,8 @@ impl Store {
source, source,
handle, handle,
domain, domain,
published,
} => { } => {
let id = self.do_create(*source, handle, domain, *published)?; let id = self.do_create(*source, handle, domain)?;
self.do_update(id, changes) self.do_update(id, changes)
} }
ProfileChangesKind::Update { id } => self.do_update(*id, changes), ProfileChangesKind::Update { id } => self.do_update(*id, changes),
@ -308,7 +419,6 @@ impl Store {
source: OwnerSource, source: OwnerSource,
handle: &str, handle: &str,
domain: &str, domain: &str,
published: DateTime<Utc>,
) -> Result<Uuid, StoreError> { ) -> Result<Uuid, StoreError> {
let handle_lower = handle.to_lowercase(); let handle_lower = handle.to_lowercase();
@ -338,7 +448,7 @@ impl Store {
description_source: None, description_source: None,
icon: None, icon: None,
banner: None, banner: None,
published, published: None,
updated: None, updated: None,
login_required: true, login_required: true,
created_at: now, created_at: now,
@ -428,13 +538,6 @@ impl Store {
let mut stored_profile: StoredProfile = serde_json::from_slice(&stored_profile_ivec)?; let mut stored_profile: StoredProfile = serde_json::from_slice(&stored_profile_ivec)?;
if let Some(updated) = profile_changes.updated {
let previously_updated = stored_profile.updated.unwrap_or(stored_profile.published);
if updated < previously_updated {
return Err(StoreError::Outdated);
}
}
if let Some(login_required) = profile_changes.login_required { if let Some(login_required) = profile_changes.login_required {
stored_profile.login_required = login_required; stored_profile.login_required = login_required;
} }
@ -456,6 +559,9 @@ impl Store {
if let Some(banner) = profile_changes.banner { if let Some(banner) = profile_changes.banner {
stored_profile.banner = Some(banner); stored_profile.banner = Some(banner);
} }
if let Some(published) = profile_changes.published {
stored_profile.published = Some(published);
}
if let Some(updated) = profile_changes.updated { if let Some(updated) = profile_changes.updated {
stored_profile.updated = Some(updated); stored_profile.updated = Some(updated);
} }

View file

@ -1,4 +1,7 @@
use crate::{store::StoreError, State}; use crate::{
store::{StoreError, ValidationError, ValidationErrorKind},
State,
};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html, strip}; use hyaenidae_content::{bbcode, html, strip};
use sled::{Db, Transactional, Tree}; use sled::{Db, Transactional, Tree};
@ -33,6 +36,7 @@ pub struct ServerChanges<'a> {
description_source: Option<String>, description_source: Option<String>,
published: Option<DateTime<Utc>>, published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>, updated: Option<DateTime<Utc>>,
errors: Vec<ValidationError>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -94,7 +98,8 @@ impl Server {
description: None, description: None,
description_source: None, description_source: None,
published: self.published, published: self.published,
updated: None, updated: self.updated,
errors: vec![],
} }
} }
} }
@ -110,11 +115,30 @@ impl<'a> ServerChanges<'a> {
description_source: None, description_source: None,
published: None, published: None,
updated: None, updated: None,
errors: vec![],
} }
} }
pub(crate) fn title(&mut self, title: &str) -> &mut Self { pub(crate) fn title(&mut self, title: &str) -> &mut Self {
self.title = Some(strip(title)); let title = strip(title.trim());
if title.len() > self.state.content_config.max_server_title_length {
self.errors.push(ValidationError {
field: String::from("title"),
kind: ValidationErrorKind::TooLong {
maximum: self.state.content_config.max_server_title_length,
proposed: title.len(),
},
});
} else if title.is_empty() {
self.errors.push(ValidationError {
field: String::from("title"),
kind: ValidationErrorKind::Empty,
});
} else {
self.title = Some(title);
}
self self
} }
@ -124,7 +148,25 @@ impl<'a> ServerChanges<'a> {
} }
pub(crate) fn description(&mut self, description: &str) -> &mut Self { pub(crate) fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(html(description)); let description = html(description.trim());
if description.len() > self.state.content_config.max_server_body_length {
self.errors.push(ValidationError {
field: String::from("description"),
kind: ValidationErrorKind::TooLong {
maximum: self.state.content_config.max_server_body_length,
proposed: description.len(),
},
});
} else if description.is_empty() {
self.errors.push(ValidationError {
field: String::from("description"),
kind: ValidationErrorKind::Empty,
});
} else {
self.description = Some(description);
}
self self
} }
@ -141,18 +183,32 @@ impl<'a> ServerChanges<'a> {
} }
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self { pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
if self.published.is_some() { if let Some(published) = self.published {
if self.updated.unwrap_or(published) > updated {
self.errors.push(ValidationError {
field: String::from("updated"),
kind: ValidationErrorKind::Outdated {
current: self.updated.unwrap_or(published),
proposed: updated,
},
});
} else {
self.updated = Some(updated); self.updated = Some(updated);
} }
}
self self
} }
pub(crate) fn any_changes(&self) -> bool { pub(crate) fn any_changes(&self) -> bool {
self.title.is_some() || self.description.is_some() self.title.is_some() || self.description.is_some() || self.updated.is_some()
} }
pub(crate) fn save(self) -> Result<Server, StoreError> { pub(crate) fn save(self) -> Result<Result<Server, Vec<ValidationError>>, StoreError> {
self.state.store.servers.save(&self) if self.errors.is_empty() {
self.state.store.servers.save(&self).map(Ok)
} else {
Ok(Err(self.errors))
}
} }
} }
@ -263,20 +319,6 @@ impl Store {
None => return Err(StoreError::Missing), None => return Err(StoreError::Missing),
}; };
let stored_updated = self.id_updated.get(id.as_bytes())?.and_then(date_from_ivec);
let stored_published = self
.id_published
.get(id.as_bytes())?
.and_then(date_from_ivec);
if let Some(updated) = changes.updated {
if let Some(previously_updated) = stored_updated.or_else(|| stored_published) {
if updated < previously_updated {
return Err(StoreError::Outdated);
}
}
}
if let Some(title) = &changes.title { if let Some(title) = &changes.title {
self.id_title.insert(id.as_bytes(), title.as_bytes())?; self.id_title.insert(id.as_bytes(), title.as_bytes())?;
} }

View file

@ -1,4 +1,4 @@
use super::{StoreError, Undo}; use super::{StoreError, Undo, ValidationError, ValidationErrorKind};
use crate::State; use crate::State;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html, strip}; use hyaenidae_content::{bbcode, html, strip};
@ -52,6 +52,7 @@ pub struct SubmissionChanges<'a> {
sensitive: Option<bool>, sensitive: Option<bool>,
original_files: Vec<Uuid>, original_files: Vec<Uuid>,
files: Vec<Uuid>, files: Vec<Uuid>,
errors: Vec<ValidationError>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -101,12 +102,13 @@ impl Submission {
description_source: None, description_source: None,
visibility: None, visibility: None,
published: self.published, published: self.published,
updated: None, updated: self.updated,
local_only: None, local_only: None,
logged_in_only: None, logged_in_only: None,
sensitive: None, sensitive: None,
original_files: self.files.clone(), original_files: self.files.clone(),
files: self.files.clone(), files: self.files.clone(),
errors: vec![],
} }
} }
@ -192,11 +194,29 @@ impl<'a> SubmissionChanges<'a> {
sensitive: None, sensitive: None,
original_files: vec![], original_files: vec![],
files: vec![], files: vec![],
errors: vec![],
} }
} }
pub(crate) fn title(&mut self, title: &str) -> &mut Self { pub(crate) fn title(&mut self, title: &str) -> &mut Self {
self.title = Some(strip(title)); let title = strip(title.trim());
if title.len() > self.state.content_config.max_subission_title_length {
self.errors.push(ValidationError {
field: String::from("title"),
kind: ValidationErrorKind::TooLong {
maximum: self.state.content_config.max_subission_title_length,
proposed: title.len(),
},
});
} else if title.is_empty() {
self.errors.push(ValidationError {
field: String::from("title"),
kind: ValidationErrorKind::Empty,
});
} else {
self.title = Some(title);
}
self self
} }
@ -206,7 +226,24 @@ impl<'a> SubmissionChanges<'a> {
} }
pub(crate) fn description(&mut self, description: &str) -> &mut Self { pub(crate) fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(html(description)); let description = html(description.trim());
if description.len() > self.state.content_config.max_submission_body_length {
self.errors.push(ValidationError {
field: String::from("description"),
kind: ValidationErrorKind::TooLong {
maximum: self.state.content_config.max_subission_title_length,
proposed: description.len(),
},
});
} else if description.is_empty() {
self.errors.push(ValidationError {
field: String::from("description"),
kind: ValidationErrorKind::Empty,
});
} else {
self.description = Some(description);
}
self self
} }
@ -230,9 +267,19 @@ impl<'a> SubmissionChanges<'a> {
} }
pub(crate) fn updated(&mut self, time: DateTime<Utc>) -> &mut Self { pub(crate) fn updated(&mut self, time: DateTime<Utc>) -> &mut Self {
if self.published.is_some() { if let Some(published) = self.published {
if self.updated.unwrap_or(published) > time {
self.errors.push(ValidationError {
field: String::from("updated"),
kind: ValidationErrorKind::Outdated {
current: self.updated.unwrap_or(published),
proposed: time,
},
})
} else {
self.updated = Some(time); self.updated = Some(time);
} }
}
self self
} }
@ -256,7 +303,14 @@ impl<'a> SubmissionChanges<'a> {
} }
pub(crate) fn add_file(&mut self, file_id: Uuid) -> &mut Self { pub(crate) fn add_file(&mut self, file_id: Uuid) -> &mut Self {
if let Ok(Some(_)) = self.state.store.files.by_id(file_id) {
self.files.push(file_id); self.files.push(file_id);
} else {
self.errors.push(ValidationError {
field: String::from("files"),
kind: ValidationErrorKind::MissingFile(file_id),
});
}
self self
} }
@ -277,8 +331,12 @@ impl<'a> SubmissionChanges<'a> {
|| self.original_files != self.files || self.original_files != self.files
} }
pub(crate) fn save(self) -> Result<Submission, StoreError> { pub(crate) fn save(self) -> Result<Result<Submission, Vec<ValidationError>>, StoreError> {
self.state.store.submissions.save(&self) if self.errors.is_empty() {
self.state.store.submissions.save(&self).map(Ok)
} else {
Ok(Err(self.errors))
}
} }
} }
@ -403,17 +461,6 @@ impl Store {
let mut stored_submission: StoredSubmission = let mut stored_submission: StoredSubmission =
serde_json::from_slice(&stored_submission_ivec)?; serde_json::from_slice(&stored_submission_ivec)?;
if let Some(updated) = changes.updated {
if let Some(previously_updated) = stored_submission
.updated
.or_else(|| stored_submission.published)
{
if updated < previously_updated {
return Err(StoreError::Outdated);
}
}
}
let already_published = stored_submission.published.is_some(); let already_published = stored_submission.published.is_some();
if let Some(title) = &changes.title { if let Some(title) = &changes.title {