#### 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
url_prefix = ''
# url_prefix = '/yunohost-app-generator'
# app = Flask(__name__)
app = Flask(__name__) # Blueprint('main', __name__, url_prefix=url_prefix)
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'),
('arm64', 'arm64'),
('i386', 'i386'),
('todo', 'TODO : list more architectures')],
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]+.*(?