PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.3.2
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.3.2
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / central / modules / posts.php

posts.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 3.3.2, at central/modules/posts.php

2,033 lines 66.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
4
5 /**
6 * Handles Posts Commands
7 */
8 class UpdraftCentral_Posts_Commands extends UpdraftCentral_Commands {
9
10 protected $switched = false;
11
12 protected $post_type = 'post';
13
14 /**
15 * Function that gets called before every action
16 *
17 * @param string $command a string that corresponds to UDC command to call a certain method for this class.
18 * @param array $data an array of data post or get fields
19 * @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
20 *
21 * link to udrpc_action main function in class UpdraftCentral_Listener
22 */
23 public function _pre_action($command, $data, $extra_info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This function is called from listener.php and $extra_info is being sent.
24 // Here we assign the current blog_id to a variable $blog_id
25 $blog_id = get_current_blog_id();
26 if (!empty($data['site_id'])) $blog_id = $data['site_id'];
27
28 if (function_exists('switch_to_blog') && is_multisite() && $blog_id) {
29 $this->switched = switch_to_blog($blog_id);
30 }
31 }
32
33 /**
34 * Function that gets called after every action
35 *
36 * @param string $command a string that corresponds to UDC command to call a certain method for this class.
37 * @param array $data an array of data post or get fields
38 * @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
39 *
40 * link to udrpc_action main function in class UpdraftCentral_Listener
41 */
42 public function _post_action($command, $data, $extra_info) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
43 // Here, we're restoring to the current (default) blog before we switched
44 if ($this->switched) restore_current_blog();
45 }
46
47 /**
48 * Returns the keys and fields names that are associated to a particular module type
49 *
50 * @param string $type The type of the module that the current request is processing
51 *
52 * @return array
53 */
54 private function get_state_fields_by_type($type) {
55 $state_fields = array(
56 'post' => array(
57 'validation_fields' => array('publish_posts', 'edit_posts', 'delete_posts'),
58 'items_key' => 'posts',
59 'count_key' => 'posts_count',
60 'list_key' => 'posts',
61 'result_key' => 'get',
62 'error_key' => 'post_state_change_failed'
63 ),
64 'page' => array(
65 'validation_fields' => array('publish_pages', 'edit_pages', 'delete_pages'),
66 'items_key' => 'pages',
67 'count_key' => 'pages_count',
68 'list_key' => 'pages',
69 'result_key' => 'get',
70 'error_key' => 'page_state_change_failed'
71 )
72 );
73
74 if (!isset($state_fields[$type])) return array();
75 return $state_fields[$type];
76 }
77
78 /**
79 * Fetch and retrieves posts based from the submitted parameters
80 *
81 * @param array $params Containing all the needed information to filter the results of the current request
82 * @return array
83 */
84 public function get($params) {
85
86 $state_fields = $this->get_state_fields_by_type($this->post_type);
87 if (empty($state_fields)) return $this->_generic_error_response('unsupported_type_on_get_posts');
88
89 $error = $this->_validate_capabilities($state_fields['validation_fields']);
90 if (!empty($error)) return $error;
91
92 // check paged parameter; if empty set to defaults
93 $paged = !empty($params['paged']) ? (int) $params['paged'] : 1;
94 $numberposts = !empty($params['numberposts']) ? (int) $params['numberposts'] : 10;
95 $offset = ($paged - 1) * $numberposts;
96
97 $args = array(
98 'posts_per_page' => $numberposts,
99 'paged' => $paged,
100 'offset' => $offset,
101 'post_type' => $this->post_type,
102 'post_status' => 'publish,private,draft,pending,future',
103 );
104
105 if (!empty($params['keyword'])) {
106 $args['s'] = $params['keyword'];
107 }
108
109 if ('post' == $this->post_type) {
110 if (!empty($params['category'])) {
111 $args['cat'] = (int) $params['category'];
112 }
113 }
114
115 if (!empty($params['date'])) {
116 list($monthnum, $year) = explode(':', $params['date']);
117
118 $args['monthnum'] = $monthnum;
119 $args['year'] = $year;
120 }
121
122 if (!empty($params['status']) && 'all' !== $params['status']) {
123 $args['post_status'] = $params['status'];
124 }
125
126 $query = new WP_Query($args);
127 $result = $query->posts;
128
129 $count_posts = (int) $query->found_posts;
130 $page_count = 0;
131
132 if ($count_posts > 0) {
133 $page_count = absint($count_posts / $numberposts);
134 $remainder = absint($count_posts % $numberposts);
135 $page_count = ($remainder > 0) ? ++$page_count : $page_count;
136 }
137
138 $info = array(
139 'page' => $paged,
140 'pages' => $page_count,
141 'results' => $count_posts,
142 'items_from' => (($paged * $numberposts) - $numberposts) + 1,
143 'items_to' => ($paged == $page_count) ? $count_posts : $paged * $numberposts,
144 );
145
146 $posts = array();
147 if (!empty($result)) {
148 foreach ($result as $post) {
149 // Pulling any other relevant and additional information regarding
150 // the post before returning it in the response.
151 $postdata = $this->get_postdata($post, false);
152 if (!empty($postdata)) {
153 array_push($posts, $postdata);
154 }
155 }
156 }
157
158 $response = array(
159 $state_fields['items_key'] => $posts,
160 'options' => $this->get_options($this->post_type),
161 'info' => $info,
162 $state_fields['count_key'] => $this->get_post_status_counts($this->post_type)
163 );
164
165 // Load any additional information if preload parameter is set. Will only be
166 // requested on initial load of items in UpdraftCentral.
167 if (isset($params['preload']) && $params['preload']) {
168 $timeout = !empty($params['timeout']) ? $params['timeout'] : 30;
169 $response = array_merge($response, $this->get_preload_data($timeout, $this->post_type));
170 }
171
172 return $this->_response($response);
173 }
174
175 /**
176 * Extracts public properties from complex object and return a simple
177 * object (stdClass) that contains the public properties of the original object.
178 *
179 * @param object $obj Any type of complex objects that needs converting (e.g. WP_Taxonomy, WP_Term or WP_User)
180 * @return stdClass
181 */
182 protected function trim_object($obj) {
183 // To preserve the object's accessibility through its properties we recreate
184 // the object using the stdClass and fill it with the public properties
185 // that will be extracted from the original object ($obj).
186 $newObj = new stdClass();
187
188 if (is_object($obj)) {
189 // Making sure that we only extract those publicly accessible properties excluding
190 // the private, protected, static ones and methods.
191 $props = get_object_vars($obj);
192 if (!empty($props)) {
193 foreach ($props as $key => $value) {
194 $newObj->{$key} = $value;
195 }
196 }
197 }
198
199 return $newObj;
200 }
201
202 /**
203 * Retrieves information that will be preloaded in UC for quick and easy access
204 * when editing a certain page or post
205 *
206 * @param int $timeout The user-defined timeout from UpdraftCentral
207 * @param string $type The type of the module that the current request is processing
208 *
209 * @return array
210 */
211 protected function get_preload_data($timeout, $type = 'post') {
212 global $updraftcentral_host_plugin, $updraftcentral_main;
213
214 if (!function_exists('get_page_templates')) {
215 require_once(ABSPATH.'wp-admin/includes/theme.php');
216 }
217
218 $templates = ('post' == $type) ? get_page_templates(null, 'post') : get_page_templates();
219 if (!empty($templates)) {
220 $templates = array_flip($templates);
221 if (!isset($templates['default'])) {
222 $templates['default'] = $updraftcentral_host_plugin->retrieve_show_message('default_template');
223 }
224 }
225
226 // Preloading elements saves time and avoid unnecessary round trips to fetch
227 // these information individually.
228 $authors = $this->get_authors();
229 $parent_pages = $this->get_parent_pages();
230
231 $data = array(
232 'authors' => $authors['data']['authors'],
233 'parent_pages' => $parent_pages['data']['pages'],
234 'templates' => $templates,
235 'editor_styles' => $this->get_editor_styles($timeout),
236 'wp_version' => $updraftcentral_main->get_wordpress_version()
237 );
238
239 if ('post' == $type) {
240 $categories = $this->get_categories();
241 $tags = $this->get_tags();
242
243 $data['taxonomies'] = $this->get_taxonomies();
244 $data['categories'] = $categories['data'];
245 $data['tags'] = $tags['data'];
246 }
247
248 global $post;
249 $context = class_exists('WP_Block_Editor_Context') ? new WP_Block_Editor_Context(array('post' => $post)) : $post;
250
251 // Load block patterns from w.org.
252 if (function_exists('_load_remote_block_patterns')) _load_remote_block_patterns();
253 if (function_exists('_load_remote_featured_patterns')) _load_remote_featured_patterns();
254
255 $block_types = class_exists('WP_Block_Type_Registry') ? WP_Block_Type_Registry::get_instance()->get_all_registered() : array();
256 $block_patterns = class_exists('WP_Block_Patterns_Registry') ? WP_Block_Patterns_Registry::get_instance()->get_all_registered() : array();
257 $block_pattern_categories = class_exists('WP_Block_Pattern_Categories_Registry') ? WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered() : array();
258 $block_styles = class_exists('WP_Block_Styles_Registry') ? WP_Block_Styles_Registry::get_instance()->get_all_registered() : array();
259
260 $block_data = array(
261 'block_categories' => get_block_categories($context),
262 'block_definitions' => get_block_editor_server_block_settings(),
263 'block_types' => $block_types,
264 'block_patterns' => $block_patterns,
265 'block_pattern_categories' => $block_pattern_categories,
266 'block_styles' => $block_styles
267 );
268 $data = array_merge($data, $block_data);
269
270 return array(
271 'preloaded' => json_encode($data)
272 );
273 }
274
275 /**
276 * Extract content from the given css path
277 *
278 * @param string $style CSS file path
279 * @param int $timeout The user-defined timeout from UpdraftCentral
280 * @return array
281 */
282 protected function extract_css_content($style, $timeout) {
283
284 $content = '';
285 if (1 === preg_match('~^(https?:)?//~i', $style)) {
286 $response = wp_remote_get($style, array('timeout' => $timeout));
287 if (!is_wp_error($response)) {
288 $result = trim(wp_remote_retrieve_body($response));
289 if (!empty($result)) $content = $result;
290 }
291 } else {
292 // Editor styles that resides in "css/dist"
293 if (false !== ($pos = stripos($style, 'css/dist'))) {
294 $file = ABSPATH.WPINC.substr_replace($style, '/', 0, $pos);
295 } else {
296 // Styles that resides in "wp-content/themes" (coming from $editor_styles global var)
297 $file = get_theme_file_path($style);
298 }
299
300 $is_valid = (function_exists('is_file')) ? is_file($file) : file_exists($file);
301 if ($is_valid) {
302 $result = trim(file_get_contents($file));
303 if (!empty($result)) $content = $result;
304 }
305 }
306
307 return $this->extract_custom_fonts($this->filter_url($content));
308 }
309
310 /**
311 * Extract custom fonts defined within the css content. Basically,
312 * separating custom font (@font-face) rules from the Style/css content.
313 *
314 * @param string $content Style content
315 * @return array
316 */
317 protected function extract_custom_fonts($content) {
318 $fonts = array();
319 while ($start = strpos($content, '@font-face')) {
320 $end = strpos($content, '}', $start) + 1;
321 $length = $end - $start;
322
323 $font = substr($content, $start, $length);
324 $fonts[]= $this->update_font_src($font);
325
326 $content = str_replace($font, '', $content);
327 }
328
329 return array(
330 'content' => $content,
331 'fonts' => $fonts
332 );
333 }
334
335 /**
336 * Updates the font URL to point to the UpdraftCentral "load_font" action
337 *
338 * @param string $font Font-face definition/content
339 * @return string
340 */
341 protected function update_font_src($font) {
342 $start = strpos($font, 'src:') + 4;
343 $end = strpos($font, ';', $start);
344 $length = $end - $start;
345
346 $src = trim(substr($font, $start, $length));
347 $temp = explode(' ', $src);
348 preg_match('/^url\((.*)\)$/i', $temp[0], $matches);
349
350 $url = '';
351 if (!empty($matches)) {
352 $url = trim(trim($matches[1], "'"), '"');
353 if (strlen($url)) {
354 $font_url = 'CENTRAL_URL/?udcentral_action=load_font&font='.urlencode($url);
355 $font = str_replace($url, $font_url, $font);
356 }
357 }
358 return $font;
359 }
360
361 /**
362 * Convert URL entries contained in the CSS content to absolute URLs
363 *
364 * @param string $content The content of the CSS file
365 * @return string
366 */
367 protected function filter_url($content) {
368
369 // Replace with valid URL (absolute)
370 preg_match_all('~url\((.+?)\)~i', $content, $all_matches);
371 if (!empty($all_matches) && isset($all_matches[1])) {
372 $urls = array_unique($all_matches[1]);
373 foreach ($urls as $url) {
374 $url = str_replace('"', '', $url);
375 if (false !== strpos($url, 'data:')) continue;
376
377 if (1 !== preg_match('~^(https?:)?//~i', $url)) {
378 if (1 === preg_match('~(plugins|themes)~i', $url, $matches)) {
379 if (false !== ($pos = stripos($url, $matches[1]))) {
380 if (!function_exists('content_url')) {
381 require_once ABSPATH.WPINC.'/link-template.php';
382 }
383
384 $absolute_url = rtrim(content_url(), '/').substr_replace($url, '/', 0, $pos);
385 $content = str_replace($url, $absolute_url, $content);
386 }
387 } else {
388 $path = preg_replace('~(\.+\/)~', '', $url);
389 $dirpath = trailingslashit(get_stylesheet_directory());
390 if (!file_exists($dirpath.$url)) $path = $this->resolve_path($path);
391
392 $absolute_url = (!empty($path)) ? trailingslashit(get_stylesheet_directory_uri()).ltrim($path, '/') : '';
393 $content = str_replace($url, $absolute_url, $content);
394 }
395 }
396 }
397 }
398
399 return $content;
400 }
401
402 /**
403 * Resolve URL to its actual absolute path
404 *
405 * @param string $path Some relative path to check
406 * @return string
407 */
408 protected function resolve_path($path) {
409 $dir = trailingslashit(get_stylesheet_directory());
410 // Some relative paths declared within the css file (e.g. only has '../fonts/etc/', called deep down from a subfolder) where parent
411 // subfolder is not articulated needs to be resolve further to get its actual absolute path. Using glob will pinpoint its actual location
412 // rather than iterating through a series of sublevels just to find the actual file.
413 $result = str_replace($dir, '', glob($dir.'{,*/}{'.$path.'}', GLOB_BRACE));
414
415 if (!empty($result)) return $result[0];
416 return false;
417 }
418
419 /**
420 * Retrieves block editor assets for iframe.
421 *
422 * @return string
423 */
424 protected function get_iframed_editor_assets() {
425 $script_handles = array();
426 $style_handles = array(
427 'wp-block-editor',
428 'wp-block-library',
429 'wp-block-library-theme',
430 'wp-edit-blocks',
431 );
432
433 if (class_exists('WP_Block_Type_Registry')) {
434 $block_registry = WP_Block_Type_Registry::get_instance();
435 foreach ($block_registry->get_all_registered() as $block_type) {
436 if (!empty($block_type->style)) {
437 if (is_array($block_type->style)) {
438 $style_handles = array_merge($style_handles, $block_type->style);
439 } else {
440 $style_handles[] = $block_type->style;
441 }
442 }
443
444 if (!empty($block_type->editor_style)) {
445 if (is_array($block_type->editor_style)) {
446 $style_handles = array_merge($style_handles, $block_type->editor_style);
447 } else {
448 $style_handles[] = $block_type->editor_style;
449 }
450 }
451
452 if (!empty($block_type->script)) {
453 if (is_array($block_type->script)) {
454 $script_handles = array_merge($script_handles, $block_type->script);
455 } else {
456 $script_handles[] = $block_type->script;
457 }
458 }
459 }
460 }
461
462 $style_handles = array_unique($style_handles);
463 $done = wp_styles()->done;
464
465 ob_start();
466 // We do not need reset styles for the iframed editor.
467 wp_styles()->done = array('wp-reset-editor-styles');
468 wp_styles()->do_items($style_handles);
469 wp_styles()->done = $done;
470 $styles = ob_get_clean();
471
472 $script_handles = array_unique($script_handles);
473 $done = wp_scripts()->done;
474
475 ob_start();
476 wp_scripts()->done = array();
477 wp_scripts()->do_items($script_handles);
478 wp_scripts()->done = $done;
479 $scripts = ob_get_clean();
480
481 return wp_json_encode(array(
482 'styles' => $styles,
483 'scripts' => $scripts,
484 ));
485 }
486
487 /**
488 * Retrieve the editor styles/assets to be use by UpdraftCentral when editing a post
489 *
490 * @param int $timeout The user-defined timeout from UpdraftCentral
491 * @return array()
492 */
493 protected function get_editor_styles($timeout) {
494 global $editor_styles, $wp_styles;
495 $editing_styles = $loaded = array();
496 $fonts = '';
497
498 $required = array('css/dist/editor/style.css', 'css/dist/block-library/style.css', 'css/dist/block-library/theme.css');
499 foreach ($required as $style) {
500 $result = $this->extract_css_content($style, $timeout);
501 if (!empty($result['fonts'])) $fonts .= implode('', $result['fonts']);
502 $editing_styles[] = array('css' => $result['content'], 'inline' => '');
503 };
504
505 do_action('enqueue_block_assets');
506 do_action('enqueue_block_editor_assets');
507 do_action('wp_enqueue_scripts');
508
509 // Checking for editor styles support since styles may vary from theme to theme
510 if ($editor_styles) {
511 foreach ($editor_styles as $style) {
512 if (false !== array_search($style, $loaded)) continue;
513
514 $result = $this->extract_css_content($style, $timeout);
515 if (!empty($result['fonts'])) $fonts .= implode('', $result['fonts']);
516 $editing_styles[] = array('css' => $result['content'], 'inline' => '');
517 $loaded[] = $style;
518 }
519 }
520
521 if ($wp_styles) {
522 foreach ($wp_styles->queue as $handle) {
523 $style = $wp_styles->registered[$handle]->src;
524 if (false !== array_search($style, $loaded)) continue;
525
526 $result = $this->extract_css_content($style, $timeout);
527 if (!empty($result['fonts'])) $fonts .= implode('', $result['fonts']);
528
529 $inline_style = $wp_styles->print_inline_style($handle, false);
530 if ($inline_style) {
531 $inline_result = $this->extract_custom_fonts($inline_style);
532 if (!empty($inline_result['fonts'])) $fonts .= implode('', $inline_result['fonts']);
533 }
534
535 $editing_styles[] = array(
536 'css' => $result['content'],
537 'inline' => (!$inline_style) ? '' : $inline_result['content']
538 );
539 $loaded[] = $style;
540 }
541 }
542
543 // Introduced in 5.9.0
544 if (function_exists('wp_get_global_stylesheet')) {
545 $result = $this->extract_custom_fonts(wp_get_global_stylesheet());
546 if (!empty($result['fonts'])) $fonts .= implode('', $result['fonts']);
547 $editing_styles[] = array('css' => $result['content'], 'inline' => '');
548 }
549
550 // Introduced in 5.8.0
551 if (function_exists('get_block_editor_settings')) {
552 $block_editor_context = new WP_Block_Editor_Context();
553 $settings = get_block_editor_settings(array(), $block_editor_context);
554
555 // Don't render but instead attached to the editor before load.
556 // We let the editor render these kind of styles as they need to be prefixed
557 // by the editor based on the current context.
558 //
559 // N.B. Leave the 'css' property empty. It is used for downward compatibility.
560 $editing_styles[] = array('editor_css' => $settings['styles'], 'inline' => '', 'css' => '');
561
562 // Get editor assets (e.g. styles) for iframe, mostly used for previewing blocks and patterns
563 $editing_styles[] = array('editor_assets' => $this->get_iframed_editor_assets(), 'inline' => '', 'css' => '');
564 }
565
566 $result = $this->extract_css_content('/style.css', $timeout);
567 if (!empty($result['fonts'])) $fonts .= implode('', $result['fonts']);
568
569 $editing_styles[] = array('css' => $result['content'], 'inline' => '');
570 if (strlen($fonts)) {
571 $editing_styles[] = array('font_css' => $fonts, 'inline' => '', 'css' => '');
572 }
573
574 // These styles are used if the "no theme styles" options is triggered or on
575 // themes without their own editor styles.
576 $default_editor_styles_file = ABSPATH.WPINC.'/css/dist/block-editor/default-editor-styles.css';
577 if (file_exists($default_editor_styles_file)) {
578 $editing_styles[] = array('default_editor_css' => file_get_contents($default_editor_styles_file), 'inline' => '', 'css' => '');
579 }
580
581 // Extract fonts from theme.json if the current theme supports it
582 $resolver = ABSPATH.WPINC.'/class-wp-theme-json-resolver.php';
583 if (!class_exists('WP_Theme_JSON_Resolver') && file_exists($resolver)) {
584 require_once($resolver);
585 }
586
587 $theme_has_support = false;
588 if (function_exists('wp_theme_has_theme_json')) {
589 $theme_has_support = wp_theme_has_theme_json();
590 } else {
591 if (class_exists('WP_Theme_JSON_Resolver')) {
592 $theme_has_support = WP_Theme_JSON_Resolver::theme_has_support();
593 }
594 }
595
596 if (class_exists('WP_Theme_JSON_Resolver') && $theme_has_support) {
597 $theme_json = ABSPATH.WPINC.'/class-wp-theme-json.php';
598 if (!class_exists('WP_Theme_JSON') && file_exists($theme_json)) require_once($theme_json);
599
600 $theme_json_instance = WP_Theme_JSON_Resolver::get_theme_data();
601 if ($theme_json_instance) {
602 $settings = $theme_json_instance->get_settings();
603 $theme_fonts = '';
604
605 if (isset($settings['typography']) && isset($settings['typography']['fontFamilies'])) {
606 $font_families = $settings['typography']['fontFamilies'];
607 if (isset($font_families['theme'])) {
608 foreach ($font_families['theme'] as $theme) {
609 if (isset($theme['fontFace'])) {
610 foreach ($theme['fontFace'] as $font) {
611 $theme_fonts .= '@font-face {';
612 $keys = array_keys($font);
613
614 foreach ($keys as $key) {
615 if (false !== stripos($key, 'font')) {
616 $prop = 'font-'.strtolower(str_replace('font', '', $key));
617 $theme_fonts .= $prop.': '.$font[$key].';';
618 } elseif (false !== stripos($key, 'src')) {
619 foreach ($font['src'] as $src_file) {
620 $url = trailingslashit(get_stylesheet_directory_uri()).str_replace('file:./', '', $src_file);
621 $theme_fonts .= 'src: url(CENTRAL_URL/?udcentral_action=load_font&font='.urlencode($url).');';
622 }
623 }
624 }
625 $theme_fonts .= '}';
626 }
627 }
628 }
629 }
630 }
631 $editing_styles[] = array('theme_json_fonts' => $theme_fonts, 'inline' => '', 'css' => '');
632 }
633 }
634
635 return $editing_styles;
636 }
637
638 /**
639 * Retrieves the total number of items found under each post statuses
640 *
641 * @param string $type The type of the module that the current request is processing
642 *
643 * @return array
644 */
645 protected function get_post_status_counts($type = 'post') {
646 $posts = wp_count_posts($type);
647
648 $publish = (int) $posts->publish;
649 $private = (int) $posts->private;
650 $draft = (int) $posts->draft;
651 $pending = (int) $posts->pending;
652 $future = (int) $posts->future;
653 $trash = (int) $posts->trash;
654
655 // We exclude "trash" from the overall total as WP doesn't actually
656 // consider or include it in the total count.
657 $all = $publish + $private + $draft + $pending + $future;
658
659 return array(
660 'all' => $all,
661 'publish' => $publish,
662 'private' => $private,
663 'draft' => $draft,
664 'pending' => $pending,
665 'future' => $future,
666 'trash' => $trash,
667 );
668 }
669
670 /**
671 * Retrieves a collection of formatted dates found for the given post statuses.
672 * It will be used as options for the date filter when managing the posts in UpdraftCentral.
673 *
674 * @param string $type The type of the module that the current request is processing
675 *
676 * @return array
677 */
678 protected function get_date_options($type = 'post') {
679 global $wpdb;
680
681 $date_options = $wpdb->get_col("SELECT DATE_FORMAT(`post_date`, '%M %Y') as `formatted_post_date` FROM {$wpdb->posts} WHERE `post_type` = '{$type}' AND `post_status` IN ('publish', 'private', 'draft', 'pending', 'future') GROUP BY `formatted_post_date` ORDER BY `post_date` DESC");
682
683 return $date_options;
684 }
685
686 /**
687 * Make sure that we have the required fields to use in UpdraftCentral for
688 * displaying the categories and tags sections. Add if missing.
689 *
690 * @param object $item Taxonomy item to check
691 * @return object
692 */
693 protected function map_tax($item) {
694 $taxs = array('category' => 'categories', 'post_tag' => 'tags');
695 if (array_key_exists($item->name, $taxs)) {
696 if (!isset($item->show_in_rest)) $item->show_in_rest = true;
697 if (!isset($item->rest_base)) $item->rest_base = $taxs[$item->name];
698 }
699
700 return $item;
701 }
702
703 /**
704 * Fetch and retrieves available taxonomies for this site and some capabilities specific
705 * to tags and categories when managing them.
706 *
707 * @return array
708 */
709 protected function get_taxonomies() {
710 $taxonomies = get_taxonomies(array(), 'objects');
711 $taxonomies = array_map(array($this, 'map_tax'), $taxonomies);
712
713 $response = array(
714 'taxonomies' => $taxonomies,
715 'current_user_cap' => array(
716 'manage_categories' => current_user_can('manage_categories'),
717 'edit_categories' => current_user_can('edit_categories'),
718 'delete_categories' => current_user_can('delete_categories'),
719 'assign_categories' => current_user_can('assign_categories'),
720 'manage_post_tags' => current_user_can('manage_post_tags'),
721 'edit_post_tags' => current_user_can('edit_post_tags'),
722 'delete_post_tags' => current_user_can('delete_post_tags'),
723 'assign_post_tags' => current_user_can('assign_post_tags'),
724 )
725 );
726
727 return $response;
728 }
729
730 /**
731 * Fetch and retrieves categories based from the submitted parameters
732 *
733 * @param array $query Containing all the needed information to filter the results of the current request
734 * @return array
735 */
736 public function get_categories($query = array()) {
737 $page = !empty($query['page']) ? (int) $query['page'] : 1;
738 $items_per_page = !empty($query['per_page']) ? (int) $query['per_page'] : 100;
739 $offset = ($page - 1) * $items_per_page;
740 $order = !empty($query['order']) ? $query['order'] : 'asc';
741 $orderby = !empty($query['orderby']) ? $query['orderby'] : 'name';
742
743 $args = array(
744 'hide_empty' => false,
745 'orderby' => $orderby,
746 'order' => $order,
747 'number' => $items_per_page,
748 'offset' => $offset
749 );
750
751 $categories = get_categories($args);
752 $category_options = array();
753
754 if (!empty($categories)) {
755 foreach ($categories as $key => $term) {
756 $parent_term = get_term((int) $term->parent, $term->taxonomy);
757 if (!is_wp_error($parent_term) && !is_null($parent_term)) {
758 $parent_term = json_encode($this->trim_object($parent_term));
759 } else {
760 $parent_term = '';
761 }
762
763 $category_options[] = array(
764 'id' => $term->term_id,
765 'name' => $term->name,
766 'parent' => $term->parent
767 );
768
769 $categories[$key] = array(
770 'term' => json_encode($this->trim_object($term)),
771 'misc' => array(
772 'link' => get_term_link($term),
773 'parent_term' => $parent_term,
774 'taxonomy' => $term->taxonomy
775 )
776 );
777 }
778 }
779
780 $categorytax = get_taxonomy('category');
781 $parent_dropdown_args = array(
782 'taxonomy' => 'category',
783 'hide_empty' => 0,
784 'name' => 'newcategory_parent',
785 'orderby' => 'name',
786 'hierarchical' => 1,
787 'show_option_none' => '&mdash; '.$categorytax->labels->parent_item.' &mdash;',
788 'echo' => false
789 );
790
791 $parent_dropdown_args = apply_filters('post_edit_category_parent_dropdown_args', $parent_dropdown_args);
792 $parent_dropdown = wp_dropdown_categories($parent_dropdown_args);
793
794 if (!function_exists('wp_popular_terms_checklist')) {
795 require_once ABSPATH . 'wp-admin/includes/template.php';
796 }
797
798 ob_start();
799 wp_popular_terms_checklist('category');
800 $popular_terms_checklist = ob_get_contents();
801 ob_end_clean();
802
803 return $this->_response(array(
804 'terms' => $categories,
805 'misc' => array(
806 'formatted' => $category_options,
807 'raw' => $categories,
808 'tax' => json_encode($this->trim_object($categorytax)),
809 'popular' => $popular_terms_checklist,
810 'parent_dropdown' => $parent_dropdown,
811 'capabilities' => array(
812 'can_edit_terms' => current_user_can($categorytax->cap->edit_terms)
813 )
814 )
815 ));
816 }
817
818 /**
819 * Fetch and retrieves tags based from the submitted parameters
820 *
821 * @param array $query Containing all the needed information to filter the results of the current request
822 * @return array
823 */
824 public function get_tags($query = array()) {
825 $page = !empty($query['page']) ? (int) $query['page'] : 1;
826 $items_per_page = !empty($query['per_page']) ? (int) $query['per_page'] : 100;
827 $offset = ($page - 1) * $items_per_page;
828 $order = !empty($query['order']) ? $query['order'] : 'desc';
829 $orderby = !empty($query['orderby']) ? $query['orderby'] : 'count';
830
831 $args = array(
832 'hide_empty' => false,
833 'orderby' => $orderby,
834 'order' => $order,
835 'number' => $items_per_page,
836 'offset' => $offset
837 );
838
839 $tags = get_tags($args);
840 $tag_options = array();
841 $tag_cloud = '';
842
843 if (!empty($tags)) {
844 $tags_for_cloud = array();
845 foreach ($tags as $key => $term) {
846 if (!isset($term->link)) $term->link = get_tag_link($term->term_id);
847 array_push($tags_for_cloud, $term);
848
849 $parent_term = get_term((int) $term->parent, $term->taxonomy);
850 if (!is_wp_error($parent_term) && !is_null($parent_term)) {
851 $parent_term = json_encode($this->trim_object($parent_term));
852 } else {
853 $parent_term = '';
854 }
855
856 $tag_options[] = array(
857 'id' => $term->term_id,
858 'name' => $term->name,
859 );
860
861 $tags[$key] = array(
862 'term' => json_encode($this->trim_object($term)),
863 'misc' => array(
864 'link' => get_term_link($term),
865 'parent_term' => $parent_term,
866 'taxonomy' => $term->taxonomy
867 )
868 );
869 }
870
871 add_filter('tag_cloud_sort', array($this, 'sort_tag_cloud'), 9, 2);
872
873 if (!function_exists('wp_generate_tag_cloud')) {
874 require_once ABSPATH.WPINC.'/category-template.php';
875 }
876
877 $tag_cloud = wp_generate_tag_cloud($tags_for_cloud, array(
878 'smallest' => 10,
879 'largest' => 22,
880 'unit' => 'pt',
881 'number' => 10,
882 'format' => 'flat',
883 'separator' => " ",
884 'orderby' => 'count',
885 'order' => 'DESC',
886 'show_count' => 1,
887 'echo' => false
888 ));
889 }
890
891 $tagtax = get_taxonomy('post_tag');
892 return $this->_response(array(
893 'terms' => $tags,
894 'misc' => array(
895 'formatted' => $tag_options,
896 'raw' => $tags,
897 'tax' => json_encode($this->trim_object($tagtax)),
898 'tag_cloud' => $tag_cloud,
899 'capabilities' => array(
900 'can_assign_terms' => current_user_can($tagtax->cap->assign_terms)
901 )
902 )
903 ));
904 }
905
906 /**
907 * Sorts the tag items that are to be shown within the tag cloud
908 *
909 * @param array $tags The array to be sorted. Contains the tag items
910 * @param array $args Additional parameters needed for the sorting process
911 * @return array
912 */
913 public function sort_tag_cloud($tags, $args) {
914 uasort($tags, array($this, '_wp_object_count_sort_cb'));
915 if ('DESC' === $args['order']) {
916 $tags = array_reverse($tags, true);
917 }
918
919 return $tags;
920 }
921
922 /**
923 * Serves as a callback for comparing objects based on count. Copied from WordPress 5.7
924 * core (wp-includes/category-template.php) and tweaked to return integer instead of boolean
925 * because returning boolean using uasort is now DEPRECATED in PHP 8.
926 *
927 * Used with `uasort()`.
928 *
929 * @since 3.1.0
930 * @access private
931 *
932 * @param object $a The first object to compare.
933 * @param object $b The second object to compare.
934 * @return bool Whether the count value for `$a` is greater than the count value for `$b`.
935 */
936 public function _wp_object_count_sort_cb($a, $b) {
937 if ($a->count == $b->count) {
938 return 0;
939 }
940 return ( $a->count > $b->count ) ? 1 : -1;
941 }
942
943 /**
944 * Fetch all available taxonomies and terms information for the given post object
945 *
946 * @param array $post The "Post" object to use when retrieving the information
947 * @return array
948 */
949 protected function get_taxonomies_terms($post) {
950 $taxonomies = get_object_taxonomies($post->post_type, 'objects');
951 $taxonomies = array_map(array($this, 'map_tax'), $taxonomies);
952
953 $taxonomy_names = array();
954 $taxonomy_terms = array();
955 $taxonomy_caps = array();
956
957 foreach ($taxonomies as $taxonomy) {
958 $terms = get_the_terms($post->ID, $taxonomy->name);
959 $terms = !is_array($terms) ? (array) $terms : $terms;
960
961 $taxonomy_terms[$taxonomy->name] = $terms;
962 $taxonomy_caps[$taxonomy->name] = array(
963 'hierarchical' => is_taxonomy_hierarchical($taxonomy->name),
964 'edit_terms' => current_user_can($taxonomy->cap->edit_terms),
965 'assign_terms' => current_user_can($taxonomy->cap->assign_terms),
966 );
967 array_push($taxonomy_names, $taxonomy->name);
968 }
969
970 return array(
971 'objects' => $taxonomies,
972 'names' => $taxonomy_names,
973 'terms' => $taxonomy_terms,
974 'caps' => $taxonomy_caps,
975 );
976 }
977
978 /**
979 * Take over the current editing of the post
980 *
981 * @param array $params An array of data that serves as parameters for the given request
982 * @return array
983 */
984 public function take_over($params) {
985
986 $error = $this->_validate_capabilities(array('edit_'.$this->post_type.'s'));
987 if (!empty($error)) return $error;
988
989 $result = array('lock_acquired' => false);
990 if (!empty($params['post_id'])) {
991 if (!function_exists('wp_set_post_lock')) {
992 require_once ABSPATH.'wp-admin/includes/post.php';
993 }
994 $lock = wp_set_post_lock($params['post_id']);
995
996 if (!empty($lock)) {
997 $result = array(
998 'lock_acquired' => true,
999 'details' => $lock
1000 );
1001 }
1002 }
1003
1004 return $this->_response($result);
1005 }
1006
1007 /**
1008 * Retrieves the underlying data for the given post. Some extra information are
1009 * passed along that will be consumed by the editor in UpdraftCentral
1010 *
1011 * @param int|object $param Post object or a post ID
1012 * @param boolean $encode True to encode the post object, false otherwise
1013 * @return array
1014 */
1015 public function get_postdata($param, $encode = true) {
1016 $response = array();
1017
1018 if (is_object($param) && isset($param->ID)) {
1019 $post = $param;
1020 } elseif (is_numeric($param)) {
1021 $post = get_post($param);
1022 }
1023
1024 if ($post) {
1025 $post_type_obj = get_post_type_object($post->post_type);
1026
1027 $is_post_type_viewable = false;
1028 if (!empty($post_type_obj)) {
1029 $is_post_type_viewable = $post_type_obj->publicly_queryable || ($post_type_obj->_builtin && $post_type_obj->public);
1030 }
1031
1032 if (!function_exists('get_sample_permalink')) {
1033 require_once ABSPATH.'wp-admin/includes/post.php';
1034 }
1035
1036 // Validate template exists on the current theme, otherwise,
1037 // reset the template to default.
1038 $template = get_page_template_slug($post->ID);
1039 if (!empty($template)) {
1040 $page_templates = wp_get_theme()->get_page_templates($post);
1041 if ('default' != $template && !isset($page_templates[$template])) {
1042 update_post_meta($post->ID, '_wp_page_template', 'default');
1043 }
1044 }
1045
1046 $published_date = array(
1047 'jj' => date('d', strtotime($post->post_date)),
1048 'mm' => date('m', strtotime($post->post_date)),
1049 'aa' => date('Y', strtotime($post->post_date)),
1050 'hh' => date('H', strtotime($post->post_date)),
1051 'mn' => date('i', strtotime($post->post_date)),
1052 'ss' => date('s', strtotime($post->post_date))
1053 );
1054
1055 $sample_permalink = get_sample_permalink($post->ID, $post->post_title, '');
1056 $permalink = get_permalink($post->ID);
1057 $slug = $post->post_name;
1058
1059 if (!empty($sample_permalink) && !empty($slug)) {
1060 if (isset($sample_permalink[0])) {
1061 if (false !== stripos($sample_permalink[0], '%pagename%/') || false !== stripos($sample_permalink[0], '%postname%/')) {
1062 $token = (false !== stripos($sample_permalink[0], '%pagename%/')) ? '%pagename%/' : '%postname%/';
1063 $permalink = str_replace($token, '', $sample_permalink[0]).$slug;
1064 }
1065 }
1066 }
1067
1068 $editor = null;
1069 $editor_id = wp_check_post_lock($post->ID);
1070 if ($editor_id) {
1071 $editor = get_userdata($editor_id);
1072 if (!$editor) {
1073 // The user with lock does not exist. This can happen if you created a backup or clone
1074 // where you excluded the users table during the process and you restore this backup to
1075 // a different site or the user was deleted or removed more recently. Thus, we will
1076 // release the lock so that other users with the right permission can edit the post.
1077 delete_post_meta($post->ID, '_edit_lock');
1078 }
1079 }
1080
1081 $response = array(
1082 'post' => $encode ? json_encode($post) : $post,
1083 'misc' => array(
1084 'guid_rendered' => apply_filters('get_the_guid', $post->guid, $post->ID),
1085 'link' => $permalink,
1086 'slug' => $slug,
1087 'site_url' => site_url('/'),
1088 'title_rendered' => get_the_title($post->ID),
1089 'content_rendered' => apply_filters('the_content', $post->post_content),
1090 'excerpt' => $post->post_excerpt,
1091 'featured_media' => 0,
1092 'sticky' => is_sticky($post->ID),
1093 'template' => get_page_template_slug($post->ID),
1094 'permalink_template' => get_permalink($post->ID, true),
1095 'author_name' => get_the_author_meta('display_name', $post->post_author),
1096 'publish_month_year' => date('F Y', strtotime($post->post_date)),
1097 'published_date' => $published_date,
1098 'format' => get_post_format($post->ID),
1099 'post_type_name' => $post_type_obj->name,
1100 'post_type_viewable' => $is_post_type_viewable,
1101 'post_type_public' => $post_type_obj->public,
1102 'post_type_hierarchical' => $post_type_obj->hierarchical,
1103 'sample_permalink' => get_sample_permalink($post->ID, $post->post_title, ''),
1104 'post_password_required' => post_password_required($post),
1105 'post_type_supports_authors' => post_type_supports($post->post_type, 'author'),
1106 'post_type_supports_comments' => post_type_supports($post->post_type, 'comments'),
1107 'post_type_supports_revisions' => post_type_supports($post->post_type, 'revisions'),
1108 'post_revisions' => array(), // N.B. We're not going to allow revisions editing for now
1109 'post_thumbnail_id' => get_post_thumbnail_id($post->ID),
1110 'can_publish_posts' => current_user_can($post_type_obj->cap->publish_posts),
1111 'can_edit_others_posts' => current_user_can($post_type_obj->cap->edit_others_posts),
1112 'can_unfiltered_html' => current_user_can('unfiltered_html'),
1113 'is_edited' => $editor ? 1 : 0,
1114 'editor_id' => $editor_id,
1115 'editor' => $editor,
1116 'edited_by_id' => $editor ? $editor->ID : 0,
1117 'edited_by_display_name' => $editor ? $editor->display_name : '',
1118 )
1119 );
1120
1121 if ('post' == $post->post_type) {
1122 $taxonomies = $this->get_taxonomies_terms($post);
1123 $response['misc']['taxonomy_objects'] = $taxonomies['objects'];
1124 $response['misc']['taxonomy_names'] = $taxonomies['names'];
1125 $response['misc']['taxonomy_terms'] = $taxonomies['terms'];
1126 $response['misc']['taxonomy_caps'] = $taxonomies['caps'];
1127
1128 if (!function_exists('wp_popular_terms_checklist') || !function_exists('get_terms_to_edit')) {
1129 require_once ABSPATH . 'wp-admin/includes/template.php';
1130 require_once ABSPATH . 'wp-admin/includes/taxonomy.php';
1131 }
1132
1133 if (!function_exists('wp_get_post_categories')) {
1134 require_once(ABSPATH.WPINC.'/post.php');
1135 }
1136
1137 $categories = wp_get_post_categories($post->ID, array('fields' => 'ids'));
1138 if (!is_wp_error($categories)) {
1139 $response['misc']['categories'] = empty($categories) ? array() : $categories;
1140 $terms_to_edit = get_terms_to_edit($post->ID, 'category');
1141 if (!empty($terms_to_edit)) {
1142 $response['misc']['categories_list'] = str_replace(',', ', ', $terms_to_edit);
1143 }
1144
1145 $popular_ids = wp_popular_terms_checklist('category', 0, 10, false);
1146 // On WP 3.4 the "wp_terms_checklist" doesn't have an "echo" parameter and will automatically
1147 // display the rendered checklist. Therefore, we're going to pull the terms so that all
1148 // versions starting from WP 3.4 will pull the content instead of displaying them.
1149
1150 ob_start();
1151 // In this call we'll have to set the "echo" parameter to true so that later version of WP
1152 // will be able to catch and process it.
1153 wp_terms_checklist($post->ID, array('taxonomy' => 'category', 'popular_cats' => $popular_ids, 'echo' => true));
1154 $popular_checklist = ob_get_contents();
1155 ob_end_clean();
1156
1157 $response['misc']['categories_checklist'] = $popular_checklist;
1158
1159 ob_start();
1160 wp_terms_checklist($post->ID, array('taxonomy' => 'category', 'checked_ontop' => 0, 'echo' => true));
1161 $quickedit_checklist = ob_get_contents();
1162 ob_end_clean();
1163
1164 $response['misc']['categories_quickedit_checklist'] = $quickedit_checklist;
1165 }
1166
1167 $tags = wp_get_post_tags($post->ID, array('fields' => 'ids'));
1168 if (!is_wp_error($tags)) {
1169 $response['misc']['tags'] = empty($tags) ? array() : $tags;
1170 $terms_to_edit = get_terms_to_edit($post->ID, 'post_tag');
1171 if (!empty($terms_to_edit)) {
1172 $response['misc']['tags_list'] = str_replace(',', ', ', $terms_to_edit);
1173 }
1174 }
1175 }
1176
1177 // Naturally, the "featured_media" will suffice when loading the image (media) in
1178 // UpdraftCentral since the value in this field is the actual image id of the featured
1179 // media used in UC. If we currently don't have an entry in the "featured_media_updraftcentral" meta,
1180 // then UC will need to download the featured media (image) for this current post/page
1181 // using the "featured_media_url" field (below) if not empty.
1182 $featured_media = get_post_meta($post->ID, 'featured_media_updraftcentral', true);
1183 if (!empty($featured_media)) {
1184 $response['misc']['featured_media'] = $featured_media;
1185 }
1186
1187 // Retrieve featured media if currently present for the given post/page.
1188 // If present, we pull the image (media) URL in case there's a need for
1189 // UpdraftCentral to download the image upon loading the editor (e.g. the featured_media id
1190 // above no longer exists).
1191 $media_id = (int) get_post_thumbnail_id($post->ID);
1192 if (!empty($media_id)) {
1193 $response['misc']['featured_media_url'] = wp_get_attachment_url($media_id);
1194 } else {
1195 // The post/page no longer has a "featured_media" or doesn't have one currently, therefore,
1196 // we're going to set the "featured_media" and "featured_media_url" fields to both empty to
1197 // to avoid any further actions (e.g. download media).
1198 $response['misc']['featured_media'] = 0;
1199 $response['misc']['featured_media_url'] = '';
1200 }
1201 }
1202
1203 return $response;
1204 }
1205
1206 /**
1207 * Changes the state/status of the submitted post(s)
1208 *
1209 * @param array $params An array of data that serves as parameters for the given request
1210 * @return array
1211 */
1212 public function set_state($params) {
1213
1214 $state_fields = $this->get_state_fields_by_type($this->post_type);
1215 if (empty($state_fields)) return $this->_generic_error_response('unsupported_type_on_set_state');
1216
1217 $error = $this->_validate_capabilities($state_fields['validation_fields']);
1218 if (!empty($error)) return $error;
1219
1220 $result = array();
1221 if (!empty($params['list'])) {
1222 $posts = array();
1223 foreach ($params['list'] as $id) {
1224 $post = $this->apply_state($id, $params['action'], $this->post_type);
1225 if (!empty($post)) {
1226 array_push($posts, $post);
1227 }
1228 }
1229
1230 if (!empty($posts)) {
1231 $result = array($state_fields['list_key'] => $posts);
1232 }
1233 } elseif (!empty($params['id'])) {
1234 $post = $this->apply_state($params['id'], $params['action'], $this->post_type);
1235 if (!empty($post)) $result = $post;
1236 }
1237
1238 if (!empty($result)) {
1239 $response = $this->get($params);
1240 if (!empty($response['response']) && 'rpcok' === $response['response']) {
1241 $result[$state_fields['result_key']] = $response['data'];
1242 }
1243
1244 return $this->_response($result);
1245 } else {
1246 return $this->_generic_error_response($state_fields['error_key'], array('action' => $params['action']));
1247 }
1248 }
1249
1250 /**
1251 * Creates new category
1252 *
1253 * @param array $params An array of data that serves as parameters for the given request
1254 * @param boolean $wrap_response Indicates whether to wrap the response based on local or UpdraftCentral calls. Default true.
1255 * @return array
1256 */
1257 public function add_category($params, $wrap_response = true) {
1258 $error = $this->_validate_capabilities(array('manage_categories'));
1259 if (!empty($error)) return $error;
1260
1261 $name = sanitize_text_field($params['name']);
1262 $args = array();
1263 if (!empty($params['parent'])) {
1264 $args['parent'] = $params['parent'];
1265 }
1266
1267 $result = wp_insert_term($name, 'category', $args);
1268 if (!is_wp_error($result)) {
1269 $term_id = $result['term_id'];
1270 $term = get_term($term_id, 'category');
1271
1272 $data = array();
1273 if (!is_wp_error($term)) {
1274 $data = array(
1275 'id' => $term->term_id,
1276 'count' => $term->count,
1277 'description' => $term->description,
1278 'link' => get_term_link($term->term_id, 'category'),
1279 'name' => $term->name,
1280 'slug' => $term->slug,
1281 'taxonomy' => $term->taxonomy,
1282 'parent' => $term->parent,
1283 'meta' => array()
1284 );
1285
1286 $categories = $this->get_categories();
1287 if ($wrap_response) $data['categories'] = json_encode($categories['data']);
1288 }
1289
1290 return $wrap_response ? $this->_response($data) : $data;
1291 } else {
1292 $error = array(
1293 'message' => $result->get_error_message()
1294 );
1295
1296 return $wrap_response ? $this->_generic_error_response('post_add_category_failed', $error) : $error;
1297 }
1298 }
1299
1300 /**
1301 * Assigns categories to a certain post object
1302 *
1303 * @param int $post_id The ID of the post object
1304 * @param array $category_ids A collection of category IDs to assign to the post object
1305 * @return void
1306 */
1307 protected function assign_category_to_post($post_id, $category_ids) {
1308 if (!empty($category_ids)) {
1309 // Making sure that we have the correct type to use and we
1310 // don't have any redundant IDs before saving.
1311 $category_ids = array_unique(array_map('intval', $category_ids));
1312
1313 // Attach (new) categories to post
1314 wp_set_object_terms($post_id, $category_ids, 'category');
1315 } else {
1316 wp_set_object_terms($post_id, get_option('default_category'), 'category');
1317 }
1318 }
1319
1320 /**
1321 * Creates new tag
1322 *
1323 * @param array $params An array of data that serves as parameters for the given request
1324 * @param boolean $wrap_response Indicates whether to wrap the response based on local or UpdraftCentral calls. Default true.
1325 * @return array
1326 */
1327 public function add_tag($params, $wrap_response = true) {
1328 // N.B. Since the "manage_post_tags" capability does not exist in WP 3.4. We'll use the "manage_categories" instead. Besides, the "manage_post_tags" along with the other tag-related capabilities in the latest versions are actually mapped to the "manage_categories" capability (refer to wp-includes/capabilities.php under the "map_meta_cap" function).
1329 $error = $this->_validate_capabilities(array('manage_categories'));
1330 if (!empty($error)) return $error;
1331
1332 $name = sanitize_text_field($params['name']);
1333 $result = wp_insert_term($name, 'post_tag');
1334 if (!is_wp_error($result)) {
1335 $term_id = $result['term_id'];
1336 $term = get_term($term_id, 'post_tag');
1337
1338 $data = array();
1339 if (!is_wp_error($term)) {
1340 $data = array(
1341 'id' => $term->term_id,
1342 'count' => $term->count,
1343 'description' => $term->description,
1344 'link' => get_term_link($term->term_id, 'post_tag'),
1345 'name' => $term->name,
1346 'slug' => $term->slug,
1347 'taxonomy' => $term->taxonomy,
1348 'meta' => array()
1349 );
1350
1351 $tags = $this->get_tags();
1352 if ($wrap_response) $data['tags'] = json_encode($tags['data']);
1353 }
1354
1355 return $wrap_response ? $this->_response($data) : $data;
1356 } else {
1357 $error = array(
1358 'message' => $result->get_error_message()
1359 );
1360
1361 return $wrap_response ? $this->_generic_error_response('post_add_tag_failed', $error) : $error;
1362 }
1363 }
1364
1365 /**
1366 * Assigns tags to a certain post object
1367 *
1368 * @param int $post_id The ID of the post object
1369 * @param array $tag_ids A collection of tag IDs to assign to the post object
1370 * @return void
1371 */
1372 protected function assign_tag_to_post($post_id, $tag_ids) {
1373 if (!empty($tag_ids)) {
1374 // Making sure that we have the correct type to use and we
1375 // don't have any redundant IDs before saving.
1376 $tag_ids = array_unique(array_map('intval', $tag_ids));
1377
1378 // Attach (new) tags to post
1379 wp_set_object_terms($post_id, $tag_ids, 'post_tag');
1380 } else {
1381 wp_set_object_terms($post_id, null, 'post_tag');
1382 }
1383 }
1384
1385 /**
1386 * Pre-validates data before running the save process
1387 *
1388 * @param WP_Post $post The post object to validate
1389 * @param array $params An array of data that serves as parameters for the given request
1390 *
1391 * @return array|void
1392 */
1393 private function pre_validation($post, $params) {
1394 if (empty($post) || empty($params)) return;
1395
1396 if (!empty($params['password'])) {
1397 if (!empty($params['sticky'])) {
1398 return $this->_generic_error_response('post_save_failed', array(
1399 'message' => __('A post can not be sticky and have a password.'),
1400 'args' => $params
1401 ));
1402 }
1403
1404 if (!isset($params['sticky']) && is_sticky($post->ID)) {
1405 return $this->_generic_error_response('post_save_failed', array(
1406 'message' => __('A sticky post can not be password protected.'),
1407 'args' => $params
1408 ));
1409 }
1410 }
1411
1412 if (!empty($params['sticky'])) {
1413 if (!isset($params['password']) && post_password_required($post->ID)) {
1414 return $this->_generic_error_response('post_save_failed', array(
1415 'message' => __('A password protected post can not be set to sticky.'),
1416 'args' => $params
1417 ));
1418 }
1419 }
1420 }
1421
1422 /**
1423 * Saves or updates post/page information based from the submitted data
1424 *
1425 * @param array $params An array of data that serves as parameters for the given request
1426 * @return array
1427 */
1428 public function save($params) {
1429 global $updraftcentral_host_plugin;
1430
1431 $validation_fields = array(
1432 'post' => array('publish_posts', 'edit_posts', 'delete_posts'),
1433 'page' => array('publish_pages', 'edit_pages', 'delete_pages')
1434 );
1435
1436 if (!isset($validation_fields[$this->post_type])) return $this->_generic_error_response('unsupported_type_on_save_post');
1437
1438 $error = $this->_validate_capabilities($validation_fields[$this->post_type]);
1439 if (!empty($error)) return $error;
1440
1441 if (!empty($params['id']) || !empty($params['new'])) {
1442 $args = array();
1443
1444 if (!empty($params['id'])) {
1445 $post = get_post($params['id']);
1446 if (!empty($post)) {
1447 $result = $this->pre_validation($post, $params);
1448 if (isset($result['response']) && 'rpcerror' == $result['response']) {
1449 return $result;
1450 }
1451 }
1452 }
1453
1454 // post_content
1455 if (!empty($params['content']))
1456 $args['post_content'] = $params['content'];
1457
1458 // post_excerpt
1459 if (!empty($params['excerpt']))
1460 $args['post_excerpt'] = $params['excerpt'];
1461
1462 // menu_order
1463 if (isset($params['order']))
1464 $args['menu_order'] = (int) $params['order'];
1465
1466 // post_parent
1467 if (isset($params['parent'])) {
1468 $args['post_parent'] = empty($params['parent']) ? 0 : $params['parent'];
1469 }
1470
1471 // post_name
1472 if (!empty($params['slug']))
1473 $args['post_name'] = $params['slug'];
1474
1475 // post_status
1476 if (!empty($params['status'])) {
1477 $args['post_status'] = $params['status'];
1478 }
1479
1480 // post_title
1481 if (!empty($params['title']))
1482 $args['post_title'] = $params['title'];
1483
1484 // post_author
1485 if (!empty($params['author']))
1486 $args['post_author'] = $params['author'];
1487
1488 // comment_status
1489 if (!empty($params['comment_status']))
1490 $args['comment_status'] = $params['comment_status'];
1491
1492 // ping_status
1493 if (!empty($params['ping_status']))
1494 $args['ping_status'] = $params['ping_status'];
1495
1496 // visibility
1497 if (!empty($params['visibility'])) {
1498 switch ($params['visibility']) {
1499 case 'public':
1500 $args['post_status'] = 'publish';
1501 $args['post_password'] = '';
1502 break;
1503 case 'password':
1504 $args['post_status'] = 'publish';
1505 $args['post_password'] = $params['password'];
1506 break;
1507 case 'private':
1508 $args['post_status'] = 'private';
1509 $args['post_password'] = '';
1510 break;
1511 default:
1512 break;
1513 }
1514 } else {
1515 if (!empty($params['password'])) {
1516 $args['post_status'] = 'publish';
1517 $args['post_password'] = $params['password'];
1518 } elseif (isset($params['password']) && '' == $params['password']) {
1519 $args['post_status'] = 'publish';
1520 $args['post_password'] = '';
1521 }
1522 }
1523
1524 // post/publish date
1525 if (!empty($params['date'])) {
1526 $datetime = strtotime($params['date']);
1527 $post_date = date('Y-m-d H:i:s', $datetime);
1528
1529 $args['post_date'] = $post_date;
1530 $args['post_date_gmt'] = gmdate('Y-m-d H:i:s', $datetime);
1531
1532 // We only change the status to "future" based from the submitted date if the post status
1533 // is not empty and equal to 'publish' and the date is for the coming future.
1534 if (!empty($params['status']) && 'publish' == $params['status']) {
1535 if (strtotime($post_date) > strtotime(date('Y-m-d H:i:s'))) $args['post_status'] = 'future';
1536 }
1537 }
1538
1539 // Make sure we have a slug/post_name generated before insert/update
1540 if (empty($params['slug']) && !empty($params['title'])) {
1541 $args['post_name'] = sanitize_title_with_dashes($params['title']);
1542 }
1543
1544 if (!empty($params['new'])) {
1545 $args['post_type'] = $this->post_type;
1546 $post_id = wp_insert_post($args, true);
1547 } else {
1548 $args['ID'] = $params['id'];
1549 $args['post_modified'] = date('Y-m-d H:i:s');
1550 $args['post_modified_gmt'] = gmdate('Y-m-d H:i:s');
1551
1552 $post_id = wp_update_post($args, true);
1553 }
1554
1555 // We have successfully created/updated a post at this point, thus, we'll continue
1556 // with implementing the other requested processes and return the result.
1557 if (!is_wp_error($post_id)) {
1558 // sticky post
1559 if (isset($params['sticky'])) {
1560 $sticky = (bool) $params['sticky'];
1561 if ($sticky) {
1562 stick_post($post_id);
1563 } else {
1564 if (is_sticky($post_id)) {
1565 unstick_post($post_id);
1566 }
1567 }
1568 }
1569
1570 // template
1571 if (!empty($params['template'])) {
1572 update_post_meta($post_id, '_wp_page_template', $params['template']);
1573 }
1574
1575 // featured_media
1576 if (isset($params['featured_media'])) {
1577 if (!empty($params['featured_media'])) {
1578 $featured_media = (int) $params['featured_media'];
1579 $attach_continue = true;
1580
1581 $url = wp_get_attachment_url($featured_media);
1582 if (!empty($url) && !empty($params['featured_media_url']) && $url == $params['featured_media_url']) {
1583 set_post_thumbnail($post_id, $featured_media);
1584 update_post_meta($post_id, 'featured_media_updraftcentral', $params['featured_media']);
1585 $attach_continue = false;
1586 }
1587
1588 if ($attach_continue) {
1589 $featured_media_data = !empty($params['featured_media_data']) ? $params['featured_media_data'] : null;
1590 $media_id = $this->attach_remote_image($params['featured_media_url'], $featured_media_data, $post_id);
1591 if (!empty($media_id)) {
1592 // If we have a successful attachment then add reference to UC's media id
1593 update_post_meta($post_id, 'featured_media_updraftcentral', $params['featured_media']);
1594 }
1595 }
1596 } else {
1597 // Remove featured image.
1598 delete_post_meta($post_id, '_thumbnail_id');
1599 delete_post_meta($post_id, 'featured_media_updraftcentral');
1600 }
1601 }
1602
1603 // categories
1604 $categories_updated = false;
1605 if (!empty($params['categories'])) {
1606 $term_ids = array();
1607 foreach ($params['categories'] as $value) {
1608 $category = sanitize_text_field($value);
1609 $parent = 0;
1610
1611 if (false !== strpos($category, ':')) {
1612 list($parent, $category) = explode(':', $category);
1613 $result = $this->add_category(array('name' => $category, 'parent' => $parent), false);
1614
1615 if (!empty($result)) {
1616 array_push($term_ids, $result['id']);
1617 }
1618 } else {
1619 $term = get_term_by('id', $category, 'category');
1620 if (!empty($term)) {
1621 $term_id = $term->term_id;
1622 array_push($term_ids, $term_id);
1623 }
1624 }
1625 }
1626
1627 $this->assign_category_to_post($post_id, $term_ids);
1628 $categories_updated = true;
1629 }
1630
1631 // tags
1632 $tags_updated = false;
1633 if (!empty($params['tags'])) {
1634 $term_ids = array();
1635 foreach ($params['tags'] as $value) {
1636 $tag = sanitize_text_field($value);
1637 $field = is_numeric($tag) ? 'id' : 'name';
1638
1639 $term = get_term_by($field, $tag, 'post_tag');
1640 if (!empty($term)) {
1641 $term_id = $term->term_id;
1642 array_push($term_ids, $term_id);
1643 } else {
1644 $result = $this->add_tag(array('name' => $tag), false);
1645 if (!empty($result)) {
1646 array_push($term_ids, $result['id']);
1647 }
1648 }
1649 }
1650
1651 $this->assign_tag_to_post($post_id, $term_ids);
1652 $tags_updated = true;
1653 }
1654
1655 // Pulling any other relevant and additional information regarding
1656 // the post before returning it in the response.
1657 $postdata = $this->get_postdata($post_id);
1658
1659 if (!empty($params['new'])) {
1660 $timeout = !empty($params['timeout']) ? $params['timeout'] : 30;
1661 $postdata = array_merge($postdata, $this->get_preload_data($timeout, $this->post_type));
1662 } else {
1663 if ($categories_updated || $tags_updated) {
1664 $categories = $this->get_categories();
1665 $tags = $this->get_tags();
1666
1667 $postdata['preloaded'] = json_encode(array(
1668 'categories' => $categories['data'],
1669 'tags' => $tags['data']
1670 ));
1671 }
1672 }
1673
1674 $postdata['options'] = $this->get_options($this->post_type);
1675 return $this->_response($postdata);
1676 } else {
1677 // ERROR: error creating or updating post
1678 return $this->_generic_error_response('post_save_failed', array(
1679 'message' => $post_id->get_error_message(),
1680 'args' => $args
1681 ));
1682 }
1683 } else {
1684 // ERROR: no id parameter, invalid request
1685 return $this->_generic_error_response('post_invalid_request', array('message' => $updraftcentral_host_plugin->retrieve_show_message('parameters_missing')));
1686 }
1687 }
1688
1689 /**
1690 * Fetch and retrieves authors based from the submitted parameters
1691 *
1692 * @param array $params Containing all the needed information to filter the results of the current request
1693 * @return array
1694 */
1695 public function get_authors($params = array()) {
1696 global $updraftcentral_main;
1697
1698 // If expected parameters are empty or does not exists then set them to some default values
1699 $page = !empty($params['page']) ? (int) $params['page'] : 1;
1700 $per_page = !empty($params['per_page']) ? (int) $params['per_page'] : 15;
1701 $offset = ($page - 1) * $per_page;
1702 $who = !empty($params['who']) ? $params['who'] : 'authors';
1703 $order = !empty($params['order']) ? strtoupper($params['order']) : 'ASC';
1704 $orderby = !empty($params['orderby']) ? $params['orderby'] : 'display_name';
1705
1706 $get_user_params = array(
1707 'number' => $per_page,
1708 'paged' => $page,
1709 'offset' => $offset,
1710 'order' => $order,
1711 'orderby' => $orderby,
1712 );
1713
1714 // WP 5.9 deprecated the 'who' parameter and introduces the 'capability'
1715 // parameter, thus we'll be replacing the 'who' parameter in 5.9 or higher
1716 if (version_compare($updraftcentral_main->get_wordpress_version(), '5.9', '<')) {
1717 $get_user_params['who'] = $who;
1718 } else {
1719 $get_user_params['capability'] = array('edit_posts');
1720 }
1721
1722 $users = get_users($get_user_params);
1723
1724 $authors = array();
1725 $locale = get_locale();
1726
1727 foreach ($users as $user) {
1728 $data = array(
1729 'user' => json_encode($this->trim_object($user)),
1730 'misc' => array(
1731 'link' => get_author_posts_url($user->ID, $user->user_nicename),
1732 'locale' => function_exists('get_user_locale') ? get_user_locale($user) : $locale,
1733 'registered_date' => date('c', strtotime($user->user_registered)),
1734 )
1735 );
1736
1737 array_push($authors, $data);
1738 }
1739
1740 return $this->_response(array(
1741 'authors' => $authors
1742 ));
1743 }
1744
1745 /**
1746 * Fetch and retrieves parent pages based from the submitted parameters
1747 *
1748 * @param array $params Containing all the needed information to filter the results of the current request
1749 * @return array
1750 */
1751 public function get_parent_pages($params = array()) {
1752 // If expected parameters are empty or does not exists then set them to some default values
1753 $page = !empty($params['page']) ? (int) $params['page'] : 1;
1754 $per_page = !empty($params['per_page']) ? (int) $params['per_page'] : 100;
1755 $offset = ($page - 1) * $per_page;
1756 $exclude = !empty($params['exclude']) ? $params['exclude'] : array();
1757 $order = !empty($params['order']) ? strtoupper($params['order']) : 'ASC';
1758 $orderby = !empty($params['orderby']) ? $params['orderby'] : 'menu_order';
1759 $status = !empty($params['status']) ? $params['status'] : 'publish';
1760
1761 $args = array(
1762 'posts_per_page' => $per_page,
1763 'paged' => $page,
1764 'offset' => $offset,
1765 'post__not_in' => $exclude,
1766 'order' => $order,
1767 'orderby' => $orderby,
1768 'post_type' => 'page',
1769 'post_status' => $status,
1770 );
1771
1772 $query = new WP_Query($args);
1773 $posts = $query->posts;
1774
1775 $pages = array();
1776 if (!empty($posts)) {
1777 foreach ($posts as $post) {
1778 // Get additional information and merge with the response
1779 $postdata = $this->get_postdata($post, true);
1780 if (!empty($postdata)) array_push($pages, $this->trim_parent_info($postdata));
1781 }
1782 }
1783
1784 return $this->_response(array(
1785 'pages' => $pages
1786 ));
1787 }
1788
1789 /**
1790 * Trim down return data for parent pages
1791 *
1792 * @param array $postdata The array containing the data to process
1793 * @return array
1794 */
1795 protected function trim_parent_info($postdata) {
1796
1797 if (isset($postdata['post'])) {
1798 $post = json_decode($postdata['post']);
1799
1800 $page = new stdClass();
1801 $page->ID = $post->ID;
1802 $page->post_title = $post->post_title;
1803 $page->post_parent = $post->post_parent;
1804 $page->post_type = $post->post_type;
1805 $page->post_status = $post->post_status;
1806
1807 $postdata['post'] = json_encode($page);
1808 }
1809
1810 return $postdata;
1811 }
1812
1813 /**
1814 * Retrieves pages, templates, authors, categories and tags data that will be
1815 * used as options when displayed on the editor in UpdraftCentral
1816 *
1817 * @param string $type The type of the module that the current request is processing
1818 *
1819 * @return array
1820 */
1821 protected function get_options($type = 'post') {
1822 // Primarily used for editor consumption so we don't include trash here. Besides,
1823 // trash posts/pages aren't included as parent options.
1824 $parent_pages = $this->get_parent_pages();
1825 $pages = $parent_pages['data']['pages'];
1826
1827 // Add flexibility by letting users filter the default roles and add their own
1828 // custom page/post "author" role(s) if need be.
1829 $author_roles = apply_filters('updraftcentral_author_roles', array('administrator', 'editor', 'author', 'contributor'));
1830 $authors = get_users(array('role__in' => $author_roles));
1831
1832 if (!function_exists('get_page_templates')) {
1833 require_once(ABSPATH.'wp-admin/includes/theme.php');
1834 }
1835
1836 $templates = ('post' == $type) ? get_page_templates(null, 'post') : get_page_templates();
1837 $template_options = array();
1838 foreach ($templates as $template => $filename) {
1839 $item = array(
1840 'filename' => $filename,
1841 'template' => $template,
1842 );
1843 $template_options[] = $item;
1844 }
1845
1846 $page_options = array();
1847 foreach ($pages as $page_item) {
1848 if (isset($page_item['post'])) {
1849 $page = json_decode($page_item['post']);
1850 $item = array(
1851 'id' => $page->ID,
1852 'title' => $page->post_title,
1853 'parent' => $page->post_parent
1854 );
1855 $page_options[] = $item;
1856 }
1857 }
1858
1859 $author_options = array();
1860 foreach ($authors as $user) {
1861 $item = array(
1862 'id' => $user->ID,
1863 'name' => $user->display_name,
1864 );
1865 $author_options[] = $item;
1866 }
1867
1868 $response = array(
1869 'page' => $page_options,
1870 'author' => $author_options,
1871 'template' => $template_options,
1872 'date' => $this->get_date_options($type),
1873 );
1874
1875 if ('post' == $type) {
1876 $categories = get_categories(array('hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC'));
1877 $tags = get_tags(array('hide_empty' => false));
1878
1879 $category_options = array();
1880 foreach ($categories as $category) {
1881 $item = array(
1882 'id' => $category->term_id,
1883 'name' => $category->name,
1884 'parent' => $category->parent
1885 );
1886 $category_options[] = $item;
1887 }
1888
1889 $tag_options = array();
1890 foreach ($tags as $tag) {
1891 $item = array(
1892 'id' => $tag->term_id,
1893 'name' => $tag->name,
1894 );
1895 $tag_options[] = $item;
1896 }
1897
1898 $response['category'] = $category_options;
1899 $response['tag'] = $tag_options;
1900 }
1901
1902 return $response;
1903 }
1904
1905 /**
1906 * Changes the state/status of the given post based from the submitted action/request
1907 *
1908 * @param int $id The ID of the current page to work on
1909 * @param string $action The type of change that the current request is going to apply
1910 * @param string $type The type of the module that the current request is processing
1911 *
1912 * @return array
1913 */
1914 protected function apply_state($id, $action, $type = 'post') {
1915 if (empty($id)) return false;
1916
1917 $post = get_post($id);
1918 if (!empty($post)) {
1919 $previous_status = $post->post_status;
1920 $deleted = false;
1921
1922 switch ($action) {
1923 case 'draft':
1924 $args = array('ID' => $id, 'post_status' => 'draft');
1925 wp_update_post($args);
1926 break;
1927 case 'trash':
1928 wp_trash_post($id);
1929 break;
1930 case 'publish':
1931 $args = array('ID' => $id, 'post_status' => 'publish');
1932 wp_update_post($args);
1933 break;
1934 case 'restore':
1935 $args = array('ID' => $id, 'post_status' => 'pending');
1936 wp_update_post($args);
1937 break;
1938 case 'delete':
1939 $result = wp_delete_post($id, true);
1940 if (!empty($result)) $deleted = true;
1941 break;
1942 default:
1943 break;
1944 }
1945
1946 $postdata = $this->get_postdata($post);
1947 if (!empty($postdata) || $deleted) {
1948 $data = $deleted ? $id : $postdata;
1949 $result = array(
1950 'id' => $id,
1951 'previous_status' => $previous_status
1952 );
1953
1954 $result[$type] = $data;
1955 return $result;
1956 }
1957 }
1958
1959 return false;
1960 }
1961
1962 /**
1963 * Imports image from UpdraftCentral's page/post editor
1964 *
1965 * @param string $image_url The URL of the image to import
1966 * @param string $image_data The image data to save. If empty, image_url will be used to download the image
1967 * @param int $post_id The ID of the page where this image is to be attached
1968 *
1969 * @return integer
1970 */
1971 protected function attach_remote_image($image_url, $image_data, $post_id) {
1972 if (empty($image_url) || empty($post_id)) return;
1973
1974 $image = pathinfo($image_url);
1975 $image_name = $image['basename'];
1976 $upload_dir = wp_upload_dir();
1977
1978 if (empty($image_data)) {
1979 $response = wp_remote_get($image_url);
1980 if (!is_wp_error($response)) {
1981 $image_data = wp_remote_retrieve_body($response);
1982 }
1983 } else {
1984 $image_data = base64_decode($image_data);
1985 }
1986
1987 $media_id = 0;
1988 if (!empty($image_data)) {
1989 $unique_file_name = wp_unique_filename($upload_dir['path'], $image_name);
1990 $filename = basename($unique_file_name);
1991
1992 if (wp_mkdir_p($upload_dir['path'])) {
1993 $file = $upload_dir['path'] . '/' . $filename;
1994 } else {
1995 $file = $upload_dir['basedir'] . '/' . $filename;
1996 }
1997
1998 file_put_contents($file, $image_data);
1999 $wp_filetype = wp_check_filetype($filename, null);
2000
2001 $attachment = array(
2002 'post_mime_type' => $wp_filetype['type'],
2003 'post_title' => sanitize_file_name($filename),
2004 'post_content' => '',
2005 'post_status' => 'inherit'
2006 );
2007
2008 $media_id = wp_insert_attachment($attachment, $file, $post_id);
2009 require_once(ABSPATH . 'wp-admin/includes/image.php');
2010
2011 $attach_data = wp_generate_attachment_metadata($media_id, $file);
2012 wp_update_attachment_metadata($media_id, $attach_data);
2013 set_post_thumbnail($post_id, $media_id);
2014 }
2015
2016 return $media_id;
2017 }
2018
2019 /**
2020 * Checks whether we have the required fields submitted and the user has
2021 * the capabilities to execute the requested action
2022 *
2023 * @param array $capabilities The capabilities to check and validate
2024 *
2025 * @return array|void
2026 */
2027 protected function _validate_capabilities($capabilities) {
2028 foreach ($capabilities as $capability) {
2029 if (!current_user_can($capability)) return $this->_generic_error_response('insufficient_permission');
2030 }
2031 }
2032 }
2033