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