Refactor plugin handling and add support for pro plugins

This commit is contained in:
Théophile Diot 2024-02-21 14:55:32 +01:00
parent 86ac600c44
commit a9e5900dc1
No known key found for this signature in database
GPG key ID: 248FEA4BAE400D06
10 changed files with 78 additions and 52 deletions

View file

@ -66,7 +66,7 @@ try:
elif db_config.get("USE_UI", {"value": "no"})["value"] == "yes":
data["use_ui"] = "yes"
data["external_plugins"] = [f"{plugin['id']}/{plugin['version']}" for plugin in db.get_plugins(external=True)]
data["external_plugins"] = [f"{plugin['id']}/{plugin['version']}" for plugin in db.get_plugins(_type="external")]
data["os"] = {
"name": "Linux",
"version": "Unknown",

View file

@ -48,7 +48,7 @@ def install_plugin(plugin_dir, db) -> bool:
if EXTERNAL_PLUGINS_DIR.joinpath(metadata["id"], "plugin.json").is_file():
old_version = None
for plugin in db.get_plugins(external=True):
for plugin in db.get_plugins(_type="external"):
if plugin["id"] == metadata["id"]:
old_version = plugin["version"]
break
@ -179,7 +179,7 @@ try:
plugin_file.update(
{
"external": True,
"type": "external",
"page": False,
"method": "scheduler",
"data": value,
@ -195,7 +195,7 @@ try:
lock = Lock()
for plugin in db.get_plugins(external=True, with_data=True):
for plugin in db.get_plugins(_type="external", with_data=True):
if plugin["method"] != "scheduler" and plugin["id"] not in external_plugins_ids:
external_plugins.append(plugin)

View file

@ -11,7 +11,7 @@ from os.path import basename, normpath, join
from pathlib import Path
from re import compile as re_compile
from sys import _getframe, path as sys_path
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
from time import sleep
from traceback import format_exc
@ -356,7 +356,9 @@ class Database:
has_all_tables = False
continue
missing_columns = []
extra_columns = []
# Check if any columns are missing
db_columns = inspector.get_columns(table)
self.__logger.debug(f'Checking table "{table}" for missing columns')
for column in Base.metadata.tables[table].columns:
@ -365,12 +367,24 @@ class Database:
self.__logger.warning(f'Column "{column.name}" is missing in table "{table}"')
missing_columns.append(column)
# Check if any columns are extra
self.__logger.debug(f'Checking table "{table}" for extra columns')
for db_column in db_columns:
self.__logger.debug(f'Checking column "{db_column["name"]}" in table "{table}"')
if not any(column.name == db_column["name"] for column in Base.metadata.tables[table].columns):
self.__logger.warning(f'Column "{db_column["name"]}" is extra in table "{table}"')
extra_columns.append(db_column)
try:
with self.__db_session() as session:
if missing_columns:
for column in missing_columns:
self.__logger.warning(f'Adding column "{column.name}" to table "{table}"')
session.execute(text(f"ALTER TABLE {table} ADD COLUMN {column.name} {column.type}"))
if extra_columns:
for column in extra_columns:
self.__logger.warning(f'Removing column "{column["name"]}" from table "{table}"')
session.execute(text(f"ALTER TABLE {table} DROP COLUMN {column['name']}"))
session.commit()
except BaseException:
return False, format_exc()
@ -400,7 +414,6 @@ class Database:
"description": "The general settings for the server",
"version": "0.1",
"stream": "partial",
"external": False,
}
else:
settings = plugin.pop("settings", {})
@ -423,8 +436,8 @@ class Database:
if plugin["stream"] != db_plugin.stream:
updates[Plugins.stream] = plugin["stream"]
if plugin.get("external", False) != db_plugin.external:
updates[Plugins.external] = plugin.get("external", False)
if plugin.get("type", "core") != db_plugin.type:
updates[Plugins.type] = plugin.get("type", "core")
if plugin.get("method", "manual") != db_plugin.method:
updates[Plugins.method] = plugin.get("method", "manual")
@ -446,7 +459,7 @@ class Database:
description=plugin["description"],
version=plugin["version"],
stream=plugin["stream"],
external=plugin.get("external", False),
type=plugin.get("type", "core"),
method=plugin.get("method"),
data=plugin.get("data"),
checksum=plugin.get("checksum"),
@ -1112,7 +1125,7 @@ class Database:
"""Update external plugins from the database"""
to_put = []
with self.__db_session() as session:
db_plugins = session.query(Plugins).with_entities(Plugins.id).filter_by(external=True).all()
db_plugins = session.query(Plugins).with_entities(Plugins.id).filter_by(type="external").all()
db_ids = []
if delete_missing and db_plugins:
@ -1128,7 +1141,7 @@ class Database:
settings = plugin.pop("settings", {})
jobs = plugin.pop("jobs", [])
page = plugin.pop("page", False)
plugin["external"] = True
plugin["type"] = "external"
db_plugin = (
session.query(Plugins)
.with_entities(
@ -1139,14 +1152,14 @@ class Database:
Plugins.method,
Plugins.data,
Plugins.checksum,
Plugins.external,
Plugins.type,
)
.filter_by(id=plugin["id"])
.first()
)
if db_plugin is not None:
if db_plugin.external is False:
if db_plugin.type != "external":
self.__logger.warning(
f"Plugin \"{plugin['id']}\" is not external, skipping update (updating a non-external plugin is forbidden for security reasons)",
)
@ -1364,7 +1377,7 @@ class Database:
description=plugin["description"],
version=plugin["version"],
stream=plugin["stream"],
external=True,
type="external",
method=plugin["method"],
data=plugin.get("data"),
checksum=plugin.get("checksum"),
@ -1474,17 +1487,19 @@ class Database:
return ""
def get_plugins(self, *, external: bool = False, with_data: bool = False) -> List[Dict[str, Any]]:
def get_plugins(self, *, _type: Literal["all", "external", "pro"] = "all", with_data: bool = False) -> List[Dict[str, Any]]:
"""Get all plugins from the database."""
plugins = []
with self.__db_session() as session:
entities = [Plugins.id, Plugins.stream, Plugins.name, Plugins.description, Plugins.version, Plugins.external, Plugins.method, Plugins.checksum]
entities = [Plugins.id, Plugins.stream, Plugins.name, Plugins.description, Plugins.version, Plugins.type, Plugins.method, Plugins.checksum]
if with_data:
entities.append(Plugins.data)
for plugin in session.query(Plugins).with_entities(*entities).all():
if external and not plugin.external:
continue
db_plugins = session.query(Plugins).with_entities(*entities)
if _type != "all":
db_plugins = db_plugins.filter_by(type=_type)
for plugin in db_plugins.all():
page = session.query(Plugin_pages).with_entities(Plugin_pages.id).filter_by(plugin_id=plugin.id).first()
data = {
"id": plugin.id,
@ -1492,7 +1507,7 @@ class Database:
"name": plugin.name,
"description": plugin.description,
"version": plugin.version,
"external": plugin.external,
"type": plugin.type,
"method": plugin.method,
"page": page is not None,
"settings": {},

View file

@ -39,6 +39,7 @@ INTEGRATIONS_ENUM = Enum(
"Unknown",
name="integrations_enum",
)
PLUGIN_TYPES_ENUM = Enum("core", "external", "pro", name="plugin_types_enum")
Base = declarative_base()
@ -50,7 +51,7 @@ class Plugins(Base):
description = Column(String(256), nullable=False)
version = Column(String(32), nullable=False)
stream = Column(String(16), nullable=False)
external = Column(Boolean, default=False, nullable=False)
type = Column(PLUGIN_TYPES_ENUM, default="core", nullable=False)
method = Column(METHODS_ENUM, default="manual", nullable=False)
data = Column(LargeBinary(length=(2**32) - 1), nullable=True)
checksum = Column(String(128), nullable=True)

View file

@ -57,7 +57,7 @@ class Configurator:
def get_settings(self) -> Dict[str, Any]:
return self.__settings
def get_plugins(self, _type: Union[Literal["core"], Literal["external"]]) -> List[Dict[str, Any]]:
def get_plugins(self, _type: Literal["core", "external", "pro"]) -> List[Dict[str, Any]]:
return self.__core_plugins if _type == "core" else self.__external_plugins
def get_plugins_settings(self, _type: Union[Literal["core"], Literal["external"]]) -> Dict[str, Any]:
@ -135,7 +135,7 @@ class Configurator:
data.update(
{
"external": True,
"type": "external",
"page": "ui" in listdir(dirname(file)),
"method": "manual",
"data": value,

View file

@ -352,7 +352,7 @@ if __name__ == "__main__":
# Check if any external plugin has been added by the user
external_plugins = []
db_plugins = db.get_plugins(external=True)
db_plugins = db.get_plugins(_type="external")
plugins_dir = Path(sep, "etc", "bunkerweb", "plugins")
for filename in glob(str(plugins_dir.joinpath("*", "plugin.json"))):
with open(filename, "r", encoding="utf-8") as f:
@ -398,7 +398,7 @@ if __name__ == "__main__":
if (scheduler_first_start and db_plugins) or changes:
generate_external_plugins(
db.get_plugins(external=True, with_data=True),
db.get_plugins(_type="external", with_data=True),
original_path=plugins_dir,
)
SCHEDULER.update_jobs()
@ -651,7 +651,7 @@ if __name__ == "__main__":
if PLUGINS_NEED_GENERATION:
CHANGES.append("external_plugins")
generate_external_plugins(
db.get_plugins(external=True, with_data=True),
db.get_plugins(_type="external", with_data=True),
original_path=plugins_dir,
)
SCHEDULER.update_jobs()

View file

@ -999,21 +999,18 @@ def plugins():
variables = deepcopy(request.form.to_dict())
del variables["csrf_token"]
if variables["external"] != "True":
flash(f"Can't delete internal plugin {variables['name']}", "error")
if variables["type"] in ("core", "pro"):
flash(f"Can't delete {variables['type']} plugin {variables['name']}", "error")
return redirect(url_for("loading", next=url_for("plugins")))
plugins = app.config["CONFIG"].get_plugins()
for plugin in deepcopy(plugins):
if plugin["external"] is False or plugin["id"] == variables["name"]:
del plugins[plugins.index(plugin)]
for x, plugin in enumerate(deepcopy(plugins)):
if plugin["type"] in ("core", "pro") or plugin["id"] == variables["name"]:
del plugins[x]
err = db.update_external_plugins(plugins)
if err:
flash(
f"Couldn't update external plugins to database: {err}",
"error",
)
flash(f"Couldn't update external plugins to database: {err}", "error")
flash(f"Deleted plugin {variables['name']} successfully")
else:
if not tmp_ui_path.exists() or not listdir(str(tmp_ui_path)):
@ -1130,7 +1127,7 @@ def plugins():
new_plugins.append(
plugin_file
| {
"external": True,
"type": "external",
"page": "ui" in listdir(str(temp_folder_path)),
"method": "ui",
"data": value,
@ -1183,7 +1180,7 @@ def plugins():
if errors >= files_count:
return redirect(url_for("loading", next=url_for("plugins")))
plugins = app.config["CONFIG"].get_plugins(external=True, with_data=True)
plugins = app.config["CONFIG"].get_plugins(_type="external", with_data=True)
for plugin in deepcopy(plugins):
if plugin["id"] in new_plugins_ids:
flash(f"Plugin {plugin['id']} already exists", "error")
@ -1191,10 +1188,7 @@ def plugins():
err = db.update_external_plugins(new_plugins, delete_missing=False)
if err:
flash(
f"Couldn't update external plugins to database: {err}",
"error",
)
flash(f"Couldn't update external plugins to database: {err}", "error")
if operation:
flash(operation)
@ -1217,10 +1211,13 @@ def plugins():
plugins = app.config["CONFIG"].get_plugins()
plugins_internal = 0
plugins_external = 0
plugins_pro = 0
for plugin in plugins:
if plugin["external"] is True:
if plugin["type"] == "external":
plugins_external += 1
elif plugin["type"] == "pro":
plugins_pro += 1
else:
plugins_internal += 1
@ -1229,6 +1226,7 @@ def plugins():
plugins=plugins,
plugins_internal=plugins_internal,
plugins_external=plugins_external,
plugins_pro=plugins_pro,
username=current_user.get_id(),
)

View file

@ -9,7 +9,7 @@ from json import loads as json_loads
from pathlib import Path
from re import search as re_search
from subprocess import run, DEVNULL, STDOUT
from typing import List, Tuple
from typing import List, Literal, Tuple
from uuid import uuid4
@ -83,8 +83,8 @@ class Config:
**self.__settings,
}
def get_plugins(self, *, external: bool = False, with_data: bool = False) -> List[dict]:
plugins = self.__db.get_plugins(external=external, with_data=with_data)
def get_plugins(self, *, _type: Literal["all", "external", "pro"] = "all", with_data: bool = False) -> List[dict]:
plugins = self.__db.get_plugins(_type=_type, with_data=with_data)
plugins.sort(key=itemgetter("name"))
general_plugin = None

View file

@ -43,6 +43,18 @@ include "plugins_modal.html" %}
{{plugins_external}}
</p>
</div>
<div class="mx-1 flex items-center my-4">
<p
class="transition duration-300 ease-in-out font-bold mb-0 font-sans text-sm leading-normal uppercase dark:text-gray-500 dark:opacity-80"
>
PRO PLUGINS
</p>
<p
class="transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-white dark:opacity-80"
>
{{plugins_pro}}
</p>
</div>
</div>
<!-- end info -->

View file

@ -282,7 +282,7 @@ try:
"description": "The general settings for the server",
"version": "0.1",
"stream": "partial",
"external": False,
"type": "core",
"checked": False,
"page_checked": True,
"settings": global_settings,
@ -316,27 +316,27 @@ try:
Plugins.description,
Plugins.version,
Plugins.stream,
Plugins.external,
Plugins.type,
Plugins.method,
)
.all()
)
for plugin in plugins:
if not plugin.external and plugin.id in core_plugins:
if plugin.type == "core" and plugin.id in core_plugins:
current_plugin = core_plugins
elif plugin.external and plugin.id in external_plugins:
elif plugin.type == "external" and plugin.id in external_plugins:
current_plugin = external_plugins
else:
print(
f"❌ The {'external' if plugin.external else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but should not be, exiting ...",
f"❌ The {'external' if plugin.type == 'external' else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but should not be, exiting ...: {plugin}",
flush=True,
)
exit(1)
if plugin.name != current_plugin[plugin.id]["name"] or plugin.description != current_plugin[plugin.id]["description"] or plugin.version != current_plugin[plugin.id]["version"] or plugin.stream != current_plugin[plugin.id]["stream"]:
print(
f"❌ The {'external' if plugin.external else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but is not correct, exiting ...\n"
f"❌ The {'external' if plugin.type == 'external' else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but is not correct, exiting ...\n"
+ f"{dumps({'name': plugin.name, 'description': plugin.description, 'version': plugin.version, 'stream': plugin.stream})}"
+ f" (database) != {dumps({'name': current_plugin[plugin.id]['name'], 'description': current_plugin[plugin.id]['description'], 'version': current_plugin[plugin.id]['version'], 'stream': current_plugin[plugin.id]['stream']})} (file)", # noqa: E501
flush=True,
@ -357,7 +357,7 @@ try:
or setting.multiple != current_plugin[plugin.id]["settings"][setting.id].get("multiple", None)
):
print(
f"❌ The {'external' if plugin.external else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but is not correct, exiting ...\n"
f"❌ The {'external' if plugin.type == 'external' else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but is not correct, exiting ...\n"
+ f"{dumps({'default': setting.default, 'help': setting.help, 'label': setting.label, 'regex': setting.regex, 'type': setting.type})}"
+ f" (database) != {dumps({'default': current_plugin[plugin.id]['settings'][setting.id]['default'], 'help': current_plugin[plugin.id]['settings'][setting.id]['help'], 'label': current_plugin[plugin.id]['settings'][setting.id]['label'], 'regex': current_plugin[plugin.id]['settings'][setting.id]['regex'], 'type': current_plugin[plugin.id]['settings'][setting.id]['type']})} (file)", # noqa: E501
flush=True,