@ -1,48 +0,0 @@ | |||||
FROM rust:slim | |||||
WORKDIR /opt | |||||
# Install needed dependecies | |||||
RUN echo "deb http://ftp.de.debian.org/debian unstable main contrib" | tee -a /etc/apt/sources.list | |||||
RUN apt-get update && apt-get install -y libmysql++-dev git | |||||
RUN git clone https://git.42l.fr/neil/sncf.git | |||||
WORKDIR /opt/sncf | |||||
COPY config.toml /opt/sncf/config.toml | |||||
# graphics individualization | |||||
COPY foorms_logo_beta.svg /opt/sncf/templates/assets/foorms_logo_beta.svg | |||||
COPY white-background.png /opt/sncf/templates/assets/index-background.png | |||||
COPY Digi_3corner.png /opt/sncf/templates/assets/flavicon.ico | |||||
COPY index.css /opt/sncf/templates/assets/index.css | |||||
COPY cloud.css /opt/sncf/templates/assets/cloud.css | |||||
COPY bootstrap.min.css /opt/sncf/templates/assets/bootstrap.min.css | |||||
COPY digitalcourage.css /opt/sncf/templates/assets/digitalcourage.css | |||||
COPY index.html /opt/sncf/templates/index.html | |||||
COPY link.html /opt/sncf/templates/link.html | |||||
COPY forward.rs /opt/sncf/src/forward.rs | |||||
#COPY templates.rs /opt/sncf/src/templates.rs | |||||
# The written is just firstly a hack | |||||
COPY lang.json /opt/sncf/lang.json | |||||
CMD cargo run --no-default-features --features mysql | |||||
@ -1,281 +0,0 @@ | |||||
use actix_web::client::Client; | |||||
use actix_web::{http, web, HttpRequest, HttpResponse}; | |||||
use base64::URL_SAFE_NO_PAD; | |||||
use percent_encoding::percent_decode_str; | |||||
use rand::rngs::OsRng; | |||||
use rand::Rng; | |||||
use rand::RngCore; | |||||
use regex::Regex; | |||||
use std::collections::HashMap; | |||||
use std::time::Duration; | |||||
use crate::config::{ADJ_LIST, NAME_LIST, PROXY_TIMEOUT, USER_AGENT}; | |||||
use crate::debug; | |||||
use crate::errors::{crash, TrainCrash}; | |||||
use crate::templates::get_lang; | |||||
use crate::CONFIG; | |||||
#[derive(Serialize)] | |||||
struct NCLoginForm<'a> { | |||||
pub user: &'a str, | |||||
pub password: &'a str, | |||||
pub timezone: &'a str, | |||||
pub timezone_offset: &'a str, | |||||
pub requesttoken: &'a str, | |||||
} | |||||
// check if the user is connected to Nextcloud | |||||
// returns Some(cookie_raw_value) if connected | |||||
// returns None if disconnected | |||||
pub fn is_logged_in(req: &HttpRequest) -> Option<&str> { | |||||
let c = req.headers().get("Cookie")?.to_str().ok()?; | |||||
if c.contains("nc_username") { | |||||
Some(c) | |||||
} else { | |||||
None | |||||
} | |||||
} | |||||
// attempts to create the account from Nextcloud's API | |||||
// returns the newly created username. | |||||
// if it fails (bad return code), returns None. | |||||
pub async fn create_account( | |||||
client: &web::Data<Client>, | |||||
user: &str, | |||||
password: &str, | |||||
lang: String, | |||||
) -> Result<String, TrainCrash> { | |||||
let mut register_query = client | |||||
.post(format!( | |||||
"{}/{}", | |||||
CONFIG.nextcloud_url, "ocs/v1.php/cloud/users" | |||||
)) | |||||
.timeout(Duration::new(PROXY_TIMEOUT, 0)) | |||||
.basic_auth(&CONFIG.admin_username, Some(&CONFIG.admin_password)) | |||||
.header( | |||||
http::header::CONTENT_TYPE, | |||||
"application/x-www-form-urlencoded", | |||||
) | |||||
.header("OCS-APIRequest", "true") | |||||
.send_form(&NCCreateAccountForm { | |||||
userid: user, | |||||
password, | |||||
quota: "0B", | |||||
language: &lang, | |||||
}) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_createaccount_post: {}", e); | |||||
crash(lang.clone(), "error_createaccount_post") | |||||
})?; | |||||
// only 200 http status code is allowed | |||||
if register_query.status() != 200 { | |||||
eprintln!("error_createaccount_status: {}", register_query.status()); | |||||
// + extract response body for debugging purposes | |||||
let response_body = register_query.body().await.map_err(|e| { | |||||
eprintln!("error_createaccount_post_body: {}", e); | |||||
crash(lang.clone(), "error_createaccount_post_body") | |||||
})?; | |||||
debug(&format!("Body: {:#?}", response_body)); | |||||
return Err(crash(lang.clone(), "error_createaccount_status")); | |||||
} | |||||
// extract response body | |||||
let response_body = register_query.body().await.map_err(|e| { | |||||
eprintln!("error_createaccount_post_body: {}", e); | |||||
crash(lang.clone(), "error_createaccount_post_body") | |||||
})?; | |||||
let response_body = String::from_utf8_lossy(&response_body); | |||||
// grasp NC status code | |||||
let status_start = response_body.find("<statuscode>").ok_or_else(|| { | |||||
eprintln!("error_createaccount_ncstatus_parse: start missing"); | |||||
crash(lang.clone(), "error_createaccount_ncstatus_parse") | |||||
})? + 12; | |||||
let status_end = response_body.find("</statuscode>").ok_or_else(|| { | |||||
eprintln!("error_createaccount_ncstatus_parse: end missing"); | |||||
crash(lang.clone(), "error_createaccount_ncstatus_parse") | |||||
})?; | |||||
let code = &response_body[status_start..status_end]; | |||||
match code.parse::<u16>() { | |||||
Ok(100) => Ok(String::from(user)), // success | |||||
Ok(r) => { | |||||
eprintln!("error_createaccount_ncstatus: {}", r); | |||||
Err(crash(lang.clone(), "error_createaccount_ncstatus")) | |||||
} | |||||
Err(e) => { | |||||
eprintln!("error_createaccount_ncstatus_parse: {}", e); | |||||
Err(crash(lang.clone(), "error_createaccount_ncstatus_parse")) | |||||
} | |||||
} | |||||
} | |||||
#[derive(Serialize)] | |||||
struct NCCreateAccountForm<'a> { | |||||
pub userid: &'a str, | |||||
pub password: &'a str, | |||||
pub quota: &'a str, | |||||
pub language: &'a str, | |||||
} | |||||
pub async fn login( | |||||
client: &web::Data<Client>, | |||||
req: &HttpRequest, | |||||
user: &str, | |||||
password: &str, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
debug(&format!("Sending forged login for user {}", user)); | |||||
// 1. GET /csrftoken | |||||
let mut login_get = client | |||||
.get(format!("{}/{}", CONFIG.nextcloud_url, "csrftoken")) | |||||
.timeout(Duration::new(PROXY_TIMEOUT, 0)) | |||||
.header("User-Agent", USER_AGENT) | |||||
.header("Accept-Language" , "fr" ) | |||||
.send() | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_login_get: {}", e); | |||||
crash(get_lang(&req), "error_login_get") | |||||
})?; | |||||
// rewrite cookie headers from GET to POST | |||||
let mut str_cookiepair = String::new(); | |||||
// remove duplicate oc<id> cookie (nextcloud bug) | |||||
// leading to sncf being unable to forge logins | |||||
let cookie_set = login_get.headers().get_all("set-cookie"); | |||||
let mut cookie_map: HashMap<String, String> = HashMap::new(); | |||||
for c in cookie_set { | |||||
// get str version of cookie header | |||||
let c_str = c.to_str().map_err(|e| { | |||||
eprintln!("error_login_cookiepair (1): {}", e); | |||||
crash(get_lang(&req), "error_login_cookiepair") | |||||
})?; | |||||
// percent decode | |||||
let c_str = percent_decode_str(c_str).decode_utf8_lossy(); | |||||
//then remove values after ';' | |||||
let c_str_arr = c_str.split(';').collect::<Vec<&str>>(); | |||||
let c_str = c_str_arr | |||||
.first() | |||||
.expect("error: cookiepair split does not have a first value. shouldn't happen."); | |||||
// split cookie key and cookie value | |||||
// split_once would work best but it's nightly-only for now | |||||
let c_str_arr = c_str.split('=').collect::<Vec<&str>>(); | |||||
let c_key = c_str_arr | |||||
.first() | |||||
.expect("error: cookie key split does not have a first value, shouldn't happen."); | |||||
let c_value = c_str.replace(&format!("{}=", c_key), ""); | |||||
if c_key != c_str { | |||||
// if the key already exists in hashmap, replace its value | |||||
// else, insert it | |||||
if let Some(c_sel) = cookie_map.get_mut(*c_key) { | |||||
*c_sel = c_value; | |||||
} else { | |||||
cookie_map.insert(c_key.to_string(), c_value); | |||||
} | |||||
} else { | |||||
eprintln!("error_login_cookiepair (2)"); | |||||
return Err(crash(get_lang(&req), "error_login_cookiepair")); | |||||
} | |||||
} | |||||
for (cookie_k, cookie_v) in cookie_map { | |||||
str_cookiepair.push_str(&format!("{}={}; ", cookie_k, cookie_v)); | |||||
} | |||||
// load requesttoken regex | |||||
lazy_static! { | |||||
static ref RE: Regex = Regex::new(r#"\{"token":"(?P<token>[^"]*)"\}"#) | |||||
.expect("Error while parsing the requesttoken regex"); | |||||
} | |||||
let post_body = login_get.body().await.map_err(|e| { | |||||
eprintln!("error_login_get_body: {}", e); | |||||
crash(get_lang(&req), "error_login_get_body") | |||||
})?; | |||||
let post_body_str = String::from_utf8_lossy(&post_body); | |||||
// save requesttoken (CSRF) for POST | |||||
let requesttoken = RE | |||||
.captures(&post_body_str) | |||||
.ok_or_else(|| { | |||||
eprintln!("error_login_regex (no capture)"); | |||||
crash(get_lang(&req), "error_login_regex") | |||||
})? | |||||
.name("token") | |||||
.ok_or_else(|| { | |||||
eprintln!("error_login_regex (no capture named token)"); | |||||
crash(get_lang(&req), "error_login_regex") | |||||
})? | |||||
.as_str(); | |||||
// 2. POST /login | |||||
let mut login_post = client | |||||
.post(format!("{}/{}", CONFIG.nextcloud_url, "login")) | |||||
.timeout(Duration::new(PROXY_TIMEOUT, 0)) | |||||
.header("User-Agent", USER_AGENT) | |||||
.header("Accept-Language" , "fr" ); | |||||
// include all NC cookies in one cookie (cookie pair) | |||||
login_post = login_post.header("Cookie", str_cookiepair); | |||||
// send the same POST data as you'd log in from a web browser | |||||
let response_post = login_post | |||||
.send_form(&NCLoginForm { | |||||
user, | |||||
password, | |||||
timezone: "UTC", | |||||
timezone_offset: "2", | |||||
requesttoken, | |||||
}) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_login_post: {}", e); | |||||
crash(get_lang(&req), "error_login_post") | |||||
})?; | |||||
// 3. set the same cookies in the user's browser | |||||
let mut user_response = HttpResponse::SeeOther(); | |||||
for item in response_post.headers().clone().get_all("set-cookie") { | |||||
user_response.header( | |||||
"Set-Cookie", | |||||
item.to_str().map_err(|e| { | |||||
eprintln!("error_login_setcookie: {}", e); | |||||
crash(get_lang(&req), "error_login_setcookie") | |||||
})?, | |||||
); | |||||
} | |||||
// redirect to forms! | |||||
Ok(user_response | |||||
.header("Accept-Language", "fr" ) | |||||
.header(http::header::LOCATION, "/apps/forms") | |||||
.finish() | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_login_redir: {}", e); | |||||
crash(get_lang(&req), "error_login_redir") | |||||
})?) | |||||
} | |||||
// checks if the token seems valid before asking the db. | |||||
// The token must be 45 bytes long and base64-encoded. | |||||
// returns true if the token is valid | |||||
pub fn check_token(token: &str) -> bool { | |||||
let token_dec = base64::decode_config(token, URL_SAFE_NO_PAD); | |||||
if let Ok(token_bytes) = token_dec { | |||||
token_bytes.len() == 45 | |||||
} else { | |||||
false | |||||
} | |||||
} | |||||
// generates a new token | |||||
pub fn gen_token(size: usize) -> String { | |||||
// Using /dev/random to generate random bytes | |||||
let mut r = OsRng; | |||||
let mut my_secure_bytes = vec![0u8; size]; | |||||
r.fill_bytes(&mut my_secure_bytes); | |||||
base64::encode_config(my_secure_bytes, URL_SAFE_NO_PAD) | |||||
} | |||||
// generates a random username composed of | |||||
// an adjective, a name and a 4-byte base64-encoded token. | |||||
// with the default list, that represents: | |||||
// 141 * 880 = 124 080 | |||||
// 255^4 / 2 = 2 114 125 312 (we lose approx. the half because of uppercase) | |||||
// 2 114 125 312 * 124 080 = 2.623206687*10^14 possible combinations?? | |||||
pub fn gen_name() -> String { | |||||
// uppercasing gen_token because NC would probably refuse two | |||||
// users with the same name but a different case | |||||
// and that'd be a pain to debug | |||||
format!( | |||||
"{}{}-{}", | |||||
list_rand(&ADJ_LIST), | |||||
list_rand(&NAME_LIST), | |||||
gen_token(4).to_uppercase() | |||||
) | |||||
} | |||||
pub fn list_rand(list: &[String]) -> &String { | |||||
let mut rng = rand::thread_rng(); | |||||
let roll = rng.gen_range(0..list.len() - 1); | |||||
&list[roll] | |||||
} |
@ -1,148 +0,0 @@ | |||||
.has-text-centered > * { | |||||
text-align: center; | |||||
} | |||||
.c-subelem, .c-fullwidth > * { | |||||
color: #2c2c2c; | |||||
} | |||||
.c-blue { | |||||
} | |||||
.c-blue > a { | |||||
color: white; | |||||
background: #4b97ca; | |||||
width: 154px; | |||||
height: 35px; | |||||
} | |||||
.c-flex { | |||||
display: flex; | |||||
flex-wrap: wrap; | |||||
justify-content: space-evenly; | |||||
} | |||||
@media screen and (min-width:1280px) { | |||||
.c-flex.c-flex-reverse { | |||||
flex-direction: row-reverse; | |||||
} | |||||
.c-jumbo { | |||||
padding: 1.5rem 0; | |||||
} | |||||
.c-subelem { | |||||
padding: 0; | |||||
max-width: 40vw; | |||||
margin: auto 0; | |||||
} | |||||
} | |||||
.c-jumbo.c-jumbo-big { | |||||
min-height: 25rem; | |||||
padding: 1rem; | |||||
} | |||||
.c-jumbo.c-jumbo-medium { | |||||
min-height: 18rem; | |||||
padding: 1rem; | |||||
} | |||||
.c-jumbo.c-jumbo-small { | |||||
min-height: 10rem; | |||||
padding: 1rem; | |||||
} | |||||
.c-button { | |||||
display: block; | |||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||||
border-radius: 10pt; | |||||
text-align: center; | |||||
transition: all .2s ease-in-out; | |||||
white-space: nowrap; | |||||
cursor: pointer; | |||||
text-decoration: none; | |||||
padding: 0.4em; | |||||
width: max-content; | |||||
height: max-content; | |||||
min-width: 154px; | |||||
min-height: 35px; | |||||
margin: 0.5rem; | |||||
color: white; | |||||
text-weight: bolder; | |||||
} | |||||
.c-button:only-child { | |||||
margin: auto; | |||||
} | |||||
.c-button.c-big { | |||||
font-size: x-large; | |||||
} | |||||
.c-subelem { | |||||
margin: auto 2rem; | |||||
padding: 1rem 0; | |||||
width: 100%; | |||||
} | |||||
.c-img-shadow { | |||||
height: auto; | |||||
max-width: 100%; | |||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||||
border-radius: 2px; | |||||
} | |||||
.c-img-center { | |||||
display: block; | |||||
margin: auto; | |||||
} | |||||
.c-fullwidth { | |||||
width: 100%; | |||||
margin: auto 2rem; | |||||
} | |||||
@media screen and (max-width:1279px) { | |||||
.c-no-margin-mobile { | |||||
margin: 0 !important; | |||||
} | |||||
} | |||||
.c-jumbo { | |||||
padding: .5rem 0; | |||||
width: 100%; | |||||
} | |||||
.c-fade-left { | |||||
opacity: 0; | |||||
transform: translateX(-100px); | |||||
animation: fadeInLeft 2s ease-in-out both; | |||||
} | |||||
.c-fade-right { | |||||
opacity: 0; | |||||
transform: translateX(100px); | |||||
animation: fadeInRight 2s ease-in-out both; | |||||
} | |||||
@keyframes fadeInLeft { | |||||
0% { | |||||
opacity: 0; | |||||
transform: translateX(-100px); | |||||
} | |||||
100% { | |||||
opacity: 1; | |||||
transform: translateX(0); | |||||
} | |||||
} | |||||
@keyframes fadeInRight { | |||||
0% { | |||||
opacity: 0; | |||||
transform: translateX(100px); | |||||
} | |||||
100% { | |||||
opacity: 1; | |||||
transform: translateX(0); | |||||
} | |||||
} |
@ -1,34 +0,0 @@ | |||||
# The address and port sncf will listen | |||||
listening_address = "0.0.0.0" | |||||
listening_port = 8000 | |||||
# Public-facing domain for sncf. | |||||
# includes protocol, FQDN and port, without the trailing slash. | |||||
sncf_url = "http://basabuuka.org" | |||||
# SQLite: path to the SQLite DB | |||||
# PostgreSQL: postgres://user:password@address:port/database | |||||
# MySQL: mysql://user:password@address:port/database | |||||
database_path = "mysql://nextcloud:KF8zUh1q4HovFmBa6lnk7xCmvoonfBoE@nextcloud-db:3306/nextcloud" | |||||
# IP address of the Nextcloud instance, including protocol and port | |||||
nextcloud_url = "http://nextcloud-web:80" | |||||
# Nextcloud admin account credentials | |||||
# TODO hash adminpw | |||||
admin_username = "sncf_admin" | |||||
admin_password = "DieHeiligeKuhDerNacht1635" | |||||
# How many days of inactivity for an admin token before deleting NC accounts | |||||
prune_days = 40 | |||||
# Displays route names and a lot of information | |||||
debug_mode = true | |||||
# Used to encrypt csrf tokens and csrf cookies. | |||||
# Generate random bytes: openssl rand -base64 32 | |||||
# Then paste the result in this variable | |||||
cookie_key = "Af3v5KMNPmwYYBRRjm/W5ds1rHDdyCEvpxVTMLKEKl0=" | |||||
# Don't touch this unless you know what you're doing | |||||
config_version = 2 |
@ -1,572 +0,0 @@ | |||||
/* This software is governed by the CeCILL-B license. If a copy of this license | |||||
* is not distributed with this file, you can obtain one at | |||||
* http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.txt | |||||
* | |||||
* Authors of STUdS (initial project) : Guilhem BORGHESI (borghesi@unistra.fr) and Raphaël DROZ | |||||
* Authors of OpenSondage : Framasoft (https://github.com/framasoft) | |||||
* | |||||
* ============================= | |||||
* | |||||
* Ce logiciel est régi par la licence CeCILL-B. Si une copie de cette licence | |||||
* ne se trouve pas avec ce fichier vous pouvez l'obtenir sur | |||||
* http://www.cecill.info/licences/Licence_CeCILL_V2.1-fr.txt | |||||
* | |||||
* Auteurs de STUdS (projet initial) : Guilhem BORGHESI (borghesi@unistra.fr) et Raphaël DROZ | |||||
* Auteurs d'OpenSondage : Framasoft (https://github.com/framasoft) | |||||
*/ | |||||
@font-face { | |||||
font-family: "DejaVu Sans"; | |||||
src: url('../fonts/DejaVuSans.ttf'); | |||||
} | |||||
body { | |||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; | |||||
color:#333; | |||||
background:#eee; | |||||
} | |||||
.trait { /* hr */ | |||||
background-color: #EEE; | |||||
height: 5px; | |||||
margin: 5px 0; | |||||
border: none; | |||||
} | |||||
.ombre { | |||||
background-color: #FFF; | |||||
box-shadow: -4px 6px 9px rgba(50, 50, 50, 0.5); | |||||
margin: 15px auto 30px; | |||||
} | |||||
.hide { | |||||
display: none; | |||||
} | |||||
/* Commentaires */ | |||||
div.comment{ | |||||
margin-bottom: 10px; | |||||
border-left: 1px dashed #999; | |||||
background: #F5F5F5; | |||||
padding-top: 4px; | |||||
padding-bottom: 4px; | |||||
padding-left: 14px; | |||||
} | |||||
.comment_date { | |||||
font-style: italic; | |||||
font-size: 12px; | |||||
letter-spacing: -0.7px; | |||||
color: grey; | |||||
} | |||||
/* Règles générales */ | |||||
a:focus { /* a11y */ | |||||
outline:#000 dotted 1px; | |||||
} | |||||
header, footer { | |||||
} | |||||
main { | |||||
margin-top: 20px; | |||||
} | |||||
header h1 { | |||||
margin-top: 0; | |||||
} | |||||
.container { | |||||
padding: 30px; | |||||
} | |||||
.container .jumbotron { | |||||
padding: 20px 20px; | |||||
border-radius: 2px; | |||||
} | |||||
.container .jumbotron p { | |||||
font-size: 1em; | |||||
} | |||||
.container .jumbotron .btn-group >.btn { | |||||
margin-bottom: 20px; | |||||
white-space: normal; | |||||
} | |||||
.summary h4 { | |||||
margin-top:0; | |||||
} | |||||
.summary { | |||||
font-weight:bold; | |||||
} | |||||
.summary img { | |||||
max-width:100px; | |||||
} | |||||
.alert { | |||||
border-radius: 2px; | |||||
} | |||||
.very-small { | |||||
font-size: 70%; | |||||
} | |||||
/* Effet sur les images en page d'accueil */ | |||||
.opacity img { | |||||
opacity: 0.8; | |||||
} | |||||
.opacity:hover img { | |||||
opacity: 1; | |||||
} | |||||
.home-choice { | |||||
margin-bottom:50px; | |||||
} | |||||
/* Description du sondage */ | |||||
/* studs.php et adminstuds.php */ | |||||
header .lead { | |||||
padding: 10px 0; | |||||
margin:0; | |||||
} | |||||
header form .input-group .form-control { | |||||
margin-bottom: 20px; | |||||
} | |||||
header form .input-group .input-group-btn { | |||||
vertical-align: top; | |||||
} | |||||
#admin-link, #public-link { | |||||
cursor:text; | |||||
} | |||||
.admin-link, .public-link, | |||||
.admin-link:hover, .public-link:hover { | |||||
color:#333; | |||||
text-decoration:none; | |||||
border:none; | |||||
} | |||||
.jumbotron h3, .jumbotron .js-title { | |||||
margin-bottom:20px; | |||||
margin-top:0; | |||||
} | |||||
.poll-description { | |||||
font-family: inherit; | |||||
word-break: initial; | |||||
} | |||||
/** Description in markdown **/ | |||||
.form-group .CodeMirror, .form-group .CodeMirror-scroll { | |||||
min-height: 200px; | |||||
} | |||||
#description-form .CodeMirror { | |||||
background-color: #f5f5f5; | |||||
} | |||||
.editor-toolbar { | |||||
margin-top: 10px; | |||||
background-color: #eee; | |||||
} | |||||
#poll_comments { | |||||
margin-top: 10px; | |||||
} | |||||
h4.control-label { | |||||
display: inline-block; | |||||
max-width: 100%; | |||||
margin-bottom: 5px; | |||||
font-weight: 700; | |||||
font-size: 14px; | |||||
line-height: 1.42857; | |||||
margin-top:0; | |||||
} | |||||
caption { | |||||
padding: 0 10px 10px; | |||||
font-weight:bold; | |||||
} | |||||
.results a.btn-default.btn-sm { | |||||
padding: 3px 7px; | |||||
font-size: 0.7em; | |||||
} | |||||
/* adminstuds.php */ | |||||
#title-form h3 .btn-edit, | |||||
#email-form .btn-edit, | |||||
#description-form .btn-edit, | |||||
#poll-rules-form .btn-edit, | |||||
#poll-hidden-form .btn-edit, | |||||
#expiration-form .btn-edit, | |||||
#password-form .btn-edit, | |||||
#name-form .btn-edit { | |||||
position:absolute; | |||||
left:-2000px; | |||||
} | |||||
#title-form .btn-edit:focus, | |||||
#title-form h3:hover .btn-edit, | |||||
#email-form .btn-edit:focus, | |||||
#email-form:hover .btn-edit, | |||||
#description-form .btn-edit:focus, | |||||
#description-form:hover .btn-edit, | |||||
#poll-rules-form .btn-edit:focus, | |||||
#poll-rules-form:hover .btn-edit, | |||||
#poll-hidden-form .btn-edit:focus, | |||||
#poll-hidden-form:hover .btn-edit, | |||||
#expiration-form .btn-edit:focus, | |||||
#expiration-form:hover .btn-edit, | |||||
#password-form .btn-edit:focus, | |||||
#password-form:hover .btn-edit, | |||||
#name-form .btn-edit:focus, | |||||
#name-form:hover .btn-edit { | |||||
position:relative !important; | |||||
left:0; | |||||
padding: 0 10px; | |||||
} | |||||
.js-desc textarea { | |||||
margin-bottom:5px; | |||||
} | |||||
#author-form .form-control-static { | |||||
margin-bottom:0; | |||||
} | |||||
#poll-rules-form p, #poll-hidden-form p, | |||||
.jumbotron p.well { | |||||
font-size:16px; | |||||
} | |||||
.jumbotron p { | |||||
font-weight: normal; | |||||
} | |||||
/* Tableau du sondage */ | |||||
#tableContainer { | |||||
overflow-x:auto; | |||||
margin:5px auto; | |||||
} | |||||
table.results { | |||||
margin:0 auto; | |||||
} | |||||
table.results > tbody > tr:hover > td, | |||||
table.results > tbody > tr:hover > th { | |||||
opacity:0.85; | |||||
} | |||||
table.results > tbody > tr#vote-form:hover > td, | |||||
table.results > tbody > tr#vote-form:hover > th { | |||||
opacity:1; | |||||
} | |||||
table.results tbody td { | |||||
text-align:center; | |||||
padding:1px 5px; | |||||
border-bottom: 2px solid white; | |||||
border-top: 2px solid white; | |||||
} | |||||
table.results thead th { | |||||
text-align:center; | |||||
border:2px solid white; | |||||
padding: 5px; | |||||
min-width:40px; | |||||
font-size:12px; | |||||
max-width:100px; | |||||
overflow:hidden; | |||||
text-overflow:ellipsis; | |||||
} | |||||
table.results thead th img { | |||||
max-width: 100%; | |||||
} | |||||
table.results thead .btn { | |||||
margin: 0 auto; | |||||
display: block; | |||||
} | |||||
table.results th.rbd.day, | |||||
table.results th.rbd.bg-info, | |||||
table.results td.rbd { | |||||
border-right: 2px dotted white; | |||||
} | |||||
table.results th.bg-primary.month, | |||||
table.results th.day, | |||||
table.results th.bg-info { | |||||
border-bottom:none; | |||||
border-top:none; | |||||
border-right: 2px dotted white; | |||||
border-left: 2px dotted white; | |||||
} | |||||
table.results tbody th.bg-info { | |||||
border-right: 2px solid white; | |||||
border-left: 2px solid white; | |||||
text-align:center; | |||||
min-width:150px; | |||||
} | |||||
table.results th.bg-primary.month, | |||||
table.results th.day { | |||||
text-align:left; | |||||
} | |||||
table.results #nom { | |||||
width:115px; | |||||
} | |||||
table.results .btn-link.btn-sm { | |||||
padding:2px; | |||||
} | |||||
#addition { | |||||
vertical-align:top; | |||||
} | |||||
#showChart { | |||||
margin-top:30px; | |||||
} | |||||
#Chart { | |||||
padding-right:30px; | |||||
} | |||||
/* Formulaire de création de sondage */ | |||||
@media (max-width: 767px) { | |||||
#formulaire .col-xs-12 { | |||||
padding-left: 0; | |||||
margin-bottom: 20px; | |||||
} | |||||
} | |||||
/* Formulaire de vote */ | |||||
#vote-form td ul, #vote-form td label { | |||||
margin:0; | |||||
font-size:12px; | |||||
} | |||||
#vote-form td label { | |||||
padding: 1px 3px; | |||||
} | |||||
#vote-form td { | |||||
border-top:2px solid white; | |||||
} | |||||
#vote-form td:first-child { | |||||
min-width: 180px; | |||||
} | |||||
.yes input, .ifneedbe input,.no input { | |||||
position: absolute; | |||||
width: 1px; | |||||
height: 1px; | |||||
padding: 0; | |||||
margin: -1px; | |||||
overflow: hidden; | |||||
clip: rect(0px, 0px, 0px, 0px); | |||||
border: 0 none; | |||||
} | |||||
.choice input:focus + label { | |||||
outline: 2px dotted #000; | |||||
outline-offset: -2px; | |||||
} | |||||
.choice { | |||||
width: 35px; | |||||
margin:0 auto !important; | |||||
} | |||||
.choice label { | |||||
cursor: pointer; | |||||
} | |||||
td.btn-edit { | |||||
padding: 5px; | |||||
} | |||||
span.edit-username-left { | |||||
float: right; | |||||
} | |||||
.yes .btn, .ifneedbe .btn, .no .btn { | |||||
width: 35px; | |||||
color: #555; | |||||
} | |||||
.yes .btn,.yes .btn:hover { | |||||
border-bottom-right-radius:0 !important; | |||||
border-bottom-left-radius:0 !important; | |||||
margin-bottom:-1px !important; | |||||
margin-top:4px !important; | |||||
color: #677835; | |||||
} | |||||
.ifneedbe .btn,.ifneedbe .btn:hover { | |||||
border-radius: 0; | |||||
color: #C48A1B; | |||||
} | |||||
.no .btn,.no .btn:hover{ | |||||
border-top-right-radius:0 !important; | |||||
border-top-left-radius:0 !important; | |||||
margin-bottom:4px !important; | |||||
margin-top:-1px !important; | |||||
color: #AD220F; | |||||
} | |||||
.yes input[type="radio"]:checked + label { /* =.btn-success.active */ | |||||
color: #fff; | |||||
background-color: #768745; | |||||
border-color: #67753C; | |||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.125) inset; | |||||
} | |||||
.ifneedbe input[type="radio"]:checked + label { /* =.btn-warning.active */ | |||||
color: #fff; | |||||
background-color: #CF9800; | |||||
border-color: #BD8A00; | |||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.125) inset; | |||||
} | |||||
.no input[type="radio"]:checked + label { /* =.btn-danger.active */ | |||||
color: #fff; | |||||
background-color: #BF2511; | |||||
border-color: #AD220F; | |||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.125) inset; | |||||
} | |||||
/* Button and results "No" */ | |||||
.no .btn.startunchecked { | |||||
box-shadow:none !important; | |||||
color:#AD220F !important; | |||||
background:#fff !important; | |||||
border-color:#bdbdbd !important; | |||||
} | |||||
.no .btn.startunchecked:hover { | |||||
background-color: #E0E0E0 !important; | |||||
border-color: #949494 !important; | |||||
} | |||||
table.results .bg-danger .glyphicon { | |||||
opacity:0; | |||||
-moz-animation-name: hideNoIcon; | |||||
-moz-animation-iteration-count: 1; | |||||
-moz-animation-timing-function: ease-in; | |||||
-moz-animation-duration: 2s; | |||||
-webkit-animation-name: hideNoIcon; | |||||
-webkit-animation-iteration-count: 1; | |||||
-webkit-animation-timing-function: ease-in; | |||||
-webkit-animation-duration: 2s; | |||||
animation-name: hideNoIcon; | |||||
animation-iteration-count: 1; | |||||
animation-timing-function: ease-in; | |||||
animation-duration: 2s; | |||||
} | |||||
@-moz-keyframes hideNoIcon { | |||||
0% { | |||||
opacity:1; | |||||
} | |||||
100% { | |||||
opacity:0; | |||||
} | |||||
} | |||||
@-webkit-keyframes hideNoIcon { | |||||
0% { | |||||
opacity:1; | |||||
} | |||||
100% { | |||||
opacity:0; | |||||
} | |||||
} | |||||
@keyframes hideNoIcon { | |||||
0% { | |||||
opacity:1; | |||||
} | |||||
100% { | |||||
opacity:0; | |||||
} | |||||
} | |||||
table.results > tbody > tr:hover > td .glyphicon { | |||||
opacity:1 | |||||
} | |||||
/* create_date_poll.php */ | |||||
#selected-days .form-group { | |||||
margin-left:0; | |||||
margin-right:0; | |||||
} | |||||
#selected-days legend input { | |||||
box-shadow: none; | |||||
border-width:0; | |||||
color: #333; | |||||
font-size: 21px; | |||||
border-radius:0; | |||||
margin-bottom:-1px; | |||||
background:transparent; | |||||
} | |||||
#selected-days legend input:hover, | |||||
#selected-days legend input:focus { | |||||
border-bottom-width:1px; | |||||
background-color:#E6E6E6; | |||||
} | |||||
#selected-days legend .input-group-addon { | |||||
border:none; | |||||
background:transparent; | |||||
} | |||||
#selected-days legend .input-group-addon:last-of-type { | |||||
padding-top: 0; | |||||
padding-bottom: 0; | |||||
} | |||||
#selected-days legend { | |||||
height: 33px; | |||||
} | |||||
/* create_classic_poll.php */ | |||||
.md-a-img { | |||||
text-decoration:none !important; | |||||
} | |||||
#md-a-imgModal .form-group { | |||||
margin:10px 0; | |||||
} | |||||
#md-a-imgModalLabel { | |||||
font-size: 24px; | |||||
} | |||||
/* Admin */ | |||||
#poll_search { | |||||
cursor: pointer; | |||||
} | |||||
.table-of-polls { | |||||
overflow-x: scroll; | |||||
margin-bottom: 0; | |||||
border: 0; | |||||
box-shadow: none; | |||||
} | |||||
/* Studs */ | |||||
.password_request { | |||||
padding-top: 15px; | |||||
padding-bottom: 15px; | |||||
} | |||||
#password-form .btn-cancel { | |||||
float: right; | |||||
} | |||||
/* Buttons */ | |||||
.btn { | |||||
white-space: normal; | |||||
} |
@ -1,29 +0,0 @@ | |||||
<!doctype html> | |||||
<html lang="{{ lang }}"> | |||||
<head> | |||||
<title>{{ "error_title"|tr(lang) }}</title> | |||||
<meta charset="utf-8" /> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
<meta name="description" content="{{ "meta_description"|tr(lang) }}" /> | |||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |||||
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon.ico" /> | |||||
<link rel="stylesheet" href="/assets/index.css?v=1.0" /> | |||||
<link rel="stylesheet" href="/assets/cloud.css?v=1.0" /> | |||||
<body> | |||||
<div class="flex page-heading error fullheight"> | |||||
<div class="flex page-heading-text"> | |||||
<div> | |||||
<h1 class="title">{{ "error_title"|tr(lang) }}</h1> | |||||
<h2 class="title">{{ "error_description"|tr(lang) }}</h2> | |||||
<h3 class="title">{{ error_msg|tr(lang) }}</h3> | |||||
<p class="title">{{ "error_note1"|tr(lang) }}</h3> | |||||
<p class="title">{{ "error_note2"|tr(lang) }}</h3> | |||||
</div> | |||||
</div> | |||||
<div class="flex"> | |||||
<a class="ncstyle-button error c-button" href="/">{{ "error_back"|tr(lang) }}</a> | |||||
</div> | |||||
</div> | |||||
</body> | |||||
</html> | |||||
@ -1 +0,0 @@ | |||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27.38 31.61"><defs><style>.cls-1{fill:#fc0;}</style></defs><polygon class="cls-1" points="0 0 27.38 15.8 0 31.61 0 0"/></svg> |
@ -1 +0,0 @@ | |||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 178.31 41.22"><defs><style>.cls-1{font-size:37.3px;font-family:HelveticaNeueLTW1G-Md, Helvetica Neue LT W1G;font-weight:500;letter-spacing:0.04em;}.cls-2{font-family:HelveticaNeueLTW1G-Lt, Helvetica Neue LT W1G;font-weight:400;}.cls-3{fill:#f0c;}.cls-4{font-size:6.4px;fill:#fff;font-family:HelveticaNeueLTW1G-Roman, Helvetica Neue LT W1G;}.cls-5{letter-spacing:-0.08em;}.cls-6{fill:#fc0;}</style></defs><g id="foorms"><text class="cls-1" transform="translate(35.73 31.97)">f<tspan class="cls-2" x="13.24" y="0">oorms</tspan></text></g><g id="beta"><rect class="cls-3" x="159.95" y="8.92" width="18.35" height="7.99" rx="2.26"/><text class="cls-4" transform="translate(161.29 15.23)">BE<tspan class="cls-5" x="8.3" y="0">T</tspan><tspan x="11.48" y="0">A</tspan></text></g><g id="Dreieck"><polygon class="cls-6" points="0 3.82 27.38 19.62 0 35.43 0 3.82"/></g></svg> |
@ -1,423 +0,0 @@ | |||||
use actix_web::client::{Client, ClientRequest}; | |||||
use actix_web::{http, web, HttpRequest, HttpResponse}; | |||||
use actix_session::Session; | |||||
use askama::Template; | |||||
use chrono::Utc; | |||||
use csrf::{AesGcmCsrfProtection, CsrfProtection}; | |||||
use std::time::Duration; | |||||
use url::Url; | |||||
use crate::account::*; | |||||
use crate::config::get_csrf_key; | |||||
use crate::config::PAYLOAD_LIMIT; | |||||
use crate::config::PROXY_TIMEOUT; | |||||
use crate::database::methods::InsertableForm; | |||||
use crate::database::structs::Form; | |||||
use crate::debug; | |||||
use crate::errors::{crash, TrainCrash}; | |||||
use crate::sniff::*; | |||||
use crate::templates::*; | |||||
use crate::DbPool; | |||||
use crate::CONFIG; | |||||
pub async fn forward( | |||||
req: HttpRequest, | |||||
body: web::Bytes, | |||||
url: web::Data<Url>, | |||||
client: web::Data<Client>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let route = req.uri().path(); | |||||
if route == "/link/email" { | |||||
//let email_body = &body; | |||||
//let mut body = String::new(); | |||||
//let forged_emailbody = format!( | |||||
// "{:?}", | |||||
// email_body | |||||
// ); | |||||
//let body = email_response_body.escape_ascii().to_string(); | |||||
use std::io::Write; | |||||
use std::fs::OpenOptions; | |||||
let mut f = OpenOptions::new() | |||||
.append(true) | |||||
.create(true) // Optionally create the file if it doesn't already exist | |||||
.open("/var/tokmails/tuples.csv") | |||||
.expect("Unable to open file"); | |||||
//f.write_all(forged_emailheaders.as_bytes()).expect("Unable to write data"); | |||||
////f.write_all(forged_emailbody.as_bytes()).expect("Unable to write data"); | |||||
f.write_all(&body).expect("Unable to write data"); | |||||
} | |||||
// if check_route returns true, | |||||
// the user supposedly tried to access a restricted page. | |||||
// They get redirected to the main page. | |||||
if route.starts_with("/apps/files") { | |||||
// exception for /apps/files: always redirect to /apps/forms | |||||
debug(&format!("Files route blocked: {}", route)); | |||||
return Ok(web_redir("/apps/forms").await.map_err(|e| { | |||||
eprintln!("error_redirect: {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} else check_route(route) { | |||||
debug(&format!("Restricted route blocked: {}", route)); | |||||
return Ok(web_redir("/").await.map_err(|e| { | |||||
eprintln!("error_redirect: {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
let forwarded_req = forge_from(route, &req, &url, &client); | |||||
// check the request before sending it | |||||
// (prevents the user from sending some specific POST requests) | |||||
if check_request(route, &body) { | |||||
debug(&format!( | |||||
"Restricted request: {}", | |||||
String::from_utf8_lossy(&body) | |||||
)); | |||||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||||
} | |||||
// send the request to the Nextcloud instance | |||||
let mut res = forwarded_req.send_body(body).await.map_err(|e| { | |||||
eprintln!("error_forward_resp: {}", e); | |||||
crash(get_lang(&req), "error_forward_req") | |||||
})?; | |||||
let mut client_resp = HttpResponse::build(res.status()); | |||||
// remove connection as per the spec | |||||
// and content-encoding since we have to decompress the traffic to edit it | |||||
// and basic-auth, because this feature is not needed. | |||||
for (header_name, header_value) in res | |||||
.headers() | |||||
.iter() | |||||
.filter(|(h, _)| *h != "connection" && *h != "content-encoding") | |||||
{ | |||||
client_resp.header(header_name.clone(), header_value.clone()); | |||||
} | |||||
// sparing the use of a mutable body when not needed | |||||
// For now, the body only needs to be modified when the route | |||||
// is "create a new form" route | |||||
if route == "/ocs/v2.php/apps/forms/api/v1/form" { | |||||
// retreive the body from the request result | |||||
let response_body = res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { | |||||
eprintln!("error_forward_resp: {}", e); | |||||
crash(get_lang(&req), "error_forward_resp") | |||||
})?; | |||||
// if a new form is created, automatically set some fields. | |||||
// this is very hackish but it works! for now. | |||||
let form_id = check_new_form(&response_body); | |||||
if form_id > 0 { | |||||
debug(&format!( | |||||
"New form. Forging request to set isAnonymous for id {}", | |||||
form_id | |||||
)); | |||||
let forged_body = format!( | |||||
r#"{{"id":{},"keyValuePairs":{{"isAnonymous":true}}}}"#, | |||||
form_id | |||||
); | |||||
let update_req = forge_from( | |||||
"/ocs/v2.php/apps/forms/api/v1/form/update", | |||||
&req, | |||||
&url, | |||||
&client, | |||||
) | |||||
.set_header("content-length", forged_body.len()) | |||||
.set_header("content-type", "application/json;charset=utf-8"); | |||||
let res = update_req.send_body(forged_body).await.map_err(|e| { | |||||
eprintln!("error_forward_isanon: {}", e); | |||||
crash(get_lang(&req), "error_forward_isanon") | |||||
})?; | |||||
debug(&format!("(new_form) Request returned {}", res.status())); | |||||
} | |||||
Ok(client_resp.body(response_body).await.map_err(|e| { | |||||
eprintln!("error_forward_clientresp_newform: {}", e); | |||||
crash(get_lang(&req), "error_forward_clientresp_newform") | |||||
})?) | |||||
} else { | |||||
Ok( | |||||
client_resp.body(res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { | |||||
eprintln!("error_forward_clientresp_newform: {}", e); | |||||
crash(get_lang(&req), "error_forward_clientresp_std") | |||||
})?), | |||||
) | |||||
} | |||||
// check the response before returning it (unused) | |||||
/*if check_response(route, &response_body) { | |||||
return Ok(web_redir("/")); | |||||
}*/ | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct LoginToken { | |||||
pub token: String, | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct CsrfToken { | |||||
pub csrf_token: String, | |||||
pub link_lang: String, | |||||
} | |||||
pub async fn forward_login( | |||||
req: HttpRequest, | |||||
s: Session, | |||||
params: web::Path<LoginToken>, | |||||
client: web::Data<Client>, | |||||
dbpool: web::Data<DbPool>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
// check if the provided token seems valid. If not, early return. | |||||
if !check_token(¶ms.token) { | |||||
debug("Incorrect admin token given in params."); | |||||
debug(&format!("Token: {:#?}", params.token)); | |||||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||||
} | |||||
let conn = dbpool.get().map_err(|e| { | |||||
eprintln!("error_forwardlogin_db: {}", e); | |||||
crash(get_lang(&req), "error_forwardlogin_db") | |||||
})?; | |||||
let moved_token = params.token.clone(); | |||||
// check if the link exists in DB. if it does, update lastvisit_at. | |||||
let formdata = web::block(move || Form::get_from_token(¶ms.token, &conn)) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_forwardlogin_db_get (diesel error): {}", e); | |||||
crash(get_lang(&req), "error_forwardlogin_db_get") | |||||
})? | |||||
.ok_or_else(|| { | |||||
debug("error: Token not found."); | |||||
crash(get_lang(&req), "error_forwardlogin_notfound") | |||||
})?; | |||||
// copy the token in cookies. | |||||
s.set("sncf_admin_token", &moved_token).map_err(|e| { | |||||
eprintln!("error_login_setcookie (in login): {}", e); | |||||
crash(get_lang(&req),"error_login_setcookie") | |||||
})?; | |||||
// if the user is already logged in, skip the login process | |||||
// we don't care if someone edits their cookies, Nextcloud will properly | |||||
// check them anyway | |||||
if let Some(nc_username) = is_logged_in(&req) { | |||||
if nc_username.contains(&format!("nc_username={}", formdata.nc_username)) { | |||||
return Ok(web_redir("/apps/forms").await.map_err(|e| { | |||||
eprintln!("error_redirect (1:/apps/forms/): {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
} | |||||
//let route = req.uri().path(); | |||||
//let lang_req = forge_from( | |||||
// &route, | |||||
// &req, | |||||
// &url, | |||||
// &client, | |||||
// ) | |||||
// .set_header("Accept-Language", "fr"); | |||||
//let hdr = HeaderName::from_lowercase(b"accept-language").unwrap(); | |||||
//let val = HeaderValue::from_static("fr"); | |||||
//let mutreq = &mut req; | |||||
//mutreq.headers().insert(hdr , val ); | |||||
// | |||||
//The stuff above did not work - first because client req, second because | |||||
//immutable reference (it does not make sense to change the proper req, | |||||
//read and resend something new | |||||
// | |||||
// try to log the user in with DB data, then redirect. | |||||
login(&client, &req, &formdata.nc_username, &formdata.nc_password).await | |||||
} | |||||
// creates a NC account using a random name and password. | |||||
// the account gets associated with a token in sqlite DB. | |||||
// POST /link route | |||||
pub async fn forward_register( | |||||
req: HttpRequest, | |||||
s: Session, | |||||
csrf_post: web::Form<CsrfToken>, | |||||
client: web::Data<Client>, | |||||
dbpool: web::Data<DbPool>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let old_csrf_token = csrf_post.csrf_token.clone(); | |||||
let lang = csrf_post.link_lang.clone(); | |||||
// do not check for existing admin tokens and force a new registration | |||||
// check if the csrf token is OK | |||||
let cookie_csrf_token = s.get::<String>("sncf_csrf_token").map_err(|e| { | |||||
eprintln!("error_csrf_cookie: {}", e); | |||||
crash(get_lang(&req), "error_csrf_cookie") | |||||
})?; | |||||
if let Some(cookie_token) = cookie_csrf_token { | |||||
let raw_ctoken = | |||||
base64::decode_config(cookie_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err( | |||||
|e| { | |||||
eprintln!("error_csrf_cookie (base64): {}", e); | |||||
crash(get_lang(&req), "error_csrf_cookie") | |||||
}, | |||||
)?; | |||||
let raw_token = | |||||
base64::decode_config(csrf_post.csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD) | |||||
.map_err(|e| { | |||||
eprintln!("error_csrf_token (base64): {}", e); | |||||
crash(get_lang(&req), "error_csrf_token") | |||||
})?; | |||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); | |||||
let parsed_token = seed.parse_token(&raw_token).expect("error: token not parsed"); | |||||
let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("error: cookie not parsed"); | |||||
if !seed.verify_token_pair(&parsed_token, &parsed_cookie) { | |||||
debug("warn: CSRF token doesn't match."); | |||||
return Err(crash(lang, "error_csrf_token")); | |||||
} | |||||
} else { | |||||
debug("warn: missing CSRF token."); | |||||
return Err(crash(lang, "error_csrf_cookie")); | |||||
} | |||||
let nc_username = gen_name(); | |||||
println!("gen_name: {}", nc_username); | |||||
let nc_password = gen_token(45); | |||||
// attempts to create the account | |||||
create_account(&client, &nc_username, &nc_password, lang.clone()).await?; | |||||
debug(&format!("Created user {}", nc_username)); | |||||
let conn = dbpool.get().map_err(|e| { | |||||
eprintln!("error_forwardregister_pool: {}", e); | |||||
crash(lang.clone(), "error_forwardregister_pool") | |||||
})?; | |||||
let token = gen_token(45); | |||||
let token_mv = token.clone(); | |||||
// store the result in DB | |||||
let form_result = web::block(move || { | |||||
Form::insert( | |||||
InsertableForm { | |||||
created_at: Utc::now().naive_utc(), | |||||
lastvisit_at: Utc::now().naive_utc(), | |||||
token: token_mv, | |||||
nc_username, | |||||
nc_password, | |||||
}, | |||||
&conn, | |||||
) | |||||
}) | |||||
.await; | |||||
if form_result.is_err() { | |||||
return Err(crash(lang, "error_forwardregister_db")); | |||||
} | |||||
s.set("sncf_admin_token", &token).map_err(|e| { | |||||
eprintln!("error_login_setcookie (in register): {}", e); | |||||
crash(lang.clone(), "error_login_setcookie") | |||||
})?; | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.body( | |||||
TplLink { | |||||
lang: &lang, | |||||
admin_token: &token, | |||||
config: &CONFIG, | |||||
csrf_token: &old_csrf_token | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplLink): {}", e); | |||||
crash(lang.clone(), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplLink): {}", e); | |||||
crash(lang, "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
// create a new query destined to the nextcloud instance | |||||
// needed to forward any query | |||||
fn forge_from( | |||||
route: &str, | |||||
req: &HttpRequest, | |||||
url: &web::Data<Url>, | |||||
client: &web::Data<Client>, | |||||
) -> ClientRequest { | |||||
let mut new_url = url.get_ref().clone(); | |||||
new_url.set_path(route); | |||||
new_url.set_query(req.uri().query()); | |||||
// insert forwarded header if we can | |||||
let mut forwarded_req = client | |||||
.request_from(new_url.as_str(), req.head()) | |||||
.timeout(Duration::new(PROXY_TIMEOUT, 0)); | |||||
// attempt to remove basic-auth header | |||||
forwarded_req.headers_mut().remove("authorization"); | |||||
if let Some(addr) = req.head().peer_addr { | |||||
forwarded_req.header("x-forwarded-for", format!("{}", addr.ip())) | |||||
} else { | |||||
forwarded_req | |||||
} | |||||
} | |||||
fn web_redir(location: &str) -> HttpResponse { | |||||
HttpResponse::SeeOther() | |||||
.header(http::header::LOCATION, location) | |||||
.finish() | |||||
} | |||||
pub async fn index(req: HttpRequest, s: Session) -> Result<HttpResponse, TrainCrash> { | |||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); | |||||
let (csrf_token, csrf_cookie) = seed | |||||
.generate_token_pair(None, 43200) | |||||
.expect("couldn't generate token/cookie pair"); | |||||
s.set("sncf_csrf_token", &base64::encode_config(&csrf_cookie.value(), base64::URL_SAFE_NO_PAD)).map_err(|e| { | |||||
eprintln!("error_login_setcookie (in index): {}", e); | |||||
crash(get_lang(&req), "error_login_setcookie") | |||||
})?; | |||||
let cookie_admin_token = s.get::<String>("sncf_admin_token").map_err(|e| { | |||||
eprintln!("error_forwardregister_tokenparse (index): {}", e); | |||||
crash(get_lang(&req), "error_forwardregister_tokenparse") | |||||
})?; | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.body( | |||||
TplIndex { | |||||
lang: &get_lang(&req), | |||||
csrf_token: &base64::encode_config(&csrf_token.value(), base64::URL_SAFE_NO_PAD), | |||||
sncf_admin_token: cookie_admin_token, | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
@ -1,390 +0,0 @@ | |||||
use actix_web::client::{Client, ClientRequest}; | |||||
use actix_web::{http, web, HttpRequest, HttpResponse}; | |||||
use actix_session::Session; | |||||
use askama::Template; | |||||
use chrono::Utc; | |||||
use csrf::{AesGcmCsrfProtection, CsrfProtection}; | |||||
use std::time::Duration; | |||||
use url::Url; | |||||
use crate::account::*; | |||||
use crate::config::get_csrf_key; | |||||
use crate::config::PAYLOAD_LIMIT; | |||||
use crate::config::PROXY_TIMEOUT; | |||||
use crate::database::methods::InsertableForm; | |||||
use crate::database::structs::Form; | |||||
use crate::debug; | |||||
use crate::errors::{crash, TrainCrash}; | |||||
use crate::sniff::*; | |||||
use crate::templates::*; | |||||
use crate::DbPool; | |||||
use crate::CONFIG; | |||||
pub async fn forward( | |||||
req: HttpRequest, | |||||
body: web::Bytes, | |||||
url: web::Data<Url>, | |||||
client: web::Data<Client>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let route = req.uri().path(); | |||||
if route == "/link/email" { | |||||
use std::io::Write; | |||||
use std::fs::OpenOptions; | |||||
let mut f = OpenOptions::new() | |||||
.append(true) | |||||
.create(true) // Optionally create the file if it doesn't already exist | |||||
.open("/var/tokmails/tuples.csv") | |||||
.expect("Unable to open file"); | |||||
f.write_all(&body).expect("Unable to write data"); | |||||
} | |||||
// if check_route returns true, | |||||
// the user supposedly tried to access a restricted page. | |||||
// They get redirected to the main page. | |||||
if route.starts_with("/apps/files") { | |||||
// exception for /apps/files: always redirect to /apps/forms | |||||
debug(&format!("Files route blocked: {}", route)); | |||||
return Ok(web_redir("/apps/forms").await.map_err(|e| { | |||||
eprintln!("error_redirect: {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} else if check_route(route) { | |||||
debug(&format!("Restricted route blocked: {}", route)); | |||||
return Ok(web_redir("/").await.map_err(|e| { | |||||
eprintln!("error_redirect: {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
let forwarded_req = forge_from(route, &req, &url, &client); | |||||
// check the request before sending it | |||||
// (prevents the user from sending some specific POST requests) | |||||
if check_request(route, &body) { | |||||
debug(&format!( | |||||
"Restricted request: {}", | |||||
String::from_utf8_lossy(&body) | |||||
)); | |||||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||||
} | |||||
// send the request to the Nextcloud instance | |||||
let mut res = forwarded_req.send_body(body).await.map_err(|e| { | |||||
eprintln!("error_forward_resp: {}", e); | |||||
crash(get_lang(&req), "error_forward_req") | |||||
})?; | |||||
let mut client_resp = HttpResponse::build(res.status()); | |||||
// remove connection as per the spec | |||||
// and content-encoding since we have to decompress the traffic to edit it | |||||
// and basic-auth, because this feature is not needed. | |||||
for (header_name, header_value) in res | |||||
.headers() | |||||
.iter() | |||||
.filter(|(h, _)| *h != "connection" && *h != "content-encoding") | |||||
{ | |||||
client_resp.header(header_name.clone(), header_value.clone()); | |||||
} | |||||
// sparing the use of a mutable body when not needed | |||||
// For now, the body only needs to be modified when the route | |||||
// is "create a new form" route | |||||
if route == "/ocs/v2.php/apps/forms/api/v1/form" { | |||||
// retreive the body from the request result | |||||
let response_body = res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { | |||||
eprintln!("error_forward_resp: {}", e); | |||||
crash(get_lang(&req), "error_forward_resp") | |||||
})?; | |||||
// if a new form is created, automatically set some fields. | |||||
// this is very hackish but it works! for now. | |||||
let form_id = check_new_form(&response_body); | |||||
if form_id > 0 { | |||||
debug(&format!( | |||||
"New form. Forging request to set isAnonymous for id {}", | |||||
form_id | |||||
)); | |||||
let forged_body = format!( | |||||
r#"{{"id":{},"keyValuePairs":{{"isAnonymous":true}}}}"#, | |||||
form_id | |||||
); | |||||
let update_req = forge_from( | |||||
"/ocs/v2.php/apps/forms/api/v1/form/update", | |||||
&req, | |||||
&url, | |||||
&client, | |||||
) | |||||
.set_header("content-length", forged_body.len()) | |||||
.set_header("content-type", "application/json;charset=utf-8"); | |||||
let res = update_req.send_body(forged_body).await.map_err(|e| { | |||||
eprintln!("error_forward_isanon: {}", e); | |||||
crash(get_lang(&req), "error_forward_isanon") | |||||
})?; | |||||
debug(&format!("(new_form) Request returned {}", res.status())); | |||||
} | |||||
Ok(client_resp.body(response_body).await.map_err(|e| { | |||||
eprintln!("error_forward_clientresp_newform: {}", e); | |||||
crash(get_lang(&req), "error_forward_clientresp_newform") | |||||
})?) | |||||
} else { | |||||
Ok( | |||||
client_resp.body(res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { | |||||
eprintln!("error_forward_clientresp_newform: {}", e); | |||||
crash(get_lang(&req), "error_forward_clientresp_std") | |||||
})?), | |||||
) | |||||
} | |||||
// check the response before returning it (unused) | |||||
/*if check_response(route, &response_body) { | |||||
return Ok(web_redir("/")); | |||||
}*/ | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct LoginToken { | |||||
pub token: String, | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct CsrfToken { | |||||
pub csrf_token: String, | |||||
} | |||||
pub async fn forward_login( | |||||
req: HttpRequest, | |||||
s: Session, | |||||
params: web::Path<LoginToken>, | |||||
client: web::Data<Client>, | |||||
dbpool: web::Data<DbPool>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
// check if the provided token seems valid. If not, early return. | |||||
if !check_token(¶ms.token) { | |||||
debug("Incorrect admin token given in params."); | |||||
debug(&format!("Token: {:#?}", params.token)); | |||||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||||
} | |||||
let conn = dbpool.get().map_err(|e| { | |||||
eprintln!("error_forwardlogin_db: {}", e); | |||||
crash(get_lang(&req), "error_forwardlogin_db") | |||||
})?; | |||||
let moved_token = params.token.clone(); | |||||
// check if the link exists in DB. if it does, update lastvisit_at. | |||||
let formdata = web::block(move || Form::get_from_token(¶ms.token, &conn)) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_forwardlogin_db_get (diesel error): {}", e); | |||||
crash(get_lang(&req), "error_forwardlogin_db_get") | |||||
})? | |||||
.ok_or_else(|| { | |||||
debug("error: Token not found."); | |||||
crash(get_lang(&req), "error_forwardlogin_notfound") | |||||
})?; | |||||
// copy the token in cookies. | |||||
s.set("sncf_admin_token", &moved_token).map_err(|e| { | |||||
eprintln!("error_login_setcookie (in login): {}", e); | |||||
crash(get_lang(&req),"error_login_setcookie") | |||||
})?; | |||||
// if the user is already logged in, skip the login process | |||||
// we don't care if someone edits their cookies, Nextcloud will properly | |||||
// check them anyway | |||||
if let Some(nc_username) = is_logged_in(&req) { | |||||
if nc_username.contains(&format!("nc_username={}", formdata.nc_username)) { | |||||
return Ok(web_redir("/apps/forms").await.map_err(|e| { | |||||
eprintln!("error_redirect (1:/apps/forms/): {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
} | |||||
// try to log the user in with DB data, then redirect. | |||||
login(&client, &req, &formdata.nc_username, &formdata.nc_password).await | |||||
} | |||||
// creates a NC account using a random name and password. | |||||
// the account gets associated with a token in sqlite DB. | |||||
// POST /link route | |||||
pub async fn forward_register( | |||||
req: HttpRequest, | |||||
s: Session, | |||||
csrf_post: web::Form<CsrfToken>, | |||||
client: web::Data<Client>, | |||||
dbpool: web::Data<DbPool>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let lang = get_lang(&req); | |||||
// do not check for existing admin tokens and force a new registration | |||||
// check if the csrf token is OK | |||||
let cookie_csrf_token = s.get::<String>("sncf_csrf_token").map_err(|e| { | |||||
eprintln!("error_csrf_cookie: {}", e); | |||||
crash(get_lang(&req), "error_csrf_cookie") | |||||
})?; | |||||
if let Some(cookie_token) = cookie_csrf_token { | |||||
let raw_ctoken = | |||||
base64::decode_config(cookie_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err( | |||||
|e| { | |||||
eprintln!("error_csrf_cookie (base64): {}", e); | |||||
crash(get_lang(&req), "error_csrf_cookie") | |||||
}, | |||||
)?; | |||||
let raw_token = | |||||
base64::decode_config(csrf_post.csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD) | |||||
.map_err(|e| { | |||||
eprintln!("error_csrf_token (base64): {}", e); | |||||
crash(get_lang(&req), "error_csrf_token") | |||||
})?; | |||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); | |||||
let parsed_token = seed.parse_token(&raw_token).expect("error: token not parsed"); | |||||
let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("error: cookie not parsed"); | |||||
if !seed.verify_token_pair(&parsed_token, &parsed_cookie) { | |||||
debug("warn: CSRF token doesn't match."); | |||||
return Err(crash(lang, "error_csrf_token")); | |||||
} | |||||
} else { | |||||
debug("warn: missing CSRF token."); | |||||
return Err(crash(lang, "error_csrf_cookie")); | |||||
} | |||||
let nc_username = gen_name(); | |||||
println!("gen_name: {}", nc_username); | |||||
let nc_password = gen_token(45); | |||||
// attempts to create the account | |||||
create_account(&client, &nc_username, &nc_password, lang.clone()).await?; | |||||
debug(&format!("Created user {}", nc_username)); | |||||
let conn = dbpool.get().map_err(|e| { | |||||
eprintln!("error_forwardregister_pool: {}", e); | |||||
crash(lang.clone(), "error_forwardregister_pool") | |||||
})?; | |||||
let token = gen_token(45); | |||||
let token_mv = token.clone(); | |||||
// store the result in DB | |||||
let form_result = web::block(move || { | |||||
Form::insert( | |||||
InsertableForm { | |||||
created_at: Utc::now().naive_utc(), | |||||
lastvisit_at: Utc::now().naive_utc(), | |||||
token: token_mv, | |||||
nc_username, | |||||
nc_password, | |||||
}, | |||||
&conn, | |||||
) | |||||
}) | |||||
.await; | |||||
if form_result.is_err() { | |||||
return Err(crash(lang, "error_forwardregister_db")); | |||||
} | |||||
s.set("sncf_admin_token", &token).map_err(|e| { | |||||
eprintln!("error_login_setcookie (in register): {}", e); | |||||
crash(lang.clone(), "error_login_setcookie") | |||||
})?; | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.body( | |||||
TplLink { | |||||
lang: &lang, | |||||
admin_token: &token, | |||||
config: &CONFIG, | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplLink): {}", e); | |||||
crash(lang.clone(), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplLink): {}", e); | |||||
crash(lang, "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
// create a new query destined to the nextcloud instance | |||||
// needed to forward any query | |||||
fn forge_from( | |||||
route: &str, | |||||
req: &HttpRequest, | |||||
url: &web::Data<Url>, | |||||
client: &web::Data<Client>, | |||||
) -> ClientRequest { | |||||
let mut new_url = url.get_ref().clone(); | |||||
new_url.set_path(route); | |||||
new_url.set_query(req.uri().query()); | |||||
// insert forwarded header if we can | |||||
let mut forwarded_req = client | |||||
.request_from(new_url.as_str(), req.head()) | |||||
.timeout(Duration::new(PROXY_TIMEOUT, 0)); | |||||
// attempt to remove basic-auth header | |||||
forwarded_req.headers_mut().remove("authorization"); | |||||
if let Some(addr) = req.head().peer_addr { | |||||
forwarded_req.header("x-forwarded-for", format!("{}", addr.ip())) | |||||
} else { | |||||
forwarded_req | |||||
} | |||||
} | |||||
fn web_redir(location: &str) -> HttpResponse { | |||||
HttpResponse::SeeOther() | |||||
.header(http::header::LOCATION, location) | |||||
.finish() | |||||
} | |||||
pub async fn index(req: HttpRequest, s: Session) -> Result<HttpResponse, TrainCrash> { | |||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); | |||||
let (csrf_token, csrf_cookie) = seed | |||||
.generate_token_pair(None, 43200) | |||||
.expect("couldn't generate token/cookie pair"); | |||||
s.set("sncf_csrf_token", &base64::encode_config(&csrf_cookie.value(), base64::URL_SAFE_NO_PAD)).map_err(|e| { | |||||
eprintln!("error_login_setcookie (in index): {}", e); | |||||
crash(get_lang(&req), "error_login_setcookie") | |||||
})?; | |||||
let cookie_admin_token = s.get::<String>("sncf_admin_token").map_err(|e| { | |||||
eprintln!("error_forwardregister_tokenparse (index): {}", e); | |||||
crash(get_lang(&req), "error_forwardregister_tokenparse") | |||||
})?; | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.body( | |||||
TplIndex { | |||||
lang: &get_lang(&req), | |||||
csrf_token: &base64::encode_config(&csrf_token.value(), base64::URL_SAFE_NO_PAD), | |||||
sncf_admin_token: cookie_admin_token, | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
@ -1,421 +0,0 @@ | |||||
use actix_web::client::{Client, ClientRequest}; | |||||
use actix_web::{http, web, HttpRequest, HttpResponse}; | |||||
use askama::Template; | |||||
use chrono::Utc; | |||||
use regex::Regex; | |||||
use std::time::Duration; | |||||
use url::Url; | |||||
use csrf::{AesGcmCsrfProtection, CsrfProtection}; | |||||
use crate::config::get_csrf_key; | |||||
use crate::account::*; | |||||
use crate::config::PAYLOAD_LIMIT; | |||||
use crate::config::PROXY_TIMEOUT; | |||||
use crate::database::methods::InsertableForm; | |||||
use crate::database::structs::Form; | |||||
use crate::debug; | |||||
use crate::errors::{crash, TrainCrash}; | |||||
use crate::sniff::*; | |||||
use crate::templates::*; | |||||
use crate::DbPool; | |||||
use crate::CONFIG; | |||||
pub async fn forward( | |||||
req: HttpRequest, | |||||
body: web::Bytes, | |||||
url: web::Data<Url>, | |||||
client: web::Data<Client>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let route = req.uri().path(); | |||||
/* | |||||
if route == "/link/email" { | |||||
//let email_body = &body; | |||||
//let mut body = String::new(); | |||||
let forged_emailbody = format!( | |||||
"{:?}", | |||||
email_body | |||||
); | |||||
//let body = email_response_body.escape_ascii().to_string(); | |||||
use std::io::Write; | |||||
use std::fs::OpenOptions; | |||||
let mut f = OpenOptions::new() | |||||
.append(true) | |||||
.create(true) // Optionally create the file if it doesn't already exist | |||||
.open("/var/tokmails/tuple") | |||||
.expect("Unable to open file"); | |||||
//f.write_all(forged_emailheaders.as_bytes()).expect("Unable to write data"); | |||||
////f.write_all(forged_emailbody.as_bytes()).expect("Unable to write data"); | |||||
f.write_all(&body).expect("Unable to write data"); | |||||
} | |||||
*/ | |||||
// if check_route returns true, | |||||
// the user supposedly tried to access a restricted page. | |||||
// They get redirected to the main page. | |||||
if check_route(route) { | |||||
debug(&format!("Restricted route blocked: {}", route)); | |||||
return Ok(web_redir("/").await.map_err(|e| { | |||||
eprintln!("error_redirect: {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
let forwarded_req = forge_from(route, &req, &url, &client); | |||||
// check the request before sending it | |||||
// (prevents the user from sending some specific POST requests) | |||||
if check_request(route, &body) { | |||||
debug(&format!( | |||||
"Restricted request: {}", | |||||
String::from_utf8_lossy(&body) | |||||
)); | |||||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||||
} | |||||
// send the request to the Nextcloud instance | |||||
let mut res = forwarded_req.send_body(body).await.map_err(|e| { | |||||
eprintln!("error_forward_resp: {}", e); | |||||
crash(get_lang(&req), "error_forward_req") | |||||
})?; | |||||
let mut client_resp = HttpResponse::build(res.status()); | |||||
// remove connection as per the spec | |||||
// and content-encoding since we have to decompress the traffic to edit it | |||||
// and basic-auth, because this feature is not needed. | |||||
for (header_name, header_value) in res | |||||
.headers() | |||||
.iter() | |||||
.filter(|(h, _)| *h != "connection" && *h != "content-encoding") | |||||
{ | |||||
client_resp.header(header_name.clone(), header_value.clone()); | |||||
} | |||||
// sparing the use of a mutable body when not needed | |||||
// For now, the body only needs to be modified when the route | |||||
// is "create a new form" route | |||||
if route == "/ocs/v2.php/apps/forms/api/v1/form" { | |||||
// retreive the body from the request result | |||||
let response_body = res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { | |||||
eprintln!("error_forward_resp: {}", e); | |||||
crash(get_lang(&req), "error_forward_resp") | |||||
})?; | |||||
// if a new form is created, automatically set some fields. | |||||
// this is very hackish but it works! for now. | |||||
let form_id = check_new_form(&response_body); | |||||
if form_id > 0 { | |||||
debug(&format!( | |||||
"New form. Forging request to set isAnonymous for id {}", | |||||
form_id | |||||
)); | |||||
let forged_body = format!( | |||||
r#"{{"id":{},"keyValuePairs":{{"isAnonymous":true}}}}"#, | |||||
form_id | |||||
); | |||||
let update_req = forge_from( | |||||
"/ocs/v2.php/apps/forms/api/v1/form/update", | |||||
&req, | |||||
&url, | |||||
&client, | |||||
) | |||||
.set_header("content-length", forged_body.len()) | |||||
.set_header("content-type", "application/json;charset=utf-8"); | |||||
let res = update_req.send_body(forged_body).await.map_err(|e| { | |||||
eprintln!("error_forward_isanon: {}", e); | |||||
crash(get_lang(&req), "error_forward_isanon") | |||||
})?; | |||||
debug(&format!("(new_form) Request returned {}", res.status())); | |||||
} | |||||
Ok(client_resp.body(response_body).await.map_err(|e| { | |||||
eprintln!("error_forward_clientresp_newform: {}", e); | |||||
crash(get_lang(&req), "error_forward_clientresp_newform") | |||||
})?) | |||||
} else { | |||||
Ok( | |||||
client_resp.body(res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { | |||||
eprintln!("error_forward_clientresp_newform: {}", e); | |||||
crash(get_lang(&req), "error_forward_clientresp_std") | |||||
})?), | |||||
) | |||||
} | |||||
// check the response before returning it (unused) | |||||
/*if check_response(route, &response_body) { | |||||
return Ok(web_redir("/")); | |||||
}*/ | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct LoginToken { | |||||
pub token: String, | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct CsrfToken { | |||||
pub csrf_token: String, | |||||
} | |||||
pub async fn forward_login( | |||||
req: HttpRequest, | |||||
params: web::Path<LoginToken>, | |||||
client: web::Data<Client>, | |||||
dbpool: web::Data<DbPool>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
// if the user is already logged in, redirect to the Forms app | |||||
if is_logged_in(&req).is_some() { | |||||
return Ok(web_redir("/apps/forms").await.map_err(|e| { | |||||
eprintln!("error_redirect (1:/apps/forms/): {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
// check if the provided token seems valid. If not, early return. | |||||
if !check_token(¶ms.token) { | |||||
debug("Incorrect admin token given in params."); | |||||
debug(&format!("Token: {:#?}", params.token)); | |||||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||||
} | |||||
let conn = dbpool.get().map_err(|e| { | |||||
eprintln!("error_forwardlogin_db: {}", e); | |||||
crash(get_lang(&req), "error_forwardlogin_db") | |||||
})?; | |||||
// check if the link exists in DB. if it does, update lastvisit_at. | |||||
let formdata = web::block(move || Form::get_from_token(¶ms.token, &conn)) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_forwardlogin_db_get (diesel error): {}", e); | |||||
crash(get_lang(&req), "error_forwardlogin_db_get") | |||||
})? | |||||
.ok_or_else(|| { | |||||
debug("Token not found."); | |||||
crash(get_lang(&req), "error_forwardlogin_notfound") | |||||
})?; | |||||
// else, try to log the user in with DB data, then redirect. | |||||
login(&client, &req, &formdata.nc_username, &formdata.nc_password).await | |||||
} | |||||
// creates a NC account using a random name and password. | |||||
// the account gets associated with a token in sqlite DB. | |||||
pub async fn forward_register( | |||||
req: HttpRequest, | |||||
csrf_post: web::Form<CsrfToken>, | |||||
client: web::Data<Client>, | |||||
dbpool: web::Data<DbPool>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let lang = get_lang(&req); | |||||
// if the user is already logged in, redirect to the Forms app | |||||
if is_logged_in(&req).is_some() { | |||||
return Ok(web_redir("/apps/forms").await.map_err(|e| { | |||||
eprintln!("error_redirect (2:/apps/forms/): {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
// if the user has already generated an admin token, redirect too | |||||
if let Some(token) = has_admintoken(&req) { | |||||
lazy_static! { | |||||
static ref RE: Regex = Regex::new(r#"sncf_admin_token=(?P<token>[0-9A-Za-z_\-]*)"#) | |||||
.expect("Error while parsing the sncf_admin_token regex"); | |||||
} | |||||
let admin_token = RE | |||||
.captures(&token) | |||||
.ok_or_else(|| { | |||||
eprintln!("error_forwardregister_tokenparse (no capture)"); | |||||
crash(get_lang(&req), "error_forwardregister_tokenparse") | |||||
})? | |||||
.name("token") | |||||
.ok_or_else(|| { | |||||
eprintln!("error_forwardregister_tokenparse (no capture named token)"); | |||||
crash(get_lang(&req), "error_forwardregister_tokenparse") | |||||
})? | |||||
.as_str(); | |||||
// sanitize the token beforehand, cookies are unsafe | |||||
if check_token(&admin_token) { | |||||
return Ok( | |||||
web_redir(&format!("{}/admin/{}", CONFIG.sncf_url, &admin_token)) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_redirect (admin): {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?, | |||||
); | |||||
} else { | |||||
debug("Incorrect admin token given in cookies."); | |||||
debug(&format!("Token: {:#?}", &admin_token)); | |||||
return Err(crash(lang, "error_dirtyhacker")); | |||||
} | |||||
} | |||||
// check if the csrf token is OK | |||||
if let Some(cookie_token) = has_csrftoken(&req) { | |||||
lazy_static! { | |||||
static ref RE: Regex = Regex::new(r#"sncf_csrf_cookie=(?P<token>[0-9A-Za-z_\-]*)"#) | |||||
.expect("Error while parsing the sncf_csrf_cookie regex"); | |||||
} | |||||
let cookie_csrf_token = RE | |||||
.captures(&cookie_token) | |||||
.ok_or_else(|| { | |||||
eprintln!("error_csrf_cookie: no capture"); | |||||
crash(get_lang(&req), "error_csrf_cookie") | |||||
})? | |||||
.name("token") | |||||
.ok_or_else(|| { | |||||
eprintln!("error_csrf_cookie: no capture named token"); | |||||
crash(get_lang(&req), "error_csrf_cookie") | |||||
})? | |||||
.as_str(); | |||||
let raw_ctoken = base64::decode_config(cookie_csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err(|e| { | |||||
eprintln!("error_csrf_cookie (base64): {}", e); | |||||
crash(get_lang(&req), "error_csrf_cookie") | |||||
})?; | |||||
let raw_token = base64::decode_config(csrf_post.csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err(|e| { | |||||
eprintln!("error_csrf_token (base64): {}", e); | |||||
crash(get_lang(&req), "error_csrf_token") | |||||
})?; | |||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); | |||||
let parsed_token = seed.parse_token(&raw_token).expect("token not parsed"); | |||||
let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("cookie not parsed"); | |||||
if !seed.verify_token_pair(&parsed_token, &parsed_cookie) { | |||||
debug("warn: CSRF token doesn't match."); | |||||
return Err(crash(lang, "error_csrf_token")); | |||||
} | |||||
} | |||||
else { | |||||
debug("warn: missing CSRF token."); | |||||
return Err(crash(lang, "error_csrf_cookie")); | |||||
} | |||||
let nc_username = gen_name(); | |||||
println!("gen_name: {}", nc_username); | |||||
let nc_password = gen_token(45); | |||||
// attempts to create the account | |||||
create_account(&client, &nc_username, &nc_password, lang.clone()).await?; | |||||
debug(&format!("Created user {}", nc_username)); | |||||
let conn = dbpool.get().map_err(|e| { | |||||
eprintln!("error_forwardregister_pool: {}", e); | |||||
crash(lang.clone(), "error_forwardregister_pool") | |||||
})?; | |||||
let token = gen_token(45); | |||||
let token_mv = token.clone(); | |||||
// store the result in DB | |||||
let form_result = web::block(move || Form::insert( | |||||
InsertableForm { | |||||
created_at: Utc::now().naive_utc(), | |||||
lastvisit_at: Utc::now().naive_utc(), | |||||
token: token_mv, | |||||
nc_username, | |||||
nc_password, | |||||
}, | |||||
&conn, | |||||
)) | |||||
.await; | |||||
if form_result.is_err() { | |||||
return Err(crash(lang, "error_forwardregister_db")); | |||||
} | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.set_header( | |||||
"Set-Cookie", | |||||
format!("sncf_admin_token={}; HttpOnly; SameSite=Strict", &token), | |||||
) | |||||
.body( | |||||
TplLink { | |||||
lang: &lang, | |||||
admin_token: &token, | |||||
config: &CONFIG, | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplLink): {}", e); | |||||
crash(lang.clone(), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplLink): {}", e); | |||||
crash(lang, "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
// create a new query destined to the nextcloud instance | |||||
// needed to forward any query | |||||
fn forge_from( | |||||
route: &str, | |||||
req: &HttpRequest, | |||||
url: &web::Data<Url>, | |||||
client: &web::Data<Client>, | |||||
) -> ClientRequest { | |||||
let mut new_url = url.get_ref().clone(); | |||||
new_url.set_path(route); | |||||
new_url.set_query(req.uri().query()); | |||||
// insert forwarded header if we can | |||||
let mut forwarded_req = client | |||||
.request_from(new_url.as_str(), req.head()) | |||||
.timeout(Duration::new(PROXY_TIMEOUT, 0)); | |||||
// attempt to remove basic-auth header | |||||
forwarded_req.headers_mut().remove("authorization"); | |||||
if let Some(addr) = req.head().peer_addr { | |||||
forwarded_req.header("x-forwarded-for", format!("{}", addr.ip())) | |||||
} else { | |||||
forwarded_req | |||||
} | |||||
} | |||||
fn web_redir(location: &str) -> HttpResponse { | |||||
HttpResponse::SeeOther() | |||||
.header(http::header::LOCATION, location) | |||||
.finish() | |||||
} | |||||
pub async fn index(req: HttpRequest) -> Result<HttpResponse, TrainCrash> { | |||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); | |||||
let (csrf_token, csrf_cookie) = seed.generate_token_pair(None, 43200) | |||||
.expect("couldn't generate token/cookie pair"); | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.set_header( | |||||
"Set-Cookie", | |||||
format!("sncf_csrf_cookie={}; HttpOnly; SameSite=Strict", | |||||
base64::encode_config(&csrf_cookie.value(), base64::URL_SAFE_NO_PAD))) | |||||
.body( | |||||
TplIndex { | |||||
lang: &get_lang(&req), | |||||
csrf_token: &base64::encode_config(&csrf_token.value(), base64::URL_SAFE_NO_PAD), | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
@ -1,376 +0,0 @@ | |||||
use actix_web::client::{Client, ClientRequest}; | |||||
use actix_web::{http, web, HttpRequest, HttpResponse}; | |||||
use actix_session::Session; | |||||
use askama::Template; | |||||
use chrono::Utc; | |||||
use csrf::{AesGcmCsrfProtection, CsrfProtection}; | |||||
use std::time::Duration; | |||||
use url::Url; | |||||
use crate::account::*; | |||||
use crate::config::get_csrf_key; | |||||
use crate::config::PAYLOAD_LIMIT; | |||||
use crate::config::PROXY_TIMEOUT; | |||||
use crate::database::methods::InsertableForm; | |||||
use crate::database::structs::Form; | |||||
use crate::debug; | |||||
use crate::errors::{crash, TrainCrash}; | |||||
use crate::sniff::*; | |||||
use crate::templates::*; | |||||
use crate::DbPool; | |||||
use crate::CONFIG; | |||||
pub async fn forward( | |||||
req: HttpRequest, | |||||
body: web::Bytes, | |||||
url: web::Data<Url>, | |||||
client: web::Data<Client>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let route = req.uri().path(); | |||||
// if check_route returns true, | |||||
// the user supposedly tried to access a restricted page. | |||||
// They get redirected to the main page. | |||||
if route.starts_with("/apps/files") { | |||||
// exception for /apps/files: always redirect to /apps/forms | |||||
debug(&format!("Files route blocked: {}", route)); | |||||
return Ok(web_redir("/apps/forms").await.map_err(|e| { | |||||
eprintln!("error_redirect: {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} else if check_route(route) { | |||||
debug(&format!("Restricted route blocked: {}", route)); | |||||
return Ok(web_redir("/").await.map_err(|e| { | |||||
eprintln!("error_redirect: {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
let forwarded_req = forge_from(route, &req, &url, &client); | |||||
// check the request before sending it | |||||
// (prevents the user from sending some specific POST requests) | |||||
if check_request(route, &body) { | |||||
debug(&format!( | |||||
"Restricted request: {}", | |||||
String::from_utf8_lossy(&body) | |||||
)); | |||||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||||
} | |||||
// send the request to the Nextcloud instance | |||||
let mut res = forwarded_req.send_body(body).await.map_err(|e| { | |||||
eprintln!("error_forward_resp: {}", e); | |||||
crash(get_lang(&req), "error_forward_req") | |||||
})?; | |||||
let mut client_resp = HttpResponse::build(res.status()); | |||||
// remove connection as per the spec | |||||
// and content-encoding since we have to decompress the traffic to edit it | |||||
// and basic-auth, because this feature is not needed. | |||||
for (header_name, header_value) in res | |||||
.headers() | |||||
.iter() | |||||
.filter(|(h, _)| *h != "connection" && *h != "content-encoding") | |||||
{ | |||||
client_resp.header(header_name.clone(), header_value.clone()); | |||||
} | |||||
// sparing the use of a mutable body when not needed | |||||
// For now, the body only needs to be modified when the route | |||||
// is "create a new form" route | |||||
if route == "/ocs/v2.php/apps/forms/api/v1/form" { | |||||
// retreive the body from the request result | |||||
let response_body = res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { | |||||
eprintln!("error_forward_resp: {}", e); | |||||
crash(get_lang(&req), "error_forward_resp") | |||||
})?; | |||||
// if a new form is created, automatically set some fields. | |||||
// this is very hackish but it works! for now. | |||||
let form_id = check_new_form(&response_body); | |||||
if form_id > 0 { | |||||
debug(&format!( | |||||
"New form. Forging request to set isAnonymous for id {}", | |||||
form_id | |||||
)); | |||||
let forged_body = format!( | |||||
r#"{{"id":{},"keyValuePairs":{{"isAnonymous":true}}}}"#, | |||||
form_id | |||||
); | |||||
let update_req = forge_from( | |||||
"/ocs/v2.php/apps/forms/api/v1/form/update", | |||||
&req, | |||||
&url, | |||||
&client, | |||||
) | |||||
.set_header("content-length", forged_body.len()) | |||||
.set_header("content-type", "application/json;charset=utf-8"); | |||||
let res = update_req.send_body(forged_body).await.map_err(|e| { | |||||
eprintln!("error_forward_isanon: {}", e); | |||||
crash(get_lang(&req), "error_forward_isanon") | |||||
})?; | |||||
debug(&format!("(new_form) Request returned {}", res.status())); | |||||
} | |||||
Ok(client_resp.body(response_body).await.map_err(|e| { | |||||
eprintln!("error_forward_clientresp_newform: {}", e); | |||||
crash(get_lang(&req), "error_forward_clientresp_newform") | |||||
})?) | |||||
} else { | |||||
Ok( | |||||
client_resp.body(res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { | |||||
eprintln!("error_forward_clientresp_newform: {}", e); | |||||
crash(get_lang(&req), "error_forward_clientresp_std") | |||||
})?), | |||||
) | |||||
} | |||||
// check the response before returning it (unused) | |||||
/*if check_response(route, &response_body) { | |||||
return Ok(web_redir("/")); | |||||
}*/ | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct LoginToken { | |||||
pub token: String, | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct CsrfToken { | |||||
pub csrf_token: String, | |||||
} | |||||
pub async fn forward_login( | |||||
req: HttpRequest, | |||||
s: Session, | |||||
params: web::Path<LoginToken>, | |||||
client: web::Data<Client>, | |||||
dbpool: web::Data<DbPool>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
// check if the provided token seems valid. If not, early return. | |||||
if !check_token(¶ms.token) { | |||||
debug("Incorrect admin token given in params."); | |||||
debug(&format!("Token: {:#?}", params.token)); | |||||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||||
} | |||||
let conn = dbpool.get().map_err(|e| { | |||||
eprintln!("error_forwardlogin_db: {}", e); | |||||
crash(get_lang(&req), "error_forwardlogin_db") | |||||
})?; | |||||
let moved_token = params.token.clone(); | |||||
// check if the link exists in DB. if it does, update lastvisit_at. | |||||
let formdata = web::block(move || Form::get_from_token(¶ms.token, &conn)) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_forwardlogin_db_get (diesel error): {}", e); | |||||
crash(get_lang(&req), "error_forwardlogin_db_get") | |||||
})? | |||||
.ok_or_else(|| { | |||||
debug("error: Token not found."); | |||||
crash(get_lang(&req), "error_forwardlogin_notfound") | |||||
})?; | |||||
// copy the token in cookies. | |||||
s.set("sncf_admin_token", &moved_token).map_err(|e| { | |||||
eprintln!("error_login_setcookie (in login): {}", e); | |||||
crash(get_lang(&req),"error_login_setcookie") | |||||
})?; | |||||
// if the user is already logged in, skip the login process | |||||
// we don't care if someone edits their cookies, Nextcloud will properly | |||||
// check them anyway | |||||
if let Some(nc_username) = is_logged_in(&req) { | |||||
if nc_username.contains(&format!("nc_username={}", formdata.nc_username)) { | |||||
return Ok(web_redir("/apps/forms").await.map_err(|e| { | |||||
eprintln!("error_redirect (1:/apps/forms/): {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
} | |||||
// try to log the user in with DB data, then redirect. | |||||
login(&client, &req, &formdata.nc_username, &formdata.nc_password).await | |||||
} | |||||
// creates a NC account using a random name and password. | |||||
// the account gets associated with a token in sqlite DB. | |||||
// POST /link route | |||||
pub async fn forward_register( | |||||
req: HttpRequest, | |||||
s: Session, | |||||
csrf_post: web::Form<CsrfToken>, | |||||
client: web::Data<Client>, | |||||
dbpool: web::Data<DbPool>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let lang = get_lang(&req); | |||||
// do not check for existing admin tokens and force a new registration | |||||
// check if the csrf token is OK | |||||
let cookie_csrf_token = s.get::<String>("sncf_csrf_token").map_err(|e| { | |||||
eprintln!("error_csrf_cookie: {}", e); | |||||
crash(get_lang(&req), "error_csrf_cookie") | |||||
})?; | |||||
if let Some(cookie_token) = cookie_csrf_token { | |||||
let raw_ctoken = | |||||
base64::decode_config(cookie_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err( | |||||
|e| { | |||||
eprintln!("error_csrf_cookie (base64): {}", e); | |||||
crash(get_lang(&req), "error_csrf_cookie") | |||||
}, | |||||
)?; | |||||
let raw_token = | |||||
base64::decode_config(csrf_post.csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD) | |||||
.map_err(|e| { | |||||
eprintln!("error_csrf_token (base64): {}", e); | |||||
crash(get_lang(&req), "error_csrf_token") | |||||
})?; | |||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); | |||||
let parsed_token = seed.parse_token(&raw_token).expect("error: token not parsed"); | |||||
let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("error: cookie not parsed"); | |||||
if !seed.verify_token_pair(&parsed_token, &parsed_cookie) { | |||||
debug("warn: CSRF token doesn't match."); | |||||
return Err(crash(lang, "error_csrf_token")); | |||||
} | |||||
} else { | |||||
debug("warn: missing CSRF token."); | |||||
return Err(crash(lang, "error_csrf_cookie")); | |||||
} | |||||
let nc_username = gen_name(); | |||||
println!("gen_name: {}", nc_username); | |||||
let nc_password = gen_token(45); | |||||
// attempts to create the account | |||||
create_account(&client, &nc_username, &nc_password, lang.clone()).await?; | |||||
debug(&format!("Created user {}", nc_username)); | |||||
let conn = dbpool.get().map_err(|e| { | |||||
eprintln!("error_forwardregister_pool: {}", e); | |||||
crash(lang.clone(), "error_forwardregister_pool") | |||||
})?; | |||||
let token = gen_token(45); | |||||
let token_mv = token.clone(); | |||||
// store the result in DB | |||||
let form_result = web::block(move || { | |||||
Form::insert( | |||||
InsertableForm { | |||||
created_at: Utc::now().naive_utc(), | |||||
lastvisit_at: Utc::now().naive_utc(), | |||||
token: token_mv, | |||||
nc_username, | |||||
nc_password, | |||||
}, | |||||
&conn, | |||||
) | |||||
}) | |||||
.await; | |||||
if form_result.is_err() { | |||||
return Err(crash(lang, "error_forwardregister_db")); | |||||
} | |||||
s.set("sncf_admin_token", &token).map_err(|e| { | |||||
eprintln!("error_login_setcookie (in register): {}", e); | |||||
crash(lang.clone(), "error_login_setcookie") | |||||
})?; | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.body( | |||||
TplLink { | |||||
lang: &lang, | |||||
admin_token: &token, | |||||
config: &CONFIG, | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplLink): {}", e); | |||||
crash(lang.clone(), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplLink): {}", e); | |||||
crash(lang, "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
// create a new query destined to the nextcloud instance | |||||
// needed to forward any query | |||||
fn forge_from( | |||||
route: &str, | |||||
req: &HttpRequest, | |||||
url: &web::Data<Url>, | |||||
client: &web::Data<Client>, | |||||
) -> ClientRequest { | |||||
let mut new_url = url.get_ref().clone(); | |||||
new_url.set_path(route); | |||||
new_url.set_query(req.uri().query()); | |||||
// insert forwarded header if we can | |||||
let mut forwarded_req = client | |||||
.request_from(new_url.as_str(), req.head()) | |||||
.timeout(Duration::new(PROXY_TIMEOUT, 0)); | |||||
// attempt to remove basic-auth header | |||||
forwarded_req.headers_mut().remove("authorization"); | |||||
if let Some(addr) = req.head().peer_addr { | |||||
forwarded_req.header("x-forwarded-for", format!("{}", addr.ip())) | |||||
} else { | |||||
forwarded_req | |||||
} | |||||
} | |||||
fn web_redir(location: &str) -> HttpResponse { | |||||
HttpResponse::SeeOther() | |||||
.header(http::header::LOCATION, location) | |||||
.finish() | |||||
} | |||||
pub async fn index(req: HttpRequest, s: Session) -> Result<HttpResponse, TrainCrash> { | |||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); | |||||
let (csrf_token, csrf_cookie) = seed | |||||
.generate_token_pair(None, 43200) | |||||
.expect("couldn't generate token/cookie pair"); | |||||
s.set("sncf_csrf_token", &base64::encode_config(&csrf_cookie.value(), base64::URL_SAFE_NO_PAD)).map_err(|e| { | |||||
eprintln!("error_login_setcookie (in index): {}", e); | |||||
crash(get_lang(&req), "error_login_setcookie") | |||||
})?; | |||||
let cookie_admin_token = s.get::<String>("sncf_admin_token").map_err(|e| { | |||||
eprintln!("error_forwardregister_tokenparse (index): {}", e); | |||||
crash(get_lang(&req), "error_forwardregister_tokenparse") | |||||
})?; | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.body( | |||||
TplIndex { | |||||
lang: &get_lang(&req), | |||||
csrf_token: &base64::encode_config(&csrf_token.value(), base64::URL_SAFE_NO_PAD), | |||||
sncf_admin_token: cookie_admin_token, | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
@ -1,292 +0,0 @@ | |||||
@font-face { | |||||
font-family: 'Ubuntu-R'; | |||||
src: url('/assets/Ubuntu-R.ttf'); | |||||
font-weight: normal; | |||||
font-style: normal; | |||||
} | |||||
.hidden { | |||||
display: none !important; | |||||
} | |||||
* { | |||||
font-family: Ubuntu,"Ubuntu-R",sans-serif; | |||||
} | |||||
a { | |||||
text-decoration: none; | |||||
/*color: #2359fb;*/ | |||||
} | |||||
.flex { | |||||
display: flex; | |||||
flex-wrap: wrap; | |||||
justify-content: center; | |||||
} | |||||
.fullheight { | |||||
min-height: 100vh; | |||||
} | |||||
.fullheight-nav { | |||||
min-height: calc(100vh - 50px); | |||||
} | |||||
.fullwidth { | |||||
width: 100%; | |||||
text-align: center; | |||||
} | |||||
.title { | |||||
color: black; | |||||
/*text-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18);*/ | |||||
} | |||||
h1 { | |||||
font-size: 4vw; | |||||
} | |||||
h2 { | |||||
font-size: 2.25vw; | |||||
} | |||||
h3 { | |||||
font-size: 17pt bold; | |||||
text-align: left; | |||||
} | |||||
p { | |||||
font-size: 15pt medium; | |||||
/*line-height: 1.6;*/ | |||||
text-align: left; | |||||
} | |||||
.beta-tag { | |||||
background: #ff00ff; | |||||
color: white; | |||||
border-radius: 5px; | |||||
font-size: 0.9rem; | |||||
padding: 0.3rem; | |||||
margin-left: 0.5rem; | |||||
} | |||||
.beta-banner a { | |||||
color: #ff00ff; | |||||
} | |||||
.beta-banner { | |||||
background: repeating-linear-gradient( 45deg, #ff00ff, #ff00ff 10px, #c44c05 10px, #c44c05 20px ); | |||||
color: white; | |||||
padding: 1rem; | |||||
text-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||||
} | |||||
.logo { | |||||
width: 10vw; | |||||
margin-right: 2vw; | |||||
} | |||||
.page-heading { | |||||
background-image: url("/assets/index-background.png"); /*, linear-gradient(0deg, #1f58c6 0%, #1c66f2 100%);*/ | |||||
background-position: 50% 50%; | |||||
background-repeat: no-repeat; | |||||
background-size: cover; | |||||
background-attachment: fixed; | |||||
} | |||||
.page-heading-text { | |||||
width: auto; | |||||
margin: auto; | |||||
padding: 1rem; | |||||
} | |||||
.page-heading > p { | |||||
color: black; | |||||
} | |||||
.page-heading > p > a { | |||||
color: #000000; | |||||
} | |||||
.page-heading.error { | |||||
background: url("/assets/index-background.png"); /*, linear-gradient(0deg, #790000 0%, #a40000 100%)*/ | |||||
} | |||||
.ncstyle-button.error { | |||||
background: #ee4040; | |||||
} | |||||
.error.ncstyle-button:hover { | |||||
background: #c82323; | |||||
} | |||||
.navbar { | |||||
height: 50px; | |||||
} | |||||
body, html { | |||||
margin: 0; | |||||
padding: 0; | |||||
} | |||||
.ncstyle-button { | |||||
background-color: #ffcc00; | |||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||||
border-radius: 1vw; | |||||
text-decoration: none; | |||||
text-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||||
white-space: nowrap; | |||||
height: 48px; | |||||
width: auto; | |||||
line-height: 2.25rem; | |||||
padding: 0.5em; | |||||
background: #ffcc00; | |||||
font-size: 20pt; | |||||
min-width: 18vw; | |||||
display: block; | |||||
transition: all .25s ease-in-out; | |||||
color: white; | |||||
} | |||||
.margin-bottom { | |||||
margin-bottom: 1rem; | |||||
} | |||||
.ncstyle-button_blue:hover { | |||||
background: #fbc617; | |||||
} | |||||
.ncstyle-button_yellow:hover { | |||||
background: #fbc617; | |||||
} | |||||
.ncstyle-input { | |||||
margin: auto; | |||||
padding: 7px 6px; | |||||
font-size: 16px; | |||||
background-color: white; | |||||
color: #454545; | |||||
border: 1px solid #dbdbdb; | |||||
outline: none; | |||||
border-radius: 3px; | |||||
cursor: text; | |||||
width: 80vw; | |||||
} | |||||
.click { | |||||
cursor: pointer; | |||||
} | |||||
#script-copy { | |||||
display: none; | |||||
} | |||||
@media only screen and (max-width: 1080px) { | |||||
h1 { | |||||
font-size: 48px; | |||||
} | |||||
h2 { | |||||
font-size: 32px; | |||||
} | |||||
h3 { | |||||
font-size: 24px; | |||||
} | |||||
p { | |||||
font-size: 16px; | |||||
} | |||||
.title { | |||||
text-align: center; | |||||
} | |||||
.logo { | |||||
width: 20vw; | |||||
margin: 0; | |||||
} | |||||
.ncstyle-button_blue { | |||||
font-size: 24px; | |||||
} | |||||
} | |||||
@media only screen and (max-width: 1080px), screen and (max-height: 600px) { | |||||
.scroll-down-arrow { | |||||
display: none; | |||||
} | |||||
} | |||||
.scroll-down-arrow { | |||||
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2hldnJvbl90aGluX2Rvd24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDIwIDIwIiBmaWxsPSJ3aGl0ZSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTE3LjQxOCw2LjEwOWMwLjI3Mi0wLjI2OCwwLjcwOS0wLjI2OCwwLjk3OSwwYzAuMjcsMC4yNjgsMC4yNzEsMC43MDEsMCwwLjk2OWwtNy45MDgsNy44M2MtMC4yNywwLjI2OC0wLjcwNywwLjI2OC0wLjk3OSwwbC03LjkwOC03LjgzYy0wLjI3LTAuMjY4LTAuMjctMC43MDEsMC0wLjk2OWMwLjI3MS0wLjI2OCwwLjcwOS0wLjI2OCwwLjk3OSwwTDEwLDEzLjI1TDE3LjQxOCw2LjEwOXoiLz48L3N2Zz4=); | |||||
background-size: contain; | |||||
background-repeat: no-repeat; | |||||
} | |||||
.scroll-down-link { | |||||
cursor:pointer; | |||||
height: 60px; | |||||
width: 80px; | |||||
margin: 0px 0 0 -40px; | |||||
line-height: 60px; | |||||
position: absolute; | |||||
left: 50%; | |||||
bottom: 10px; | |||||
color: #FFF; | |||||
text-align: center; | |||||
font-size: 70px; | |||||
z-index: 100; | |||||
text-decoration: none; | |||||
text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.4); | |||||
animation: fade_move_down 2s ease-in-out infinite; | |||||
} | |||||
/*animated scroll arrow animation*/ | |||||
@keyframes fade_move_down { | |||||
0% { transform:translate(0,-20px); opacity: 0; } | |||||
50% { opacity: 1; } | |||||
100% { transform:translate(0,20px); opacity: 0; } | |||||
} | |||||
.lds-ring { | |||||
display: inline-block; | |||||
position: relative; | |||||
width: 80px; | |||||
height: 80px; | |||||
} | |||||
.lds-ring div { | |||||
box-sizing: border-box; | |||||
display: block; | |||||
position: absolute; | |||||
width: 64px; | |||||
height: 64px; | |||||
margin: 8px; | |||||
border: 8px solid #fff; | |||||
border-radius: 50%; | |||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; | |||||
border-color: #fff transparent transparent transparent; | |||||
} | |||||
.lds-ring div:nth-child(1) { | |||||
animation-delay: -0.45s; | |||||
} | |||||
.lds-ring div:nth-child(2) { | |||||
animation-delay: -0.3s; | |||||
} | |||||
.lds-ring div:nth-child(3) { | |||||
animation-delay: -0.15s; | |||||
} | |||||
@keyframes lds-ring { | |||||
0% { | |||||
transform: rotate(0deg); | |||||
} | |||||
100% { | |||||
transform: rotate(360deg); | |||||
} | |||||
} | |||||
@ -1,349 +0,0 @@ | |||||
<div id="container"> | |||||
<!doctype html> | |||||
<html lang="{{ "lang_code"|tr(lang) }}"> | |||||
<head> | |||||
<title>{{ "index_title"|tr(lang) }} – {{ "index_description"|tr(lang) }}</title> | |||||
<meta charset="utf-8" /> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||||
<meta name="description" content="{{ "meta_description"|tr(lang) }}" /> | |||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |||||
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon.svg" /> | |||||
<link rel="stylesheet" href="/assets/index.css?v=1.2" /> | |||||
<link rel="stylesheet" href="/assets/cloud.css?v=1.0" /> | |||||
<link rel="stylesheet" href="/assets/digitalcourage.css" /> | |||||
<link rel="stylesheet" href="/assets/bootstrap.min.css" /> | |||||
<style> | |||||
.break { | |||||
flex-basis: 100%; | |||||
height: 0; | |||||
} | |||||
.grid-container { | |||||
display: grid; | |||||
grid-template-columns: auto auto; | |||||
width: 725px; | |||||
grid-gap: 25px; | |||||
} | |||||
.grid-container2 { | |||||
display: grid; | |||||
grid-template-columns: auto auto auto; | |||||
width: 532px; | |||||
grid-gap: 35px; | |||||
} | |||||
@media only screen and (max-width: 768px) { | |||||
/* For mobile phones: */ | |||||
[class*="grid-container"] { | |||||
grid-template-columns: auto; | |||||
max-width: 100%; | |||||
justify-content: center; | |||||
} | |||||
[class*="grid-container2"] { | |||||
grid-template-columns: auto; | |||||
max-width: 100%; | |||||
justify-content: center; | |||||
grid-gap: 20px; | |||||
} | |||||
[class*="item2"] { | |||||
grid-template-columns: auto; | |||||
max-width: 100%; | |||||
align-items: center; | |||||
} | |||||
} | |||||
.div_120 { | |||||
flex-basis: 100%; | |||||
height: 120px; | |||||
} | |||||
.div_60 { | |||||
flex-basis: 100%; | |||||
height: 60px; | |||||
} | |||||
.div_45 { | |||||
flex-basis: 100%; | |||||
height: 45px; | |||||
} | |||||
.div_35 { | |||||
flex-basis: 100%; | |||||
height: 35px; | |||||
} | |||||
.div_25 { | |||||
flex-basis: 100%; | |||||
height: 25px; | |||||
} | |||||
.div_10 { | |||||
flex-basis: 100%; | |||||
height: 10px; | |||||
} | |||||
.item1 { | |||||
width: 350px; | |||||
height: 200px; | |||||
display: flex; | |||||
justify-content: center; | |||||
align-items: center; | |||||
} | |||||
.item2 { | |||||
width: 350px; | |||||
height: 200px; | |||||
#display: flex; | |||||
#justify-content: center; | |||||
#align-items: center; | |||||
} | |||||
.h3 { | |||||
font-size: 20pt; | |||||
} | |||||
h2 { | |||||
font-size: 30pt; | |||||
} | |||||
.a1 { | |||||
font-size: 20pt; | |||||
} | |||||
p { | |||||
font-size: 14pt; | |||||
} | |||||
p1 { | |||||
font-size: 20pt; | |||||
} | |||||
.downDC { | |||||
height: 90px; | |||||
padding: 10px; | |||||
} | |||||
.c-img-shadow { | |||||
height: 200px; | |||||
max-width: 100%; | |||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||||
border-radius: 2px; | |||||
} | |||||
</style> | |||||
<noscript><style> .jsonly { display: none } </style></noscript> | |||||
<script> | |||||
window.onload = function() { | |||||
// retrieved from server-side template | |||||
let csrf_token = "{{ csrf_token }}"; | |||||
let lang = "{{ lang }}"; | |||||
document.getElementById('langs').value=lang; | |||||
document.getElementById('new_link_button1').addEventListener('click', function () { | |||||
new_link(csrf_token); | |||||
}); | |||||
document.getElementById('new_link_button2').addEventListener('click', function () { | |||||
new_link(csrf_token); | |||||
}); | |||||
} | |||||
function getSelectedOption(sel) { | |||||
var opt; | |||||
for ( var i = 0, len = sel.options.length; i < len; i++ ) { | |||||
opt = sel.options[i]; | |||||
if ( opt.selected === true ) { | |||||
break; | |||||
} | |||||
} | |||||
return opt; | |||||
} | |||||
function new_link(csrf) { | |||||
var sel = document.getElementById('langs'); | |||||
let opt = getSelectedOption(sel); | |||||
let lang = opt.value; | |||||
document.getElementById('langs').value = lang; | |||||
document.getElementById("link_lang").value = lang; | |||||
document.getElementById("csrf_token").value = csrf; | |||||
document.getElementById('new_link').submit(); | |||||
document.getElementById('new_link_button').classList.add("hidden"); | |||||
document.getElementById('loading_ring').classList.remove("hidden"); | |||||
} | |||||
</script> | |||||
</head> | |||||
<body> | |||||
<div class="container ombre"> | |||||
<header role="banner" class="clearfix"> | |||||
<form method="get" action="/" class="hidden-print"> | |||||
<div class="input-group input-group-sm pull-right col-xs-12 col-sm-2 langs" style="margin-right: 8px"> | |||||
<select id="langs" name="lang" class="form-control" title="Select language" > | |||||
<option lang="fr" value="fr">Français</option> | |||||
<option lang="en" selected value="en">English</option> | |||||
<option lang="oc" value="oc">Occitan</option> | |||||
<option lang="es" value="es">Español</option> | |||||
<option lang="de" value="de">Deutsch</option> | |||||
<option lang="nl" value="nl">Dutch</option> | |||||
<option lang="it" value="it">Italiano</option> | |||||
<option lang="br" value="br">Brezhoneg</option> | |||||
</select> | |||||
<span class="input-group-btn"> | |||||
<button type="submit" id="language_button" class="btn btn-default btn-sm language_button" title="Change language">OK</button> | |||||
</span> | |||||
</div> | |||||
</form> | |||||
<a href="https://foorms.digitalcourage.de/" title="Home - foorms" style="margin-left: 8px" > | |||||
<img src="/assets/foorms_logo_beta.svg" alt="foorms" class="" height="58vh" /> | |||||
</a> | |||||
<h2 class="lead col-xs-12"></h2> <div class="trait col-xs-12" role="presentation"></div> | |||||
</header> | |||||
<main role="main"> | |||||
<div class="div_10"></div> | |||||
<div class="div_10"></div> | |||||
<div class="div_10"></div> | |||||
<div class="flex has-text-centered"> | |||||
<p> | |||||
<div> | |||||
<h2 class="title">{{ "index_title2"|tr(lang) }}</h2> | |||||
</div> | |||||
<br/> | |||||
<div class="break"></div> | |||||
</div> | |||||
<div class="div_25"></div> | |||||
<div class="flex has-text-centered"> | |||||
<div> | |||||
<h3 class="title">{{ "index_description"|tr(lang) }}</h3> | |||||
</div> | |||||
<div class="break"></div> | |||||
<div> | |||||
<h3 class="title">{{ "index_description2"|tr(lang) }}</h3> | |||||
</div> | |||||
</div> | |||||
</p> | |||||
<div class="div_60"></div> | |||||
<div class="flex has-text-centered"> | |||||
<div class=" flex"> | |||||
<noscript> | |||||
<a class="ncstyle-button">{{ "index_nojs"|tr(lang) }}</a> | |||||
</noscript> | |||||
<form id="new_link" action="/link" method="post"> | |||||
<input id="csrf_token" name="csrf_token" type="text" class="hidden"> | |||||
<input id="link_lang" name="link_lang" type="text" class="hidden"> | |||||
<a id="new_link_button1" class="c-button ncstyle-button" >{{ "index_createform_button"|tr(lang) }}</a> </form> | |||||
<div id="loading_ring" class="hidden lds-ring"><div></div><div></div><div></div><div></div></div> | |||||
</div> | |||||
</div> | |||||
<div class="break"></div> | |||||
<div class="div_120"></div> | |||||
<div class="has-text-centered"> | |||||
<h2>{{ "index_panel1_title"|tr(lang) }}</h2> | |||||
</div> | |||||
<div class="div_25"></div> | |||||
<center> | |||||
<div class="grid-container"> | |||||
<div class="item1"> | |||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/fields.png" width="350px" height="200px"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/fields.png" height="200px" width="350px" /></a> | |||||
</div> | |||||
<div class= "item2"> | |||||
<h3 id="item2_header">{{ "index_panel2_title"|tr(lang) }}</h3> | |||||
<p class="item2_paragraph">{{ "index_panel2_desc1"|tr(lang) }}</p><p class="item2_paragraph">{{ "index_panel2_desc2"|tr(lang) }}<a href="https://github.com/nextcloud/forms/issues?q=is%3Aissue+is%3Aopen+label%3A%22feature%3A+%E2%9D%93+question+types%22">{{ "index_panel2_desc2_link"|tr(lang) }}</a>.</p> | |||||
</div> | |||||
</div> | |||||
</br> | |||||
<div class="grid2gridspace"></div> | |||||
<div class="grid-container"> | |||||
<div class="item1"> | |||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/responses.png" height="200px" width="350px"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/responses.png" height="200px" width="350px" /></a> | |||||
</div> | |||||
<div class="item2"> | |||||
<h3>{{ "index_panel3_title"|tr(lang) }}</h3> | |||||
<p>{{ "index_panel3_desc1"|tr(lang) }}</p> | |||||
</div> | |||||
</div> | |||||
</br> | |||||
<div class="grid-container"> | |||||
<div class="item1"> | |||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/responses-export.png" height="200px" width="350px"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/responses-export.png" height="200px" width="350px" /></a> | |||||
</div> | |||||
<div class="item2"> | |||||
<h3>{{ "index_panel4_title"|tr(lang) }}</h3> | |||||
<p>{{ "index_panel4_desc1"|tr(lang) }}</p> | |||||
</div> | |||||
</div> | |||||
</br> | |||||
<div class="grid-container"> | |||||
<div class="item1"> | |||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/params.png" height="200px" width="350px"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/params.png"height="200px" width="350px" /></a> | |||||
</div> | |||||
<div class="item2"> | |||||
<h3>{{ "index_panel5_title"|tr(lang) }}</h3> | |||||
<p>{{ "index_panel5_desc1"|tr(lang) }}</p> | |||||
<p>{{ "index_panel5_desc2"|tr(lang) }}</p> | |||||
</div> | |||||
</div> | |||||
</br> | |||||
<div class="grid-container"> | |||||
<div class="item1"> | |||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/formslist.png"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/formslist.png" height="200px" width="350px" /></a> | |||||
</div> | |||||
<div class="item2"> | |||||
<h3>{{ "index_panel6_title"|tr(lang) }}</h3> | |||||
<p>{{ "index_panel5_desc1"|tr(lang) }}</p> | |||||
</div> | |||||
</div> | |||||
<div class="div_60"></div> | |||||
<div class="flex has-text-centered"> | |||||
<div class=" flex"> | |||||
<noscript> | |||||
<a class="ncstyle-button">{{ "index_nojs"|tr(lang) }}</a> | |||||
</noscript> | |||||
<form id="new_link" action="/link" method="post"> | |||||
<input id="csrf_token" name="csrf_token" type="text" class="hidden"> | |||||
<input id="link_lang" name="link_lang" type="text" class="hidden"> | |||||
<a id="new_link_button2" class="c-button ncstyle-button" >{{ "index_createform_button"|tr(lang) }}</a> </form> | |||||
<div id="loading_ring" class="hidden lds-ring"><div></div><div></div><div></div><div></div></div> | |||||
</div> | |||||
</div> | |||||
<div class="div_120"></div> | |||||
<p> | |||||
<div class="flex has-text-centered"> | |||||
<div> | |||||
<h2 class="title">{{ "index_disclaimer_title"|tr(lang) }}</h2> | |||||
</div> | |||||
<div class="break"></div> | |||||
<div class="div_25"></div> | |||||
<div> | |||||
<p1 class="title">{{ "index_disclaimer1"|tr(lang) }}</p1> | |||||
<a href="https://www.digitalcourage.de" class="a1">{{ "index_disclaimer2_link_org"|tr(lang) }}</a1> | |||||
<p1 class="title">{{ "index_disclaimer2"|tr(lang) }}</p1> | |||||
</div> | |||||
<br> | |||||
<div class="break"></div> | |||||
<div> | |||||
<p1 class="title">{{ "index_disclaimer2_but"|tr(lang) }}</p1> | |||||
<a href="https://www.digitalcourage.de" class="a1">{{ "index_disclaimer2_link_don"|tr(lang) }}</a> | |||||
</div> | |||||
<br> | |||||
<div class="break"></div> | |||||
<div> | |||||
<p1 class="title">{{ "index_disclaimer3"|tr(lang) }}</p1> | |||||
<a href="https://www.digitalcourage.de" class="a1">{{ "index_disclaimer3_link"|tr(lang) }}</a> | |||||
<p1 class="title">{{ "index_disclaimer4"|tr(lang) }}</p1> | |||||
</div> | |||||
</div> | |||||
</p> | |||||
<div class="div_120"></div> | |||||
<div class="c-blue grid-container2"> | |||||
<a href="https://42l.fr/Rapport-technique" style="font-size:15px" class="c-button" target="_blank">{{ "index_bottom_docs"|tr(lang) }}</a> | |||||
<a href="https://git.42l.fr/neil/sncf" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_source"|tr(lang) }}</a> | |||||
<a href="https://git.42l.fr/neil/sncf/src/branch/root/LICENSE" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_lic"|tr(lang) }}</a> | |||||
</div> | |||||
<div class="div_10"></div> | |||||
</center> | |||||
</main> | |||||
</div> <!-- .container --> | |||||
<div class="container ombre downDC" style="display:flex;align-items:center;"> | |||||
<h2 class="lead"><a target="_blank" href="https://digitalcourage.de/">Digitalcourage</a> | <a target="_blank" href="https://digitalcourage.de/newsletter">Newsletter</a> | <a target="_blank" href="https://digitalcourage.de/spenden">{{ "impressum_donations"|tr(lang) }}</a> | <a target="_blank" href="https://digitalcourage.de/en">Impressum</a> | <a target="_blank" href="https://digitalcourage.de/en">{{ "impressum_privacy"|tr(lang) }}</a> </h2> | |||||
</div> | |||||
</body> | |||||
</html> |
@ -1,558 +0,0 @@ | |||||
{ | |||||
"lang_code": { | |||||
"en": "en", | |||||
"fr": "fr", | |||||
"de": "de", | |||||
"it": "it" | |||||
}, | |||||
"lang_full": { | |||||
"en": "English", | |||||
"fr": "Français", | |||||
"de": "Deutsch", | |||||
"it": "Italiano" | |||||
}, | |||||
"meta_description": { | |||||
"en": "pluriton : don't do work twice", | |||||
"fr": "pluriton : ne faites pas double travail", | |||||
"de": "pluriton : das Werkzeug gegen Monotonie und doppelte Arbeit", | |||||
"it": "pluriton : per non fare doppio lavoro" | |||||
}, | |||||
"impressum_donations": { | |||||
"en": "Donations", | |||||
"fr": "Dons", | |||||
"de": "Spenden", | |||||
"it": "Donazioni" | |||||
}, | |||||
"impressum_privacy": { | |||||
"en": "Privacy", | |||||
"fr": "Protection des données", | |||||
"de": "Datenschutz", | |||||
"it": "Protezione dati" | |||||
}, | |||||
"index_title": { | |||||
"en": "basabuuka", | |||||
"fr": "basabuuka", | |||||
"de": "basabuuka", | |||||
"it": "basabuuka" | |||||
}, | |||||
"index_title2": { | |||||
"en": "Open Language?", | |||||
"fr": "ouvrir la langue?", | |||||
"de": "Sprache oeffnen?", | |||||
"it": "Aprire le lingue?" | |||||
}, | |||||
"index_title3": { | |||||
"en": "How does pluriton work?", | |||||
"fr": "Comme pluriton functionne?", | |||||
"de": "Wie funktioniert pluriton?", | |||||
"it": "Come funziona pluriton?" | |||||
}, | |||||
"index_description": { | |||||
"en": "Based on databases of wikidata, and open source LLMs, you are invited to simplify your read.", | |||||
"fr": "Bas�sur des bases de donn�es wikidata, et des LLM open source, vous etes invites simplifier votre lecture.", | |||||
"de": "Basierend auf Datenbanken aus wikidata, und open source LLMs, bist du eingeladen dein Lesen zu vereinfachen.", | |||||
"it": "Basandosi su database di wikidata e su LLM open source, siete invitati a semplificare la vostra lettura ." | |||||
}, | |||||
"index_description2": { | |||||
"en": "Enter the text you want to translate - and klick on translate.", | |||||
"fr": "Saisissez le texte que vous souhaitez traduire - et clickez sur traduire.", | |||||
"de": "Geben Sie den Text ein, den sie übersetzen möchten - und klicken sie auf übersetzen.", | |||||
"it": "Inserisci il testo che vuoi tradurre - e clicca su tradurre." | |||||
}, | |||||
"index_description3": { | |||||
"en": "algorithmic translation", | |||||
"fr": "traduction algorithmique", | |||||
"de": "algorithmische Uebersetzung", | |||||
"it": "traduzione algorithmica" | |||||
}, | |||||
"index_description4": { | |||||
"en": "Corrected Translation", | |||||
"fr": "traduction corrige", | |||||
"de": "verbesserte Uebersetzung", | |||||
"it": "La traduzione corretta" | |||||
}, | |||||
"index_beta_tag": { | |||||
"en": "BETA", | |||||
"fr": "BETA", | |||||
"de": "BETA" | |||||
}, | |||||
"index_nojs": { | |||||
"en": "Please enable JavaScript in your browser!", | |||||
"fr": "Veuillez activer JavaScript dans votre navigateur !", | |||||
"de": "Bitte aktivieren Sie JavaScript in ihrem Browser!", | |||||
"it": "Si prega di attivare Javascript nel tuo browser!" | |||||
}, | |||||
"index_search_button": { | |||||
"en": "Search", | |||||
"fr": "Rechercher", | |||||
"de": "Suchen", | |||||
"it": "Cerca" | |||||
}, | |||||
"index_continueform_button": { | |||||
"en": "Access your forms", | |||||
"fr": "Accéder à vos formulaires", | |||||
"de": "Zu deinen Umfragen" | |||||
}, | |||||
"index_beta_banner_title": { | |||||
"en": "Warning: Service in beta.", | |||||
"fr": "Attention : Service en bêta.", | |||||
"de": "Achtung: Seite in Beta Version" | |||||
}, | |||||
"index_beta_banner_desc1": { | |||||
"en": "This service is currently under development and might behave in an unexpected way.", | |||||
"fr": "Ce service est en cours de développement et pourrait se comporter de manière inattendue.", | |||||
"de": "Diese Seite ist in Entwicklung und könnte sich unerwartet verhalten." | |||||
}, | |||||
"index_beta_banner_desc2": { | |||||
"en": "Feel free to send feedbacks on our ", | |||||
"fr": "Vous pouvez nous envoyer vos retours sur ", | |||||
"de": "Feedback gerne an " | |||||
}, | |||||
"index_beta_banner_desc_link": { | |||||
"en": "our contact page", | |||||
"fr": "notre page de contact", | |||||
"de": "unsere Kontaktseite" | |||||
}, | |||||
"index_disclaimer_title": { | |||||
"en": "Who keeps pluriton running?", | |||||
"fr": "Qui a organisé pluriton?", | |||||
"de": "Wer betreibt pluriton?", | |||||
"it": "Chi opera pluriton?" | |||||
}, | |||||
"index_disclaimer1": { | |||||
"en": "This service is maintained for you from ", | |||||
"fr": "Ce service vous est fourni gratuitement de ", | |||||
"de": "Diese Seite wird von ", | |||||
"it": "Questo sito e mantenuto gratuitamente di " | |||||
}, | |||||
"index_disclaimer2": { | |||||
"en": " for free.", | |||||
"fr": " .", | |||||
"de": " für Sie kostenlos angeboten", | |||||
"it": " ." | |||||
}, | |||||
"index_disclaimer2_link_org": { | |||||
"en": " basabuuka - to open language ", | |||||
"fr": " basabuuka - ouvrir langue ", | |||||
"de": " basabuuka - Sprache öffnen ", | |||||
"it": " basabuuka - aprire linguaggi " | |||||
}, | |||||
"index_disclaimer2_but": { | |||||
"en": " But you have the possibility to ", | |||||
"fr": " Mais vous avez la possibilité de ", | |||||
"de": " Aber Sie können gern ", | |||||
"it": " Pero hai la possibilità di " | |||||
}, | |||||
"index_disclaimer2_link_don": { | |||||
"en": "donate.", | |||||
"fr": "faire une donation.", | |||||
"de": "spenden.", | |||||
"it": "fare una donazione" | |||||
}, | |||||
"index_disclaimer3": { | |||||
"en": "Or get in touch with ", | |||||
"fr": "Ou contactez ", | |||||
"de": "Oder schreiben Sie ", | |||||
"it": "Oppure contatta " | |||||
}, | |||||
"index_disclaimer3_link": { | |||||
"en": "basabuuka, ", | |||||
"fr": "basabuuka, ", | |||||
"de": "basabuuka, ", | |||||
"it": "basabuuka, " | |||||
}, | |||||
"index_disclaimer4": { | |||||
"en": " if you have ideas or data to contribute!", | |||||
"fr": " si vous avez des idées ou des données à nous contribuer!", | |||||
"de": " wenn Sie mit Ideen oder Daten beitragen möchten!", | |||||
"it": " se hai delle idee o dei dati da contribuire!" | |||||
}, | |||||
"index_panel1_title": { | |||||
"en": "How does foorms work?", | |||||
"fr": "Comme foorms functionne?", | |||||
"de": "Wie funktioniert foorms?" | |||||
}, | |||||
"index_panel1_desc1": { | |||||
"en": "Are you searching for a privacy-friendly alternative to Google Forms while keeping its ease of use?", | |||||
"fr": "Cherchez-vous une alternative éthique à Google Forms qui reste simple d'utilisation ?", | |||||
"de": "Suchen Sie eine ethisch sinnvolle Alternative zu Google Forms, welche gleichzeitig einfach in der Bedienung ist?" | |||||
}, | |||||
"index_panel1_desc2": { | |||||
"en": "You've just found it.", | |||||
"fr": "Vous venez de la trouver.", | |||||
"de": "Sie haben sie gefunden." | |||||
}, | |||||
"index_panel2_title": { | |||||
"en": "Choose and order your fields", | |||||
"fr": "Choisissez et ordonnez vos champs", | |||||
"de": "Wählen und Ordnen Sie ihre Felder" | |||||
}, | |||||
"index_panel2_desc1": { | |||||
"en": "The software currently supports seven field types.", | |||||
"fr": "Pour le moment, le logiciel supporte sept types de champs.", | |||||
"de": "Im Moment unterstützt die Software sieben Typen von Feldern." | |||||
}, | |||||
"index_panel2_desc2": { | |||||
"en": "New field types are ", | |||||
"fr": "De nouveaux types de champs sont ", | |||||
"de": "Neue Typen von Feldern sind " | |||||
}, | |||||
"index_panel2_desc2_link": { | |||||
"en": "currently in the works", | |||||
"fr": "en cours d'élaboration", | |||||
"de": "momentan in Bearbeitung" | |||||
}, | |||||
"index_panel3_title": { | |||||
"en": "Analyze the answers", | |||||
"fr": "Analysez les réponses", | |||||
"de": "Analysieren Sie die Antworten" | |||||
}, | |||||
"index_panel3_desc1": { | |||||
"en": "See detailed graphs of the answers to your form.", | |||||
"fr": "Visualisez les réponses à votre formulaire avec un graphique.", | |||||
"de": "Visualisieren Sie die Antworten Ihrer Umfrage graphisch." | |||||
}, | |||||
"index_panel4_title": { | |||||
"en": "Export the answers", | |||||
"fr": "Exportez les réponses", | |||||
"de": "Export der Antworten" | |||||
}, | |||||
"index_panel4_desc1": { | |||||
"en": "Export the raw data of your form in CSV format to integrate the answers in other software (e.g. LibreOffice Calc or Microsoft Excel).", | |||||
"fr": "Exportez les données brutes de votre formulaire en format CSV pour intégrer les réponses dans d'autres logiciels (ex. LibreOffice Calc ou Microsoft Excel).", | |||||
"de": "Exportieren Sie die Rohdaten Ihrer Umfrage im CSV Format um die Antworten in anderer Software zu integrieren( z.B. LibreOffice Calc)" | |||||
}, | |||||
"index_panel5_title": { | |||||
"en": "Edit your form's settings", | |||||
"fr": "Paramétrez vos formulaires", | |||||
"de": "Einstellungen Ihrer Umfragen" | |||||
}, | |||||
"index_panel5_desc1": { | |||||
"en": "Use the share link to send your form to other people.", | |||||
"fr": "Utilisez le lien de partage pour envoyer votre formulaire à d'autres personnes.", | |||||
"de": "Nutzen Sie den Teilen Link um Ihre Umfrage anderen Menschen zu schicken." | |||||
}, | |||||
"index_panel5_desc2": { | |||||
"en": "You can also define an expiration date for your form.", | |||||
"fr": "Vous pouvez également définir une date d'expiration pour votre formulaire.", | |||||
"de": "Sie können auch ein Ablaufdatum für ihre Umfrage festsetzen." | |||||
}, | |||||
"index_panel6_title": { | |||||
"en": "All your forms in one place", | |||||
"fr": "Tous vos formulaires au même endroit", | |||||
"de": "Alle Ihre Umfragen an einem Ort" | |||||
}, | |||||
"index_panel6_desc1": { | |||||
"en": "Find all your forms in the same panel.", | |||||
"fr": "Retrouvez tous vos formulaires sur un même panel.", | |||||
"de": "Finde alle deine Umfragen in einem Panel." | |||||
}, | |||||
"index_bottom_docs": { | |||||
"en": "Documentation", | |||||
"fr": "Documentation", | |||||
"de": "Dokumentation", | |||||
"it": "Documentazione" | |||||
}, | |||||
"index_bottom_source": { | |||||
"en": "Source code", | |||||
"fr": "Code source", | |||||
"de": "Quellcode", | |||||
"it": "Codice" | |||||
}, | |||||
"index_bottom_lic": { | |||||
"en": "License", | |||||
"fr": "Licence", | |||||
"de": "Lizenz", | |||||
"it": "Licenza" | |||||
}, | |||||
"index_credits_title": { | |||||
"en": "Credits", | |||||
"fr": "Crédits", | |||||
"de": "Credits" | |||||
}, | |||||
"index_credits_desc1": { | |||||
"en": "The Nextcloud software suite and the Nextcloud Forms application has been developed by ", | |||||
"fr": "La suite logicielle Nextcloud et l'application Nextcloud Forms a été développée par ", | |||||
"de": "Die Nextcloud Software Sammlung und die Nextcloud Forms Applikation wurden entwickelt von " | |||||
}, | |||||
"index_credits_desc1_link": { | |||||
"en": "the Nextcloud team", | |||||
"fr": "l'équipe Nextcloud", | |||||
"de": "dem Nextcloud Team" | |||||
}, | |||||
"index_credits_desc1_a": { | |||||
"en": " and its contributors.", | |||||
"fr": " et ses contributeur·ices.", | |||||
"de": " und ihren Kontributor*innen" | |||||
}, | |||||
"index_credits_desc2": { | |||||
"en": "The Simple Nextcloud Forms software, which simplifies the form creation process, has been developed by ", | |||||
"fr": "Le logiciel Simple Nextcloud Forms, qui simplifie la création de formulaires, a été développé par ", | |||||
"de": "Die Simple Nextcloud Forms Software, welche die Erstellung von Umfragen erleichtert, wurde entwickelt von " | |||||
}, | |||||
"index_credits_desc2_for": { | |||||
"en": " for ", | |||||
"fr": " pour ", | |||||
"de": " für " | |||||
}, | |||||
"index_credits_desc2_org": { | |||||
"en": "the 42l association", | |||||
"fr": "l'association 42l", | |||||
"de": "die 42l Assoziation" | |||||
}, | |||||
"index_credits_desc3": { | |||||
"en": "source code", | |||||
"fr": "code source", | |||||
"de": "Quellcode" | |||||
}, | |||||
"link_title": { | |||||
"en": "Link created", | |||||
"fr": "Lien créé", | |||||
"de": "Link erstellt" | |||||
}, | |||||
"link_desc1_1": { | |||||
"en": "Here's an <b>administration link</b>, which will allow you to access all", | |||||
"fr": "Voici un <b>lien d'administration</b>, qui vous permettra d'accéder à tous", | |||||
"de": "Hier ist ein <b>Administrations Link</b>, der es ermöglicht wieder zu" | |||||
}, | |||||
"link_desc1_2": { | |||||
"en": "your forms and check your answers.", | |||||
"fr": "vos formulaires et de consulter vos réponses.", | |||||
"de": "ihren Umfragen zu gelangen und die Antworten einzusehen." | |||||
}, | |||||
"link_desc2_1": { | |||||
"en": "<b>Keep it</b> carefully and don't give it away", | |||||
"fr": "<b>Conservez-le</B> bien précieusement et ne le donnez pas", | |||||
"de": "<b>Bewahren Sie diese</b> gut und sicher auf" | |||||
}, | |||||
"link_desc2_2": { | |||||
"en": "(it'd be the same as giving out your password!).", | |||||
"fr": "(cela reviendrait à donner un mot de passe!).", | |||||
"de": "(Die Weitergabe entspricht der Weitergabe eines Passwortes!)." | |||||
}, | |||||
"link_desc3_1": { | |||||
"en": "Once your link copied, click on the button below to", | |||||
"fr": "Une fois votre lien copié, cliquez sur le bouton ci-dessous pour", | |||||
"de": "Ist der Link kopiert, drücken sie auf den unteren Button um" | |||||
}, | |||||
"link_desc3_2": { | |||||
"en": "start editing your forms.", | |||||
"fr": "commencer à éditer vos formulaires.", | |||||
"de": "Umfragen zu erstellen oder zu bearbeiten." | |||||
}, | |||||
"link_access_btn": { | |||||
"en": "to foorms", | |||||
"fr": "Accéder foorms", | |||||
"de": "zu foorms" | |||||
}, | |||||
"link_note": { | |||||
"en": "Note: If you don't use your administration link during more than ", | |||||
"fr": "Note : Si vous n'utilisez pas votre lien d'administration pendant plus de ", | |||||
"de": "Notiz: Wenn Sie den Administrations Link für länger als " | |||||
}, | |||||
"link_note2": { | |||||
"en": " days, your forms will be automatically deleted.", | |||||
"fr": " jours, vos formulaires seront automatiquement supprimés.", | |||||
"de": " Tage nicht benutzen, werden ihre Umfragen automatisch gelöscht." | |||||
}, | |||||
"link_copy": { | |||||
"en": "Copy link", | |||||
"fr": "Copier le lien", | |||||
"de": "Link kopieren" | |||||
}, | |||||
"link_copied": { | |||||
"en": "Link copied!", | |||||
"fr": "Lien copié !", | |||||
"de": "Link kopiert !" | |||||
}, | |||||
"link_mail": { | |||||
"en": "send Link", | |||||
"fr": "envoyer lien", | |||||
"de": "Link senden" | |||||
}, | |||||
"error_title": { | |||||
"en": "Oops!...", | |||||
"fr": "Oups !...", | |||||
"de": "Ups !..." | |||||
}, | |||||
"error_description": { | |||||
"en": "The application encountered a problem:", | |||||
"fr": "L'application a rencontré un problème :", | |||||
"de": "Die Anwendung hat ein Problem festgestellt:" | |||||
}, | |||||
"error_back": { | |||||
"en": "Back to the main page", | |||||
"fr": "Retour à la page principale", | |||||
"de": "Zurück zur Hauptseite" | |||||
}, | |||||
"error_note1": { | |||||
"en": "We are (probably) aware of this bug, but feel free to contact us if you need assistance.", | |||||
"fr": "Nous sommes (probablement) au courant, mais n'hésitez pas à nous contacter si vous avez besoin d'aide.", | |||||
"de": "Wir sind uns (wahrscheinlich) bewusst, was diesen Fehler angeht. Fühlen sie sich frei uns zu kontaktieren, wenn Sie Hilfe benötigen." | |||||
}, | |||||
"error_note2": { | |||||
"en": "Sorry for the inconvenience.", | |||||
"fr": "Désolés pour les désagréments occasionnés.", | |||||
"de": "Entschuldigen Sie die Störung." | |||||
}, | |||||
"error_forward_req": { | |||||
"en": "Error while connecting to the Nextcloud instance.", | |||||
"fr": "Erreur lors de la connexion à l'instance Nextcloud.", | |||||
"de": "Fehler beim Verbinden zur Nextcloud Instanz." | |||||
}, | |||||
"error_forward_resp": { | |||||
"en": "Error while reading Nextcloud instance's response.", | |||||
"fr": "Erreur lors de la lecture de la réponse de l'instance Nextcloud.", | |||||
"de": "Feher beim Lesen der Antwort der Nextcloud Instanz." | |||||
}, | |||||
"error_forward_isanon": { | |||||
"en": "Couldn't set the form's isAnonymous value.", | |||||
"fr": "Échec lors de la définition de la valeur isAnonymous du formulaire.", | |||||
"de": "Es ist nicht möglich, die isAnonymous Wert des Formulars zu setzen." | |||||
}, | |||||
"error_forward_clientresp_newform": { | |||||
"en": "Failed to send the response body (new form).", | |||||
"fr": "Échec lors de l'envoi du corps de la réponse (nouveau formulaire).", | |||||
"de": "Fehler beim senden des Response body (neues Formular)." | |||||
}, | |||||
"error_forward_clientresp_std": { | |||||
"en": "Failed to send the response body.", | |||||
"fr": "Échec lors de l'envoi du corps de la réponse.", | |||||
"de": "Fehler beim Senden des Response Body." | |||||
}, | |||||
"error_forwardlogin_db": { | |||||
"en": "Couldn't connect to the local database.", | |||||
"fr": "Échec lors de la connexion à la base de données locale.", | |||||
"de": "Fehler beim verbinden zur lokalen Datenbank." | |||||
}, | |||||
"error_forwardlogin_db_get": { | |||||
"en": "Error during information retrieval from the local database.", | |||||
"fr": "Erreur lors de la récupération des informations dans la base de données locale.", | |||||
"de": "Fehler beim Empfangen von Daten der lokalen Datenbank." | |||||
}, | |||||
"error_forwardlogin_notfound": { | |||||
"en": "The specified token doesn't exist in local database.", | |||||
"fr": "Le token spécifié n'existe pas dans la base de données locale.", | |||||
"de": "Der gesetzte Token existiert nicht in der lokalen Datenbank." | |||||
}, | |||||
"error_login_get": { | |||||
"en": "The account creation request (GET) to Nextcloud has failed.", | |||||
"fr": "La requête de création de compte (GET) vers l'instance Nextcloud a échoué.", | |||||
"de": "Das Account Erstellungs Request (GET) zu Nextcloud hat nicht funktioniert." | |||||
}, | |||||
"error_login_get_body": { | |||||
"en": "Reading response from the account creation request to Nextcloud has failed.", | |||||
"fr": "La lecture de la réponse à la requête de création de compte vers l'instance Nextcloud a échoué.", | |||||
"de": "Das Lesen der Response vom Account Erstellungs Request zu Nextcloud hat nicht funktioniert." | |||||
}, | |||||
"error_login_post": { | |||||
"en": "The account creation request (POST) to Nextcloud has failed.", | |||||
"fr": "La requête de création de compte (POST) vers l'instance Nextcloud a échoué.", | |||||
"de": "Der Account Erstellungs Request (POST) zu Nextcloud hat nicht funktioniert. " | |||||
}, | |||||
"error_login_redir": { | |||||
"en": "Redirection to Nextcloud account failed.", | |||||
"fr": "La redirection vers le compte Nextcloud a échoué.", | |||||
"de": "Die Weiterleitung zum Nextcloud account hat nicht funktioniert." | |||||
}, | |||||
"error_createaccount_post": { | |||||
"en": "Account creation: connection to the Nextcloud API failed.", | |||||
"fr": "Création de compte : la connexion à l'API Nextcloud a échoué.", | |||||
"de": "Account Erstellung: Verbindung zur Nextcloud API hat nicht funktioniert." | |||||
}, | |||||
"error_createaccount_post_body": { | |||||
"en": "Account creation: reading the answer from the Nextcloud API failed.", | |||||
"fr": "Création de compte : le traitement de la réponse de l'API Nextcloud a échoué.", | |||||
"de": "Account Erstellung : das Lesen der Antwort der Nextcloud API hat nicht funktioniert." | |||||
}, | |||||
"error_createaccount_status": { | |||||
"en": "The Nextcloud instance responded with an unexpected status code.", | |||||
"fr": "L'instance Nextcloud a répondu avec un code de statut inattendu.", | |||||
"de": "Die Nextcloud Instanz hat mit einem unexpected status code geantwortet." | |||||
}, | |||||
"error_createaccount_ncstatus": { | |||||
"en": "The Nextcloud API responded with an unexpected status code.", | |||||
"fr": "L'API Nextcloud a répondu avec un code de statut inattendu.", | |||||
"de": "Die Nextcloud API hat mit unexpected ncstatus geantwortet." | |||||
}, | |||||
"error_createaccount_ncstatus_parse": { | |||||
"en": "Error parsing Nextcloud API's status code.", | |||||
"fr": "Erreur lors de la lecture du code de statut de l'API Nextcloud.", | |||||
"de": "Fehler beim Lesen des Nextcloud API status codes." | |||||
}, | |||||
"error_forwardregister_pool": { | |||||
"en": "Error while connecting to the local database.", | |||||
"fr": "Erreur lors de la connexion à la base de données locale.", | |||||
"de": "Fehler beim Verbinden zu der lokalen Datenbank." | |||||
}, | |||||
"error_forwardregister_db": { | |||||
"en": "Failed adding the Nextcloud account in the local database.", | |||||
"fr": "L'ajout du compte Nextcloud dans la base de données locale a échoué.", | |||||
"de": "Fehlre beim Hinzufügen des Nextcloud Accounts zur lokalen Datenbank." | |||||
}, | |||||
"error_forwardregister_tokenparse": { | |||||
"en": "Failed parsing the admin token.", | |||||
"fr": "Échec lors de la lecture du token administrateur.", | |||||
"de": "Fehler beim Parsen des Admin Tokens." | |||||
}, | |||||
"error_login_cookiepair": { | |||||
"en": "Couldn't read cookies.", | |||||
"fr": "Échec lors de la lecture de cookies.", | |||||
"de": "Fehler beim Lesen der Cookies" | |||||
}, | |||||
"error_login_regex": { | |||||
"en": "Couldn't read the CSRF token.", | |||||
"fr": "Échec lors de la lecture du token CSRF.", | |||||
"de": "Fehler beim Lesen des CSRF Tokens." | |||||
}, | |||||
"error_login_setcookie": { | |||||
"en": "Error during cookies transfer.", | |||||
"fr": "Erreur lors du transfert de cookies.", | |||||
"de": "Feheler beim Transfer der Cookies." | |||||
}, | |||||
"error_form_insert": { | |||||
"en": "The local database couldn't be reached.", | |||||
"fr": "Échec de la connexion avec la base de données locale.", | |||||
"de": "Die lokale Datenbank ist nicht erreichbar." | |||||
}, | |||||
"error_createaccount": { | |||||
"en": "The Nextcloud API returned an unexpected result.", | |||||
"fr": "L'API de Nextcloud a retourné un résultat inattendu.", | |||||
"de": "Die Nextcloud API hat ein unerwartetes Resultat zurückgesendet." | |||||
}, | |||||
"error_redirect": { | |||||
"en": "Failed to redirect.", | |||||
"fr": "La redirection a échoué.", | |||||
"de": "Weiterleitung (Redirect) hat nicht funktioniert." | |||||
}, | |||||
"error_csrf_cookie": { | |||||
"en": "Your CSRF token (cookie) seems incorrect, please retry.", | |||||
"fr": "Votre token CSRF (cookie) semble incorrect, veuillez réessayer.", | |||||
"de": "Dein CSRF Token (Cookie) scheint inkorrekt, versuchen Sie es erneut." | |||||
}, | |||||
"error_csrf_token": { | |||||
"en": "Your CSRF token seems incorrect, please retry.", | |||||
"fr": "Votre token CSRF semble incorrect, veuillez réessayer.", | |||||
"de": "Ihr CSRF Token scheint nicht korrekt, versuchen Sie es erneut. " | |||||
}, | |||||
"error_dirtyhacker": { | |||||
"en": "Attempt to access an unauthorized resource.", | |||||
"fr": "Tentative d'accès à une ressource non autorisée.", | |||||
"de": "Zugangs-Versuch einer unauthorisierten Quelle." | |||||
}, | |||||
"error_tplrender": { | |||||
"en": "Template rendering failed.", | |||||
"fr": "Le rendu du template a échoué.", | |||||
"de": "Template rendering hat nicht funktioniert." | |||||
}, | |||||
"error_tplrender_resp": { | |||||
"en": "Sending response failed.", | |||||
"fr": "L'envoi de la réponse a échoué.", | |||||
"de": "Senden der Antwort hat nicht funktioniert." | |||||
} | |||||
} |
@ -1,520 +0,0 @@ | |||||
{ | |||||
"lang_code": { | |||||
"en": "en", | |||||
"fr": "fr", | |||||
"de": "de" | |||||
}, | |||||
"lang_full": { | |||||
"en": "English", | |||||
"fr": "Français", | |||||
"de": "Deutsch" | |||||
}, | |||||
"meta_description": { | |||||
"en": "foorms : create forms for free, without registration while protecting your privacy", | |||||
"fr": "foorms : créez des formulaires ou questionnaires gratuitement, sans inscription et dans le respect de votre vie privée", | |||||
"de": "foorms: erstellen Sie gratis Umfragen, ohne Registrierung und unter Wahrung Ihrer Privatssphäre" | |||||
}, | |||||
"impressum_donations": { | |||||
"en": "Donations", | |||||
"fr": "Dons", | |||||
"de": "Spenden" | |||||
}, | |||||
"impressum_privacy": { | |||||
"en": "Privacy", | |||||
"fr": "Protection des données", | |||||
"de": "Datenschutz" | |||||
}, | |||||
"index_title": { | |||||
"en": "foorms", | |||||
"fr": "foorms", | |||||
"de": "foorms" | |||||
}, | |||||
"index_title2": { | |||||
"en": "What is foorms?", | |||||
"fr": "Qu'est-ce que c'est foorms?", | |||||
"de": "Was ist foorms?" | |||||
}, | |||||
"index_title3": { | |||||
"en": "How does foorms work?", | |||||
"fr": "Comme foorms functionne?", | |||||
"de": "Wie funktioniert foorms?" | |||||
}, | |||||
"index_description": { | |||||
"en": "Create forms fast and simple - without registration,", | |||||
"fr": "Créez des questionnaires en facon simple et vite - sans inscription,", | |||||
"de": "Erstellen Sie schnell und einfach Umfragen - ohne Registrierung," | |||||
}, | |||||
"index_description2": { | |||||
"en": "advertisement, tracking and saving of metadata.", | |||||
"fr": "publicité, tracking et sauvegarde des métadonnées.", | |||||
"de": "Werbung, Tracking und Speicherung von Metadaten." | |||||
}, | |||||
"index_beta_tag": { | |||||
"en": "BETA", | |||||
"fr": "BETA", | |||||
"de": "BETA" | |||||
}, | |||||
"index_nojs": { | |||||
"en": "Please enable JavaScript in your browser!", | |||||
"fr": "Veuillez activer JavaScript dans votre navigateur !", | |||||
"de": "Bitte aktivieren Sie JavaScript in ihrem Browser!" | |||||
}, | |||||
"index_createform_button": { | |||||
"en": "Create a form", | |||||
"fr": "Créer un formulaire", | |||||
"de": "Umfrage erstellen" | |||||
}, | |||||
"index_continueform_button": { | |||||
"en": "Access your forms", | |||||
"fr": "Accéder à vos formulaires", | |||||
"de": "Zu deinen Umfragen" | |||||
}, | |||||
"index_beta_banner_title": { | |||||
"en": "Warning: Service in beta.", | |||||
"fr": "Attention : Service en bêta.", | |||||
"de": "Achtung: Seite in Beta Version" | |||||
}, | |||||
"index_beta_banner_desc1": { | |||||
"en": "This service is currently under development and might behave in an unexpected way.", | |||||
"fr": "Ce service est en cours de développement et pourrait se comporter de manière inattendue.", | |||||
"de": "Diese Seite ist in Entwicklung und könnte sich unerwartet verhalten." | |||||
}, | |||||
"index_beta_banner_desc2": { | |||||
"en": "Feel free to send feedbacks on our ", | |||||
"fr": "Vous pouvez nous envoyer vos retours sur ", | |||||
"de": "Feedback gerne an " | |||||
}, | |||||
"index_beta_banner_desc_link": { | |||||
"en": "our contact page", | |||||
"fr": "notre page de contact", | |||||
"de": "unsere Kontaktseite" | |||||
}, | |||||
"index_disclaimer_title": { | |||||
"en": "Who keeps foorms running?", | |||||
"fr": "Qui a organisé foorms?", | |||||
"de": "Wer betreibt foorms?" | |||||
}, | |||||
"index_disclaimer1": { | |||||
"en": "This service is maintained for you from ", | |||||
"fr": "Ce service vous est fourni gratuitement de ", | |||||
"de": "Diese Seite wird von " | |||||
}, | |||||
"index_disclaimer2": { | |||||
"en": " for free.", | |||||
"fr": " gratuitement.", | |||||
"de": " für Sie kostenlos angeboten" | |||||
}, | |||||
"index_disclaimer2_link_org": { | |||||
"en": " Digitalcourage e.V. ", | |||||
"fr": " Digitalcourage e.V. ", | |||||
"de": " Digitalcourage e.V. " | |||||
}, | |||||
"index_disclaimer2_but": { | |||||
"en": " But you have the possibility to ", | |||||
"fr": " Mais vous avez la possibilité de ", | |||||
"de": " Aber Sie können gern " | |||||
}, | |||||
"index_disclaimer2_link_don": { | |||||
"en": "donate.", | |||||
"fr": "faire une donation.", | |||||
"de": "spenden." | |||||
}, | |||||
"index_disclaimer3": { | |||||
"en": "Subscribe to the ", | |||||
"fr": "Inscrivez-vous à notre ", | |||||
"de": "Abonnieren Sie den " | |||||
}, | |||||
"index_disclaimer3_link": { | |||||
"en": "newsletter, ", | |||||
"fr": "newsletter, ", | |||||
"de": "Newsletter, " | |||||
}, | |||||
"index_disclaimer4": { | |||||
"en": " to stay informed about our work!", | |||||
"fr": " pour rester informé de notre travail!", | |||||
"de": " um über unsere Arbeit informiert zu bleiben!" | |||||
}, | |||||
"index_panel1_title": { | |||||
"en": "How does foorms work?", | |||||
"fr": "Comme foorms functionne?", | |||||
"de": "Wie funktioniert foorms?" | |||||
}, | |||||
"index_panel1_desc1": { | |||||
"en": "Are you searching for a privacy-friendly alternative to Google Forms while keeping its ease of use?", | |||||
"fr": "Cherchez-vous une alternative éthique à Google Forms qui reste simple d'utilisation ?", | |||||
"de": "Suchen Sie eine ethisch sinnvolle Alternative zu Google Forms, welche gleichzeitig einfach in der Bedienung ist?" | |||||
}, | |||||
"index_panel1_desc2": { | |||||
"en": "You've just found it.", | |||||
"fr": "Vous venez de la trouver.", | |||||
"de": "Sie haben sie gefunden." | |||||
}, | |||||
"index_panel2_title": { | |||||
"en": "Choose and order your fields", | |||||
"fr": "Choisissez et ordonnez vos champs", | |||||
"de": "Wählen und Ordnen Sie ihre Felder" | |||||
}, | |||||
"index_panel2_desc1": { | |||||
"en": "The software currently supports seven field types.", | |||||
"fr": "Pour le moment, le logiciel supporte sept types de champs.", | |||||
"de": "Im Moment unterstützt die Software sieben Typen von Feldern." | |||||
}, | |||||
"index_panel2_desc2": { | |||||
"en": "New field types are ", | |||||
"fr": "De nouveaux types de champs sont ", | |||||
"de": "Neue Typen von Feldern sind " | |||||
}, | |||||
"index_panel2_desc2_link": { | |||||
"en": "currently in the works", | |||||
"fr": "en cours d'élaboration", | |||||
"de": "momentan in Bearbeitung" | |||||
}, | |||||
"index_panel3_title": { | |||||
"en": "Analyze the answers", | |||||
"fr": "Analysez les réponses", | |||||
"de": "Analysieren Sie die Antworten" | |||||
}, | |||||
"index_panel3_desc1": { | |||||
"en": "See detailed graphs of the answers to your form.", | |||||
"fr": "Visualisez les réponses à votre formulaire avec un graphique.", | |||||
"de": "Visualisieren Sie die Antworten Ihrer Umfrage graphisch." | |||||
}, | |||||
"index_panel4_title": { | |||||
"en": "Export the answers", | |||||
"fr": "Exportez les réponses", | |||||
"de": "Export der Antworten" | |||||
}, | |||||
"index_panel4_desc1": { | |||||
"en": "Export the raw data of your form in CSV format to integrate the answers in other software (e.g. LibreOffice Calc or Microsoft Excel).", | |||||
"fr": "Exportez les données brutes de votre formulaire en format CSV pour intégrer les réponses dans d'autres logiciels (ex. LibreOffice Calc ou Microsoft Excel).", | |||||
"de": "Exportieren Sie die Rohdaten Ihrer Umfrage im CSV Format um die Antworten in anderer Software zu integrieren( z.B. LibreOffice Calc)" | |||||
}, | |||||
"index_panel5_title": { | |||||
"en": "Edit your form's settings", | |||||
"fr": "Paramétrez vos formulaires", | |||||
"de": "Einstellungen Ihrer Umfragen" | |||||
}, | |||||
"index_panel5_desc1": { | |||||
"en": "Use the share link to send your form to other people.", | |||||
"fr": "Utilisez le lien de partage pour envoyer votre formulaire à d'autres personnes.", | |||||
"de": "Nutzen Sie den Teilen Link um Ihre Umfrage anderen Menschen zu schicken." | |||||
}, | |||||
"index_panel5_desc2": { | |||||
"en": "You can also define an expiration date for your form.", | |||||
"fr": "Vous pouvez également définir une date d'expiration pour votre formulaire.", | |||||
"de": "Sie können auch ein Ablaufdatum für ihre Umfrage festsetzen." | |||||
}, | |||||
"index_panel6_title": { | |||||
"en": "All your forms in one place", | |||||
"fr": "Tous vos formulaires au même endroit", | |||||
"de": "Alle Ihre Umfragen an einem Ort" | |||||
}, | |||||
"index_panel6_desc1": { | |||||
"en": "Find all your forms in the same panel.", | |||||
"fr": "Retrouvez tous vos formulaires sur un même panel.", | |||||
"de": "Finde alle deine Umfragen in einem Panel." | |||||
}, | |||||
"index_bottom_docs": { | |||||
"en": "Documentation", | |||||
"fr": "Documentation", | |||||
"de": "Dokumentation" | |||||
}, | |||||
"index_bottom_source": { | |||||
"en": "Source code", | |||||
"fr": "Code source", | |||||
"de": "Quellcode" | |||||
}, | |||||
"index_bottom_lic": { | |||||
"en": "License", | |||||
"fr": "Licence", | |||||
"de": "Lizenz" | |||||
}, | |||||
"index_credits_title": { | |||||
"en": "Credits", | |||||
"fr": "Crédits", | |||||
"de": "Credits" | |||||
}, | |||||
"index_credits_desc1": { | |||||
"en": "The Nextcloud software suite and the Nextcloud Forms application has been developed by ", | |||||
"fr": "La suite logicielle Nextcloud et l'application Nextcloud Forms a été développée par ", | |||||
"de": "Die Nextcloud Software Sammlung und die Nextcloud Forms Applikation wurden entwickelt von " | |||||
}, | |||||
"index_credits_desc1_link": { | |||||
"en": "the Nextcloud team", | |||||
"fr": "l'équipe Nextcloud", | |||||
"de": "dem Nextcloud Team" | |||||
}, | |||||
"index_credits_desc1_a": { | |||||
"en": " and its contributors.", | |||||
"fr": " et ses contributeur·ices.", | |||||
"de": " und ihren Kontributor*innen" | |||||
}, | |||||
"index_credits_desc2": { | |||||
"en": "The Simple Nextcloud Forms software, which simplifies the form creation process, has been developed by ", | |||||
"fr": "Le logiciel Simple Nextcloud Forms, qui simplifie la création de formulaires, a été développé par ", | |||||
"de": "Die Simple Nextcloud Forms Software, welche die Erstellung von Umfragen erleichtert, wurde entwickelt von " | |||||
}, | |||||
"index_credits_desc2_for": { | |||||
"en": " for ", | |||||
"fr": " pour ", | |||||
"de": " für " | |||||
}, | |||||
"index_credits_desc2_org": { | |||||
"en": "the 42l association", | |||||
"fr": "l'association 42l", | |||||
"de": "die 42l Assoziation" | |||||
}, | |||||
"index_credits_desc3": { | |||||
"en": "source code", | |||||
"fr": "code source", | |||||
"de": "Quellcode" | |||||
}, | |||||
"link_title": { | |||||
"en": "Link created", | |||||
"fr": "Lien créé", | |||||
"de": "Link erstellt" | |||||
}, | |||||
"link_desc1_1": { | |||||
"en": "Here's an <b>administration link</b>, which will allow you to access all", | |||||
"fr": "Voici un <b>lien d'administration</b>, qui vous permettra d'accéder à tous", | |||||
"de": "Hier ist ein <b>Administrations Link</b>, der es ermöglicht wieder zu" | |||||
}, | |||||
"link_desc1_2": { | |||||
"en": "your forms and check your answers.", | |||||
"fr": "vos formulaires et de consulter vos réponses.", | |||||
"de": "ihren Umfragen zu gelangen und die Antworten einzusehen." | |||||
}, | |||||
"link_desc2_1": { | |||||
"en": "<b>Keep it</b> carefully and don't give it away", | |||||
"fr": "<b>Conservez-le</B> bien précieusement et ne le donnez pas", | |||||
"de": "<b>Bewahren Sie diese</b> gut und sicher auf" | |||||
}, | |||||
"link_desc2_2": { | |||||
"en": "(it'd be the same as giving out your password!).", | |||||
"fr": "(cela reviendrait à donner un mot de passe!).", | |||||
"de": "(Die Weitergabe entspricht der Weitergabe eines Passwortes!)." | |||||
}, | |||||
"link_desc3_1": { | |||||
"en": "Once your link copied, click on the button below to", | |||||
"fr": "Une fois votre lien copié, cliquez sur le bouton ci-dessous pour", | |||||
"de": "Ist der Link kopiert, drücken sie auf den unteren Button um" | |||||
}, | |||||
"link_desc3_2": { | |||||
"en": "start editing your forms.", | |||||
"fr": "commencer à éditer vos formulaires.", | |||||
"de": "Umfragen zu erstellen oder zu bearbeiten." | |||||
}, | |||||
"link_access_btn": { | |||||
"en": "to foorms", | |||||
"fr": "Accéder foorms", | |||||
"de": "zu foorms" | |||||
}, | |||||
"link_note": { | |||||
"en": "Note: If you don't use your administration link during more than ", | |||||
"fr": "Note : Si vous n'utilisez pas votre lien d'administration pendant plus de ", | |||||
"de": "Notiz: Wenn Sie den Administrations Link für länger als " | |||||
}, | |||||
"link_note2": { | |||||
"en": " days, your forms will be automatically deleted.", | |||||
"fr": " jours, vos formulaires seront automatiquement supprimés.", | |||||
"de": " Tage nicht benutzen, werden ihre Umfragen automatisch gelöscht." | |||||
}, | |||||
"link_copy": { | |||||
"en": "Copy link", | |||||
"fr": "Copier le lien", | |||||
"de": "Link kopieren" | |||||
}, | |||||
"link_copied": { | |||||
"en": "Link copied!", | |||||
"fr": "Lien copié !", | |||||
"de": "Link kopiert !" | |||||
}, | |||||
"link_mail": { | |||||
"en": "send Link", | |||||
"fr": "envoyer lien", | |||||
"de": "Link senden" | |||||
}, | |||||
"error_title": { | |||||
"en": "Oops!...", | |||||
"fr": "Oups !...", | |||||
"de": "Ups !..." | |||||
}, | |||||
"error_description": { | |||||
"en": "The application encountered a problem:", | |||||
"fr": "L'application a rencontré un problème :", | |||||
"de": "Die Anwendung hat ein Problem festgestellt:" | |||||
}, | |||||
"error_back": { | |||||
"en": "Back to the main page", | |||||
"fr": "Retour à la page principale", | |||||
"de": "Zurück zur Hauptseite" | |||||
}, | |||||
"error_note1": { | |||||
"en": "We are (probably) aware of this bug, but feel free to contact us if you need assistance.", | |||||
"fr": "Nous sommes (probablement) au courant, mais n'hésitez pas à nous contacter si vous avez besoin d'aide.", | |||||
"de": "Wir sind uns (wahrscheinlich) bewusst, was diesen Fehler angeht. Fühlen sie sich frei uns zu kontaktieren, wenn Sie Hilfe benötigen." | |||||
}, | |||||
"error_note2": { | |||||
"en": "Sorry for the inconvenience.", | |||||
"fr": "Désolés pour les désagréments occasionnés.", | |||||
"de": "Entschuldigen Sie die Störung." | |||||
}, | |||||
"error_forward_req": { | |||||
"en": "Error while connecting to the Nextcloud instance.", | |||||
"fr": "Erreur lors de la connexion à l'instance Nextcloud.", | |||||
"de": "Fehler beim Verbinden zur Nextcloud Instanz." | |||||
}, | |||||
"error_forward_resp": { | |||||
"en": "Error while reading Nextcloud instance's response.", | |||||
"fr": "Erreur lors de la lecture de la réponse de l'instance Nextcloud.", | |||||
"de": "Feher beim Lesen der Antwort der Nextcloud Instanz." | |||||
}, | |||||
"error_forward_isanon": { | |||||
"en": "Couldn't set the form's isAnonymous value.", | |||||
"fr": "Échec lors de la définition de la valeur isAnonymous du formulaire.", | |||||
"de": "Es ist nicht möglich, die isAnonymous Wert des Formulars zu setzen." | |||||
}, | |||||
"error_forward_clientresp_newform": { | |||||
"en": "Failed to send the response body (new form).", | |||||
"fr": "Échec lors de l'envoi du corps de la réponse (nouveau formulaire).", | |||||
"de": "Fehler beim senden des Response body (neues Formular)." | |||||
}, | |||||
"error_forward_clientresp_std": { | |||||
"en": "Failed to send the response body.", | |||||
"fr": "Échec lors de l'envoi du corps de la réponse.", | |||||
"de": "Fehler beim Senden des Response Body." | |||||
}, | |||||
"error_forwardlogin_db": { | |||||
"en": "Couldn't connect to the local database.", | |||||
"fr": "Échec lors de la connexion à la base de données locale.", | |||||
"de": "Fehler beim verbinden zur lokalen Datenbank." | |||||
}, | |||||
"error_forwardlogin_db_get": { | |||||
"en": "Error during information retrieval from the local database.", | |||||
"fr": "Erreur lors de la récupération des informations dans la base de données locale.", | |||||
"de": "Fehler beim Empfangen von Daten der lokalen Datenbank." | |||||
}, | |||||
"error_forwardlogin_notfound": { | |||||
"en": "The specified token doesn't exist in local database.", | |||||
"fr": "Le token spécifié n'existe pas dans la base de données locale.", | |||||
"de": "Der gesetzte Token existiert nicht in der lokalen Datenbank." | |||||
}, | |||||
"error_login_get": { | |||||
"en": "The account creation request (GET) to Nextcloud has failed.", | |||||
"fr": "La requête de création de compte (GET) vers l'instance Nextcloud a échoué.", | |||||
"de": "Das Account Erstellungs Request (GET) zu Nextcloud hat nicht funktioniert." | |||||
}, | |||||
"error_login_get_body": { | |||||
"en": "Reading response from the account creation request to Nextcloud has failed.", | |||||
"fr": "La lecture de la réponse à la requête de création de compte vers l'instance Nextcloud a échoué.", | |||||
"de": "Das Lesen der Response vom Account Erstellungs Request zu Nextcloud hat nicht funktioniert." | |||||
}, | |||||
"error_login_post": { | |||||
"en": "The account creation request (POST) to Nextcloud has failed.", | |||||
"fr": "La requête de création de compte (POST) vers l'instance Nextcloud a échoué.", | |||||
"de": "Der Account Erstellungs Request (POST) zu Nextcloud hat nicht funktioniert. " | |||||
}, | |||||
"error_login_redir": { | |||||
"en": "Redirection to Nextcloud account failed.", | |||||
"fr": "La redirection vers le compte Nextcloud a échoué.", | |||||
"de": "Die Weiterleitung zum Nextcloud account hat nicht funktioniert." | |||||
}, | |||||
"error_createaccount_post": { | |||||
"en": "Account creation: connection to the Nextcloud API failed.", | |||||
"fr": "Création de compte : la connexion à l'API Nextcloud a échoué.", | |||||
"de": "Account Erstellung: Verbindung zur Nextcloud API hat nicht funktioniert." | |||||
}, | |||||
"error_createaccount_post_body": { | |||||
"en": "Account creation: reading the answer from the Nextcloud API failed.", | |||||
"fr": "Création de compte : le traitement de la réponse de l'API Nextcloud a échoué.", | |||||
"de": "Account Erstellung : das Lesen der Antwort der Nextcloud API hat nicht funktioniert." | |||||
}, | |||||
"error_createaccount_status": { | |||||
"en": "The Nextcloud instance responded with an unexpected status code.", | |||||
"fr": "L'instance Nextcloud a répondu avec un code de statut inattendu.", | |||||
"de": "Die Nextcloud Instanz hat mit einem unexpected status code geantwortet." | |||||
}, | |||||
"error_createaccount_ncstatus": { | |||||
"en": "The Nextcloud API responded with an unexpected status code.", | |||||
"fr": "L'API Nextcloud a répondu avec un code de statut inattendu.", | |||||
"de": "Die Nextcloud API hat mit unexpected ncstatus geantwortet." | |||||
}, | |||||
"error_createaccount_ncstatus_parse": { | |||||
"en": "Error parsing Nextcloud API's status code.", | |||||
"fr": "Erreur lors de la lecture du code de statut de l'API Nextcloud.", | |||||
"de": "Fehler beim Lesen des Nextcloud API status codes." | |||||
}, | |||||
"error_forwardregister_pool": { | |||||
"en": "Error while connecting to the local database.", | |||||
"fr": "Erreur lors de la connexion à la base de données locale.", | |||||
"de": "Fehler beim Verbinden zu der lokalen Datenbank." | |||||
}, | |||||
"error_forwardregister_db": { | |||||
"en": "Failed adding the Nextcloud account in the local database.", | |||||
"fr": "L'ajout du compte Nextcloud dans la base de données locale a échoué.", | |||||
"de": "Fehlre beim Hinzufügen des Nextcloud Accounts zur lokalen Datenbank." | |||||
}, | |||||
"error_forwardregister_tokenparse": { | |||||
"en": "Failed parsing the admin token.", | |||||
"fr": "Échec lors de la lecture du token administrateur.", | |||||
"de": "Fehler beim Parsen des Admin Tokens." | |||||
}, | |||||
"error_login_cookiepair": { | |||||
"en": "Couldn't read cookies.", | |||||
"fr": "Échec lors de la lecture de cookies.", | |||||
"de": "Fehler beim Lesen der Cookies" | |||||
}, | |||||
"error_login_regex": { | |||||
"en": "Couldn't read the CSRF token.", | |||||
"fr": "Échec lors de la lecture du token CSRF.", | |||||
"de": "Fehler beim Lesen des CSRF Tokens." | |||||
}, | |||||
"error_login_setcookie": { | |||||
"en": "Error during cookies transfer.", | |||||
"fr": "Erreur lors du transfert de cookies.", | |||||
"de": "Feheler beim Transfer der Cookies." | |||||
}, | |||||
"error_form_insert": { | |||||
"en": "The local database couldn't be reached.", | |||||
"fr": "Échec de la connexion avec la base de données locale.", | |||||
"de": "Die lokale Datenbank ist nicht erreichbar." | |||||
}, | |||||
"error_createaccount": { | |||||
"en": "The Nextcloud API returned an unexpected result.", | |||||
"fr": "L'API de Nextcloud a retourné un résultat inattendu.", | |||||
"de": "Die Nextcloud API hat ein unerwartetes Resultat zurückgesendet." | |||||
}, | |||||
"error_redirect": { | |||||
"en": "Failed to redirect.", | |||||
"fr": "La redirection a échoué.", | |||||
"de": "Weiterleitung (Redirect) hat nicht funktioniert." | |||||
}, | |||||
"error_csrf_cookie": { | |||||
"en": "Your CSRF token (cookie) seems incorrect, please retry.", | |||||
"fr": "Votre token CSRF (cookie) semble incorrect, veuillez réessayer.", | |||||
"de": "Dein CSRF Token (Cookie) scheint inkorrekt, versuchen Sie es erneut." | |||||
}, | |||||
"error_csrf_token": { | |||||
"en": "Your CSRF token seems incorrect, please retry.", | |||||
"fr": "Votre token CSRF semble incorrect, veuillez réessayer.", | |||||
"de": "Ihr CSRF Token scheint nicht korrekt, versuchen Sie es erneut. " | |||||
}, | |||||
"error_dirtyhacker": { | |||||
"en": "Attempt to access an unauthorized resource.", | |||||
"fr": "Tentative d'accès à une ressource non autorisée.", | |||||
"de": "Zugangs-Versuch einer unauthorisierten Quelle." | |||||
}, | |||||
"error_tplrender": { | |||||
"en": "Template rendering failed.", | |||||
"fr": "Le rendu du template a échoué.", | |||||
"de": "Template rendering hat nicht funktioniert." | |||||
}, | |||||
"error_tplrender_resp": { | |||||
"en": "Sending response failed.", | |||||
"fr": "L'envoi de la réponse a échoué.", | |||||
"de": "Senden der Antwort hat nicht funktioniert." | |||||
} | |||||
} | |||||
@ -1,305 +0,0 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="it"> | |||||
<head> | |||||
<meta charset="utf-8"> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |||||
<title>{{ "link_title"|tr(lang) }} – {{ "index_title"|tr(lang) }}</title> | |||||
<meta name="robots" content="noindex" /> | |||||
<meta name="description" content="{{ "meta_description"|tr(lang) }}" /> | |||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |||||
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon.svg" /> | |||||
<link rel="stylesheet" href="/assets/index.css?v=1.0" /> | |||||
<link rel="stylesheet" href="/assets/cloud.css?v=1.0" /> | |||||
<link rel="stylesheet" href="/assets/digitalcourage.css" /> | |||||
<link rel="stylesheet" href="/assets/bootstrap.min.css" /> | |||||
<script type="text/javascript"> | |||||
window.onload = function () { | |||||
// show link copy button if javascript is enabled | |||||
document.getElementById("script-copy").style.display = "unset"; | |||||
let btn = document.getElementById("script-copy-btn"); | |||||
btn.style.cursor = "pointer"; | |||||
let csrf_token = "{{ csrf_token }}"; | |||||
let lang = "{{ lang }}"; | |||||
document.getElementById('langs').value=lang; | |||||
document.getElementById('new_link_button').addEventListener('click', function () { | |||||
new_link(csrf_token); | |||||
}); | |||||
btn.addEventListener('click', function() { | |||||
var copyText = document.getElementById("link"); | |||||
/* Select the text field */ | |||||
copyText.select(); | |||||
copyText.setSelectionRange(0, 99999); | |||||
document.execCommand("copy"); | |||||
btn.innerHTML = '{{ "link_copied"|tr(lang) }}'; | |||||
}); | |||||
function ValidateEmail(mail) | |||||
{ | |||||
if (/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(mail)) | |||||
{ | |||||
return (true) | |||||
} | |||||
alert("Die eingegebene Mail Adresse ist ungültig. Sie können sich auch anmelden, ohne den Token zugeschickt zu bekommen.") | |||||
return (false) | |||||
} | |||||
document.getElementById("email-register").style.display = "unset"; | |||||
let btn2 = document.getElementById("email-register-btn"); | |||||
btn2.style.cursor = "pointer"; | |||||
btn2.addEventListener('click', function() { | |||||
var email = document.getElementById("email").value; | |||||
var adtok = document.getElementById("link").value; | |||||
console.log(email); | |||||
var validation = ValidateEmail(email); | |||||
/* var emailjsonstring = JSON.stringify(JSON.parse(document.getElementById('email'))); */ | |||||
if (validation == true) | |||||
{ | |||||
var xhr1=new XMLHttpRequest(); | |||||
xhr1.open("POST",'link/email', true); | |||||
xhr1.send(email + ',' + adtok + '\n'); | |||||
document.getElementById("email").value = "Die Email ist auf dem Weg"; | |||||
} | |||||
}); | |||||
} | |||||
function new_link(csrf) { | |||||
var sel = document.getElementById('langs'); | |||||
let opt = getSelectedOption(sel); | |||||
let lang = opt.value; | |||||
document.getElementById('langs').value = lang; | |||||
document.getElementById("link_lang").value = lang; | |||||
document.getElementById("csrf_token").value = csrf; | |||||
document.getElementById('new_link').submit(); | |||||
document.getElementById('new_link_button').classList.add("hidden"); | |||||
document.getElementById('loading_ring').classList.remove("hidden"); | |||||
} | |||||
function getSelectedOption(sel) { | |||||
var opt; | |||||
for ( var i = 0, len = sel.options.length; i < len; i++ ) { | |||||
opt = sel.options[i]; | |||||
if ( opt.selected === true ) { | |||||
break; | |||||
} | |||||
} | |||||
return opt; | |||||
} | |||||
</script> | |||||
<style> | |||||
.break { | |||||
flex-basis: 100%; | |||||
height: 0; | |||||
} | |||||
.div_120 { | |||||
flex-basis: 100%; | |||||
height: 120px; | |||||
} | |||||
.div_45 { | |||||
flex-basis: 100%; | |||||
height: 45px; | |||||
} | |||||
.div_35 { | |||||
flex-basis: 100%; | |||||
height: 35px; | |||||
} | |||||
.div_25 { | |||||
flex-basis: 100%; | |||||
height: 25px; | |||||
} | |||||
.div_10 { | |||||
flex-basis: 100%; | |||||
height: 10px; | |||||
} | |||||
.grid-container { | |||||
display: grid; | |||||
grid-template-columns: auto auto; | |||||
width: 725px; | |||||
grid-gap: 25px; | |||||
} | |||||
.grid-container2 { | |||||
display: grid; | |||||
grid-template-columns: auto auto auto; | |||||
width: 532px; | |||||
grid-gap: 20px; | |||||
} | |||||
@media only screen and (max-width: 768px) { | |||||
/* For mobile phones: */ | |||||
[class*="grid-container"] { | |||||
grid-template-columns: auto; | |||||
max-width: 100%; | |||||
justify-content: center; | |||||
} | |||||
[class*="grid-container2"] { | |||||
grid-template-columns: auto; | |||||
max-width: 100%; | |||||
justify-content: center; | |||||
} | |||||
[class*="item2"] { | |||||
grid-template-columns: auto; | |||||
max-width: 100%; | |||||
align-items: center; | |||||
} | |||||
} | |||||
.center { | |||||
display: flex; | |||||
justify-content: center; | |||||
align-items: center; | |||||
} | |||||
.item1 { | |||||
width: 350px; | |||||
height: 200px; | |||||
display: flex; | |||||
justify-content: center; | |||||
align-items: center; | |||||
} | |||||
.item2 { | |||||
width: 350px; | |||||
height: 200px; | |||||
#display: flex; | |||||
#justify-content: center; | |||||
#align-items: center; | |||||
} | |||||
.h2 { | |||||
font-size: 30pt; | |||||
} | |||||
p { | |||||
font-size: 16pt; | |||||
} | |||||
.downDC { | |||||
height: 90px; | |||||
padding: 10px; | |||||
} | |||||
.c-img-shadow { | |||||
height: 200px; | |||||
max-width: 100%; | |||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||||
border-radius: 2px; | |||||
} | |||||
</style> | |||||
<body> | |||||
<div class="container ombre"> | |||||
<header role="banner" class="clearfix"> | |||||
<form id="new_link" method="post" action="/link" class="hidden-print"> | |||||
<div class="input-group input-group-sm pull-right col-xs-12 col-sm-2 langs" style="margin-right: 8px"> | |||||
<select id="langs" name="lang" class="form-control" title="Select language" > | |||||
<option lang="fr" value="fr">Français</option> | |||||
<option lang="en" selected value="en">English</option> | |||||
<option lang="oc" value="oc">Occitan</option> | |||||
<option lang="es" value="es">Español</option> | |||||
<option lang="de" value="de">Deutsch</option> | |||||
<option lang="nl" value="nl">Dutch</option> | |||||
<option lang="it" value="it">Italiano</option> | |||||
<option lang="br" value="br">Brezhoneg</option> | |||||
</select> | |||||
<input id="csrf_token" name="csrf_token" type="text" class="hidden"> | |||||
<input id="link_lang" name="link_lang" type="text" class="hidden"> | |||||
<span class="input-group-btn"> | |||||
<a id="new_link_button" class="btn btn-default btn-sm language_button" title="Change language">OK</a> | |||||
</span> | |||||
</div> | |||||
</form> | |||||
<a href="https://foorms.digitalcourage.de/" title="Home - foorms" style="margin-left: 8px" > | |||||
<img src="/assets/foorms_logo_beta.svg" alt="foorms" class="" height="58vh" /> | |||||
</a> | |||||
<h2 class="lead col-xs-12"></h2> <div class="trait col-xs-12" role="presentation"></div> | |||||
</header> | |||||
<main role="main"> | |||||
<center> | |||||
<div class="has-text-centered"> | |||||
<br /> | |||||
<h2>{{ "link_title"|tr(lang) }}</h2> | |||||
<div class="div_25"> </div> | |||||
<p>{{ "link_desc1_1"|tr(lang)|safe }}</p> | |||||
<div class="break"> </div> | |||||
<p>{{ "link_desc1_2"|tr(lang)|safe }}</p> | |||||
<div class="div_25"> </div> | |||||
<div class="c-flex c-jumbo"> | |||||
<input id="link" class="ncstyle-input" type="text" style='font-size: 16px; text-align:center' size="80" readonly value="{{ config.sncf_url }}/admin/{{ admin_token }}" /> | |||||
</div> | |||||
<div class="div_35"> </div> | |||||
<div id="script-copy"> | |||||
<div class="c-flex"> | |||||
<a id="script-copy-btn" class="ncstyle-button margin-bottom">{{ "link_copy"|tr(lang) }}</a> | |||||
</div> | |||||
</div> | |||||
<div class="div_120"> </div> | |||||
<p>{{ "link_desc2_1"|tr(lang)|safe }}</p> | |||||
<div class="break"> </div> | |||||
<p>{{ "link_desc2_2"|tr(lang)|safe }}</p> | |||||
<div class="div_25"> </div> | |||||
<div class="c-flex"> | |||||
<input id="email" class="ncstyle-input" style="text-align:center;" type="text" value="Send_Password_Link@invalid" /> | |||||
</div> | |||||
<div class="div_35"> </div> | |||||
<div id="email-register"> | |||||
<div class="c-flex"> | |||||
<a id="email-register-btn" class="ncstyle-button margin-bottom">{{ "link_mail"|tr(lang) }}</a> | |||||
</div> | |||||
<div class="div_120"> </div> | |||||
</div> | |||||
<p>{{ "link_desc3_1"|tr(lang) }}</p> | |||||
<div class="break"></div> | |||||
<p>{{ "link_desc3_2"|tr(lang) }}</p> | |||||
<div class=div_35></div> | |||||
<div class="c-flex"> | |||||
<a id="forms-btn" class="ncstyle-button margin-bottom" href="{{ config.sncf_url }}/admin/{{ admin_token }}">{{ "link_access_btn"|tr(lang) }}</a> | |||||
</div> | |||||
</div> | |||||
<div class="div_120"></div> | |||||
</center> | |||||
<center> | |||||
<div class="c-blue grid-container2"> | |||||
<a href="https://42l.fr/Rapport-technique" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_docs"|tr(lang) }}</a> | |||||
<a href="https://git.42l.fr/neil/sncf" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_source"|tr(lang) }}</a> | |||||
<a href="https://git.42l.fr/neil/sncf/src/branch/root/LICENSE" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_lic"|tr(lang) }}</a> | |||||
</div> | |||||
</center> | |||||
<div class="div_10"></div> | |||||
<div class="div_10"></div> | |||||
<div class="div_10"></div> | |||||
</main> | |||||
</div> <!-- .container --> | |||||
<div class="container ombre downDC" style="display:flex; align-items:center;"> | |||||
<h2 class="lead"><a target="_blank" href="https://digitalcourage.de/">Digitalcourage</a> | <a target="_blank" href="https://digitalcourage.de/newsletter">Newsletter</a> | <a target="_blank" href="https://digitalcourage.de/spenden">{{ "impressum_donations"|tr(lang)|safe }}</a> | <a target="_blank" href="https://digitalcourage.de/en">Impressum</a> | <a target="_blank" href="https://digitalcourage.de/en">{{ "impressum_privacy"|tr(lang)|safe }}</a> </h2> | |||||
</div> | |||||
</body> | |||||
</html> | |||||
@ -1,104 +0,0 @@ | |||||
#[macro_use] | |||||
extern crate lazy_static; | |||||
#[macro_use] | |||||
extern crate serde_derive; | |||||
#[macro_use] | |||||
extern crate diesel; | |||||
#[macro_use] | |||||
extern crate diesel_migrations; | |||||
use actix_session::CookieSession; | |||||
use actix_web::cookie::SameSite; | |||||
use actix_files::Files; | |||||
use actix_web::client::Client; | |||||
use actix_web::{web, App, FromRequest, HttpServer}; | |||||
use diesel::prelude::*; | |||||
use diesel::r2d2::{self, ConnectionManager}; | |||||
use url::Url; | |||||
use crate::config::CONFIG; | |||||
use crate::config::PAYLOAD_LIMIT; | |||||
use crate::forward::*; | |||||
mod account; | |||||
mod config; | |||||
mod database; | |||||
mod errors; | |||||
mod forward; | |||||
mod sniff; | |||||
mod templates; | |||||
// default to postgres | |||||
#[cfg(feature = "default")] | |||||
type DbConn = PgConnection; | |||||
#[cfg(feature = "default")] | |||||
embed_migrations!("migrations/postgres"); | |||||
#[cfg(feature = "postgres")] | |||||
type DbConn = PgConnection; | |||||
#[cfg(feature = "postgres")] | |||||
embed_migrations!("migrations/postgres"); | |||||
#[cfg(feature = "sqlite")] | |||||
type DbConn = SqliteConnection; | |||||
#[cfg(feature = "sqlite")] | |||||
embed_migrations!("migrations/sqlite"); | |||||
#[cfg(feature = "mysql")] | |||||
type DbConn = MysqlConnection; | |||||
#[cfg(feature = "mysql")] | |||||
embed_migrations!("migrations/mysql"); | |||||
type DbPool = r2d2::Pool<ConnectionManager<DbConn>>; | |||||
#[actix_web::main] | |||||
async fn main() -> std::io::Result<()> { | |||||
/* std::env::set_var("RUST_LOG", "actix_web=debug"); | |||||
env_logger::init();*/ | |||||
println!("ta ta tala ~ SNCF init"); | |||||
println!("Checking configuration file..."); | |||||
CONFIG.check_version(); | |||||
if CONFIG.database_path.is_empty() { | |||||
println!("No database specified. Please enter a MySQL, PostgreSQL or SQLite connection string in config.toml."); | |||||
} | |||||
debug(&format!("Opening database {}", CONFIG.database_path)); | |||||
let manager = ConnectionManager::<DbConn>::new(&CONFIG.database_path); | |||||
let pool = r2d2::Pool::builder() | |||||
.build(manager) | |||||
.expect("ERROR: main: Failed to create the database pool."); | |||||
let conn = pool.get().expect("ERROR: main: DB connection failed"); | |||||
println!("Running migrations..."); | |||||
embedded_migrations::run(&*conn).expect("ERROR: main: Failed to run database migrations"); | |||||
let forward_url = | |||||
Url::parse(&CONFIG.nextcloud_url).expect("Couldn't parse the forward url from config"); | |||||
println!( | |||||
"Now listening at {}:{}", | |||||
CONFIG.listening_address, CONFIG.listening_port | |||||
); | |||||
// starting the http server | |||||
HttpServer::new(move || { | |||||
App::new() | |||||
.data(pool.clone()) | |||||
.data(Client::new()) | |||||
.data(forward_url.clone()) | |||||
.wrap( | |||||
CookieSession::signed(&[0; 32]) | |||||
.secure(true) | |||||
.same_site(SameSite::Strict) | |||||
.http_only(true) | |||||
.name("sncf_cookies") | |||||
) | |||||
/*.route("/mimolette", web::get().to(login))*/ | |||||
/*.route("/login", web::post().to(forward))*/ | |||||
/*.wrap(middleware::Compress::default())*/ | |||||
.service(Files::new("/assets/", "./templates/assets/").index_file("index.html")) | |||||
.route("/", web::get().to(index)) | |||||
.route("/link", web::post().to(forward_register)) | |||||
.route("/admin/{token}", web::get().to(forward_login)) | |||||
.default_service(web::route().to(forward)) | |||||
.data(String::configure(|cfg| cfg.limit(PAYLOAD_LIMIT))) | |||||
.app_data(actix_web::web::Bytes::configure(|cfg| { | |||||
cfg.limit(PAYLOAD_LIMIT) | |||||
})) | |||||
}) | |||||
.bind((CONFIG.listening_address.as_str(), CONFIG.listening_port))? | |||||
.system_exit() | |||||
.run() | |||||
.await | |||||
} | |||||
pub fn debug(text: &str) { | |||||
if CONFIG.debug_mode { | |||||
println!("{}", text); | |||||
} | |||||
} |
@ -1,76 +0,0 @@ | |||||
use serde_json::Value; | |||||
use std::fs::File; | |||||
use std::io::Read; | |||||
use std::io::{self, BufRead, BufReader}; | |||||
use std::path::Path; | |||||
// payload limit set to 5MiB | |||||
pub const PAYLOAD_LIMIT: usize = 10_000_000; | |||||
pub const PROXY_TIMEOUT: u64 = 15; | |||||
pub const CONFIG_FILE: &str = "./config.toml"; | |||||
pub const CONFIG_VERSION: u8 = 2; | |||||
pub const ADJ_LIST_FILE: &str = "./adj-list.txt"; | |||||
pub const NAME_LIST_FILE: &str = "./name-list.txt"; | |||||
pub const LOC_FILE: &str = "./lang.json"; | |||||
pub const USER_AGENT: &str = "Actix-web"; | |||||
lazy_static! { | |||||
pub static ref CONFIG: Config = Config::init(); | |||||
pub static ref ADJ_LIST: Vec<String> = | |||||
lines_from_file(ADJ_LIST_FILE).expect("Failed to load adjectives list"); | |||||
pub static ref NAME_LIST: Vec<String> = | |||||
lines_from_file(NAME_LIST_FILE).expect("Failed to load names list"); | |||||
pub static ref LOC: Value = init_lang(); | |||||
} | |||||
// Open LOC_FILE and store it in memory (LOC) | |||||
fn init_lang() -> Value { | |||||
let mut file = File::open(LOC_FILE).expect("init_lang: Can't open translations file"); | |||||
let mut data = String::new(); | |||||
file.read_to_string(&mut data) | |||||
.expect("init_lang: Can't read translations file"); | |||||
serde_json::from_str(&data).expect("init_lang(): Can't parse translations file") | |||||
} | |||||
// Open a file from its path | |||||
fn lines_from_file(filename: impl AsRef<Path>) -> io::Result<Vec<String>> { | |||||
BufReader::new(File::open(filename)?).lines().collect() | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct Config { | |||||
pub listening_address: String, | |||||
pub listening_port: u16, | |||||
pub website_url: String, | |||||
pub debug_mode: bool, | |||||
pub config_version: u8, | |||||
} | |||||
// totally not copypasted from rs-short | |||||
impl Config { | |||||
// open and parse CONFIG_FILE | |||||
pub fn init() -> Self { | |||||
let mut conffile = File::open(CONFIG_FILE).expect( | |||||
r#"Config file config.toml not found. | |||||
Please create it using config.toml.sample."#, | |||||
); | |||||
let mut confstr = String::new(); | |||||
conffile | |||||
.read_to_string(&mut confstr) | |||||
.expect("Couldn't read config to string"); | |||||
toml::from_str(&confstr).expect("Couldn't deserialize the config. Please update at https://git.42l.fr/neil/sncf/wiki/Upgrade-from-a-previous-version --- Error") | |||||
} | |||||
// if config.config_version doesn't match the hardcoded version, | |||||
// ask the admin to manually upgrade its config file | |||||
pub fn check_version(&self) { | |||||
if self.config_version != CONFIG_VERSION { | |||||
eprintln!("Your configuration file is obsolete!\nPlease update it following the instructions in https://git.42l.fr/neil/sncf/wiki/Upgrade-from-a-previous-version and update its version to {}.", CONFIG_VERSION); | |||||
panic!(); | |||||
} | |||||
} | |||||
} | |||||
@ -1,58 +0,0 @@ | |||||
use crate::templates::TplError; | |||||
use actix_web::dev::HttpResponseBuilder; | |||||
use actix_web::{error, http::header, http::StatusCode, HttpResponse}; | |||||
use askama::Template; | |||||
use std::fmt; | |||||
pub fn crash(lang: String, error_msg: &'static str) -> TrainCrash { | |||||
TrainCrash { lang, error_msg } | |||||
} | |||||
#[derive(Debug)] | |||||
pub struct TrainCrash { | |||||
pub error_msg: &'static str, | |||||
pub lang: String, | |||||
} | |||||
// gonna avoid using failure crate | |||||
// by implementing display | |||||
impl fmt::Display for TrainCrash { | |||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |||||
write!(f, "{:?}", self.error_msg) | |||||
} | |||||
} | |||||
impl error::ResponseError for TrainCrash { | |||||
fn error_response(&self) -> HttpResponse { | |||||
eprintln!("Error reached: {}", self.error_msg); | |||||
HttpResponseBuilder::new(self.status_code()) | |||||
.set_header(header::CONTENT_TYPE, "text/html; charset=utf-8") | |||||
.body( | |||||
TplError { | |||||
lang: &self.lang, | |||||
error_msg: self.error_msg, | |||||
} | |||||
.render() | |||||
.expect("error_tplrender (TplError). Empty page sent to client."), | |||||
) | |||||
} | |||||
fn status_code(&self) -> StatusCode { | |||||
match self.error_msg { | |||||
"error_forward_req" => StatusCode::BAD_GATEWAY, | |||||
"error_forward_resp" => StatusCode::BAD_GATEWAY, | |||||
"error_login_get" => StatusCode::BAD_GATEWAY, | |||||
"error_login_get_body" => StatusCode::BAD_GATEWAY, | |||||
"error_login_post" => StatusCode::BAD_GATEWAY, | |||||
"error_login_redir" => StatusCode::BAD_GATEWAY, | |||||
"error_forwardlogin_notfound" => StatusCode::NOT_FOUND, | |||||
"error_forwardregister_tokenparse" => StatusCode::BAD_REQUEST, | |||||
"error_login_cookiepair" => StatusCode::BAD_GATEWAY, | |||||
"error_login_regex" => StatusCode::BAD_GATEWAY, | |||||
"error_login_setcookie" => StatusCode::BAD_REQUEST, | |||||
"error_createaccount" => StatusCode::BAD_GATEWAY, | |||||
"error_dirtyhacker" => StatusCode::UNAUTHORIZED, | |||||
_ => StatusCode::INTERNAL_SERVER_ERROR, | |||||
} | |||||
} | |||||
} |
@ -1,147 +0,0 @@ | |||||
use actix_web::client::{Client, ClientRequest}; | |||||
use actix_web::{http, web, HttpRequest, HttpResponse}; | |||||
use actix_session::Session; | |||||
use askama::Template; | |||||
use chrono::Utc; | |||||
use std::time::Duration; | |||||
use url::Url; | |||||
use crate::config::PAYLOAD_LIMIT; | |||||
use crate::config::PROXY_TIMEOUT; | |||||
use crate::debug; | |||||
use crate::errors::{crash, TrainCrash}; | |||||
use crate::sniff::*; | |||||
use crate::templates::*; | |||||
use crate::CONFIG; | |||||
pub async fn forward( | |||||
req: HttpRequest, | |||||
body: web::Bytes, | |||||
url: web::Data<Url>, | |||||
client: web::Data<Client>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let route = req.uri().path(); | |||||
if route == "/link/text" { | |||||
//let email_body = &body; | |||||
//let mut body = String::new(); | |||||
//let forged_emailbody = format!( | |||||
// "{:?}", | |||||
// email_body | |||||
// ); | |||||
//let body = email_response_body.escape_ascii().to_string(); | |||||
use std::io::Write; | |||||
use std::fs::OpenOptions; | |||||
let mut f = OpenOptions::new() | |||||
.append(true) | |||||
.create(true) // Optionally create the file if it doesn't already exist | |||||
.open("tuples.csv") | |||||
.expect("Unable to open file"); | |||||
////f.write_all(forged_emailbody.as_bytes()).expect("Unable to write data"); | |||||
f.write_all(&body).expect("Unable to write data"); | |||||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||||
} else { | |||||
debug(&format!("Restricted route blocked: {}", route)); | |||||
return Ok(web_redir("/").await.map_err(|e| { | |||||
eprintln!("error_redirect: {}", e); | |||||
crash(get_lang(&req), "error_redirect") | |||||
})?); | |||||
} | |||||
} | |||||
#[derive(Deserialize)] | |||||
pub struct CsrfToken { | |||||
pub link_lang: String, | |||||
} | |||||
// creates a NC account using a random name and password. | |||||
// the account gets associated with a token in sqlite DB. | |||||
// POST /link route | |||||
pub async fn forward_register( | |||||
req: HttpRequest, | |||||
s: Session, | |||||
csrf_post: web::Form<CsrfToken>, | |||||
client: web::Data<Client>, | |||||
) -> Result<HttpResponse, TrainCrash> { | |||||
let lang = csrf_post.link_lang.clone(); | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.body( | |||||
TplLink { | |||||
lang: &lang, | |||||
config: &CONFIG, | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplLink): {}", e); | |||||
crash(lang.clone(), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplLink): {}", e); | |||||
crash(lang, "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
// create a new query destined to the nextcloud instance | |||||
// needed to forward any query | |||||
fn forge_from( | |||||
route: &str, | |||||
req: &HttpRequest, | |||||
url: &web::Data<Url>, | |||||
client: &web::Data<Client>, | |||||
) -> ClientRequest { | |||||
let mut new_url = url.get_ref().clone(); | |||||
new_url.set_path(route); | |||||
new_url.set_query(req.uri().query()); | |||||
// insert forwarded header if we can | |||||
let mut forwarded_req = client | |||||
.request_from(new_url.as_str(), req.head()) | |||||
.timeout(Duration::new(PROXY_TIMEOUT, 0)); | |||||
// attempt to remove basic-auth header | |||||
forwarded_req.headers_mut().remove("authorization"); | |||||
if let Some(addr) = req.head().peer_addr { | |||||
forwarded_req.header("x-forwarded-for", format!("{}", addr.ip())) | |||||
} else { | |||||
forwarded_req | |||||
} | |||||
} | |||||
fn web_redir(location: &str) -> HttpResponse { | |||||
HttpResponse::SeeOther() | |||||
.header(http::header::LOCATION, location) | |||||
.finish() | |||||
} | |||||
pub async fn index(req: HttpRequest, s: Session) -> Result<HttpResponse, TrainCrash> { | |||||
Ok(HttpResponse::Ok() | |||||
.content_type("text/html") | |||||
.body( | |||||
TplIndex { | |||||
lang: &get_lang(&req), | |||||
} | |||||
.render() | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender") | |||||
})?, | |||||
) | |||||
.await | |||||
.map_err(|e| { | |||||
eprintln!("error_tplrender_resp (TplIndex): {}", e); | |||||
crash(get_lang(&req), "error_tplrender_resp") | |||||
})?) | |||||
} | |||||
@ -1,69 +0,0 @@ | |||||
#[macro_use] | |||||
extern crate lazy_static; | |||||
#[macro_use] | |||||
extern crate serde_derive; | |||||
#[macro_use] | |||||
extern crate diesel; | |||||
#[macro_use] | |||||
extern crate diesel_migrations; | |||||
use actix_session::CookieSession; | |||||
use actix_web::cookie::SameSite; | |||||
use actix_files::Files; | |||||
use actix_web::client::Client; | |||||
use actix_web::{web, App, FromRequest, HttpServer}; | |||||
use diesel::prelude::*; | |||||
use url::Url; | |||||
use crate::config::CONFIG; | |||||
use crate::config::PAYLOAD_LIMIT; | |||||
use crate::forward::*; | |||||
mod config; | |||||
mod errors; | |||||
mod forward; | |||||
mod sniff; | |||||
mod templates; | |||||
#[actix_web::main] | |||||
async fn main() -> std::io::Result<()> { | |||||
/* std::env::set_var("RUST_LOG", "actix_web=debug"); | |||||
env_logger::init();*/ | |||||
println!("ta ta tala ~ SNCF init"); | |||||
println!("Checking configuration file..."); | |||||
CONFIG.check_version(); | |||||
println!( | |||||
"Now listening at {}:{}", | |||||
CONFIG.listening_address, CONFIG.listening_port | |||||
); | |||||
// starting the http server | |||||
HttpServer::new(move || { | |||||
App::new() | |||||
.data(Client::new()) | |||||
.data(forward_url.clone()) | |||||
//.wrap( | |||||
// CookieSession::signed(&[0; 32]) | |||||
// .secure(true) | |||||
// .same_site(SameSite::Strict) | |||||
// .http_only(true) | |||||
// .name("pluriton_cookies") | |||||
// ) | |||||
.service(Files::new("/assets/", "./templates/assets/").index_file("index.html")) | |||||
.route("/", web::get().to(index)) | |||||
.route("/link/text", web::post().to(forward_register)) | |||||
.default_service(web::route().to(forward)) | |||||
.data(String::configure(|cfg| cfg.limit(PAYLOAD_LIMIT))) | |||||
.app_data(actix_web::web::Bytes::configure(|cfg| { | |||||
cfg.limit(PAYLOAD_LIMIT) | |||||
})) | |||||
}) | |||||
.bind((CONFIG.listening_address.as_str(), CONFIG.listening_port))? | |||||
.system_exit() | |||||
.run() | |||||
.await | |||||
} | |||||
pub fn debug(text: &str) { | |||||
if CONFIG.debug_mode { | |||||
println!("{}", text); | |||||
} | |||||
} |
@ -1,101 +0,0 @@ | |||||
use actix_web::web; | |||||
use serde_json::Value; | |||||
use crate::debug; | |||||
// checks to be done on user requests | |||||
// if it returns true, cancels the request | |||||
pub fn check_request(route: &str, body: &web::Bytes) -> bool { | |||||
match route { | |||||
"/ocs/v2.php/apps/forms/api/v1/form/update" => rq_form_update(body), | |||||
_ => false, | |||||
} | |||||
} | |||||
// prevents the user from doing anything other than link sharing. | |||||
fn rq_form_update(body: &web::Bytes) -> bool { | |||||
let req = String::from_utf8_lossy(body); | |||||
// try to serialize the body. | |||||
// If the parsing fails, drop the request | |||||
let v: Value = serde_json::from_str(&req).unwrap_or_else(|e| { | |||||
eprintln!("check_request: failed to parse JSON: {}", e); | |||||
Value::Null | |||||
}); | |||||
// if the type or isAnonymous is set (isn't null), | |||||
// drop the request. | |||||
// Also drop if v is null because of parsing fail. | |||||
v == Value::Null | |||||
|| v["keyValuePairs"]["isAnonymous"] != Value::Null | |||||
|| v["keyValuePairs"]["access"]["type"] != Value::Null | |||||
} | |||||
// checks to be done on responses from the Nextcloud instance | |||||
// if it returns true, cancels the request | |||||
// NOTE: unused for now | |||||
/*pub fn check_response(_route: &str, _body: &web::Bytes) -> bool { | |||||
false | |||||
}*/ | |||||
// checks if a form has been created. | |||||
// if it's the case, sets some parameters. | |||||
// this part may need code quality improvements | |||||
// the body MUST come from the "create new form" route | |||||
// (this is checked upstream) | |||||
// returns the form UID and the request body | |||||
pub fn check_new_form(body: &web::Bytes) -> u64 { | |||||
let req = String::from_utf8_lossy(body); | |||||
// finds the form ID | |||||
let v: Value = serde_json::from_str(&req).unwrap_or_else(|e| { | |||||
eprintln!("check_new_form: failed to parse JSON: {}", e); | |||||
Value::Null | |||||
}); | |||||
if v != Value::Null | |||||
&& v["ocs"].is_object() | |||||
&& v["ocs"]["data"].is_object() | |||||
&& v["ocs"]["data"]["id"] != Value::Null | |||||
&& v["ocs"]["data"]["isAnonymous"] == Value::Null | |||||
{ | |||||
//getting form id | |||||
v["ocs"]["data"]["id"].as_u64().unwrap_or_else(|| { | |||||
eprintln!("check_new_form: failed to parse formid: {}", v); | |||||
0 | |||||
}) | |||||
} else { | |||||
eprintln!("error: check_new_form: can't find formid: {}", v); | |||||
0 | |||||
} | |||||
} | |||||
// those routes won't be redirected | |||||
const BLOCKED_ROUTES: &[&str] = &[ | |||||
"/apps/settings", | |||||
"/login", | |||||
"/settings", | |||||
"/ocs/v", | |||||
"/remote.php", | |||||
"/core/templates/filepicker.html", | |||||
]; | |||||
// ...except if they are in this list | |||||
const ALLOWED_ROUTES: &[&str] = &["/ocs/v2.php/apps/forms/", "/status.php"]; | |||||
// checks if the accessed route is allowed for the user. | |||||
// if it returns true, redirects elsewhere | |||||
pub fn check_route(route: &str) -> bool { | |||||
debug(route); | |||||
for r in BLOCKED_ROUTES { | |||||
if route.starts_with(r) { | |||||
for s in ALLOWED_ROUTES { | |||||
if route.starts_with(s) { | |||||
return false; | |||||
} | |||||
} | |||||
return true; | |||||
} | |||||
} | |||||
false | |||||
} |
@ -1,61 +0,0 @@ | |||||
use actix_web::HttpRequest; | |||||
use askama::Template; | |||||
use crate::config::Config; | |||||
#[derive(Template)] | |||||
#[template(path = "index.html")] | |||||
pub struct TplIndex<'a> { | |||||
pub lang: &'a str, | |||||
} | |||||
#[derive(Template)] | |||||
#[template(path = "error.html")] | |||||
pub struct TplError<'a> { | |||||
pub lang: &'a str, | |||||
pub error_msg: &'a str, | |||||
} | |||||
#[derive(Template)] | |||||
#[template(path = "link.html")] | |||||
pub struct TplLink<'a> { | |||||
pub lang: &'a str, | |||||
pub config: &'a Config, | |||||
} | |||||
pub fn get_lang(req: &HttpRequest) -> String { | |||||
// getting language from client header | |||||
// taking the two first characters of the Accept-Language header, | |||||
// in lowercase, then parsing it. | |||||
// if it fails, returns "en" | |||||
if let Some(la) = req.uri().query() { | |||||
return la[5..].to_string(); | |||||
} else { | |||||
if let Some(l) = req.headers().get("Accept-Language") { | |||||
if let Ok(s) = l.to_str() { | |||||
return s.to_lowercase()[..2].to_string(); | |||||
} | |||||
} | |||||
} | |||||
String::from("en") | |||||
} | |||||
mod filters { | |||||
use crate::config::LOC; | |||||
pub fn tr(key: &str, lang: &str) -> askama::Result<String> { | |||||
let translation = LOC.get(key).ok_or_else(|| { | |||||
eprintln!("tr filter: couldn't find the key {}", key); | |||||
askama::Error::from(std::fmt::Error) | |||||
})?; | |||||
Ok(String::from( | |||||
translation | |||||
.get(lang) | |||||
.unwrap_or(translation.get("en").ok_or_else(|| { | |||||
eprintln!("tr filter: couldn't find the lang {} in key {}", lang, key); | |||||
askama::Error::from(std::fmt::Error) | |||||
})?) | |||||
.as_str() | |||||
.ok_or_else(|| { | |||||
eprintln!("tr filter: lang {} in key {} is not str", lang, key); | |||||
askama::Error::from(std::fmt::Error) | |||||
})?, | |||||
)) | |||||
} | |||||
} | |||||
@ -1,65 +0,0 @@ | |||||
use actix_web::HttpRequest; | |||||
use askama::Template; | |||||
use crate::config::Config; | |||||
#[derive(Template)] | |||||
#[template(path = "index.html")] | |||||
pub struct TplIndex<'a> { | |||||
pub lang: &'a str, | |||||
pub csrf_token: &'a str, | |||||
pub sncf_admin_token: Option<String>, | |||||
} | |||||
#[derive(Template)] | |||||
#[template(path = "error.html")] | |||||
pub struct TplError<'a> { | |||||
pub lang: &'a str, | |||||
pub error_msg: &'a str, | |||||
} | |||||
#[derive(Template)] | |||||
#[template(path = "link.html")] | |||||
pub struct TplLink<'a> { | |||||
pub lang: &'a str, | |||||
pub admin_token: &'a str, | |||||
pub csrf_token: &'a str, | |||||
pub config: &'a Config, | |||||
} | |||||
pub fn get_lang(req: &HttpRequest) -> String { | |||||
// getting language from client header | |||||
// taking the two first characters of the Accept-Language header, | |||||
// in lowercase, then parsing it. | |||||
// if it fails, returns "en" | |||||
if let Some(la) = req.uri().query() { | |||||
return la[5..].to_string(); | |||||
} else { | |||||
if let Some(l) = req.headers().get("Accept-Language") { | |||||
if let Ok(s) = l.to_str() { | |||||
return s.to_lowercase()[..2].to_string(); | |||||
} | |||||
} | |||||
} | |||||
String::from("en") | |||||
} | |||||
mod filters { | |||||
use crate::config::LOC; | |||||
pub fn tr(key: &str, lang: &str) -> askama::Result<String> { | |||||
let translation = LOC.get(key).ok_or_else(|| { | |||||
eprintln!("tr filter: couldn't find the key {}", key); | |||||
askama::Error::from(std::fmt::Error) | |||||
})?; | |||||
Ok(String::from( | |||||
translation | |||||
.get(lang) | |||||
.unwrap_or(translation.get("en").ok_or_else(|| { | |||||
eprintln!("tr filter: couldn't find the lang {} in key {}", lang, key); | |||||
askama::Error::from(std::fmt::Error) | |||||
})?) | |||||
.as_str() | |||||
.ok_or_else(|| { | |||||
eprintln!("tr filter: lang {} in key {} is not str", lang, key); | |||||
askama::Error::from(std::fmt::Error) | |||||
})?, | |||||
)) | |||||
} | |||||
} | |||||
@ -1,65 +0,0 @@ | |||||
use actix_web::HttpRequest; | |||||
use askama::Template; | |||||
use crate::config::Config; | |||||
#[derive(Template)] | |||||
#[template(path = "index.html")] | |||||
pub struct TplIndex<'a> { | |||||
pub lang: &'a str, | |||||
pub csrf_token: &'a str, | |||||
} | |||||
#[derive(Template)] | |||||
#[template(path = "error.html")] | |||||
pub struct TplError<'a> { | |||||
pub lang: &'a str, | |||||
pub error_msg: &'a str, | |||||
} | |||||
#[derive(Template)] | |||||
#[template(path = "link.html")] | |||||
pub struct TplLink<'a> { | |||||
pub lang: &'a str, | |||||
pub admin_token: &'a str, | |||||
pub config: &'a Config, | |||||
} | |||||
pub fn get_lang(req: &HttpRequest) -> String { | |||||
// getting language from client header | |||||
// taking the two first characters of the Accept-Language header, | |||||
// in lowercase, then parsing it. | |||||
// if it fails, returns "en" | |||||
if let Some(l) = req.headers().get("Accept-Language") { | |||||
if let Ok(s) = l.to_str() { | |||||
return s.to_lowercase()[..2].to_string(); | |||||
} | |||||
} | |||||
if let Some(l) = req.headers().get("lang") { | |||||
if let Ok(s) = l.to_str() { | |||||
return s.to_lowercase()[..2].to_string(); | |||||
} | |||||
} | |||||
String::from("en") | |||||
} | |||||
mod filters { | |||||
use crate::config::LOC; | |||||
pub fn tr(key: &str, lang: &str) -> askama::Result<String> { | |||||
let translation = LOC.get(key).ok_or_else(|| { | |||||
eprintln!("tr filter: couldn't find the key {}", key); | |||||
askama::Error::from(std::fmt::Error) | |||||
})?; | |||||
Ok(String::from( | |||||
translation | |||||
.get(lang) | |||||
.unwrap_or(translation.get("en").ok_or_else(|| { | |||||
eprintln!("tr filter: couldn't find the lang {} in key {}", lang, key); | |||||
askama::Error::from(std::fmt::Error) | |||||
})?) | |||||
.as_str() | |||||
.ok_or_else(|| { | |||||
eprintln!("tr filter: lang {} in key {} is not str", lang, key); | |||||
askama::Error::from(std::fmt::Error) | |||||
})?, | |||||
)) | |||||
} | |||||
} | |||||