PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / trunk
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback vtrunk
5.1.3 5.1.2 5.1.1 5.1 5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / third-party / backup / jetbackup / class-avcf-jetbackup-helpers.php
atarim-visual-collaboration / third-party / backup / jetbackup Last commit date
class-avcf-abilities-jetbackup-backups.php 2 days ago class-avcf-abilities-jetbackup-destinations.php 2 weeks ago class-avcf-abilities-jetbackup-jobs.php 2 weeks ago class-avcf-abilities-jetbackup-queue.php 2 days ago class-avcf-abilities-jetbackup-restore-point.php 2 days ago class-avcf-abilities-jetbackup-restore.php 2 days ago class-avcf-abilities-jetbackup-schedules.php 2 weeks ago class-avcf-abilities-jetbackup-settings.php 2 weeks ago class-avcf-abilities-jetbackup-system.php 2 weeks ago class-avcf-jetbackup-detector.php 2 weeks ago class-avcf-jetbackup-helpers.php 2 days ago
class-avcf-jetbackup-helpers.php
361 lines
1 <?php
2 /**
3 * JetBackup — shared helpers for the atarim/jetbackup-* ability cluster.
4 *
5 * One invocation path for every ability: instantiate the JetBackup AJAX Call
6 * class, setData(), execute(), read the response — mirroring JetBackup's own
7 * JetBackup\Wordpress\Abilities::_execute(). Also holds the request field-name
8 * and enum constants so the ability files never reach into JetBackup internals.
9 *
10 * We deliberately bypass JetBackup's own "Abilities" toggle and MFA-for-abilities
11 * gate (those only guard JetBackup's own ability registration, not these direct
12 * AJAX-class calls); our gate is manage_options. Destructive abilities add a
13 * confirm:true guard on top.
14 *
15 * Built on JetBackup's AJAX Call classes (v3.1.23.x). Every call is guarded with
16 * class_exists and wrapped in try/catch, so a missing or renamed class degrades
17 * cleanly instead of fataling. Not runtime-tested here.
18 *
19 * @package atarim-visual-collaboration
20 */
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 class AVCF_JetBackup_Helpers {
27
28 const PLUGIN_SLUG = 'jetbackup';
29 const PLUGIN_LABEL = 'JetBackup';
30
31 // Request field names (mirror JetBackup constants so ability files stay decoupled).
32 const ID_FIELD = '_id'; // \JetBackup\JetBackup::ID_FIELD
33 const TYPE_FIELD = 'type'; // \JetBackup\Queue\QueueItem::TYPE
34
35 // Pagination (\JetBackup\Wordpress\Abilities contract).
36 const KEY_SKIP = 'skip';
37 const KEY_LIMIT = 'limit';
38 const LIMIT_DEFAULT = 50;
39 const LIMIT_MAX = 100;
40
41 // Queue types (\JetBackup\Queue\Queue::QUEUE_TYPE_*).
42 const QUEUE_BACKUP = 1;
43 const QUEUE_RESTORE = 2;
44 const QUEUE_DOWNLOAD = 4;
45 const QUEUE_DOWNLOAD_LOG = 5;
46 const QUEUE_REINDEX = 8;
47 const QUEUE_EXPORT = 64;
48 const QUEUE_EXTRACT = 128;
49
50 // Restore option bitmask (\JetBackup\Queue\QueueItemRestore::OPTION_RESTORE_*).
51 const R_DB_ENTIRE = 1; // 1 << 0 import full db dump
52 const R_FILES_ENTIRE = 2; // 1 << 1 restore full homedir
53 const R_DB_EXCLUDE = 64; // 1 << 6 restore db minus selectedTables
54 const R_DB_SKIP = 128; // 1 << 7 skip database
55 const R_DB_INCLUDE = 256; // 1 << 8 restore only selectedTables
56 const R_FILES_EXCLUDE = 512; // 1 << 9 restore files minus folderList
57 const R_FILES_SKIP = 1024; // 1 << 10 skip files
58 const R_FILES_INCLUDE = 2048; // 1 << 11 restore only folderList
59
60 // Keys redacted from get-system-info output (parity with JetBackup's own sanitiser).
61 const SYSTEM_INFO_REDACT = [ 'secured_dir', 'data_dir', 'wordpress_path', 'jetbackup_data_dir' ];
62
63 /**
64 * Is the JetBackup cluster usable right now?
65 */
66 public static function is_available() {
67 return class_exists( 'AVCF_JetBackup_Detector' )
68 && ( new AVCF_JetBackup_Detector() )->avcf_jetbackup_is_available();
69 }
70
71 /**
72 * The single permission gate for every JetBackup ability.
73 */
74 public static function can_manage() {
75 return function_exists( 'current_user_can' ) && current_user_can( 'manage_options' );
76 }
77
78 /**
79 * Invoke a JetBackup AJAX Call by short class name (e.g. 'ListBackups',
80 * 'AddToQueue'). Returns a normalised array: [ success, message, data ].
81 *
82 * @param string $call Short class name under \JetBackup\Ajax\Calls\.
83 * @param array $data Request payload keyed by JetBackup field names.
84 * @return array{success:bool,message:string,data:array}
85 */
86 public static function invoke( $call, array $data = [] ) {
87 $fqcn = '\\JetBackup\\Ajax\\Calls\\' . $call;
88
89 if ( ! class_exists( $fqcn ) ) {
90 return [
91 'success' => false,
92 'message' => sprintf( 'JetBackup action "%s" is unavailable on this JetBackup version.', $call ),
93 'data' => [],
94 ];
95 }
96
97 try {
98 $obj = new $fqcn();
99 $obj->setData( $data );
100 $obj->execute();
101
102 return [
103 'success' => true,
104 'message' => (string) $obj->getResponseMessage(),
105 'data' => (array) $obj->getResponseData(),
106 ];
107 } catch ( \Throwable $e ) {
108 // JetBackup's AjaxException messages are the same user-facing validation
109 // strings its own UI shows (e.g. "No backup job id was provided"), so
110 // surfacing them helps the caller correct the request.
111 //
112 // Some of those messages are printf templates whose arguments live on
113 // the exception's getData() rather than in the message itself, e.g.
114 // AjaxException( 'Failed adding to queue. Error: %s', [ 'Already in queue' ] ).
115 // getMessage() alone would surface a bare "%s", dropping the reason, so
116 // interpolate the data into the template when both are present. Guarded
117 // so a placeholder/argument mismatch can never fatal: only attempt it
118 // when there is data and a placeholder, and fall back to the raw message.
119 $message = (string) $e->getMessage();
120 if ( '' !== $message && method_exists( $e, 'getData' ) ) {
121 $args = $e->getData();
122 if ( is_array( $args ) && ! empty( $args ) && false !== strpos( $message, '%' ) ) {
123 try {
124 $interpolated = vsprintf( $message, $args );
125 if ( is_string( $interpolated ) && '' !== $interpolated ) {
126 $message = $interpolated;
127 }
128 } catch ( \Throwable $ignore ) {
129 // Template/argument mismatch — keep the raw message.
130 }
131 }
132 }
133 return [
134 'success' => false,
135 'message' => '' !== $message ? $message : 'JetBackup action failed.',
136 'data' => [],
137 ];
138 }
139 }
140
141 /**
142 * Map a friendly {id} input to JetBackup's _id request field, merging any extra
143 * pre-built fields.
144 */
145 public static function id_payload( array $input, array $extra = [] ) {
146 $id = isset( $input['id'] ) ? (int) $input['id'] : 0;
147 return array_merge( [ self::ID_FIELD => $id ], $extra );
148 }
149
150 /**
151 * Bound pagination inputs to JetBackup's skip/limit contract.
152 */
153 public static function paginate( array $input ) {
154 $skip = isset( $input['skip'] ) ? max( 0, (int) $input['skip'] ) : 0;
155 $limit = isset( $input['limit'] ) ? (int) $input['limit'] : self::LIMIT_DEFAULT;
156 if ( $limit < 1 ) {
157 $limit = self::LIMIT_DEFAULT;
158 }
159 if ( $limit > self::LIMIT_MAX ) {
160 $limit = self::LIMIT_MAX;
161 }
162 return [ self::KEY_SKIP => $skip, self::KEY_LIMIT => $limit ];
163 }
164
165 /**
166 * Was an explicit confirm:true supplied? Destructive abilities require this.
167 */
168 public static function confirmed( array $input ) {
169 return ! empty( $input['confirm'] ) && filter_var( $input['confirm'], FILTER_VALIDATE_BOOLEAN );
170 }
171
172 /**
173 * Standard "needs confirmation" response for destructive abilities.
174 */
175 public static function confirm_required_response( $what ) {
176 return [
177 'success' => false,
178 'message' => sprintf( 'This is a destructive action (%s). Re-run with confirm:true to proceed.', $what ),
179 'data' => [],
180 ];
181 }
182
183 /**
184 * Recursively strip redacted keys from get-system-info output.
185 */
186 public static function redact_system_info( array $data ) {
187 foreach ( $data as $key => $value ) {
188 if ( in_array( $key, self::SYSTEM_INFO_REDACT, true ) ) {
189 unset( $data[ $key ] );
190 continue;
191 }
192 if ( is_array( $value ) ) {
193 $data[ $key ] = self::redact_system_info( $value );
194 }
195 }
196 return $data;
197 }
198
199 /**
200 * Lift the created task's id to the top level so callers that start a task
201 * (e.g. run-backup) get an unmissable handle instead of digging JetBackup's
202 * internal _id out of the raw data.
203 *
204 * AddToQueue answers with the JOB id, not the id of the queue item it just
205 * created, so polling get-queue-item with it returns "Invalid queue item id
206 * provided". The job id is still useful, so it keeps its own field, and
207 * queue_item_id is resolved from the queue itself when $queue_type is given.
208 */
209 public static function surface_queue_id( array $result, $queue_type = null ) {
210 if ( empty( $result['success'] ) || empty( $result['data'] ) || ! is_array( $result['data'] ) ) {
211 return $result;
212 }
213
214 $jid = $result['data'][ self::ID_FIELD ] ?? ( $result['data']['id'] ?? null );
215
216 if ( null === $jid || '' === $jid ) {
217 return $result;
218 }
219
220 $jid = is_numeric( $jid ) ? (int) $jid : $jid;
221
222 $result['job_id'] = $jid;
223 $result['queue_item_id'] = ( null === $queue_type )
224 ? $jid
225 : ( self::latest_queue_item_id( $queue_type ) ?? $jid );
226
227 return $result;
228 }
229
230 /**
231 * Newest queue item of the given type, used to name the item AddToQueue just
232 * created. Returns null when the queue cannot be read, in which case the
233 * caller keeps the job id and list-queue-items remains the way through.
234 */
235 private static function latest_queue_item_id( $queue_type ) {
236 $res = self::invoke( 'ListQueueItems', [ self::KEY_SKIP => 0, self::KEY_LIMIT => self::LIMIT_DEFAULT ] );
237
238 if ( empty( $res['success'] ) || empty( $res['data'] ) ) {
239 return null;
240 }
241
242 $newest = null;
243
244 foreach ( self::extract_snapshot_list( $res['data'] ) as $item ) {
245 if ( ! is_array( $item ) || (int) ( $item[ self::TYPE_FIELD ] ?? 0 ) !== (int) $queue_type ) {
246 continue;
247 }
248
249 $id = $item[ self::ID_FIELD ] ?? ( $item['id'] ?? null );
250
251 if ( is_numeric( $id ) && ( null === $newest || (int) $id > $newest ) ) {
252 $newest = (int) $id;
253 }
254 }
255
256 return $newest;
257 }
258
259 /**
260 * Pull the array of snapshots out of a ListBackups response, tolerating the
261 * common wrapper shapes.
262 */
263 private static function extract_snapshot_list( $data ) {
264 if ( ! is_array( $data ) ) {
265 return [];
266 }
267 if ( isset( $data[0] ) && is_array( $data[0] ) ) {
268 return $data;
269 }
270 foreach ( [ 'data', 'items', 'backups', 'snapshots', 'list' ] as $k ) {
271 if ( isset( $data[ $k ] ) && is_array( $data[ $k ] ) ) {
272 return $data[ $k ];
273 }
274 }
275 return [];
276 }
277
278 /**
279 * Resolve a snapshot NAME (as reported by a completed backup queue item's
280 * snapshot_name) to its numeric snapshot id, so restore-backup can accept a
281 * name directly and callers can skip a manual list-backups lookup. Searches
282 * the snapshot list in pages, capped, and returns null if not found.
283 */
284 public static function resolve_snapshot_id_by_name( $name ) {
285 $name = (string) $name;
286 if ( '' === $name ) {
287 return null;
288 }
289 $skip = 0;
290 $limit = 100;
291 for ( $page = 0; $page < 10; $page++ ) { // cap the search (<= 1000 snapshots)
292 $res = self::invoke( 'ListBackups', [ self::KEY_SKIP => $skip, self::KEY_LIMIT => $limit ] );
293 if ( empty( $res['success'] ) || empty( $res['data'] ) ) {
294 break;
295 }
296 $list = self::extract_snapshot_list( $res['data'] );
297 if ( empty( $list ) ) {
298 break;
299 }
300 foreach ( $list as $snap ) {
301 if ( is_array( $snap ) && isset( $snap['name'] ) && (string) $snap['name'] === $name ) {
302 $id = $snap[ self::ID_FIELD ] ?? ( $snap['id'] ?? null );
303 return ( null !== $id ) ? (int) $id : null;
304 }
305 }
306 if ( count( $list ) < $limit ) {
307 break; // last page
308 }
309 $skip += $limit;
310 }
311 return null;
312 }
313
314 /**
315 * Call an Atarim restore-point endpoint with this site's Atarim token.
316 *
317 * @param string $path Path under AVCF_CRM_API, e.g. 'v1/jetbackup/restore-point'.
318 * @param string $method 'GET' or 'POST'.
319 * @param array $body Body for POST.
320 * @return array {success:bool, message:string, data:array}
321 */
322 public static function atarim_call( $path, $method = 'GET', $body = [] ) {
323 $functions = new AVCF_Functions();
324 $token = $functions->avcf_get_setting_data( 'avc_atarim_secret_token' );
325
326 if ( empty( $token ) ) {
327 return [ 'success' => false, 'message' => 'This site is not connected to Atarim, so restore points cannot be tracked. Reconnect the site in the Atarim plugin settings.', 'data' => [] ];
328 }
329
330 $response = $functions->avcf_make_api_call(
331 rtrim( AVCF_CRM_API, '/' ) . '/' . ltrim( $path, '/' ),
332 $body,
333 '',
334 $token,
335 $method
336 );
337
338 $code = isset( $response['status_code'] ) ? (int) $response['status_code'] : 0;
339
340 if ( $code === 401 ) {
341 return [ 'success' => false, 'message' => 'Atarim rejected this site\'s token. Reconnect the site in the Atarim plugin settings.', 'data' => [] ];
342 }
343
344 // avcf_make_api_call() only returns a decoded body on a 200; anything
345 // else arrives under 'error', so the endpoint's own message would be
346 // lost if we only ever read 'data'.
347 $payload = isset( $response['data'] ) && is_array( $response['data'] )
348 ? $response['data']
349 : ( isset( $response['error'] ) && is_array( $response['error'] ) ? $response['error'] : [] );
350
351 if ( empty( $payload ) ) {
352 return [ 'success' => false, 'message' => 'Atarim returned an unreadable response (HTTP ' . $code . ').', 'data' => [] ];
353 }
354
355 return [
356 'success' => ! empty( $payload['success'] ),
357 'message' => isset( $payload['message'] ) ? (string) $payload['message'] : '',
358 'data' => isset( $payload['data'] ) && is_array( $payload['data'] ) ? $payload['data'] : [],
359 ];
360 }
361 }