#### 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]+.*(?