diff --git a/.gitignore b/.gitignore
index 0e69ddeac7..df9931cce2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ composer.lock
checksums.json
/.node_cache/
/release/
+/backups/
diff --git a/composer.json b/composer.json
index ac55acfb57..950a2855e2 100644
--- a/composer.json
+++ b/composer.json
@@ -31,7 +31,8 @@
"geoip2/geoip2": "^3.0",
"jenssegers/agent": "^2.6",
"php-di/php-di": "^7.0",
- "twig/twig": "^3.0"
+ "twig/twig": "^3.0",
+ "druidfi/mysqldump-php": "^2.0"
},
"require-dev": {
"phpstan/phpstan": "1.6.9",
diff --git a/core/classes/Core/Util.php b/core/classes/Core/Util.php
index aeb007627a..893c1c9277 100644
--- a/core/classes/Core/Util.php
+++ b/core/classes/Core/Util.php
@@ -303,4 +303,18 @@ public static function isCompatible(string $version, string $nameless_version):
return $major == $nameless_major && $minor == $nameless_minor;
}
+
+ /**
+ * Format bytes into a human-readable string.
+ *
+ * @param int $bytes Number of bytes to format.
+ * @return string Formatted string.
+ */
+ public static function formatBytes(int $bytes): string
+ {
+ $sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ $factor = floor((strlen($bytes) - 1) / 3);
+
+ return sprintf('%.2f', $bytes / pow(1024, $factor)) . $sizes[$factor];
+ }
}
diff --git a/core/classes/Database/DatabaseInitialiser.php b/core/classes/Database/DatabaseInitialiser.php
index 9edf10146f..437e36586b 100644
--- a/core/classes/Database/DatabaseInitialiser.php
+++ b/core/classes/Database/DatabaseInitialiser.php
@@ -46,7 +46,7 @@ private function initialiseGroups(): void
'group_username_color' => '#ff0000',
'group_username_css' => '',
'admin_cp' => true,
- 'permissions' => '{"administrator":1,"admincp.core":1,"admincp.core.api":1,"admincp.core.seo":1,"admincp.core.general":1,"admincp.core.avatars":1,"admincp.core.fields":1,"admincp.core.debugging":1,"admincp.core.emails":1,"admincp.core.queue":1,"admincp.core.navigation":1,"admincp.core.announcements":1,"admincp.core.reactions":1,"admincp.core.registration":1,"admincp.core.social_media":1,"admincp.core.terms":1,"admincp.errors":1,"admincp.core.placeholders":1,"admincp.members":1,"admincp.integrations":1,"admincp.integrations.edit":1,"admincp.discord":1,"admincp.minecraft":1,"admincp.minecraft.authme":1,"admincp.minecraft.servers":1,"admincp.minecraft.query_errors":1,"admincp.minecraft.banners":1,"admincp.modules":1,"admincp.pages":1,"admincp.security":1,"admincp.security.acp_logins":1,"admincp.security.template":1,"admincp.styles":1,"admincp.styles.panel_templates":1,"admincp.styles.templates":1,"admincp.styles.templates.edit":1,"admincp.styles.images":1,"admincp.update":1,"admincp.users":1,"admincp.users.edit":1,"admincp.groups":1,"admincp.groups.self":1,"admincp.widgets":1,"modcp.ip_lookup":1,"modcp.punishments":1,"modcp.punishments.warn":1,"modcp.punishments.ban":1,"modcp.punishments.banip":1,"modcp.punishments.revoke":1,"modcp.reports":1,"modcp.profile_banner_reset":1,"usercp.messaging":1,"usercp.signature":1,"admincp.forums":1,"usercp.private_profile":1,"usercp.nickname":1,"usercp.title":1,"usercp.profile_banner":1,"profile.private.bypass":1, "admincp.security.all":1,"admincp.core.hooks":1,"admincp.security.group_sync":1,"admincp.core.emails_mass_message":1,"modcp.punishments.reset_avatar":1,"usercp.gif_avatar":1,"profile.post":1}',
+ 'permissions' => '{"administrator":1,"admincp.core":1,"admincp.core.api":1,"admincp.core.seo":1,"admincp.core.general":1,"admincp.core.avatars":1,"admincp.core.fields":1,"admincp.core.debugging":1,"admincp.core.backups":1."admincp.core.emails":1,"admincp.core.queue":1,"admincp.core.navigation":1,"admincp.core.announcements":1,"admincp.core.reactions":1,"admincp.core.registration":1,"admincp.core.social_media":1,"admincp.core.terms":1,"admincp.errors":1,"admincp.core.placeholders":1,"admincp.members":1,"admincp.integrations":1,"admincp.integrations.edit":1,"admincp.discord":1,"admincp.minecraft":1,"admincp.minecraft.authme":1,"admincp.minecraft.servers":1,"admincp.minecraft.query_errors":1,"admincp.minecraft.banners":1,"admincp.modules":1,"admincp.pages":1,"admincp.security":1,"admincp.security.acp_logins":1,"admincp.security.template":1,"admincp.styles":1,"admincp.styles.panel_templates":1,"admincp.styles.templates":1,"admincp.styles.templates.edit":1,"admincp.styles.images":1,"admincp.update":1,"admincp.users":1,"admincp.users.edit":1,"admincp.groups":1,"admincp.groups.self":1,"admincp.widgets":1,"modcp.ip_lookup":1,"modcp.punishments":1,"modcp.punishments.warn":1,"modcp.punishments.ban":1,"modcp.punishments.banip":1,"modcp.punishments.revoke":1,"modcp.reports":1,"modcp.profile_banner_reset":1,"usercp.messaging":1,"usercp.signature":1,"admincp.forums":1,"usercp.private_profile":1,"usercp.nickname":1,"usercp.profile_banner":1,"profile.private.bypass":1, "admincp.security.all":1,"admincp.core.hooks":1,"admincp.security.group_sync":1,"admincp.core.emails_mass_message":1,"modcp.punishments.reset_avatar":1,"usercp.gif_avatar":1,"profile.post":1}',
'order' => 1,
'staff' => true,
]);
diff --git a/core/includes/updates/230.php b/core/includes/updates/230.php
index b36c2309db..50087c8756 100644
--- a/core/includes/updates/230.php
+++ b/core/includes/updates/230.php
@@ -66,6 +66,16 @@ public function run(): void
Settings::set('discord_widget_theme', $discord_widget_theme, 'Discord Integration');
$this->_cache->eraseAll();
+ // Add admincp.core.backups permission to Admin group
+ $admin_group = Group::find(1);
+ $permissions = json_decode($admin_group->permissions, true);
+ if (!isset($permissions['admincp.core.backups'])) {
+ $permissions['admincp.core.backups'] = 1;
+ DB::getInstance()->update('groups', $admin_group->id, [
+ 'permissions' => json_encode($permissions),
+ ]);
+ }
+
$this->setVersion('2.3.0');
}
};
diff --git a/core/init.php b/core/init.php
index 70ab4cd5d4..9781eaa137 100644
--- a/core/init.php
+++ b/core/init.php
@@ -34,6 +34,7 @@
ROOT_PATH . '/cache/logs',
ROOT_PATH . '/cache/sitemaps',
ROOT_PATH . '/cache/templates_c',
+ ROOT_PATH . '/cache/backups',
ROOT_PATH . '/uploads',
ROOT_PATH . '/core/config.php',
];
diff --git a/custom/panel_templates/Default/core/backups.tpl b/custom/panel_templates/Default/core/backups.tpl
new file mode 100644
index 0000000000..e8608ed680
--- /dev/null
+++ b/custom/panel_templates/Default/core/backups.tpl
@@ -0,0 +1,154 @@
+{include file='header.tpl'}
+
+
+
+
+
+
+
+ {include file='sidebar.tpl'}
+
+
+
+
+
+
+
+
+ {include file='navbar.tpl'}
+
+
+
+
+
+
+
{$DEBUGGING_AND_MAINTENANCE}
+
+ - {$DASHBOARD}
+ - {$CONFIGURATION}
+ - {$DEBUGGING_AND_MAINTENANCE}
+ - {$BACKUPS}
+
+
+
+
+ {include file='includes/update.tpl'}
+
+
+
+
+
{$BACKUPS}
+
+
+
+
+
+ {include file='includes/alerts.tpl'}
+
+
+
+
{$INFO}
+ {$BACKUPS_INFO}
+
+
+
+
+
+
+
+
+
{$BACKUP_SETTINGS}
+
+
+
+
+
+
+ {if isset($EXISTING_BACKUPS) && count($EXISTING_BACKUPS) > 0}
+
{$EXISTING}
+
+
+
+
+
+ | {$FILENAME} |
+ {$DATE_CREATED} |
+ {$FILE_SIZE} |
+ {if $CAN_DOWNLOAD}
+ {$ACTIONS} |
+ {/if}
+
+
+
+ {foreach from=$EXISTING_BACKUPS item=backup}
+
+ | {$backup.filename} |
+ {$backup.date} |
+ {$backup.size} |
+ {if $CAN_DOWNLOAD}
+
+
+ {$DOWNLOAD}
+
+ |
+ {/if}
+
+ {/foreach}
+
+
+
+ {else}
+
+ {$NO_BACKUPS}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+ {include file='footer.tpl'}
+
+
+
+
+
+
+
+ {include file='scripts.tpl'}
+
+
+
+