mirror of
https://git.42l.fr/neil/sncf.git
synced 2024-06-02 15:52:13 +02:00
Compare commits
48 commits
Author | SHA1 | Date | |
---|---|---|---|
507e8877c8 | |||
44c0e27a72 | |||
d1c112f12a | |||
fd4d721be4 | |||
d419aea412 | |||
11504c00e0 | |||
b1fd3fccae | |||
8d67cb340c | |||
730a023e55 | |||
e354b5b14b | |||
002a0c9ef2 | |||
fafe2bf3fe | |||
7a4839541d | |||
15c73715a8 | |||
8fc232d022 | |||
b45e65d427 | |||
ade36cf053 | |||
8d6a68b33c | |||
9612086790 | |||
05a15b1680 | |||
162cdad7fe | |||
7fbfcf485c | |||
d6a4a6591a | |||
83a80c0969 | |||
9fecf9ace9 | |||
0d38e2f2d4 | |||
112ea773a2 | |||
c24b98bcca | |||
5a521b0497 | |||
1251b431a6 | |||
6e231a73b6 | |||
240baca044 | |||
fcc9ceaedc | |||
8276b35a01 | |||
c1191f3f45 | |||
3d6b9f96e8 | |||
255cf8ba9e | |||
31bf380e12 | |||
3b82283cfd | |||
329fe20553 | |||
ae439b25bb | |||
e012505247 | |||
554cbd25ab | |||
cc8a7a90d8 | |||
0d273bcad2 | |||
84a52032fe | |||
564a138480 | |||
fa368044be |
16
Cargo.toml
16
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "sncf"
|
name = "sncf"
|
||||||
version = "1.2.0"
|
version = "1.5.0"
|
||||||
authors = ["Association 42l <contact@noreply.example.org>"]
|
authors = ["Association 42l <contact@noreply.example.org>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
@ -11,20 +11,22 @@ mysql = [ "diesel/mysql" ]
|
||||||
sqlite = [ "diesel/sqlite" ]
|
sqlite = [ "diesel/sqlite" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-rt = "1.1.1"
|
actix-rt = "2.2.0"
|
||||||
actix-web = "3.2.0"
|
actix-web = "3.3.2"
|
||||||
actix-files = "0.4.0"
|
actix-files = "0.5.0"
|
||||||
|
actix-session = "0.4"
|
||||||
diesel = { version = "1.4", features = ["r2d2", "chrono"] }
|
diesel = { version = "1.4", features = ["r2d2", "chrono"] }
|
||||||
diesel_migrations = "1.4"
|
diesel_migrations = "1.4"
|
||||||
url = "2.1"
|
url = "2.2"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
regex = "1.3"
|
regex = "1.5"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
rand = "0.7"
|
rand = "0.8"
|
||||||
askama = "0.10"
|
askama = "0.10"
|
||||||
csrf = "0.4"
|
csrf = "0.4"
|
||||||
|
percent-encoding = "2.1"
|
||||||
|
|
13
README.md
13
README.md
|
@ -1,5 +1,8 @@
|
||||||
# sncf
|
# sncf
|
||||||
|
|
||||||
|
**Warning: Breaking changes introduced on a minor Nextcloud release (>= 22.3.0) broke sncf. Please do not update until it is fixed. It seems easy to fix (use `/login` instead of `/csrftoken`) but I need time, feel free to try to fix it.**
|
||||||
|
|
||||||
|
|
||||||
Simple Nextcloud Forms (sncf) is a lightweight proxy written in Rust with the [Actix](https://actix.rs) framework.
|
Simple Nextcloud Forms (sncf) is a lightweight proxy written in Rust with the [Actix](https://actix.rs) framework.
|
||||||
|
|
||||||
It is meant to make form creation easier, through the use of the [Nextcloud Forms](https://github.com/nextcloud/forms) application, by generating administration links for forms: **users do not need to log in or register**, they just need to keep a link (in the form of `https://your-instance.com/admin/<45-byte base64 key>`) to log them in and give them access to their forms.
|
It is meant to make form creation easier, through the use of the [Nextcloud Forms](https://github.com/nextcloud/forms) application, by generating administration links for forms: **users do not need to log in or register**, they just need to keep a link (in the form of `https://your-instance.com/admin/<45-byte base64 key>`) to log them in and give them access to their forms.
|
||||||
|
@ -31,9 +34,13 @@ Compatibility with sncf has been tested for the following Nextcloud and Nextclou
|
||||||
|--------------|------------|------------------|
|
|--------------|------------|------------------|
|
||||||
| 1.0.0 | 19.0.1, 19.0.2 | 2.0.2, 2.0.3 |
|
| 1.0.0 | 19.0.1, 19.0.2 | 2.0.2, 2.0.3 |
|
||||||
| 1.0.1, 1.0.2, 1.1.0, 1.2.0 | 19.0.1, 19.0.2, 20.0.0\*, 20.0.1 | 2.0.4 |
|
| 1.0.1, 1.0.2, 1.1.0, 1.2.0 | 19.0.1, 19.0.2, 20.0.0\*, 20.0.1 | 2.0.4 |
|
||||||
|
| **Unsupported** \*\* | above 20.0.1, below 21.x | above 2.0.4, below 2.2.2
|
||||||
|
| 1.3.0, 1.4.0 | 21.0.0 | 2.2.2, 2.2.3, 2.2.4 |
|
||||||
|
| 1.5.0 | 22.2.0 | 2.3.0 |
|
||||||
|
|
||||||
|
|
||||||
\* Breaking changes, please check [the wiki](https://git.42l.fr/neil/sncf/wiki/Upgrade-from-a-previous-version) if you need to upgrade from a previous version.
|
\* Breaking changes, please check [the wiki](https://git.42l.fr/neil/sncf/wiki/Upgrade-from-a-previous-version) if you need to upgrade from a previous version.
|
||||||
|
\*\* Untested versions, use at your own risk.
|
||||||
|
|
||||||
If your NC or NC Forms version isn't in this list, sncf **may** or **may not** work. We **do not** ensure backwards compatibility with older versions.
|
If your NC or NC Forms version isn't in this list, sncf **may** or **may not** work. We **do not** ensure backwards compatibility with older versions.
|
||||||
|
|
||||||
|
@ -45,11 +52,11 @@ If you upgrade anyway and notice a breaking change, please file an issue.
|
||||||
|
|
||||||
#### Donations
|
#### Donations
|
||||||
|
|
||||||
If you like this work, please donate to the [42l association](https://42l.fr) (maintaining sncf) or [Nextcloud](https://www.bountysource.com/teams/nextcloud) (maintaining Nextcloud and Nextcloud Forms).
|
If you like this work, please donate to the [42l association](https://42l.fr) (maintaining sncf) or [Nextcloud](https://nextcloud.com/include/) (maintaining Nextcloud and Nextcloud Forms).
|
||||||
|
|
||||||
#### Translating
|
#### Translating
|
||||||
|
|
||||||
Currently, this software is translated in French and English.
|
Currently, this software is translated in French, English and German (thanks [alpcentaur](https://git.42l.fr/alpcentaur)!)
|
||||||
|
|
||||||
Feel free to take a look at the [lang.json](https://git.42l.fr/neil/sncf/src/branch/root/lang.json) file and send a pull request.
|
Feel free to take a look at the [lang.json](https://git.42l.fr/neil/sncf/src/branch/root/lang.json) file and send a pull request.
|
||||||
|
|
||||||
|
|
270
lang.json
270
lang.json
|
@ -1,207 +1,262 @@
|
||||||
{
|
{
|
||||||
"lang_code": {
|
"lang_code": {
|
||||||
"en": "en",
|
"en": "en",
|
||||||
"fr": "fr"
|
"fr": "fr",
|
||||||
|
"de": "de"
|
||||||
},
|
},
|
||||||
"lang_full": {
|
"lang_full": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"fr": "Français"
|
"fr": "Français",
|
||||||
|
"de": "Deutsch"
|
||||||
},
|
},
|
||||||
"meta_description": {
|
"meta_description": {
|
||||||
"en": "42l Forms : create forms for free, without registration while protecting your privacy",
|
"en": "42l Forms : create forms for free, without registration while protecting your privacy",
|
||||||
"fr": "42l Formulaires (Forms) : créez des formulaires ou questionnaires gratuitement, sans inscription et dans le respect de votre vie privée"
|
"fr": "42l Formulaires (Forms) : créez des formulaires ou questionnaires gratuitement, sans inscription et dans le respect de votre vie privée",
|
||||||
|
"de": "42l Forms: erstellen Sie gratis Umfragen, ohne Registrierung und unter Wahrung Ihrer Privatssphäre"
|
||||||
},
|
},
|
||||||
"index_title": {
|
"index_title": {
|
||||||
"en": "42l Forms",
|
"en": "42l Forms",
|
||||||
"fr": "42l Formulaires"
|
"fr": "42l Formulaires",
|
||||||
|
"de": "42l Forms"
|
||||||
},
|
},
|
||||||
"index_description": {
|
"index_description": {
|
||||||
"en": "Create forms without registration",
|
"en": "Create forms without registration",
|
||||||
"fr": "Créez des questionnaires sans inscription"
|
"fr": "Créez des questionnaires sans inscription",
|
||||||
|
"de": "Erstellen Sie Umfragen ohne Registrierung"
|
||||||
},
|
},
|
||||||
"index_beta_tag": {
|
"index_beta_tag": {
|
||||||
"en": "BETA",
|
"en": "BETA",
|
||||||
"fr": "BETA"
|
"fr": "BETA",
|
||||||
|
"de": "BETA"
|
||||||
},
|
},
|
||||||
"index_nojs": {
|
"index_nojs": {
|
||||||
"en": "Please enable JavaScript in your browser!",
|
"en": "Please enable JavaScript in your browser!",
|
||||||
"fr": "Veuillez activer JavaScript dans votre navigateur !"
|
"fr": "Veuillez activer JavaScript dans votre navigateur !",
|
||||||
|
"de": "Bitte aktivieren Sie JavaScript in ihrem Browser!"
|
||||||
},
|
},
|
||||||
"index_createform_button": {
|
"index_createform_button": {
|
||||||
"en": "Create a form",
|
"en": "Create a form",
|
||||||
"fr": "Créer un formulaire"
|
"fr": "Créer un formulaire",
|
||||||
|
"de": "Erstellen einer Umfrage"
|
||||||
|
},
|
||||||
|
"index_continueform_button": {
|
||||||
|
"en": "Access your forms",
|
||||||
|
"fr": "Accéder à vos formulaires"
|
||||||
},
|
},
|
||||||
"index_beta_banner_title": {
|
"index_beta_banner_title": {
|
||||||
"en": "Warning: Service in beta.",
|
"en": "Warning: Service in beta.",
|
||||||
"fr": "Attention : Service en bêta."
|
"fr": "Attention : Service en bêta.",
|
||||||
|
"de": "Achtung: Seite in Beta Version"
|
||||||
},
|
},
|
||||||
"index_beta_banner_desc1": {
|
"index_beta_banner_desc1": {
|
||||||
"en": "This service is currently under development and might behave in an unexpected way.",
|
"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."
|
"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": {
|
"index_beta_banner_desc2": {
|
||||||
"en": "Feel free to send feedbacks on our ",
|
"en": "Feel free to send feedbacks on our ",
|
||||||
"fr": "Vous pouvez nous envoyer vos retours sur "
|
"fr": "Vous pouvez nous envoyer vos retours sur ",
|
||||||
|
"de": "Feedback gerne an "
|
||||||
},
|
},
|
||||||
"index_beta_banner_desc_link": {
|
"index_beta_banner_desc_link": {
|
||||||
"en": "our contact page",
|
"en": "our contact page",
|
||||||
"fr": "notre page de contact"
|
"fr": "notre page de contact",
|
||||||
|
"de": "unsere Kontaktseite"
|
||||||
},
|
},
|
||||||
"index_disclaimer1": {
|
"index_disclaimer1": {
|
||||||
"en": "This service is maintained for free, without subscription nor advertising nor tracking or selling of your personal data, on a server hosted in France.",
|
"en": "This service is maintained for free, without subscription nor advertising nor tracking or selling of your personal data, on a server hosted in France.",
|
||||||
"fr": "Ce service vous est fourni gratuitement, sans inscription, sans publicités, sans pistage ou revente de vos données personnelles, sur un serveur hébergé en France."
|
"fr": "Ce service vous est fourni gratuitement, sans inscription, sans publicités, sans pistage ou revente de vos données personnelles, sur un serveur hébergé en France.",
|
||||||
|
"de": "Diese Seite wird frei, ohne Registrierung, ohne Werbung, ohne Tracking, oder den Verkauf von Ihren persönlichen Daten, auf einem Server in Frankreich betrieben."
|
||||||
},
|
},
|
||||||
"index_disclaimer2": {
|
"index_disclaimer2": {
|
||||||
"en": "If you appreciate our work, please consider donating to ",
|
"en": "If you appreciate our work, please consider donating to ",
|
||||||
"fr": "Si vous appréciez notre travail, merci d'envisager de faire un don à "
|
"fr": "Si vous appréciez notre travail, merci d'envisager de faire un don à ",
|
||||||
|
"de": "Wenn Sie unsere Arbeit wertschätzen, Spenden Sie gerne an"
|
||||||
},
|
},
|
||||||
"index_disclaimer2_link_org": {
|
"index_disclaimer2_link_org": {
|
||||||
"en": "the 42l association",
|
"en": "the 42l association",
|
||||||
"fr": "l'association 42l"
|
"fr": "l'association 42l",
|
||||||
|
"de": "die 42l Assoziation"
|
||||||
},
|
},
|
||||||
"index_disclaimer2_or": {
|
"index_disclaimer2_or": {
|
||||||
"en": " or ",
|
"en": " or ",
|
||||||
"fr": " ou à "
|
"fr": " ou à ",
|
||||||
|
"de": " oder an"
|
||||||
},
|
},
|
||||||
"index_disclaimer2_nc": {
|
"index_disclaimer2_nc": {
|
||||||
"en": "Nextcloud",
|
"en": "Nextcloud",
|
||||||
"fr": "Nextcloud"
|
"fr": "Nextcloud",
|
||||||
|
"de": "Nextcloud"
|
||||||
},
|
},
|
||||||
"index_panel1_title": {
|
"index_panel1_title": {
|
||||||
"en": "Responsive and intuitive interface",
|
"en": "Responsive and intuitive interface",
|
||||||
"fr": "Interface intuitive et compatible mobile"
|
"fr": "Interface intuitive et compatible mobile",
|
||||||
|
"de": "mobil-freundliche und intuitive Benutzeroberfläche"
|
||||||
},
|
},
|
||||||
"index_panel1_desc1": {
|
"index_panel1_desc1": {
|
||||||
"en": "Are you searching for a privacy-friendly alternative to Google Forms while keeping its ease of use?",
|
"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 ?"
|
"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": {
|
"index_panel1_desc2": {
|
||||||
"en": "You've just found it.",
|
"en": "You've just found it.",
|
||||||
"fr": "Vous venez de la trouver."
|
"fr": "Vous venez de la trouver.",
|
||||||
|
"de": "Sie haben sie gefunden."
|
||||||
},
|
},
|
||||||
"index_panel2_title": {
|
"index_panel2_title": {
|
||||||
"en": "Choose and order your fields",
|
"en": "Choose and order your fields",
|
||||||
"fr": "Choisissez et ordonnez vos champs"
|
"fr": "Choisissez et ordonnez vos champs",
|
||||||
|
"de": "Wählen und Ordnen Sie ihre Felder"
|
||||||
},
|
},
|
||||||
"index_panel2_desc1": {
|
"index_panel2_desc1": {
|
||||||
"en": "The software currently supports five field types.",
|
"en": "The software currently supports seven field types.",
|
||||||
"fr": "Pour le moment, le logiciel supporte cinq types de champs."
|
"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": {
|
"index_panel2_desc2": {
|
||||||
"en": "New field types are ",
|
"en": "New field types are ",
|
||||||
"fr": "De nouveaux types de champs sont "
|
"fr": "De nouveaux types de champs sont ",
|
||||||
|
"de": "Neue Typen von Feldern sind "
|
||||||
},
|
},
|
||||||
"index_panel2_desc2_link": {
|
"index_panel2_desc2_link": {
|
||||||
"en": "currently in the works",
|
"en": "currently in the works",
|
||||||
"fr": "en cours d'élaboration"
|
"fr": "en cours d'élaboration",
|
||||||
|
"de": "momentan in Bearbeitung"
|
||||||
},
|
},
|
||||||
"index_panel3_title": {
|
"index_panel3_title": {
|
||||||
"en": "Analyze the answers",
|
"en": "Analyze the answers",
|
||||||
"fr": "Analysez les réponses"
|
"fr": "Analysez les réponses",
|
||||||
|
"de": "Analysieren Sie die Antworten"
|
||||||
},
|
},
|
||||||
"index_panel3_desc1": {
|
"index_panel3_desc1": {
|
||||||
"en": "See detailed graphs of the answers to your form.",
|
"en": "See detailed graphs of the answers to your form.",
|
||||||
"fr": "Visualisez les réponses à votre formulaire avec un graphique."
|
"fr": "Visualisez les réponses à votre formulaire avec un graphique.",
|
||||||
|
"de": "Visualisieren Sie die Antworten Ihrer Umfrage graphisch."
|
||||||
},
|
},
|
||||||
"index_panel4_title": {
|
"index_panel4_title": {
|
||||||
"en": "Export the answers",
|
"en": "Export the answers",
|
||||||
"fr": "Exportez les réponses"
|
"fr": "Exportez les réponses",
|
||||||
|
"de": "Export der Antworten"
|
||||||
},
|
},
|
||||||
"index_panel4_desc1": {
|
"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).",
|
"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)."
|
"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": {
|
"index_panel5_title": {
|
||||||
"en": "Edit your form's settings",
|
"en": "Edit your form's settings",
|
||||||
"fr": "Paramétrez vos formulaires"
|
"fr": "Paramétrez vos formulaires",
|
||||||
|
"de": "Einstellungen Ihrer Umfragen"
|
||||||
},
|
},
|
||||||
"index_panel5_desc1": {
|
"index_panel5_desc1": {
|
||||||
"en": "Use the share link to send your form to other people.",
|
"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."
|
"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": {
|
"index_panel5_desc2": {
|
||||||
"en": "You can also define an expiration date for your form.",
|
"en": "You can also define an expiration date for your form.",
|
||||||
"fr": "Vous pouvez également définir une date d'expiration pour votre formulaire."
|
"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": {
|
"index_panel6_title": {
|
||||||
"en": "All your forms in one place",
|
"en": "All your forms in one place",
|
||||||
"fr": "Tous vos formulaires au même endroit"
|
"fr": "Tous vos formulaires au même endroit",
|
||||||
|
"de": "Alle Ihre Umfragen an einem Ort"
|
||||||
},
|
},
|
||||||
"index_panel6_desc1": {
|
"index_panel6_desc1": {
|
||||||
"en": "Find all your forms in the same panel.",
|
"en": "Find all your forms in the same panel.",
|
||||||
"fr": "Retrouvez tous vos formulaires sur un même panel."
|
"fr": "Retrouvez tous vos formulaires sur un même panel.",
|
||||||
|
"de": "Finde alle deine Umfragen in einem Panel."
|
||||||
},
|
},
|
||||||
"index_bottom_docs": {
|
"index_bottom_docs": {
|
||||||
"en": "Documentation",
|
"en": "Documentation",
|
||||||
"fr": "Documentation"
|
"fr": "Documentation",
|
||||||
|
"de": "Dokumentation"
|
||||||
},
|
},
|
||||||
"index_bottom_source": {
|
"index_bottom_source": {
|
||||||
"en": "Source code",
|
"en": "Source code",
|
||||||
"fr": "Code source"
|
"fr": "Code source",
|
||||||
|
"de": "Quellcode"
|
||||||
},
|
},
|
||||||
"index_bottom_lic": {
|
"index_bottom_lic": {
|
||||||
"en": "License",
|
"en": "License",
|
||||||
"fr": "Licence"
|
"fr": "Licence",
|
||||||
|
"de": "Lizenz"
|
||||||
},
|
},
|
||||||
"index_credits_title": {
|
"index_credits_title": {
|
||||||
"en": "Credits",
|
"en": "Credits",
|
||||||
"fr": "Crédits"
|
"fr": "Crédits",
|
||||||
|
"de": "Credits"
|
||||||
},
|
},
|
||||||
"index_credits_desc1": {
|
"index_credits_desc1": {
|
||||||
"en": "The Nextcloud software suite and the Nextcloud Forms application has been developed by ",
|
"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 "
|
"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": {
|
"index_credits_desc1_link": {
|
||||||
"en": "the Nextcloud team",
|
"en": "the Nextcloud team",
|
||||||
"fr": "l'équipe Nextcloud"
|
"fr": "l'équipe Nextcloud",
|
||||||
|
"de": "dem Nextcloud Team"
|
||||||
},
|
},
|
||||||
"index_credits_desc1_a": {
|
"index_credits_desc1_a": {
|
||||||
"en": " and its contributors.",
|
"en": " and its contributors.",
|
||||||
"fr": " et ses contributeur·ices."
|
"fr": " et ses contributeur·ices.",
|
||||||
|
"de": " und ihren Kontributor*innen"
|
||||||
},
|
},
|
||||||
"index_credits_desc2": {
|
"index_credits_desc2": {
|
||||||
"en": "The Simple Nextcloud Forms software, which simplifies the form creation process, has been developed by ",
|
"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 "
|
"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": {
|
"index_credits_desc2_for": {
|
||||||
"en": " for ",
|
"en": " for ",
|
||||||
"fr": " pour "
|
"fr": " pour ",
|
||||||
|
"de": " für "
|
||||||
},
|
},
|
||||||
"index_credits_desc2_org": {
|
"index_credits_desc2_org": {
|
||||||
"en": "the 42l association",
|
"en": "the 42l association",
|
||||||
"fr": "l'association 42l"
|
"fr": "l'association 42l",
|
||||||
|
"de": "die 42l Assoziation"
|
||||||
},
|
},
|
||||||
"index_credits_desc3": {
|
"index_credits_desc3": {
|
||||||
"en": "source code",
|
"en": "source code",
|
||||||
"fr": "code source"
|
"fr": "code source",
|
||||||
|
"de": "Quellcode"
|
||||||
},
|
},
|
||||||
"link_title": {
|
"link_title": {
|
||||||
"en": "Link created",
|
"en": "Link created",
|
||||||
"fr": "Lien créé"
|
"fr": "Lien créé",
|
||||||
|
"de": "Link erstellt"
|
||||||
},
|
},
|
||||||
"link_desc1": {
|
"link_desc1": {
|
||||||
"en": "Here's an <b>administration link</b>, which will allow you to access all your forms and check your answers.",
|
"en": "Here's an <b>administration link</b>, which will allow you to access all your forms and check your answers.",
|
||||||
"fr": "Voici un <b>lien d'administration</b>, qui vous permettra d'accéder à tous vos formulaires et de consulter vos réponses."
|
"fr": "Voici un <b>lien d'administration</b>, qui vous permettra d'accéder à tous vos formulaires et de consulter vos réponses.",
|
||||||
|
"de": "Hier ist ein <b>Administrations Link</b>, der es ermöglicht wieder zu ihren Umfragen zu gelangen und die Antworten einzusehen."
|
||||||
},
|
},
|
||||||
"link_desc2": {
|
"link_desc2": {
|
||||||
"en": "<b>Keep it</b> carefully and don't give it away (it'd be the same as giving out your password!).",
|
"en": "<b>Keep it</b> carefully and don't give it away (it'd be the same as giving out your password!).",
|
||||||
"fr": "<b>Conservez-le</B> bien précieusement et ne le donnez pas (cela reviendrait à donner un mot de passe !)."
|
"fr": "<b>Conservez-le</B> bien précieusement et ne le donnez pas (cela reviendrait à donner un mot de passe !).",
|
||||||
|
"de": "<b>Bewahren Sie diese</b> gut und sicher auf ( Die Weitergabe entspricht der Weitergabe eines Passwortes! )."
|
||||||
},
|
},
|
||||||
"link_desc3": {
|
"link_desc3": {
|
||||||
"en": "Once your link copied, click on the button below to start editing your forms.",
|
"en": "Once your link copied, click on the button below to start editing your forms.",
|
||||||
"fr": "Une fois votre lien copié, cliquez sur le bouton ci-dessous pour commencer à éditer vos formulaires."
|
"fr": "Une fois votre lien copié, cliquez sur le bouton ci-dessous pour commencer à éditer vos formulaires.",
|
||||||
|
"de": "Ist der Link kopiert, drücken sie auf den unteren Button um Umfragen zu erstellen oder zu bearbeiten."
|
||||||
},
|
},
|
||||||
"link_access_btn": {
|
"link_access_btn": {
|
||||||
"en": "Access the forms",
|
"en": "Access the forms",
|
||||||
"fr": "Accéder aux formulaires"
|
"fr": "Accéder aux formulaires",
|
||||||
|
"de": "Zugang zu den Umfragen"
|
||||||
},
|
},
|
||||||
"link_note": {
|
"link_note": {
|
||||||
"en": "Note: If you don't use your administration link during more than ",
|
"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 "
|
"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": {
|
"link_note2": {
|
||||||
"en": " days, your forms will be automatically deleted.",
|
"en": " days, your forms will be automatically deleted.",
|
||||||
"fr": " jours, vos formulaires seront automatiquement supprimés."
|
"fr": " jours, vos formulaires seront automatiquement supprimés.",
|
||||||
|
"de": " Tage nicht benutzen, werden ihre Umfragen automatisch gelöscht."
|
||||||
},
|
},
|
||||||
"link_copy": {
|
"link_copy": {
|
||||||
"en": "Copy link",
|
"en": "Copy link",
|
||||||
|
@ -209,150 +264,187 @@
|
||||||
},
|
},
|
||||||
"link_copied": {
|
"link_copied": {
|
||||||
"en": "Link copied!",
|
"en": "Link copied!",
|
||||||
"fr": "Lien copié !"
|
"fr": "Lien copié !",
|
||||||
|
"de": "Link kopiert !"
|
||||||
},
|
},
|
||||||
"error_title": {
|
"error_title": {
|
||||||
"en": "Oops!...",
|
"en": "Oops!...",
|
||||||
"fr": "Oups !..."
|
"fr": "Oups !...",
|
||||||
|
"de": "Ups !..."
|
||||||
},
|
},
|
||||||
"error_description": {
|
"error_description": {
|
||||||
"en": "The application encountered a problem:",
|
"en": "The application encountered a problem:",
|
||||||
"fr": "L'application a rencontré un problème :"
|
"fr": "L'application a rencontré un problème :",
|
||||||
|
"de": "Die Anwendung hat ein Problem festgestellt:"
|
||||||
},
|
},
|
||||||
"error_back": {
|
"error_back": {
|
||||||
"en": "Back to the main page",
|
"en": "Back to the main page",
|
||||||
"fr": "Retour à la page principale"
|
"fr": "Retour à la page principale",
|
||||||
|
"de": "Zurück zur Hauptseite"
|
||||||
},
|
},
|
||||||
"error_note1": {
|
"error_note1": {
|
||||||
"en": "We are (probably) aware of this bug, but feel free to contact us if you need assistance.",
|
"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."
|
"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": {
|
"error_note2": {
|
||||||
"en": "Sorry for the inconvenience.",
|
"en": "Sorry for the inconvenience.",
|
||||||
"fr": "Désolés pour les désagréments occasionnés."
|
"fr": "Désolés pour les désagréments occasionnés.",
|
||||||
|
"de": "Entschuldigen Sie die Störung."
|
||||||
},
|
},
|
||||||
"error_forward_req": {
|
"error_forward_req": {
|
||||||
"en": "Error while connecting to the Nextcloud instance.",
|
"en": "Error while connecting to the Nextcloud instance.",
|
||||||
"fr": "Erreur lors de la connexion à l'instance Nextcloud."
|
"fr": "Erreur lors de la connexion à l'instance Nextcloud.",
|
||||||
|
"de": "Fehler beim Verbinden zur Nextcloud Instanz."
|
||||||
},
|
},
|
||||||
"error_forward_resp": {
|
"error_forward_resp": {
|
||||||
"en": "Error while reading Nextcloud instance's response.",
|
"en": "Error while reading Nextcloud instance's response.",
|
||||||
"fr": "Erreur lors de la lecture de la réponse de l'instance Nextcloud."
|
"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": {
|
"error_forward_isanon": {
|
||||||
"en": "Couldn't set the form's isAnonymous value.",
|
"en": "Couldn't set the form's isAnonymous value.",
|
||||||
"fr": "Échec lors de la définition de la valeur isAnonymous du formulaire."
|
"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": {
|
"error_forward_clientresp_newform": {
|
||||||
"en": "Failed to send the response body (new form).",
|
"en": "Failed to send the response body (new form).",
|
||||||
"fr": "Échec lors de l'envoi du corps de la réponse (nouveau formulaire)."
|
"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": {
|
"error_forward_clientresp_std": {
|
||||||
"en": "Failed to send the response body.",
|
"en": "Failed to send the response body.",
|
||||||
"fr": "Échec lors de l'envoi du corps de la réponse."
|
"fr": "Échec lors de l'envoi du corps de la réponse.",
|
||||||
|
"de": "Fehler beim Senden des Response Body."
|
||||||
},
|
},
|
||||||
"error_forwardlogin_db": {
|
"error_forwardlogin_db": {
|
||||||
"en": "Couldn't connect to the local database.",
|
"en": "Couldn't connect to the local database.",
|
||||||
"fr": "Échec lors de la connexion à la base de données locale."
|
"fr": "Échec lors de la connexion à la base de données locale.",
|
||||||
|
"de": "Fehler beim verbinden zur lokalen Datenbank."
|
||||||
},
|
},
|
||||||
"error_forwardlogin_db_get": {
|
"error_forwardlogin_db_get": {
|
||||||
"en": "Error during information retrieval from the local database.",
|
"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."
|
"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": {
|
"error_forwardlogin_notfound": {
|
||||||
"en": "The specified token doesn't exist in local database.",
|
"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."
|
"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": {
|
"error_login_get": {
|
||||||
"en": "The account creation request (GET) to Nextcloud has failed.",
|
"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é."
|
"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": {
|
"error_login_get_body": {
|
||||||
"en": "Reading response from the account creation request to Nextcloud has failed.",
|
"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é."
|
"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": {
|
"error_login_post": {
|
||||||
"en": "The account creation request (POST) to Nextcloud has failed.",
|
"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é."
|
"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": {
|
"error_login_redir": {
|
||||||
"en": "Redirection to Nextcloud account failed.",
|
"en": "Redirection to Nextcloud account failed.",
|
||||||
"fr": "La redirection vers le compte Nextcloud a échoué."
|
"fr": "La redirection vers le compte Nextcloud a échoué.",
|
||||||
|
"de": "Die Weiterleitung zum Nextcloud account hat nicht funktioniert."
|
||||||
},
|
},
|
||||||
"error_createaccount_post": {
|
"error_createaccount_post": {
|
||||||
"en": "Account creation: connection to the Nextcloud API failed.",
|
"en": "Account creation: connection to the Nextcloud API failed.",
|
||||||
"fr": "Création de compte : la connexion à l'API Nextcloud a échoué."
|
"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": {
|
"error_createaccount_post_body": {
|
||||||
"en": "Account creation: reading the answer from the Nextcloud API failed.",
|
"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é."
|
"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": {
|
"error_createaccount_status": {
|
||||||
"en": "The Nextcloud instance responded with an unexpected status code.",
|
"en": "The Nextcloud instance responded with an unexpected status code.",
|
||||||
"fr": "L'instance Nextcloud a répondu avec un code de statut inattendu."
|
"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": {
|
"error_createaccount_ncstatus": {
|
||||||
"en": "The Nextcloud API responded with an unexpected status code.",
|
"en": "The Nextcloud API responded with an unexpected status code.",
|
||||||
"fr": "L'API Nextcloud a répondu avec un code de statut inattendu."
|
"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": {
|
"error_createaccount_ncstatus_parse": {
|
||||||
"en": "Error parsing Nextcloud API's status code.",
|
"en": "Error parsing Nextcloud API's status code.",
|
||||||
"fr": "Erreur lors de la lecture du code de statut de l'API Nextcloud."
|
"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": {
|
"error_forwardregister_pool": {
|
||||||
"en": "Error while connecting to the local database.",
|
"en": "Error while connecting to the local database.",
|
||||||
"fr": "Erreur lors de la connexion à la base de données locale."
|
"fr": "Erreur lors de la connexion à la base de données locale.",
|
||||||
|
"de": "Fehler beim Verbinden zu der lokalen Datenbank."
|
||||||
},
|
},
|
||||||
"error_forwardregister_db": {
|
"error_forwardregister_db": {
|
||||||
"en": "Failed adding the Nextcloud account in the local database.",
|
"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é."
|
"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": {
|
"error_forwardregister_tokenparse": {
|
||||||
"en": "Failed parsing the admin token.",
|
"en": "Failed parsing the admin token.",
|
||||||
"fr": "Échec lors de la lecture du token administrateur."
|
"fr": "Échec lors de la lecture du token administrateur.",
|
||||||
|
"de": "Fehler beim Parsen des Admin Tokens."
|
||||||
},
|
},
|
||||||
"error_login_cookiepair": {
|
"error_login_cookiepair": {
|
||||||
"en": "Couldn't read cookies.",
|
"en": "Couldn't read cookies.",
|
||||||
"fr": "Échec lors de la lecture de cookies."
|
"fr": "Échec lors de la lecture de cookies.",
|
||||||
|
"de": "Fehler beim Lesen der Cookies"
|
||||||
},
|
},
|
||||||
"error_login_regex": {
|
"error_login_regex": {
|
||||||
"en": "Couldn't read the CSRF token.",
|
"en": "Couldn't read the CSRF token.",
|
||||||
"fr": "Échec lors de la lecture du token CSRF."
|
"fr": "Échec lors de la lecture du token CSRF.",
|
||||||
|
"de": "Fehler beim Lesen des CSRF Tokens."
|
||||||
},
|
},
|
||||||
"error_login_setcookie": {
|
"error_login_setcookie": {
|
||||||
"en": "Error during cookies transfer.",
|
"en": "Error during cookies transfer.",
|
||||||
"fr": "Erreur lors du transfert de cookies."
|
"fr": "Erreur lors du transfert de cookies.",
|
||||||
|
"de": "Feheler beim Transfer der Cookies."
|
||||||
},
|
},
|
||||||
"error_form_insert": {
|
"error_form_insert": {
|
||||||
"en": "The local database couldn't be reached.",
|
"en": "The local database couldn't be reached.",
|
||||||
"fr": "Échec de la connexion avec la base de données locale."
|
"fr": "Échec de la connexion avec la base de données locale.",
|
||||||
|
"de": "Die lokale Datenbank ist nicht erreichbar."
|
||||||
},
|
},
|
||||||
"error_createaccount": {
|
"error_createaccount": {
|
||||||
"en": "The Nextcloud API returned an unexpected result.",
|
"en": "The Nextcloud API returned an unexpected result.",
|
||||||
"fr": "L'API de Nextcloud a retourné un résultat inattendu."
|
"fr": "L'API de Nextcloud a retourné un résultat inattendu.",
|
||||||
|
"de": "Die Nextcloud API hat ein unerwartetes Resultat zurückgesendet."
|
||||||
},
|
},
|
||||||
"error_redirect": {
|
"error_redirect": {
|
||||||
"en": "Failed to redirect.",
|
"en": "Failed to redirect.",
|
||||||
"fr": "La redirection a échoué."
|
"fr": "La redirection a échoué.",
|
||||||
|
"de": "Weiterleitung (Redirect) hat nicht funktioniert."
|
||||||
},
|
},
|
||||||
"error_csrf_cookie": {
|
"error_csrf_cookie": {
|
||||||
"en": "Your CSRF token (cookie) seems incorrect, please retry.",
|
"en": "Your CSRF token (cookie) seems incorrect, please retry.",
|
||||||
"fr": "Votre token CSRF (cookie) semble incorrect, veuillez réessayer."
|
"fr": "Votre token CSRF (cookie) semble incorrect, veuillez réessayer.",
|
||||||
|
"de": "Dein CSRF Token (Cookie) scheint inkorrekt, versuchen Sie es erneut."
|
||||||
},
|
},
|
||||||
"error_csrf_token": {
|
"error_csrf_token": {
|
||||||
"en": "Your CSRF token seems incorrect, please retry.",
|
"en": "Your CSRF token seems incorrect, please retry.",
|
||||||
"fr": "Votre token CSRF semble incorrect, veuillez réessayer."
|
"fr": "Votre token CSRF semble incorrect, veuillez réessayer.",
|
||||||
|
"de": "Ihr CSRF Token scheint nicht korrekt, versuchen Sie es erneut. "
|
||||||
},
|
},
|
||||||
"error_dirtyhacker": {
|
"error_dirtyhacker": {
|
||||||
"en": "Attempt to access an unauthorized resource.",
|
"en": "Attempt to access an unauthorized resource.",
|
||||||
"fr": "Tentative d'accès à une ressource non autorisée."
|
"fr": "Tentative d'accès à une ressource non autorisée.",
|
||||||
|
"de": "Zugangs-Versuch einer unauthorisierten Quelle."
|
||||||
},
|
},
|
||||||
"error_tplrender": {
|
"error_tplrender": {
|
||||||
"en": "Template rendering failed.",
|
"en": "Template rendering failed.",
|
||||||
"fr": "Le rendu du template a échoué."
|
"fr": "Le rendu du template a échoué.",
|
||||||
|
"de": "Template rendering hat nicht funktioniert."
|
||||||
},
|
},
|
||||||
"error_tplrender_resp": {
|
"error_tplrender_resp": {
|
||||||
"en": "Sending response failed.",
|
"en": "Sending response failed.",
|
||||||
"fr": "L'envoi de la réponse a échoué."
|
"fr": "L'envoi de la réponse a échoué.",
|
||||||
|
"de": "Senden der Antwort hat nicht funktioniert."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use actix_web::client::Client;
|
use actix_web::client::Client;
|
||||||
use actix_web::{http, web, HttpRequest, HttpResponse};
|
use actix_web::{http, web, HttpRequest, HttpResponse};
|
||||||
use base64::URL_SAFE_NO_PAD;
|
use base64::URL_SAFE_NO_PAD;
|
||||||
|
use percent_encoding::percent_decode_str;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::config::PROXY_TIMEOUT;
|
use crate::config::{ADJ_LIST, NAME_LIST, PROXY_TIMEOUT, USER_AGENT};
|
||||||
use crate::config::{ADJ_LIST, NAME_LIST};
|
|
||||||
use crate::debug;
|
use crate::debug;
|
||||||
use crate::errors::{crash, TrainCrash};
|
use crate::errors::{crash, TrainCrash};
|
||||||
use crate::templates::get_lang;
|
use crate::templates::get_lang;
|
||||||
|
@ -35,23 +36,6 @@ pub fn is_logged_in(req: &HttpRequest) -> Option<&str> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_admintoken(req: &HttpRequest) -> Option<String> {
|
|
||||||
get_cookie(req, "sncf_admin_token")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_csrftoken(req: &HttpRequest) -> Option<String> {
|
|
||||||
get_cookie(req, "sncf_csrf_cookie")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cookie(req: &HttpRequest, cookie_name: &str) -> Option<String> {
|
|
||||||
let c = req.headers().get("Cookie")?.to_str().ok()?;
|
|
||||||
if c.contains(cookie_name) {
|
|
||||||
Some(c.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// attempts to create the account from Nextcloud's API
|
// attempts to create the account from Nextcloud's API
|
||||||
// returns the newly created username.
|
// returns the newly created username.
|
||||||
// if it fails (bad return code), returns None.
|
// if it fails (bad return code), returns None.
|
||||||
|
@ -142,11 +126,11 @@ pub async fn login(
|
||||||
) -> Result<HttpResponse, TrainCrash> {
|
) -> Result<HttpResponse, TrainCrash> {
|
||||||
debug(&format!("Sending forged login for user {}", user));
|
debug(&format!("Sending forged login for user {}", user));
|
||||||
|
|
||||||
// 1. GET /login
|
// 1. GET /csrftoken
|
||||||
let mut login_get = client
|
let mut login_get = client
|
||||||
.get(format!("{}/{}", CONFIG.nextcloud_url, "login"))
|
.get(format!("{}/{}", CONFIG.nextcloud_url, "csrftoken"))
|
||||||
.timeout(Duration::new(PROXY_TIMEOUT, 0))
|
.timeout(Duration::new(PROXY_TIMEOUT, 0))
|
||||||
.header("User-Agent", "Actix-web")
|
.header("User-Agent", USER_AGENT)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
@ -156,20 +140,59 @@ pub async fn login(
|
||||||
|
|
||||||
// rewrite cookie headers from GET to POST
|
// rewrite cookie headers from GET to POST
|
||||||
let mut str_cookiepair = String::new();
|
let mut str_cookiepair = String::new();
|
||||||
for h_value in login_get.headers().get_all("set-cookie") {
|
|
||||||
str_cookiepair = format!(
|
// remove duplicate oc<id> cookie (nextcloud bug)
|
||||||
"{}; {}",
|
// leading to sncf being unable to forge logins
|
||||||
str_cookiepair,
|
let cookie_set = login_get.headers().get_all("set-cookie");
|
||||||
h_value.clone().to_str().map_err(|e| {
|
let mut cookie_map: HashMap<String, String> = HashMap::new();
|
||||||
eprintln!("error_login_cookiepair: {}", e);
|
for c in cookie_set {
|
||||||
crash(get_lang(&req), "error_login_cookiepair")
|
// 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
|
// load requesttoken regex
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref RE: Regex = Regex::new(r#"requesttoken="(?P<token>.*)""#)
|
static ref RE: Regex = Regex::new(r#"\{"token":"(?P<token>[^"]*)"\}"#)
|
||||||
.expect("Error while parsing the requesttoken regex");
|
.expect("Error while parsing the requesttoken regex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +220,7 @@ pub async fn login(
|
||||||
let mut login_post = client
|
let mut login_post = client
|
||||||
.post(format!("{}/{}", CONFIG.nextcloud_url, "login"))
|
.post(format!("{}/{}", CONFIG.nextcloud_url, "login"))
|
||||||
.timeout(Duration::new(PROXY_TIMEOUT, 0))
|
.timeout(Duration::new(PROXY_TIMEOUT, 0))
|
||||||
.header("User-Agent", "Actix-web");
|
.header("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
// include all NC cookies in one cookie (cookie pair)
|
// include all NC cookies in one cookie (cookie pair)
|
||||||
login_post = login_post.header("Cookie", str_cookiepair);
|
login_post = login_post.header("Cookie", str_cookiepair);
|
||||||
|
@ -219,6 +242,7 @@ pub async fn login(
|
||||||
|
|
||||||
// 3. set the same cookies in the user's browser
|
// 3. set the same cookies in the user's browser
|
||||||
let mut user_response = HttpResponse::SeeOther();
|
let mut user_response = HttpResponse::SeeOther();
|
||||||
|
|
||||||
for item in response_post.headers().clone().get_all("set-cookie") {
|
for item in response_post.headers().clone().get_all("set-cookie") {
|
||||||
user_response.header(
|
user_response.header(
|
||||||
"Set-Cookie",
|
"Set-Cookie",
|
||||||
|
@ -282,6 +306,6 @@ pub fn gen_name() -> String {
|
||||||
|
|
||||||
pub fn list_rand(list: &[String]) -> &String {
|
pub fn list_rand(list: &[String]) -> &String {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let roll = rng.gen_range(0, list.len() - 1);
|
let roll = rng.gen_range(0..list.len() - 1);
|
||||||
&list[roll]
|
&list[roll]
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ pub const ADJ_LIST_FILE: &str = "./adj-list.txt";
|
||||||
pub const NAME_LIST_FILE: &str = "./name-list.txt";
|
pub const NAME_LIST_FILE: &str = "./name-list.txt";
|
||||||
pub const LOC_FILE: &str = "./lang.json";
|
pub const LOC_FILE: &str = "./lang.json";
|
||||||
|
|
||||||
|
pub const USER_AGENT: &str = "Actix-web";
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref CONFIG: Config = Config::init();
|
pub static ref CONFIG: Config = Config::init();
|
||||||
pub static ref ADJ_LIST: Vec<String> =
|
pub static ref ADJ_LIST: Vec<String> =
|
||||||
|
|
|
@ -7,8 +7,8 @@ use crate::database::schema::form::dsl::*;
|
||||||
use crate::database::structs::Form;
|
use crate::database::structs::Form;
|
||||||
use crate::DbConn;
|
use crate::DbConn;
|
||||||
|
|
||||||
#[table_name = "form"]
|
|
||||||
#[derive(Serialize, Insertable)]
|
#[derive(Serialize, Insertable)]
|
||||||
|
#[table_name = "form"]
|
||||||
pub struct InsertableForm {
|
pub struct InsertableForm {
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub lastvisit_at: NaiveDateTime,
|
pub lastvisit_at: NaiveDateTime,
|
||||||
|
@ -44,7 +44,7 @@ impl Form {
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert<'b>(
|
pub fn insert(
|
||||||
i_form: InsertableForm,
|
i_form: InsertableForm,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) -> Result<InsertableForm, diesel::result::Error> {
|
) -> Result<InsertableForm, diesel::result::Error> {
|
||||||
|
|
|
@ -4,8 +4,8 @@ use chrono::NaiveDateTime;
|
||||||
use crate::database::schema::form;
|
use crate::database::schema::form;
|
||||||
//use crate::config::CONFIG;
|
//use crate::config::CONFIG;
|
||||||
|
|
||||||
#[table_name = "form"]
|
|
||||||
#[derive(Serialize, Queryable, Insertable, Debug, Clone)]
|
#[derive(Serialize, Queryable, Insertable, Debug, Clone)]
|
||||||
|
#[table_name = "form"]
|
||||||
pub struct Form {
|
pub struct Form {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
|
|
234
src/forward.rs
234
src/forward.rs
|
@ -1,14 +1,14 @@
|
||||||
use actix_web::client::{Client, ClientRequest};
|
use actix_web::client::{Client, ClientRequest};
|
||||||
use actix_web::{http, web, HttpRequest, HttpResponse};
|
use actix_web::{http, web, HttpRequest, HttpResponse};
|
||||||
|
use actix_session::Session;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use regex::Regex;
|
use csrf::{AesGcmCsrfProtection, CsrfProtection};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use csrf::{AesGcmCsrfProtection, CsrfProtection};
|
|
||||||
|
|
||||||
use crate::config::get_csrf_key;
|
|
||||||
use crate::account::*;
|
use crate::account::*;
|
||||||
|
use crate::config::get_csrf_key;
|
||||||
use crate::config::PAYLOAD_LIMIT;
|
use crate::config::PAYLOAD_LIMIT;
|
||||||
use crate::config::PROXY_TIMEOUT;
|
use crate::config::PROXY_TIMEOUT;
|
||||||
use crate::database::methods::InsertableForm;
|
use crate::database::methods::InsertableForm;
|
||||||
|
@ -31,7 +31,14 @@ pub async fn forward(
|
||||||
// if check_route returns true,
|
// if check_route returns true,
|
||||||
// the user supposedly tried to access a restricted page.
|
// the user supposedly tried to access a restricted page.
|
||||||
// They get redirected to the main page.
|
// They get redirected to the main page.
|
||||||
if check_route(route) {
|
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));
|
debug(&format!("Restricted route blocked: {}", route));
|
||||||
return Ok(web_redir("/").await.map_err(|e| {
|
return Ok(web_redir("/").await.map_err(|e| {
|
||||||
eprintln!("error_redirect: {}", e);
|
eprintln!("error_redirect: {}", e);
|
||||||
|
@ -45,8 +52,8 @@ pub async fn forward(
|
||||||
// (prevents the user from sending some specific POST requests)
|
// (prevents the user from sending some specific POST requests)
|
||||||
if check_request(route, &body) {
|
if check_request(route, &body) {
|
||||||
debug(&format!(
|
debug(&format!(
|
||||||
"Restricted request: {}",
|
"Restricted request: {}",
|
||||||
String::from_utf8_lossy(&body)
|
String::from_utf8_lossy(&body)
|
||||||
));
|
));
|
||||||
return Err(crash(get_lang(&req), "error_dirtyhacker"));
|
return Err(crash(get_lang(&req), "error_dirtyhacker"));
|
||||||
}
|
}
|
||||||
|
@ -63,8 +70,8 @@ pub async fn forward(
|
||||||
// and basic-auth, because this feature is not needed.
|
// and basic-auth, because this feature is not needed.
|
||||||
for (header_name, header_value) in res
|
for (header_name, header_value) in res
|
||||||
.headers()
|
.headers()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(h, _)| *h != "connection" && *h != "content-encoding")
|
.filter(|(h, _)| *h != "connection" && *h != "content-encoding")
|
||||||
{
|
{
|
||||||
client_resp.header(header_name.clone(), header_value.clone());
|
client_resp.header(header_name.clone(), header_value.clone());
|
||||||
}
|
}
|
||||||
|
@ -72,7 +79,7 @@ pub async fn forward(
|
||||||
// sparing the use of a mutable body when not needed
|
// sparing the use of a mutable body when not needed
|
||||||
// For now, the body only needs to be modified when the route
|
// For now, the body only needs to be modified when the route
|
||||||
// is "create a new form" route
|
// is "create a new form" route
|
||||||
if route == "/ocs/v2.php/apps/forms/api/v1/form" {
|
if route == "/ocs/v2.php/apps/forms/api/v1.1/form" {
|
||||||
// retreive the body from the request result
|
// retreive the body from the request result
|
||||||
let response_body = res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| {
|
let response_body = res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| {
|
||||||
eprintln!("error_forward_resp: {}", e);
|
eprintln!("error_forward_resp: {}", e);
|
||||||
|
@ -84,8 +91,8 @@ pub async fn forward(
|
||||||
let form_id = check_new_form(&response_body);
|
let form_id = check_new_form(&response_body);
|
||||||
if form_id > 0 {
|
if form_id > 0 {
|
||||||
debug(&format!(
|
debug(&format!(
|
||||||
"New form. Forging request to set isAnonymous for id {}",
|
"New form. Forging request to set isAnonymous for id {}",
|
||||||
form_id
|
form_id
|
||||||
));
|
));
|
||||||
|
|
||||||
let forged_body = format!(
|
let forged_body = format!(
|
||||||
|
@ -93,13 +100,13 @@ pub async fn forward(
|
||||||
form_id
|
form_id
|
||||||
);
|
);
|
||||||
let update_req = forge_from(
|
let update_req = forge_from(
|
||||||
"/ocs/v2.php/apps/forms/api/v1/form/update",
|
"/ocs/v2.php/apps/forms/api/v1.1/form/update",
|
||||||
&req,
|
&req,
|
||||||
&url,
|
&url,
|
||||||
&client,
|
&client,
|
||||||
)
|
)
|
||||||
.set_header("content-length", forged_body.len())
|
.set_header("content-length", forged_body.len())
|
||||||
.set_header("content-type", "application/json;charset=utf-8");
|
.set_header("content-type", "application/json;charset=utf-8");
|
||||||
|
|
||||||
let res = update_req.send_body(forged_body).await.map_err(|e| {
|
let res = update_req.send_body(forged_body).await.map_err(|e| {
|
||||||
eprintln!("error_forward_isanon: {}", e);
|
eprintln!("error_forward_isanon: {}", e);
|
||||||
|
@ -122,8 +129,8 @@ pub async fn forward(
|
||||||
|
|
||||||
// check the response before returning it (unused)
|
// check the response before returning it (unused)
|
||||||
/*if check_response(route, &response_body) {
|
/*if check_response(route, &response_body) {
|
||||||
return Ok(web_redir("/"));
|
return Ok(web_redir("/"));
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -138,17 +145,11 @@ pub struct CsrfToken {
|
||||||
|
|
||||||
pub async fn forward_login(
|
pub async fn forward_login(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
s: Session,
|
||||||
params: web::Path<LoginToken>,
|
params: web::Path<LoginToken>,
|
||||||
client: web::Data<Client>,
|
client: web::Data<Client>,
|
||||||
dbpool: web::Data<DbPool>,
|
dbpool: web::Data<DbPool>,
|
||||||
) -> Result<HttpResponse, TrainCrash> {
|
) -> 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.
|
// check if the provided token seems valid. If not, early return.
|
||||||
if !check_token(¶ms.token) {
|
if !check_token(¶ms.token) {
|
||||||
|
@ -162,6 +163,7 @@ pub async fn forward_login(
|
||||||
crash(get_lang(&req), "error_forwardlogin_db")
|
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.
|
// 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))
|
let formdata = web::block(move || Form::get_from_token(¶ms.token, &conn))
|
||||||
.await
|
.await
|
||||||
|
@ -169,106 +171,76 @@ pub async fn forward_login(
|
||||||
eprintln!("error_forwardlogin_db_get (diesel error): {}", e);
|
eprintln!("error_forwardlogin_db_get (diesel error): {}", e);
|
||||||
crash(get_lang(&req), "error_forwardlogin_db_get")
|
crash(get_lang(&req), "error_forwardlogin_db_get")
|
||||||
})?
|
})?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
debug("Token not found.");
|
debug("error: Token not found.");
|
||||||
crash(get_lang(&req), "error_forwardlogin_notfound")
|
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")
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// else, try to log the user in with DB data, then redirect.
|
// try to log the user in with DB data, then redirect.
|
||||||
login(&client, &req, &formdata.nc_username, &formdata.nc_password).await
|
login(&client, &req, &formdata.nc_username, &formdata.nc_password).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// creates a NC account using a random name and password.
|
// creates a NC account using a random name and password.
|
||||||
// the account gets associated with a token in sqlite DB.
|
// the account gets associated with a token in sqlite DB.
|
||||||
|
// POST /link route
|
||||||
pub async fn forward_register(
|
pub async fn forward_register(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
s: Session,
|
||||||
csrf_post: web::Form<CsrfToken>,
|
csrf_post: web::Form<CsrfToken>,
|
||||||
client: web::Data<Client>,
|
client: web::Data<Client>,
|
||||||
dbpool: web::Data<DbPool>,
|
dbpool: web::Data<DbPool>,
|
||||||
) -> Result<HttpResponse, TrainCrash> {
|
) -> Result<HttpResponse, TrainCrash> {
|
||||||
let lang = get_lang(&req);
|
let lang = get_lang(&req);
|
||||||
|
|
||||||
// if the user is already logged in, redirect to the Forms app
|
// do not check for existing admin tokens and force a new registration
|
||||||
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
|
// check if the csrf token is OK
|
||||||
if let Some(cookie_token) = has_csrftoken(&req) {
|
let cookie_csrf_token = s.get::<String>("sncf_csrf_token").map_err(|e| {
|
||||||
lazy_static! {
|
eprintln!("error_csrf_cookie: {}", e);
|
||||||
static ref RE: Regex = Regex::new(r#"sncf_csrf_cookie=(?P<token>[0-9A-Za-z_\-]*)"#)
|
crash(get_lang(&req), "error_csrf_cookie")
|
||||||
.expect("Error while parsing the sncf_csrf_cookie regex");
|
})?;
|
||||||
}
|
if let Some(cookie_token) = cookie_csrf_token {
|
||||||
let cookie_csrf_token = RE
|
let raw_ctoken =
|
||||||
.captures(&cookie_token)
|
base64::decode_config(cookie_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err(
|
||||||
.ok_or_else(|| {
|
|e| {
|
||||||
eprintln!("error_csrf_cookie: no capture");
|
eprintln!("error_csrf_cookie (base64): {}", e);
|
||||||
crash(get_lang(&req), "error_csrf_cookie")
|
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| {
|
let raw_token =
|
||||||
eprintln!("error_csrf_cookie (base64): {}", e);
|
base64::decode_config(csrf_post.csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD)
|
||||||
crash(get_lang(&req), "error_csrf_cookie")
|
.map_err(|e| {
|
||||||
})?;
|
eprintln!("error_csrf_token (base64): {}", e);
|
||||||
|
crash(get_lang(&req), "error_csrf_token")
|
||||||
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 seed = AesGcmCsrfProtection::from_key(get_csrf_key());
|
||||||
let parsed_token = seed.parse_token(&raw_token).expect("token not parsed");
|
let parsed_token = seed.parse_token(&raw_token).expect("error: token not parsed");
|
||||||
let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("cookie not parsed");
|
let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("error: cookie not parsed");
|
||||||
if !seed.verify_token_pair(&parsed_token, &parsed_cookie) {
|
if !seed.verify_token_pair(&parsed_token, &parsed_cookie) {
|
||||||
debug("warn: CSRF token doesn't match.");
|
debug("warn: CSRF token doesn't match.");
|
||||||
return Err(crash(lang, "error_csrf_token"));
|
return Err(crash(lang, "error_csrf_token"));
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
debug("warn: missing CSRF token.");
|
debug("warn: missing CSRF token.");
|
||||||
return Err(crash(lang, "error_csrf_cookie"));
|
return Err(crash(lang, "error_csrf_cookie"));
|
||||||
}
|
}
|
||||||
|
@ -291,39 +263,41 @@ pub async fn forward_register(
|
||||||
let token_mv = token.clone();
|
let token_mv = token.clone();
|
||||||
|
|
||||||
// store the result in DB
|
// store the result in DB
|
||||||
let form_result = web::block(move || Form::insert(
|
let form_result = web::block(move || {
|
||||||
InsertableForm {
|
Form::insert(
|
||||||
created_at: Utc::now().naive_utc(),
|
InsertableForm {
|
||||||
lastvisit_at: Utc::now().naive_utc(),
|
created_at: Utc::now().naive_utc(),
|
||||||
token: token_mv,
|
lastvisit_at: Utc::now().naive_utc(),
|
||||||
nc_username,
|
token: token_mv,
|
||||||
nc_password,
|
nc_username,
|
||||||
},
|
nc_password,
|
||||||
&conn,
|
},
|
||||||
))
|
&conn,
|
||||||
|
)
|
||||||
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if form_result.is_err() {
|
if form_result.is_err() {
|
||||||
return Err(crash(lang, "error_forwardregister_db"));
|
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()
|
Ok(HttpResponse::Ok()
|
||||||
.content_type("text/html")
|
.content_type("text/html")
|
||||||
.set_header(
|
|
||||||
"Set-Cookie",
|
|
||||||
format!("sncf_admin_token={}; HttpOnly; SameSite=Strict", &token),
|
|
||||||
)
|
|
||||||
.body(
|
.body(
|
||||||
TplLink {
|
TplLink {
|
||||||
lang: &lang,
|
lang: &lang,
|
||||||
admin_token: &token,
|
admin_token: &token,
|
||||||
config: &CONFIG,
|
config: &CONFIG,
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
eprintln!("error_tplrender (TplLink): {}", e);
|
eprintln!("error_tplrender (TplLink): {}", e);
|
||||||
crash(lang.clone(), "error_tplrender")
|
crash(lang.clone(), "error_tplrender")
|
||||||
})?,
|
})?,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
@ -364,22 +338,28 @@ fn web_redir(location: &str) -> HttpResponse {
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn index(req: HttpRequest) -> Result<HttpResponse, TrainCrash> {
|
pub async fn index(req: HttpRequest, s: Session) -> Result<HttpResponse, TrainCrash> {
|
||||||
|
|
||||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key());
|
let seed = AesGcmCsrfProtection::from_key(get_csrf_key());
|
||||||
let (csrf_token, csrf_cookie) = seed.generate_token_pair(None, 43200)
|
let (csrf_token, csrf_cookie) = seed
|
||||||
|
.generate_token_pair(None, 43200)
|
||||||
.expect("couldn't generate token/cookie pair");
|
.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()
|
Ok(HttpResponse::Ok()
|
||||||
.content_type("text/html")
|
.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(
|
.body(
|
||||||
TplIndex {
|
TplIndex {
|
||||||
lang: &get_lang(&req),
|
lang: &get_lang(&req),
|
||||||
csrf_token: &base64::encode_config(&csrf_token.value(), base64::URL_SAFE_NO_PAD),
|
csrf_token: &base64::encode_config(&csrf_token.value(), base64::URL_SAFE_NO_PAD),
|
||||||
|
sncf_admin_token: cookie_admin_token,
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|
14
src/main.rs
14
src/main.rs
|
@ -7,6 +7,9 @@ extern crate diesel;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel_migrations;
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
|
|
||||||
|
use actix_session::CookieSession;
|
||||||
|
use actix_web::cookie::SameSite;
|
||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
use actix_web::client::Client;
|
use actix_web::client::Client;
|
||||||
use actix_web::{web, App, FromRequest, HttpServer};
|
use actix_web::{web, App, FromRequest, HttpServer};
|
||||||
|
@ -49,7 +52,7 @@ embed_migrations!("migrations/mysql");
|
||||||
|
|
||||||
type DbPool = r2d2::Pool<ConnectionManager<DbConn>>;
|
type DbPool = r2d2::Pool<ConnectionManager<DbConn>>;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
/* std::env::set_var("RUST_LOG", "actix_web=debug");
|
/* std::env::set_var("RUST_LOG", "actix_web=debug");
|
||||||
env_logger::init();*/
|
env_logger::init();*/
|
||||||
|
@ -59,7 +62,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
println!("Checking configuration file...");
|
println!("Checking configuration file...");
|
||||||
CONFIG.check_version();
|
CONFIG.check_version();
|
||||||
|
|
||||||
if CONFIG.database_path.len() == 0 {
|
if CONFIG.database_path.is_empty() {
|
||||||
println!("No database specified. Please enter a MySQL, PostgreSQL or SQLite connection string in config.toml.");
|
println!("No database specified. Please enter a MySQL, PostgreSQL or SQLite connection string in config.toml.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,6 +92,13 @@ async fn main() -> std::io::Result<()> {
|
||||||
.data(pool.clone())
|
.data(pool.clone())
|
||||||
.data(Client::new())
|
.data(Client::new())
|
||||||
.data(forward_url.clone())
|
.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("/mimolette", web::get().to(login))*/
|
||||||
/*.route("/login", web::post().to(forward))*/
|
/*.route("/login", web::post().to(forward))*/
|
||||||
/*.wrap(middleware::Compress::default())*/
|
/*.wrap(middleware::Compress::default())*/
|
||||||
|
|
18
src/sniff.rs
18
src/sniff.rs
|
@ -7,7 +7,7 @@ use crate::debug;
|
||||||
// if it returns true, cancels the request
|
// if it returns true, cancels the request
|
||||||
pub fn check_request(route: &str, body: &web::Bytes) -> bool {
|
pub fn check_request(route: &str, body: &web::Bytes) -> bool {
|
||||||
match route {
|
match route {
|
||||||
"/ocs/v2.php/apps/forms/api/v1/form/update" => rq_form_update(body),
|
"/ocs/v2.php/apps/forms/api/v1.1/form/update" => rq_form_update(body),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@ fn rq_form_update(body: &web::Bytes) -> bool {
|
||||||
// this part may need code quality improvements
|
// this part may need code quality improvements
|
||||||
// the body MUST come from the "create new form" route
|
// the body MUST come from the "create new form" route
|
||||||
// (this is checked upstream)
|
// (this is checked upstream)
|
||||||
|
// returns the form UID and the request body
|
||||||
pub fn check_new_form(body: &web::Bytes) -> u64 {
|
pub fn check_new_form(body: &web::Bytes) -> u64 {
|
||||||
let req = String::from_utf8_lossy(body);
|
let req = String::from_utf8_lossy(body);
|
||||||
|
|
||||||
|
@ -51,12 +52,19 @@ pub fn check_new_form(body: &web::Bytes) -> u64 {
|
||||||
Value::Null
|
Value::Null
|
||||||
});
|
});
|
||||||
|
|
||||||
if v != Value::Null && v["id"] != Value::Null && v["isAnonymous"] == Value::Null {
|
if v != Value::Null
|
||||||
v["id"].as_u64().unwrap_or_else(|| {
|
&& 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);
|
eprintln!("check_new_form: failed to parse formid: {}", v);
|
||||||
0
|
0
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
eprintln!("error: check_new_form: can't find formid: {}", v);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,11 +76,11 @@ const BLOCKED_ROUTES: &[&str] = &[
|
||||||
"/settings",
|
"/settings",
|
||||||
"/ocs/v",
|
"/ocs/v",
|
||||||
"/remote.php",
|
"/remote.php",
|
||||||
"/apps/files",
|
"/core/templates/filepicker.html",
|
||||||
];
|
];
|
||||||
|
|
||||||
// ...except if they are in this list
|
// ...except if they are in this list
|
||||||
const ALLOWED_ROUTES: &[&str] = &["/ocs/v2.php/apps/forms/"];
|
const ALLOWED_ROUTES: &[&str] = &["/ocs/v2.php/apps/forms/", "/status.php"];
|
||||||
|
|
||||||
// checks if the accessed route is allowed for the user.
|
// checks if the accessed route is allowed for the user.
|
||||||
// if it returns true, redirects elsewhere
|
// if it returns true, redirects elsewhere
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::config::Config;
|
||||||
pub struct TplIndex<'a> {
|
pub struct TplIndex<'a> {
|
||||||
pub lang: &'a str,
|
pub lang: &'a str,
|
||||||
pub csrf_token: &'a str,
|
pub csrf_token: &'a str,
|
||||||
|
pub sncf_admin_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
|
@ -5,6 +5,26 @@
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
scrollbar-color: #4684f9 #c8dbfd;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #568aec;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
@ -144,6 +164,10 @@ body, html {
|
||||||
transition: all .25s ease-in-out;
|
transition: all .25s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ncstyle-button:not(:last-child) {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.margin-bottom {
|
.margin-bottom {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
36
templates/assets/index.js
Normal file
36
templates/assets/index.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// on clicking Previous button in browser, reset the page
|
||||||
|
// needed to get another CSRF token and remove the spinning wheel
|
||||||
|
window.onpageshow = function() {
|
||||||
|
if (performance.getEntriesByType("navigation")[0].type == "back_forward") {
|
||||||
|
location.reload(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let browse_forms_button = get('browse_forms_button');
|
||||||
|
let new_link_button = get('new_link_button');
|
||||||
|
|
||||||
|
// csrf_token is retrieved from server-side template
|
||||||
|
new_link_button.addEventListener('click', function() {
|
||||||
|
get("csrf_token").value = csrf_token;
|
||||||
|
get("new_link").submit();
|
||||||
|
hideButtonsAndSpin();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (browse_forms_button != undefined) {
|
||||||
|
browse_forms_button.addEventListener('click', function () {
|
||||||
|
hideButtonsAndSpin();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideButtonsAndSpin() {
|
||||||
|
new_link_button.classList.add("hidden");
|
||||||
|
// hide the access forms button if it exists
|
||||||
|
if (browse_forms_button != undefined) {
|
||||||
|
browse_forms_button.classList.add("hidden");
|
||||||
|
}
|
||||||
|
get('loading_ring').classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(elemId) {
|
||||||
|
return document.getElementById(elemId);
|
||||||
|
}
|
|
@ -10,26 +10,8 @@
|
||||||
<link rel="stylesheet" href="/assets/index.css?v=1.2" />
|
<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/cloud.css?v=1.0" />
|
||||||
<noscript><style> .jsonly { display: none } </style></noscript>
|
<noscript><style> .jsonly { display: none } </style></noscript>
|
||||||
<script>
|
<script>const csrf_token = "{{ csrf_token }}";</script>
|
||||||
/* junk javascript with basic spambot protection features.
|
<script src="/assets/index.js" defer></script>
|
||||||
Drunk indentation is vim's fault.
|
|
||||||
unsatisifed? Please make a PR. : ) */
|
|
||||||
window.onload = function() {
|
|
||||||
// retrieved from server-side template
|
|
||||||
let csrf_token = "{{ csrf_token }}";
|
|
||||||
document.getElementById('new_link_button').addEventListener('click', function () {
|
|
||||||
new_link(csrf_token);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function new_link(csrf) {
|
|
||||||
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="flex page-heading fullheight">
|
<div class="flex page-heading fullheight">
|
||||||
|
@ -48,9 +30,13 @@
|
||||||
<noscript>
|
<noscript>
|
||||||
<a class="ncstyle-button margin-bottom">{{ "index_nojs"|tr(lang) }}</a>
|
<a class="ncstyle-button margin-bottom">{{ "index_nojs"|tr(lang) }}</a>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
{% if sncf_admin_token.is_some() %}
|
||||||
|
<a id="browse_forms_button" href="/admin/{{ sncf_admin_token.as_ref().unwrap() }}" class="ncstyle-button margin-bottom">{{ "index_continueform_button"|tr(lang) }}</a>
|
||||||
|
{% endif %}
|
||||||
<form id="new_link" action="/link" method="post">
|
<form id="new_link" action="/link" method="post">
|
||||||
<input id="csrf_token" name="csrf_token" type="text" class="hidden">
|
<input id="csrf_token" name="csrf_token" type="text" class="hidden">
|
||||||
<a id="new_link_button" class="click jsonly ncstyle-button margin-bottom">{{ "index_createform_button"|tr(lang) }}</a> </form>
|
<a id="new_link_button" class="click jsonly ncstyle-button margin-bottom">{{ "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 id="loading_ring" class="hidden lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="scroll-down-link scroll-down-arrow"></a>
|
<a class="scroll-down-link scroll-down-arrow"></a>
|
||||||
|
@ -66,7 +52,7 @@
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<p>{{ "index_disclaimer1"|tr(lang) }}</p>
|
<p>{{ "index_disclaimer1"|tr(lang) }}</p>
|
||||||
<p>{{ "index_disclaimer2"|tr(lang) }}<a href="https://42l.fr/Faire-un-don">{{ "index_disclaimer2_link_org"|tr(lang) }}</a>{{ "index_disclaimer2_or"|tr(lang) }}<a href="https://www.bountysource.com/teams/nextcloud">{{ "index_disclaimer2_nc"|tr(lang) }}</a>.</p>
|
<p>{{ "index_disclaimer2"|tr(lang) }}<a href="https://42l.fr/Faire-un-don">{{ "index_disclaimer2_link_org"|tr(lang) }}</a>{{ "index_disclaimer2_or"|tr(lang) }}<a href="https://nextcloud.com/include/">{{ "index_disclaimer2_nc"|tr(lang) }}</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -125,7 +111,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="c-subelem">
|
<div class="c-subelem">
|
||||||
<h3>{{ "index_panel6_title"|tr(lang) }}</h3>
|
<h3>{{ "index_panel6_title"|tr(lang) }}</h3>
|
||||||
<p>{{ "index_panel5_desc1"|tr(lang) }}</p>
|
<p>{{ "index_panel6_desc1"|tr(lang) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue