From e77e4d86e83d26d8e8dcea79da37b782c4261a43 Mon Sep 17 00:00:00 2001 From: fdb-hiroshima <35889323+fdb-hiroshima@users.noreply.github.com> Date: Sun, 27 Jan 2019 10:55:22 +0100 Subject: [PATCH] Better big form handling (#430) * Allow customizing max form size from env vars * Add error page for unprocessable entities And change default http port to 7878 * Improve char counter: under the editor, more discrete, and give it a default value --- plume-front/src/main.rs | 37 ++++++++++++++++--- src/main.rs | 19 +++++++++- src/routes/errors.rs | 10 +++++ src/routes/mod.rs | 19 +++++++++- src/routes/posts.rs | 22 ++++++----- templates/errors/unprocessable_entity.rs.html | 10 +++++ templates/posts/new.rs.html | 7 ++-- 7 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 templates/errors/unprocessable_entity.rs.html diff --git a/plume-front/src/main.rs b/plume-front/src/main.rs index 5b08b191..be33e279 100644 --- a/plume-front/src/main.rs +++ b/plume-front/src/main.rs @@ -1,16 +1,17 @@ +#![recursion_limit="128"] #[macro_use] extern crate stdweb; -use stdweb::{unstable::TryFrom, web::{*, event::*}}; +use stdweb::{unstable::{TryFrom, TryInto}, web::{*, event::*}}; fn main() { - auto_expand(); + editor_loop(); menu(); search(); } -/// Auto expands the editor when adding text -fn auto_expand() { +/// Auto expands the editor when adding text and count chars +fn editor_loop() { match document().query_selector("#plume-editor") { Ok(Some(x)) => HtmlElement::try_from(x).map(|article_content| { let offset = article_content.offset_height() - (article_content.get_bounding_client_rect().get_height() as i32); @@ -19,7 +20,33 @@ fn auto_expand() { js! { @{&article_content}.style.height = "auto"; @{&article_content}.style.height = @{&article_content}.scrollHeight - @{offset} + "px"; - } + }; + window().set_timeout(|| {match document().query_selector("#post-form") { + Ok(Some(form)) => HtmlElement::try_from(form).map(|form| { + if let Some(len) = form.get_attribute("content-size").and_then(|s| s.parse::().ok()) { + let consumed: i32 = js!{ + var len = - 1; + for(var i = 0; i < @{&form}.length; i++) { + if(@{&form}[i].name != "") { + len += @{&form}[i].name.length + encodeURIComponent(@{&form}[i].value) + .replace(/%20/g, "+") + .replace(/%0A/g, "%0D%0A") + .replace(new RegExp("[!'*()]", "g"), "XXX") //replace exceptions of encodeURIComponent with placeholder + .length + 2; + } + } + return len; + }.try_into().unwrap_or_default(); + match document().query_selector("#editor-left") { + Ok(Some(e)) => HtmlElement::try_from(e).map(|e| { + js!{@{e}.innerText = (@{len-consumed})}; + }).ok(), + _ => None, + }; + } + }).ok(), + _ => None, + };}, 0); }); }).ok(), _ => None diff --git a/src/main.rs b/src/main.rs index 8e98ebe0..a88da0f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,10 @@ extern crate validator_derive; extern crate webfinger; use diesel::r2d2::ConnectionManager; -use rocket::State; +use rocket::{ + Config, State, + config::Limits +}; use rocket_csrf::CsrfFairingBuilder; use plume_models::{ DATABASE_URL, Connection, Error, @@ -44,6 +47,7 @@ use plume_models::{ search::{Searcher as UnmanagedSearcher, SearcherError}, }; use scheduled_thread_pool::ScheduledThreadPool; +use std::env; use std::process::exit; use std::sync::Arc; use std::time::Duration; @@ -95,7 +99,17 @@ Then try to restart Plume. exit(0); }).expect("Error setting Ctrl-c handler"); - rocket::ignite() + let mut config = Config::active().unwrap(); + config.set_address(env::var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned())).unwrap(); + config.set_port(env::var("ROCKET_PORT").ok().map(|s| s.parse::().unwrap()).unwrap_or(7878)); + let _ = env::var("ROCKET_SECRET_KEY").map(|k| config.set_secret_key(k).unwrap()); + let form_size = &env::var("FORM_SIZE").unwrap_or_else(|_| "32".to_owned()).parse::().unwrap(); + let activity_size = &env::var("ACTIVITY_SIZE").unwrap_or_else(|_| "1024".to_owned()).parse::().unwrap(); + config.set_limits(Limits::new() + .limit("forms", form_size * 1024) + .limit("json", activity_size * 1024)); + + rocket::custom(config) .mount("/", routes![ routes::blogs::details, routes::blogs::activity_details, @@ -196,6 +210,7 @@ Then try to restart Plume. ]) .register(catchers![ routes::errors::not_found, + routes::errors::unprocessable_entity, routes::errors::server_error ]) .manage(dbpool) diff --git a/src/routes/errors.rs b/src/routes/errors.rs index d91bd9fb..ec2e510c 100644 --- a/src/routes/errors.rs +++ b/src/routes/errors.rs @@ -47,6 +47,16 @@ pub fn not_found(req: &Request) -> Ructe { )) } +#[catch(422)] +pub fn unprocessable_entity(req: &Request) -> Ructe { + let conn = req.guard::().succeeded(); + let intl = req.guard::().succeeded(); + let user = User::from_request(req).succeeded(); + render!(errors::unprocessable_entity( + &(&*conn.unwrap(), &intl.unwrap().catalog, user) + )) +} + #[catch(500)] pub fn server_error(req: &Request) -> Ructe { let conn = req.guard::().succeeded(); diff --git a/src/routes/mod.rs b/src/routes/mod.rs index a6ff1917..0fd3e4bd 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,7 +1,8 @@ use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder}; use rocket::{ - http::{RawStr, uri::{FromUriParam, Query}}, - request::FromFormValue, + http::{RawStr, Status, uri::{FromUriParam, Query}}, + Outcome, + request::{self, FromFormValue, FromRequest, Request}, response::NamedFile, }; use std::path::{Path, PathBuf}; @@ -46,6 +47,20 @@ impl Page { } } +pub struct ContentLen(pub u64); + +impl<'a, 'r> FromRequest<'a, 'r> for ContentLen { + type Error = (); + + fn from_request(r: &'a Request<'r>) -> request::Outcome { + match r.limits().get("forms") { + Some(l) => Outcome::Success(ContentLen(l)), + None => Outcome::Failure((Status::InternalServerError, ())), + } + } +} + + impl Default for Page { fn default() -> Self { Page(1) diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 42a59716..a7038c4d 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -24,7 +24,7 @@ use plume_models::{ tags::*, users::User }; -use routes::{errors::ErrorPage, comments::NewCommentForm}; +use routes::{errors::ErrorPage, comments::NewCommentForm, ContentLen}; use template_utils::Ructe; use Worker; use Searcher; @@ -103,7 +103,7 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash { } #[get("/~//new", rank = 1)] -pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Result { +pub fn new(blog: String, user: User, cl: ContentLen, conn: DbConn, intl: I18n) -> Result { let b = Blog::find_by_fqn(&*conn, &blog)?; if !user.is_author_in(&*conn, &b)? { @@ -125,13 +125,14 @@ pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Result//edit")] -pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Result { +pub fn edit(blog: String, slug: String, user: User, cl: ContentLen, conn: DbConn, intl: I18n) -> Result { let b = Blog::find_by_fqn(&*conn, &blog)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?; @@ -168,13 +169,14 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> !post.published, Some(post), ValidationErrors::default(), - medias + medias, + cl.0 ))) } } #[post("/~///edit", data = "
")] -pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: LenientForm, worker: Worker, intl: I18n, searcher: Searcher) +pub fn update(blog: String, slug: String, user: User, cl: ContentLen, form: LenientForm, worker: Worker, conn: DbConn, intl: I18n, searcher: Searcher) -> Result { let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error"); let mut post = Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error"); @@ -261,7 +263,8 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien form.draft.clone(), Some(post), errors.clone(), - medias.clone() + medias.clone(), + cl.0 ))) } } @@ -290,7 +293,7 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> { } #[post("/~//new", data = "")] -pub fn create(blog_name: String, form: LenientForm, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result> { +pub fn create(blog_name: String, form: LenientForm, user: User, cl: ContentLen, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result> { let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");; let slug = form.title.to_string().to_kebab_case(); @@ -384,7 +387,8 @@ pub fn create(blog_name: String, form: LenientForm, user: User, con form.draft, None, errors.clone(), - medias + medias, + cl.0 )))) } } diff --git a/templates/errors/unprocessable_entity.rs.html b/templates/errors/unprocessable_entity.rs.html new file mode 100644 index 00000000..af3692fa --- /dev/null +++ b/templates/errors/unprocessable_entity.rs.html @@ -0,0 +1,10 @@ +@use templates::errors::base; +@use template_utils::*; + +@(ctx: BaseContext) + +@:base(ctx, "Unprocessable entity", { +

@i18n!(ctx.1, "The content you sent can't be processed.")

+

@i18n!(ctx.1, "Maybe it was too long.")

+}) + diff --git a/templates/posts/new.rs.html b/templates/posts/new.rs.html index 3b399512..c8d270b2 100644 --- a/templates/posts/new.rs.html +++ b/templates/posts/new.rs.html @@ -8,7 +8,7 @@ @use routes::posts::NewPostForm; @use routes::*; -@(ctx: BaseContext, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option, errors: ValidationErrors, medias: Vec) +@(ctx: BaseContext, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option, errors: ValidationErrors, medias: Vec, content_len: u64) @:base(ctx, &i18n!(ctx.1, if editing { "Edit {0}" } else { "New post" }; &form.title), {}, {}, {

@@ -19,9 +19,9 @@ }

@if let Some(article) = article { - + } else { - + } @input!(ctx.1, title (text), "Title", form, errors.clone(), "required") @input!(ctx.1, subtitle (optional text), "Subtitle", form, errors.clone(), "") @@ -32,6 +32,7 @@ + @content_len @input!(ctx.1, tags (optional text), "Tags, separated by commas", form, errors.clone(), "")