.htaccess
1 year ago
Abilities.php
1 day ago
Blog.php
1 year ago
Helper.php
5 months ago
Init.php
5 months ago
Installer.php
7 months ago
MySQL.php
1 year ago
UI.php
4 months ago
Update.php
1 year ago
Wordpress.php
1 day ago
index.html
1 year ago
web.config
1 year ago
Abilities.php
209 lines
| 1 | <?php |
| 2 | |
| 3 | namespace JetBackup\Wordpress; |
| 4 | |
| 5 | use JetBackup\Ajax\iAjax; |
| 6 | use JetBackup\Exception\JBException; |
| 7 | use JetBackup\Factory; |
| 8 | use JetBackup\JetBackup; |
| 9 | |
| 10 | if (!defined( '__JETBACKUP__')) die('Direct access is not allowed'); |
| 11 | |
| 12 | class Abilities { |
| 13 | |
| 14 | const CAPABILITY = 'manage_options'; |
| 15 | |
| 16 | const CATEGORY = 'jetbackup'; |
| 17 | const CATEGORY_LABEL = 'JetBackup'; |
| 18 | const CATEGORY_DESCRIPTION = 'JetBackup backup, restore and status capabilities.'; |
| 19 | |
| 20 | const INPUT_NONE = 0; |
| 21 | const INPUT_PAGINATION = 1; |
| 22 | const INPUT_ID = 2; |
| 23 | |
| 24 | const KEY_SKIP = 'skip'; |
| 25 | const KEY_LIMIT = 'limit'; |
| 26 | const LIMIT_DEFAULT = 50; |
| 27 | const LIMIT_MAX = 100; |
| 28 | |
| 29 | const ARG_LABEL = 'label'; |
| 30 | const ARG_DESCRIPTION = 'description'; |
| 31 | const ARG_CATEGORY = 'category'; |
| 32 | const ARG_INPUT_SCHEMA = 'input_schema'; |
| 33 | const ARG_OUTPUT_SCHEMA = 'output_schema'; |
| 34 | const ARG_EXECUTE_CALLBACK = 'execute_callback'; |
| 35 | const ARG_PERMISSION_CALLBACK = 'permission_callback'; |
| 36 | const ARG_META = 'meta'; |
| 37 | const ARG_SHOW_IN_REST = 'show_in_rest'; |
| 38 | const ARG_MCP = 'mcp'; |
| 39 | const ARG_PUBLIC = 'public'; |
| 40 | |
| 41 | const SCHEMA_TYPE = 'type'; |
| 42 | const SCHEMA_PROPERTIES = 'properties'; |
| 43 | const SCHEMA_ADDITIONAL_PROPERTIES = 'additionalProperties'; |
| 44 | const SCHEMA_DEFAULT = 'default'; |
| 45 | const SCHEMA_MINIMUM = 'minimum'; |
| 46 | const SCHEMA_MAXIMUM = 'maximum'; |
| 47 | const SCHEMA_TYPE_OBJECT = 'object'; |
| 48 | const SCHEMA_TYPE_INTEGER = 'integer'; |
| 49 | |
| 50 | const RESPONSE_MESSAGE = 'message'; |
| 51 | const RESPONSE_DATA = 'data'; |
| 52 | |
| 53 | const ERROR_INVALID = 'jetbackup_invalid_ability'; |
| 54 | const ERROR_FAILED = 'jetbackup_ability_failed'; |
| 55 | const MESSAGE_FAILED = 'JetBackup ability execution failed'; |
| 56 | |
| 57 | const SANITIZE_OUTPUT = [ |
| 58 | 'getSystemInfo' => ['secured_dir', 'data_dir', 'wordpress_path', 'jetbackup_data_dir'], |
| 59 | ]; |
| 60 | |
| 61 | const ABILITIES = [ |
| 62 | 'listBackups' => ['List Backups', 'List available backup snapshots.', self::INPUT_PAGINATION], |
| 63 | 'listBackupJobs' => ['List Backup Jobs', 'List configured backup jobs.', self::INPUT_PAGINATION], |
| 64 | 'listDestinations' => ['List Destinations', 'List configured backup destinations.', self::INPUT_PAGINATION], |
| 65 | 'listSchedules' => ['List Schedules', 'List configured backup schedules.', self::INPUT_PAGINATION], |
| 66 | 'listQueueItems' => ['List Queue Items', 'List queue items.', self::INPUT_PAGINATION], |
| 67 | 'getBackup' => ['Get Backup', 'Get a single backup snapshot by id.', self::INPUT_ID], |
| 68 | 'getBackupJob' => ['Get Backup Job', 'Get a single backup job by id.', self::INPUT_ID], |
| 69 | 'getQueueItem' => ['Get Queue Item', 'Get a single queue item by id.', self::INPUT_ID], |
| 70 | 'getDashboard' => ['Get Dashboard', 'Get the dashboard summary.', self::INPUT_NONE], |
| 71 | 'getSystemInfo' => ['Get System Info', 'Get system information.', self::INPUT_NONE], |
| 72 | ]; |
| 73 | |
| 74 | private function __construct() {} |
| 75 | |
| 76 | public static function registerCategories(): void { |
| 77 | |
| 78 | try { |
| 79 | if (!Factory::getSettingsSecurity()->isAbilitiesEnabled()) return; |
| 80 | } catch (\Throwable $e) { |
| 81 | return; |
| 82 | } |
| 83 | |
| 84 | Wordpress::registerAbilityCategory(self::CATEGORY, [ |
| 85 | self::ARG_LABEL => self::CATEGORY_LABEL, |
| 86 | self::ARG_DESCRIPTION => self::CATEGORY_DESCRIPTION, |
| 87 | ]); |
| 88 | } |
| 89 | |
| 90 | public static function register(): void { |
| 91 | |
| 92 | try { |
| 93 | if (!Factory::getSettingsSecurity()->isAbilitiesEnabled()) return; |
| 94 | } catch (\Throwable $e) { |
| 95 | return; |
| 96 | } |
| 97 | |
| 98 | foreach (self::ABILITIES as $action => $ability) { |
| 99 | |
| 100 | [$label, $description, $input] = $ability; |
| 101 | |
| 102 | Wordpress::registerAbility(self::_name($action), [ |
| 103 | self::ARG_LABEL => $label, |
| 104 | self::ARG_DESCRIPTION => $description, |
| 105 | self::ARG_CATEGORY => self::CATEGORY, |
| 106 | self::ARG_INPUT_SCHEMA => self::_inputSchema($input), |
| 107 | self::ARG_OUTPUT_SCHEMA => [self::SCHEMA_TYPE => self::SCHEMA_TYPE_OBJECT], |
| 108 | self::ARG_EXECUTE_CALLBACK => static function ($input=[]) use ($action) { return self::_execute($action, is_array($input) ? $input : []); }, |
| 109 | self::ARG_PERMISSION_CALLBACK => [self::class, 'permission'], |
| 110 | self::ARG_META => [self::ARG_SHOW_IN_REST => true, self::ARG_MCP => [self::ARG_PUBLIC => true]], |
| 111 | ]); |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | private static function _name(string $action): string { |
| 116 | return self::CATEGORY . '/' . strtolower(preg_replace('/([a-z0-9])([A-Z])/', '$1-$2', $action)); |
| 117 | } |
| 118 | |
| 119 | public static function permission(): bool { |
| 120 | try { |
| 121 | if (!function_exists('current_user_can') || !function_exists('is_user_logged_in')) return false; |
| 122 | if (!is_user_logged_in()) return false; |
| 123 | if (!current_user_can(self::CAPABILITY)) return false; |
| 124 | if (Helper::isMultisite() && !Helper::isNetworkAdminUser()) return false; |
| 125 | |
| 126 | $security = Factory::getSettingsSecurity(); |
| 127 | if (!$security->isAbilitiesEnabled()) return false; |
| 128 | if ($security->isMFAEnabled() && !UI::validateMFA() && !$security->isMFAAllowAbilities()) return false; |
| 129 | |
| 130 | return true; |
| 131 | |
| 132 | } catch (\Throwable $e) { |
| 133 | if (Wordpress::isDebugModeEnabled()) error_log('[JetBackup] ability permission check failed: ' . $e->getMessage()); |
| 134 | return false; |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | private static function _inputSchema(int $input): array { |
| 139 | |
| 140 | $properties = []; |
| 141 | |
| 142 | if ($input === self::INPUT_PAGINATION) $properties = [ |
| 143 | self::KEY_SKIP => [self::SCHEMA_TYPE => self::SCHEMA_TYPE_INTEGER, self::SCHEMA_MINIMUM => 0], |
| 144 | self::KEY_LIMIT => [ |
| 145 | self::SCHEMA_TYPE => self::SCHEMA_TYPE_INTEGER, |
| 146 | self::SCHEMA_MINIMUM => 1, |
| 147 | self::SCHEMA_MAXIMUM => self::LIMIT_MAX, |
| 148 | self::SCHEMA_DEFAULT => self::LIMIT_DEFAULT, |
| 149 | ], |
| 150 | ]; |
| 151 | else if ($input === self::INPUT_ID) $properties = [ |
| 152 | JetBackup::ID_FIELD => [self::SCHEMA_TYPE => self::SCHEMA_TYPE_INTEGER, self::SCHEMA_MINIMUM => 1], |
| 153 | ]; |
| 154 | |
| 155 | return [ |
| 156 | self::SCHEMA_TYPE => self::SCHEMA_TYPE_OBJECT, |
| 157 | self::SCHEMA_PROPERTIES => $properties, |
| 158 | self::SCHEMA_ADDITIONAL_PROPERTIES => false, |
| 159 | self::SCHEMA_DEFAULT => (object) [], |
| 160 | ]; |
| 161 | } |
| 162 | |
| 163 | private static function _execute(string $action, array $input) { |
| 164 | |
| 165 | $class = "\\JetBackup\\Ajax\\Calls\\" . ucfirst($action); |
| 166 | |
| 167 | if (!class_exists($class)) return new \WP_Error(self::ERROR_INVALID, 'Unknown JetBackup ability', ['status' => 404]); |
| 168 | |
| 169 | try { |
| 170 | /** @var iAjax $call */ |
| 171 | $call = new $class(); |
| 172 | $call->setData(self::_boundInput($action, $input)); |
| 173 | $call->execute(); |
| 174 | |
| 175 | $data = $call->getResponseData(); |
| 176 | if (isset(self::SANITIZE_OUTPUT[$action])) $data = self::_sanitize($data, self::SANITIZE_OUTPUT[$action]); |
| 177 | |
| 178 | return [ |
| 179 | self::RESPONSE_MESSAGE => $call->getResponseMessage(), |
| 180 | self::RESPONSE_DATA => $data, |
| 181 | ]; |
| 182 | |
| 183 | } catch (JBException $e) { |
| 184 | if (Wordpress::isDebugModeEnabled()) error_log('[JetBackup] ability failed (' . $action . '): ' . $e->getMessage()); |
| 185 | return new \WP_Error(self::ERROR_FAILED, self::MESSAGE_FAILED, ['status' => 400]); |
| 186 | } catch (\Throwable $e) { |
| 187 | if (Wordpress::isDebugModeEnabled()) error_log('[JetBackup] ability error (' . $action . '): ' . $e->getMessage()); |
| 188 | return new \WP_Error(self::ERROR_FAILED, self::MESSAGE_FAILED, ['status' => 500]); |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | private static function _boundInput(string $action, array $input): array { |
| 193 | if ((self::ABILITIES[$action][2] ?? self::INPUT_NONE) !== self::INPUT_PAGINATION) return $input; |
| 194 | $limit = isset($input[self::KEY_LIMIT]) ? (int) $input[self::KEY_LIMIT] : self::LIMIT_DEFAULT; |
| 195 | if ($limit < 1) $limit = self::LIMIT_DEFAULT; |
| 196 | if ($limit > self::LIMIT_MAX) $limit = self::LIMIT_MAX; |
| 197 | $input[self::KEY_LIMIT] = $limit; |
| 198 | return $input; |
| 199 | } |
| 200 | |
| 201 | private static function _sanitize(array $data, array $keys): array { |
| 202 | foreach ($data as $key => $value) { |
| 203 | if (in_array($key, $keys, true)) { unset($data[$key]); continue; } |
| 204 | if (is_array($value)) $data[$key] = self::_sanitize($value, $keys); |
| 205 | } |
| 206 | return $data; |
| 207 | } |
| 208 | } |
| 209 |