#### Imports
import jinja2 as j2
from flask import (
Flask,
render_template,
render_template_string,
request,
redirect,
flash,
send_file,
)
from markupsafe import Markup # No longer imported from Flask
# Form libraries
from flask_wtf import FlaskForm
from wtforms import (
StringField,
RadioField,
SelectField,
SubmitField,
TextAreaField,
BooleanField,
SelectMultipleField,
)
from wtforms.validators import (
DataRequired,
InputRequired,
Optional,
Regexp,
URL,
Length,
)
# Markdown to HTML - for debugging purposes
from misaka import Markdown, HtmlRenderer
# Managing zipfiles
import zipfile
from flask_cors import CORS
from urllib import parse
from secrets import token_urlsafe
#### GLOBAL VARIABLES
YOLOGEN_VERSION = "0.7.5"
GENERATOR_DICT = {"GENERATOR_VERSION": YOLOGEN_VERSION}
#### Create FLASK and Jinja Environments
app = Flask(__name__)
app.config["SECRET_KEY"] = token_urlsafe(16) # Necessary for the form CORS
cors = CORS(app)
environment = j2.Environment(loader=j2.FileSystemLoader("templates/"))
#### Custom functions
# Define custom filter
@app.template_filter("render_markdown")
def render_markdown(text):
renderer = HtmlRenderer()
markdown = Markdown(renderer)
return markdown(text)
# Add custom filter
j2.filters.FILTERS["render_markdown"] = render_markdown
# Converting markdown to html
def markdown_file_to_html_string(file):
with open(file, "r") as file:
markdown_content = file.read()
# Convert content from Markdown to HTML
html_content = render_markdown(markdown_content)
# Return Markdown and HTML contents
return markdown_content, html_content
### Forms
## PHP forms
class Form_PHP_Config(FlaskForm):
php_config_file = SelectField(
"Type de fichier PHP :",
choices=[
(
"php-fpm.conf",
Markup(
'Fichier de configuration PHP complet (php-fpm.conf)'
),
),
(
"php_extra-fpm.conf",
Markup(
'Fichier de configuration PHP particulier (extra_php-fpm.conf)'
),
),
],
default="php_extra-fpm.conf",
validators=[DataRequired()],
)
## TODO : figure out how to include these comments/title values
# 'title': 'Remplace la configuration générée par défaut par un fichier de configuration complet. À éviter si possible.
# 'title': "Choisir un fichier permettant un paramétrage d'options complémentaires. C'est généralement recommandé."
php_config_file_content = TextAreaField(
"Saisissez le contenu du fichier de configuration PHP :",
validators=[Optional()],
render_kw={
"class": "form-control",
"style": "width: 50%;height:11em;min-height: 5.5em; max-height: 55em;flex-grow: 1;box-sizing: border-box;",
"title": "TODO",
"placeholder": "; Additional php.ini defines, specific to this pool of workers. \n\nphp_admin_value[upload_max_filesize] = 100M \nphp_admin_value[post_max_size] = 100M",
},
)
class Form_PHP(Form_PHP_Config):
use_php = BooleanField("Nécessite PHP", default=False)
## NodeJS forms
class Form_NodeJS(FlaskForm):
use_nodejs = BooleanField("Nécessite NodeJS", default=False)
use_nodejs_version = StringField(
"Version de NodeJS :",
render_kw={
"placeholder": "20",
"class": "form-control",
"title": "Saisissez la version de NodeJS à installer. Cela peut-être une version majeure (ex: 20) ou plus précise (ex: 20.1).",
},
) # TODO : this should be validated using a regex, should be only numbers and any (≥0) number of dots in between
use_nodejs_needs_yarn = BooleanField(
"Nécessite Yarn",
default=False,
render_kw={
"title": "Faut-il installer automatiquement Yarn ? Cela configurera les dépôts spécifiques à Yarn."
},
)
## Python forms
class Form_Python(FlaskForm):
use_python = BooleanField(
"Nécessite Python", default=False
) ## TODO -> python3, python3-pip, python3-ven dependencies by default
python_dependencies_type = SelectField(
"Configuration des dépendances Python :",
choices=[
("requirements.txt", Markup("Fichier requirements.txt")),
("manual_list", "Liste manuelle"),
],
default="requirements.txt",
validators=[DataRequired(), Optional()],
)
python_requirements = TextAreaField(
"La liste de dépendances inclue dans le fichier requirements.txt :",
render_kw={
"class": "form-control",
"style": "width: 50%;height:5.5em;min-height: 5.5em; max-height: 55em;flex-grow: 1;box-sizing: border-box;",
"title": "Lister les dépendances à installer, une par ligne, avec un numéro de version derrière.\nEx: 'dépendance==1.0'.",
"placeholder": "tensorflow==2.3.1 \nuvicorn==0.12.2 \nfastapi==0.63.0",
},
)
python_dependencies_list = StringField(
"Liste de dépendances python :",
render_kw={
"placeholder": "tensorflow uvicorn fastapi",
"class": "form-control",
"title": "Lister les dépendances à installer, séparées d'un espace.",
},
)
## Manifest form
# Dependencies form
class DependenciesForm(FlaskForm):
auto_update = BooleanField(
"Activer le robot de mise à jour automatiques :",
default=False,
render_kw={
"title": "Si le logiciel est disponible sur github et publie des releases ou des tags pour ses nouvelles versions, un robot proposera automatiquement des mises à jours."
},
)
## TODO
# These infos are used by https://github.com/YunoHost/apps/blob/master/tools/autoupdate_app_sources/autoupdate_app_sources.py
# to auto-update the previous asset urls and sha256sum + manifest version
# assuming the upstream's code repo is on github and relies on tags or releases
# See the 'sources' resource documentation for more details
# autoupdate.strategy = "latest_github_tag"
dependencies = StringField(
"Dépendances de l'application (liste des paquets apt) à installer :",
render_kw={
"placeholder": "foo foo2.1-ext somerandomdep",
"class": "form-control",
"title": "Lister les paquets dont dépend l'application, séparés par un espace.",
},
)
use_db = SelectField(
"Configurer une base de données :",
choices=[
("false", "Non"),
("mysql", "MySQL/MariaDB"),
("postgresql", "PostgreSQL"),
],
default="false",
render_kw={"title": "L'application nécessite-t-elle une base de données ?"},
)
# manifest
class manifestForm(DependenciesForm):
version = StringField(
"Version",
validators=[Regexp("\d{1,4}.\d{1,4}(.\d{1,4})?(.\d{1,4})?~ynh\d+")],
render_kw={"class": "form-control", "placeholder": "1.0~ynh1"},
)
description_en = TextAreaField(
"Description en quelques lignes de l'application, en anglais :",
validators=[DataRequired()],
render_kw={
"class": "form-control",
"style": "resize: none;",
"title": "Explain in *a few (10~15) words* the purpose of the app \\"
"or what it actually does (it is meant to give a rough idea to users browsing a catalog of 100+ apps)",
},
)
description_fr = TextAreaField(
"Description en quelques lignes de l'application :",
validators=[DataRequired()],
render_kw={
"class": "form-control",
"style": "resize: none;",
"title": "Expliquez en *quelques* (10~15) mots l'utilité de l'app \\"
"ou ce qu'elle fait (l'objectif est de donner une idée grossière pour des utilisateurs qui naviguent dans un catalogue de 100+ apps)",
},
)
# TODO : handle multiple names separated by commas (.split(',') ?
maintainers = StringField(
"Mainteneurs et mainteneuses",
render_kw={
"class": "form-control",
"placeholder": "Généralement vous mettez votre nom ici… Si vous êtes d'accord ;)",
},
) # TODO : Usually you put your name here… if you like ;)
architectures = SelectMultipleField(
"Architectures supportées :",
choices=[
("all", "Toutes les architectures"),
("amd64", "amd64"),
("i386", "i386"),
("armhf", "armhf"),
("arm64", "arm64"),
],
default=["all"],
validators=[DataRequired()],
)
yunohost_required_version = StringField(
"Mainteneurs et mainteneuses",
render_kw={
"class": "form-control",
"placeholder": "11.1.21",
"title": "Version minimale de Yunohost pour que l'application fonctionne.",
},
)
multi_instance = BooleanField(
"Application multi-instance",
default=False,
render_kw={
"class": "",
"title": "Peux-t-on installer simultannément plusieurs fois l'application sur un même serveur ?",
},
)
ldap = SelectField(
"Integrate with LDAP (user can login using Yunohost credentials :",
choices=[
("false", "False"),
("true", "True"),
("not_relevant", "Not relevant"),
],
default="not_relevant",
validators=[DataRequired()],
render_kw={
"title": """Not to confuse with the "sso" key: the "ldap" key corresponds to wether or not a user *can* login on the app using its YunoHost credentials."""
},
)
sso = SelectField(
"Integrate with Yunohost SingleSignOn (SSO) :",
choices=[
("false", "False"),
("true", "True"),
("not_relevant", "Not relevant"),
],
default="not_relevant",
validators=[DataRequired()],
render_kw={
"title": """Not to confuse with the "ldap" key: the "sso" key corresponds to wether or not a user is *automatically logged-in* on the app when logged-in on the YunoHost portal."""
},
)
license = StringField(
"Licence",
validators=[DataRequired()],
render_kw={"class": "form-control", "placeholder": "GPL"},
)
website = StringField(
"Site web",
validators=[URL(), Optional()],
render_kw={
"class": "form-control",
"placeholder": "https://awesome-app-website.com",
},
)
demo = StringField(
"Site de démonstration",
validators=[URL(), Optional()],
render_kw={
"class": "form-control",
"placeholder": "https://awesome-app-website.com/demo",
},
)
admindoc = StringField(
"Documentation d'aministration",
validators=[URL(), Optional()],
render_kw={
"class": "form-control",
"placeholder": "https://awesome-app-website.com/doc/admin",
},
)
userdoc = StringField(
"Documentation d'utilisation",
validators=[URL(), Optional()],
render_kw={
"class": "form-control",
"placeholder": "https://awesome-app-website.com/doc/user",
},
)
code = StringField(
"Dépôt de code",
validators=[URL(), Optional()],
render_kw={
"class": "form-control",
"placeholder": "https://awesome-app-website.com/get-the-code",
},
)
data_dir = BooleanField(
"L'application nécessite un répertoire dédié pour ses données",
default=False,
render_kw={
"title": "Faut-il créer un répertoire /home/yunohost.app/votreApplication ?"
},
)
data_subdirs = StringField(
"Si nécessaire, lister les sous-répertoires à configurer :",
validators=[Optional()],
render_kw={"class": "form-control", "placeholder": "data, uploads, themes"},
)
use_whole_domain = BooleanField(
"L'application nécessite d'utiliser tout un domaine (installation à la racine) :",
default=False,
render_kw={
"title": "Doit-on installer l'application à la racine du domaine ? Sinon, on pourra l'installer dans un sous-dossier, par exemple /mon_app."
},
)
supports_change_url = BooleanField(
"L'application autorise le changement d'adresse (changement de domaine ou de chemin)",
default=True,
render_kw={
"title": "Faut-il permettre le changement d'URL pour l'application ? (fichier change_url)"
},
)
needs_admin = BooleanField(
"L'application nécessite de configurer un compte d'administration :",
default=False,
render_kw={
"class": "",
"title": "Faut-il configurer un compte admin à l'installation ?",
},
)
# admin_password_help_message = BooleanField("TODO :", default=False,
# render_kw={"class": "",
# "title": "TODO"})
language = SelectMultipleField(
"Langues supportées :",
choices=[
("en", "English"),
("fr", "Français"),
("en", "Spanish"),
("it", "Italian"),
("de", "German"),
("zh", "Chinese"),
("jp", "Japanese"),
("da", "Danish"),
("pt", "Portugese"),
("nl", "Dutch"),
("ru", "Russian"),
],
default=["en"],
validators=[DataRequired()],
)
default_language = SelectField(
"Langues par défaut :",
choices=[
("en", "English"),
("fr", "Français"),
("en", "Spanish"),
("it", "Italian"),
("zh", "Chinese"),
("jp", "Japanese"),
("da", "Danish"),
("pt", "Portugese"),
("nl", "Dutch"),
("ru", "Russian"),
],
default=["en"],
)
visibility = RadioField(
"Visibilité de l'application :",
choices=[
("admin", "Administrateur/administratrice uniquement"),
("all_users", "Personnes connectées"),
("visitors", "Publique"),
],
default="all_users",
validators=[DataRequired()],
)
source_url = StringField(
"Code source ou exécutable de l'application",
validators=[DataRequired(), URL()],
render_kw={
"class": "form-control",
"placeholder": "https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz",
},
) # Application source code URL
sha256sum = StringField(
"Empreinte du code source (format sha256sum)",
validators=[DataRequired(), Length(min=64, max=64)],
render_kw={
"class": "form-control",
"placeholder": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"title": "Sha256sum of the archive. Should be 64 characters-long.",
},
) # Source code hash (sha256sum format)
## Main form
class appGeneratorForm(
manifestForm, DependenciesForm, Form_PHP, Form_NodeJS, Form_Python
):
app_name = StringField(
"Nom de l'application :",
validators=[DataRequired()],
render_kw={
"placeholder": "My Great App",
"class": "form-control",
"title": "Définir le nom de l'application, affiché dans l'interface",
},
)
app_id = StringField(
Markup(
"""Identifiant (id) de l'application (en minuscule et sans espaces) :"""
),
validators=[DataRequired(), Regexp("[a-z_1-9]+.*(?