PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.21
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.21
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.2.21, at central/modules/posts.php

2,024 lines 65.7 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 if (class_exists('WP_Theme_JSON_Resolver') && WP_Theme_JSON_Resolver::theme_has_support()) {
588 $theme_json = ABSPATH.WPINC.'/class-wp-theme-json.php';
589 if (!class_exists('WP_Theme_JSON') && file_exists($theme_json)) require_once($theme_json);
590
591 $theme_json_instance = WP_Theme_JSON_Resolver::get_theme_data();
592 if ($theme_json_instance) {
593 $settings = $theme_json_instance->get_settings();
594 $theme_fonts = '';
595
596 if (isset($settings['typography']) && isset($settings['typography']['fontFamilies'])) {
597 $font_families = $settings['typography']['fontFamilies'];
598 if (isset($font_families['theme'])) {
599 foreach ($font_families['theme'] as $theme) {
600 if (isset($theme['fontFace'])) {
601 foreach ($theme['fontFace'] as $font) {
602 $theme_fonts .= '@font-face {';
603 $keys = array_keys($font);
604
605 foreach ($keys as $key) {
606 if (false !== stripos($key, 'font')) {
607 $prop = 'font-'.strtolower(str_replace('font', '', $key));
608 $theme_fonts .= $prop.': '.$font[$key].';';
609 } elseif (false !== stripos($key, 'src')) {
610 foreach ($font['src'] as $src_file) {
611 $url = trailingslashit(get_stylesheet_directory_uri()).str_replace('file:./', '', $src_file);
612 $theme_fonts .= 'src: url(CENTRAL_URL/?udcentral_action=load_font&font='.urlencode($url).');';
613 }
614 }
615 }
616 $theme_fonts .= '}';
617 }
618 }
619 }
620 }
621 }
622 $editing_styles[] = array('theme_json_fonts' => $theme_fonts, 'inline' => '', 'css' => '');
623 }
624 }
625
626 return $editing_styles;
627 }
628
629 /**
630 * Retrieves the total number of items found under each post statuses
631 *
632 * @param string $type The type of the module that the current request is processing
633 *
634 * @return array
635 */
636 protected function get_post_status_counts($type = 'post') {
637 $posts = wp_count_posts($type);
638
639 $publish = (int) $posts->publish;
640 $private = (int) $posts->private;
641 $draft = (int) $posts->draft;
642 $pending = (int) $posts->pending;
643 $future = (int) $posts->future;
644 $trash = (int) $posts->trash;
645
646 // We exclude "trash" from the overall total as WP doesn't actually
647 // consider or include it in the total count.
648 $all = $publish + $private + $draft + $pending + $future;
649
650 return array(
651 'all' => $all,
652 'publish' => $publish,
653 'private' => $private,
654 'draft' => $draft,
655 'pending' => $pending,
656 'future' => $future,
657 'trash' => $trash,
658 );
659 }
660
661 /**
662 * Retrieves a collection of formatted dates found for the given post statuses.
663 * It will be used as options for the date filter when managing the posts in UpdraftCentral.
664 *
665 * @param string $type The type of the module that the current request is processing
666 *
667 * @return array
668 */
669 protected function get_date_options($type = 'post') {
670 global $wpdb;
671
672 $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");
673
674 return $date_options;
675 }
676
677 /**
678 * Make sure that we have the required fields to use in UpdraftCentral for
679 * displaying the categories and tags sections. Add if missing.
680 *
681 * @param object $item Taxonomy item to check
682 * @return object
683 */
684 protected function map_tax($item) {
685 $taxs = array('category' => 'categories', 'post_tag' => 'tags');
686 if (array_key_exists($item->name, $taxs)) {
687 if (!isset($item->show_in_rest)) $item->show_in_rest = true;
688 if (!isset($item->rest_base)) $item->rest_base = $taxs[$item->name];
689 }
690
691 return $item;
692 }
693
694 /**
695 * Fetch and retrieves available taxonomies for this site and some capabilities specific
696 * to tags and categories when managing them.
697 *
698 * @return array
699 */
700 protected function get_taxonomies() {
701 $taxonomies = get_taxonomies(array(), 'objects');
702 $taxonomies = array_map(array($this, 'map_tax'), $taxonomies);
703
704 $response = array(
705 'taxonomies' => $taxonomies,
706 'current_user_cap' => array(
707 'manage_categories' => current_user_can('manage_categories'),
708 'edit_categories' => current_user_can('edit_categories'),
709 'delete_categories' => current_user_can('delete_categories'),
710 'assign_categories' => current_user_can('assign_categories'),
711 'manage_post_tags' => current_user_can('manage_post_tags'),
712 'edit_post_tags' => current_user_can('edit_post_tags'),
713 'delete_post_tags' => current_user_can('delete_post_tags'),
714 'assign_post_tags' => current_user_can('assign_post_tags'),
715 )
716 );
717
718 return $response;
719 }
720
721 /**
722 * Fetch and retrieves categories based from the submitted parameters
723 *
724 * @param array $query Containing all the needed information to filter the results of the current request
725 * @return array
726 */
727 public function get_categories($query = array()) {
728 $page = !empty($query['page']) ? (int) $query['page'] : 1;
729 $items_per_page = !empty($query['per_page']) ? (int) $query['per_page'] : 100;
730 $offset = ($page - 1) * $items_per_page;
731 $order = !empty($query['order']) ? $query['order'] : 'asc';
732 $orderby = !empty($query['orderby']) ? $query['orderby'] : 'name';
733
734 $args = array(
735 'hide_empty' => false,
736 'orderby' => $orderby,
737 'order' => $order,
738 'number' => $items_per_page,
739 'offset' => $offset
740 );
741
742 $categories = get_categories($args);
743 $category_options = array();
744
745 if (!empty($categories)) {
746 foreach ($categories as $key => $term) {
747 $parent_term = get_term((int) $term->parent, $term->taxonomy);
748 if (!is_wp_error($parent_term) && !is_null($parent_term)) {
749 $parent_term = json_encode($this->trim_object($parent_term));
750 } else {
751 $parent_term = '';
752 }
753
754 $category_options[] = array(
755 'id' => $term->term_id,
756 'name' => $term->name,
757 'parent' => $term->parent
758 );
759
760 $categories[$key] = array(
761 'term' => json_encode($this->trim_object($term)),
762 'misc' => array(
763 'link' => get_term_link($term),
764 'parent_term' => $parent_term,
765 'taxonomy' => $term->taxonomy
766 )
767 );
768 }
769 }
770
771 $categorytax = get_taxonomy('category');
772 $parent_dropdown_args = array(
773 'taxonomy' => 'category',
774 'hide_empty' => 0,
775 'name' => 'newcategory_parent',
776 'orderby' => 'name',
777 'hierarchical' => 1,
778 'show_option_none' => '&mdash; '.$categorytax->labels->parent_item.' &mdash;',
779 'echo' => false
780 );
781
782 $parent_dropdown_args = apply_filters('post_edit_category_parent_dropdown_args', $parent_dropdown_args);
783 $parent_dropdown = wp_dropdown_categories($parent_dropdown_args);
784
785 if (!function_exists('wp_popular_terms_checklist')) {
786 require_once ABSPATH . 'wp-admin/includes/template.php';
787 }
788
789 ob_start();
790 wp_popular_terms_checklist('category');
791 $popular_terms_checklist = ob_get_contents();
792 ob_end_clean();
793
794 return $this->_response(array(
795 'terms' => $categories,
796 'misc' => array(
797 'formatted' => $category_options,
798 'raw' => $categories,
799 'tax' => json_encode($this->trim_object($categorytax)),
800 'popular' => $popular_terms_checklist,
801 'parent_dropdown' => $parent_dropdown,
802 'capabilities' => array(
803 'can_edit_terms' => current_user_can($categorytax->cap->edit_terms)
804 )
805 )
806 ));
807 }
808
809 /**
810 * Fetch and retrieves tags based from the submitted parameters
811 *
812 * @param array $query Containing all the needed information to filter the results of the current request
813 * @return array
814 */
815 public function get_tags($query = array()) {
816 $page = !empty($query['page']) ? (int) $query['page'] : 1;
817 $items_per_page = !empty($query['per_page']) ? (int) $query['per_page'] : 100;
818 $offset = ($page - 1) * $items_per_page;
819 $order = !empty($query['order']) ? $query['order'] : 'desc';
820 $orderby = !empty($query['orderby']) ? $query['orderby'] : 'count';
821
822 $args = array(
823 'hide_empty' => false,
824 'orderby' => $orderby,
825 'order' => $order,
826 'number' => $items_per_page,
827 'offset' => $offset
828 );
829
830 $tags = get_tags($args);
831 $tag_options = array();
832 $tag_cloud = '';
833
834 if (!empty($tags)) {
835 $tags_for_cloud = array();
836 foreach ($tags as $key => $term) {
837 if (!isset($term->link)) $term->link = get_tag_link($term->term_id);
838 array_push($tags_for_cloud, $term);
839
840 $parent_term = get_term((int) $term->parent, $term->taxonomy);
841 if (!is_wp_error($parent_term) && !is_null($parent_term)) {
842 $parent_term = json_encode($this->trim_object($parent_term));
843 } else {
844 $parent_term = '';
845 }
846
847 $tag_options[] = array(
848 'id' => $term->term_id,
849 'name' => $term->name,
850 );
851
852 $tags[$key] = array(
853 'term' => json_encode($this->trim_object($term)),
854 'misc' => array(
855 'link' => get_term_link($term),
856 'parent_term' => $parent_term,
857 'taxonomy' => $term->taxonomy
858 )
859 );
860 }
861
862 add_filter('tag_cloud_sort', array($this, 'sort_tag_cloud'), 9, 2);
863
864 if (!function_exists('wp_generate_tag_cloud')) {
865 require_once ABSPATH.WPINC.'/category-template.php';
866 }
867
868 $tag_cloud = wp_generate_tag_cloud($tags_for_cloud, array(
869 'smallest' => 10,
870 'largest' => 22,
871 'unit' => 'pt',
872 'number' => 10,
873 'format' => 'flat',
874 'separator' => " ",
875 'orderby' => 'count',
876 'order' => 'DESC',
877 'show_count' => 1,
878 'echo' => false
879 ));
880 }
881
882 $tagtax = get_taxonomy('post_tag');
883 return $this->_response(array(
884 'terms' => $tags,
885 'misc' => array(
886 'formatted' => $tag_options,
887 'raw' => $tags,
888 'tax' => json_encode($this->trim_object($tagtax)),
889 'tag_cloud' => $tag_cloud,
890 'capabilities' => array(
891 'can_assign_terms' => current_user_can($tagtax->cap->assign_terms)
892 )
893 )
894 ));
895 }
896
897 /**
898 * Sorts the tag items that are to be shown within the tag cloud
899 *
900 * @param array $tags The array to be sorted. Contains the tag items
901 * @param array $args Additional parameters needed for the sorting process
902 * @return array
903 */
904 public function sort_tag_cloud($tags, $args) {
905 uasort($tags, array($this, '_wp_object_count_sort_cb'));
906 if ('DESC' === $args['order']) {
907 $tags = array_reverse($tags, true);
908 }
909
910 return $tags;
911 }
912
913 /**
914 * Serves as a callback for comparing objects based on count. Copied from WordPress 5.7
915 * core (wp-includes/category-template.php) and tweaked to return integer instead of boolean
916 * because returning boolean using uasort is now DEPRECATED in PHP 8.
917 *
918 * Used with `uasort()`.
919 *
920 * @since 3.1.0
921 * @access private
922 *
923 * @param object $a The first object to compare.
924 * @param object $b The second object to compare.
925 * @return bool Whether the count value for `$a` is greater than the count value for `$b`.
926 */
927 public function _wp_object_count_sort_cb($a, $b) {
928 if ($a->count == $b->count) {
929 return 0;
930 }
931 return ( $a->count > $b->count ) ? 1 : -1;
932 }
933
934 /**
935 * Fetch all available taxonomies and terms information for the given post object
936 *
937 * @param array $post The "Post" object to use when retrieving the information
938 * @return array
939 */
940 protected function get_taxonomies_terms($post) {
941 $taxonomies = get_object_taxonomies($post->post_type, 'objects');
942 $taxonomies = array_map(array($this, 'map_tax'), $taxonomies);
943
944 $taxonomy_names = array();
945 $taxonomy_terms = array();
946 $taxonomy_caps = array();
947
948 foreach ($taxonomies as $taxonomy) {
949 $terms = get_the_terms($post->ID, $taxonomy->name);
950 $terms = !is_array($terms) ? (array) $terms : $terms;
951
952 $taxonomy_terms[$taxonomy->name] = $terms;
953 $taxonomy_caps[$taxonomy->name] = array(
954 'hierarchical' => is_taxonomy_hierarchical($taxonomy->name),
955 'edit_terms' => current_user_can($taxonomy->cap->edit_terms),
956 'assign_terms' => current_user_can($taxonomy->cap->assign_terms),
957 );
958 array_push($taxonomy_names, $taxonomy->name);
959 }
960
961 return array(
962 'objects' => $taxonomies,
963 'names' => $taxonomy_names,
964 'terms' => $taxonomy_terms,
965 'caps' => $taxonomy_caps,
966 );
967 }
968
969 /**
970 * Take over the current editing of the post
971 *
972 * @param array $params An array of data that serves as parameters for the given request
973 * @return array
974 */
975 public function take_over($params) {
976
977 $error = $this->_validate_capabilities(array('edit_'.$this->post_type.'s'));
978 if (!empty($error)) return $error;
979
980 $result = array('lock_acquired' => false);
981 if (!empty($params['post_id'])) {
982 if (!function_exists('wp_set_post_lock')) {
983 require_once ABSPATH.'wp-admin/includes/post.php';
984 }
985 $lock = wp_set_post_lock($params['post_id']);
986
987 if (!empty($lock)) {
988 $result = array(
989 'lock_acquired' => true,
990 'details' => $lock
991 );
992 }
993 }
994
995 return $this->_response($result);
996 }
997
998 /**
999 * Retrieves the underlying data for the given post. Some extra information are
1000 * passed along that will be consumed by the editor in UpdraftCentral
1001 *
1002 * @param int|object $param Post object or a post ID
1003 * @param boolean $encode True to encode the post object, false otherwise
1004 * @return array
1005 */
1006 public function get_postdata($param, $encode = true) {
1007 $response = array();
1008
1009 if (is_object($param) && isset($param->ID)) {
1010 $post = $param;
1011 } elseif (is_numeric($param)) {
1012 $post = get_post($param);
1013 }
1014
1015 if ($post) {
1016 $post_type_obj = get_post_type_object($post->post_type);
1017
1018 $is_post_type_viewable = false;
1019 if (!empty($post_type_obj)) {
1020 $is_post_type_viewable = $post_type_obj->publicly_queryable || ($post_type_obj->_builtin && $post_type_obj->public);
1021 }
1022
1023 if (!function_exists('get_sample_permalink')) {
1024 require_once ABSPATH.'wp-admin/includes/post.php';
1025 }
1026
1027 // Validate template exists on the current theme, otherwise,
1028 // reset the template to default.
1029 $template = get_page_template_slug($post->ID);
1030 if (!empty($template)) {
1031 $page_templates = wp_get_theme()->get_page_templates($post);
1032 if ('default' != $template && !isset($page_templates[$template])) {
1033 update_post_meta($post->ID, '_wp_page_template', 'default');
1034 }
1035 }
1036
1037 $published_date = array(
1038 'jj' => date('d', strtotime($post->post_date)),
1039 'mm' => date('m', strtotime($post->post_date)),
1040 'aa' => date('Y', strtotime($post->post_date)),
1041 'hh' => date('H', strtotime($post->post_date)),
1042 'mn' => date('i', strtotime($post->post_date)),
1043 'ss' => date('s', strtotime($post->post_date))
1044 );
1045
1046 $sample_permalink = get_sample_permalink($post->ID, $post->post_title, '');
1047 $permalink = get_permalink($post->ID);
1048 $slug = $post->post_name;
1049
1050 if (!empty($sample_permalink) && !empty($slug)) {
1051 if (isset($sample_permalink[0])) {
1052 if (false !== stripos($sample_permalink[0], '%pagename%/') || false !== stripos($sample_permalink[0], '%postname%/')) {
1053 $token = (false !== stripos($sample_permalink[0], '%pagename%/')) ? '%pagename%/' : '%postname%/';
1054 $permalink = str_replace($token, '', $sample_permalink[0]).$slug;
1055 }
1056 }
1057 }
1058
1059 $editor = null;
1060 $editor_id = wp_check_post_lock($post->ID);
1061 if ($editor_id) {
1062 $editor = get_userdata($editor_id);
1063 if (!$editor) {
1064 // The user with lock does not exist. This can happen if you created a backup or clone
1065 // where you excluded the users table during the process and you restore this backup to
1066 // a different site or the user was deleted or removed more recently. Thus, we will
1067 // release the lock so that other users with the right permission can edit the post.
1068 delete_post_meta($post->ID, '_edit_lock');
1069 }
1070 }
1071
1072 $response = array(
1073 'post' => $encode ? json_encode($post) : $post,
1074 'misc' => array(
1075 'guid_rendered' => apply_filters('get_the_guid', $post->guid, $post->ID),
1076 'link' => $permalink,
1077 'slug' => $slug,
1078 'site_url' => site_url('/'),
1079 'title_rendered' => get_the_title($post->ID),
1080 'content_rendered' => apply_filters('the_content', $post->post_content),
1081 'excerpt' => $post->post_excerpt,
1082 'featured_media' => 0,
1083 'sticky' => is_sticky($post->ID),
1084 'template' => get_page_template_slug($post->ID),
1085 'permalink_template' => get_permalink($post->ID, true),
1086 'author_name' => get_the_author_meta('display_name', $post->post_author),
1087 'publish_month_year' => date('F Y', strtotime($post->post_date)),
1088 'published_date' => $published_date,
1089 'format' => get_post_format($post->ID),
1090 'post_type_name' => $post_type_obj->name,
1091 'post_type_viewable' => $is_post_type_viewable,
1092 'post_type_public' => $post_type_obj->public,
1093 'post_type_hierarchical' => $post_type_obj->hierarchical,
1094 'sample_permalink' => get_sample_permalink($post->ID, $post->post_title, ''),
1095 'post_password_required' => post_password_required($post),
1096 'post_type_supports_authors' => post_type_supports($post->post_type, 'author'),
1097 'post_type_supports_comments' => post_type_supports($post->post_type, 'comments'),
1098 'post_type_supports_revisions' => post_type_supports($post->post_type, 'revisions'),
1099 'post_revisions' => array(), // N.B. We're not going to allow revisions editing for now
1100 'post_thumbnail_id' => get_post_thumbnail_id($post->ID),
1101 'can_publish_posts' => current_user_can($post_type_obj->cap->publish_posts),
1102 'can_edit_others_posts' => current_user_can($post_type_obj->cap->edit_others_posts),
1103 'can_unfiltered_html' => current_user_can('unfiltered_html'),
1104 'is_edited' => $editor ? 1 : 0,
1105 'editor_id' => $editor_id,
1106 'editor' => $editor,
1107 'edited_by_id' => $editor ? $editor->ID : 0,
1108 'edited_by_display_name' => $editor ? $editor->display_name : '',
1109 )
1110 );
1111
1112 if ('post' == $post->post_type) {
1113 $taxonomies = $this->get_taxonomies_terms($post);
1114 $response['misc']['taxonomy_objects'] = $taxonomies['objects'];
1115 $response['misc']['taxonomy_names'] = $taxonomies['names'];
1116 $response['misc']['taxonomy_terms'] = $taxonomies['terms'];
1117 $response['misc']['taxonomy_caps'] = $taxonomies['caps'];
1118
1119 if (!function_exists('wp_popular_terms_checklist') || !function_exists('get_terms_to_edit')) {
1120 require_once ABSPATH . 'wp-admin/includes/template.php';
1121 require_once ABSPATH . 'wp-admin/includes/taxonomy.php';
1122 }
1123
1124 if (!function_exists('wp_get_post_categories')) {
1125 require_once(ABSPATH.WPINC.'/post.php');
1126 }
1127
1128 $categories = wp_get_post_categories($post->ID, array('fields' => 'ids'));
1129 if (!is_wp_error($categories)) {
1130 $response['misc']['categories'] = empty($categories) ? array() : $categories;
1131 $terms_to_edit = get_terms_to_edit($post->ID, 'category');
1132 if (!empty($terms_to_edit)) {
1133 $response['misc']['categories_list'] = str_replace(',', ', ', $terms_to_edit);
1134 }
1135
1136 $popular_ids = wp_popular_terms_checklist('category', 0, 10, false);
1137 // On WP 3.4 the "wp_terms_checklist" doesn't have an "echo" parameter and will automatically
1138 // display the rendered checklist. Therefore, we're going to pull the terms so that all
1139 // versions starting from WP 3.4 will pull the content instead of displaying them.
1140
1141 ob_start();
1142 // In this call we'll have to set the "echo" parameter to true so that later version of WP
1143 // will be able to catch and process it.
1144 wp_terms_checklist($post->ID, array('taxonomy' => 'category', 'popular_cats' => $popular_ids, 'echo' => true));
1145 $popular_checklist = ob_get_contents();
1146 ob_end_clean();
1147
1148 $response['misc']['categories_checklist'] = $popular_checklist;
1149
1150 ob_start();
1151 wp_terms_checklist($post->ID, array('taxonomy' => 'category', 'checked_ontop' => 0, 'echo' => true));
1152 $quickedit_checklist = ob_get_contents();
1153 ob_end_clean();
1154
1155 $response['misc']['categories_quickedit_checklist'] = $quickedit_checklist;
1156 }
1157
1158 $tags = wp_get_post_tags($post->ID, array('fields' => 'ids'));
1159 if (!is_wp_error($tags)) {
1160 $response['misc']['tags'] = empty($tags) ? array() : $tags;
1161 $terms_to_edit = get_terms_to_edit($post->ID, 'post_tag');
1162 if (!empty($terms_to_edit)) {
1163 $response['misc']['tags_list'] = str_replace(',', ', ', $terms_to_edit);
1164 }
1165 }
1166 }
1167
1168 // Naturally, the "featured_media" will suffice when loading the image (media) in
1169 // UpdraftCentral since the value in this field is the actual image id of the featured
1170 // media used in UC. If we currently don't have an entry in the "featured_media_updraftcentral" meta,
1171 // then UC will need to download the featured media (image) for this current post/page
1172 // using the "featured_media_url" field (below) if not empty.
1173 $featured_media = get_post_meta($post->ID, 'featured_media_updraftcentral', true);
1174 if (!empty($featured_media)) {
1175 $response['misc']['featured_media'] = $featured_media;
1176 }
1177
1178 // Retrieve featured media if currently present for the given post/page.
1179 // If present, we pull the image (media) URL in case there's a need for
1180 // UpdraftCentral to download the image upon loading the editor (e.g. the featured_media id
1181 // above no longer exists).
1182 $media_id = (int) get_post_thumbnail_id($post->ID);
1183 if (!empty($media_id)) {
1184 $response['misc']['featured_media_url'] = wp_get_attachment_url($media_id);
1185 } else {
1186 // The post/page no longer has a "featured_media" or doesn't have one currently, therefore,
1187 // we're going to set the "featured_media" and "featured_media_url" fields to both empty to
1188 // to avoid any further actions (e.g. download media).
1189 $response['misc']['featured_media'] = 0;
1190 $response['misc']['featured_media_url'] = '';
1191 }
1192 }
1193
1194 return $response;
1195 }
1196
1197 /**
1198 * Changes the state/status of the submitted post(s)
1199 *
1200 * @param array $params An array of data that serves as parameters for the given request
1201 * @return array
1202 */
1203 public function set_state($params) {
1204
1205 $state_fields = $this->get_state_fields_by_type($this->post_type);
1206 if (empty($state_fields)) return $this->_generic_error_response('unsupported_type_on_set_state');
1207
1208 $error = $this->_validate_capabilities($state_fields['validation_fields']);
1209 if (!empty($error)) return $error;
1210
1211 $result = array();
1212 if (!empty($params['list'])) {
1213 $posts = array();
1214 foreach ($params['list'] as $id) {
1215 $post = $this->apply_state($id, $params['action'], $this->post_type);
1216 if (!empty($post)) {
1217 array_push($posts, $post);
1218 }
1219 }
1220
1221 if (!empty($posts)) {
1222 $result = array($state_fields['list_key'] => $posts);
1223 }
1224 } elseif (!empty($params['id'])) {
1225 $post = $this->apply_state($params['id'], $params['action'], $this->post_type);
1226 if (!empty($post)) $result = $post;
1227 }
1228
1229 if (!empty($result)) {
1230 $response = $this->get($params);
1231 if (!empty($response['response']) && 'rpcok' === $response['response']) {
1232 $result[$state_fields['result_key']] = $response['data'];
1233 }
1234
1235 return $this->_response($result);
1236 } else {
1237 return $this->_generic_error_response($state_fields['error_key'], array('action' => $params['action']));
1238 }
1239 }
1240
1241 /**
1242 * Creates new category
1243 *
1244 * @param array $params An array of data that serves as parameters for the given request
1245 * @param boolean $wrap_response Indicates whether to wrap the response based on local or UpdraftCentral calls. Default true.
1246 * @return array
1247 */
1248 public function add_category($params, $wrap_response = true) {
1249 $error = $this->_validate_capabilities(array('manage_categories'));
1250 if (!empty($error)) return $error;
1251
1252 $name = sanitize_text_field($params['name']);
1253 $args = array();
1254 if (!empty($params['parent'])) {
1255 $args['parent'] = $params['parent'];
1256 }
1257
1258 $result = wp_insert_term($name, 'category', $args);
1259 if (!is_wp_error($result)) {
1260 $term_id = $result['term_id'];
1261 $term = get_term($term_id, 'category');
1262
1263 $data = array();
1264 if (!is_wp_error($term)) {
1265 $data = array(
1266 'id' => $term->term_id,
1267 'count' => $term->count,
1268 'description' => $term->description,
1269 'link' => get_term_link($term->term_id, 'category'),
1270 'name' => $term->name,
1271 'slug' => $term->slug,
1272 'taxonomy' => $term->taxonomy,
1273 'parent' => $term->parent,
1274 'meta' => array()
1275 );
1276
1277 $categories = $this->get_categories();
1278 if ($wrap_response) $data['categories'] = json_encode($categories['data']);
1279 }
1280
1281 return $wrap_response ? $this->_response($data) : $data;
1282 } else {
1283 $error = array(
1284 'message' => $result->get_error_message()
1285 );
1286
1287 return $wrap_response ? $this->_generic_error_response('post_add_category_failed', $error) : $error;
1288 }
1289 }
1290
1291 /**
1292 * Assigns categories to a certain post object
1293 *
1294 * @param int $post_id The ID of the post object
1295 * @param array $category_ids A collection of category IDs to assign to the post object
1296 * @return void
1297 */
1298 protected function assign_category_to_post($post_id, $category_ids) {
1299 if (!empty($category_ids)) {
1300 // Making sure that we have the correct type to use and we
1301 // don't have any redundant IDs before saving.
1302 $category_ids = array_unique(array_map('intval', $category_ids));
1303
1304 // Attach (new) categories to post
1305 wp_set_object_terms($post_id, $category_ids, 'category');
1306 } else {
1307 wp_set_object_terms($post_id, get_option('default_category'), 'category');
1308 }
1309 }
1310
1311 /**
1312 * Creates new tag
1313 *
1314 * @param array $params An array of data that serves as parameters for the given request
1315 * @param boolean $wrap_response Indicates whether to wrap the response based on local or UpdraftCentral calls. Default true.
1316 * @return array
1317 */
1318 public function add_tag($params, $wrap_response = true) {
1319 // 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).
1320 $error = $this->_validate_capabilities(array('manage_categories'));
1321 if (!empty($error)) return $error;
1322
1323 $name = sanitize_text_field($params['name']);
1324 $result = wp_insert_term($name, 'post_tag');
1325 if (!is_wp_error($result)) {
1326 $term_id = $result['term_id'];
1327 $term = get_term($term_id, 'post_tag');
1328
1329 $data = array();
1330 if (!is_wp_error($term)) {
1331 $data = array(
1332 'id' => $term->term_id,
1333 'count' => $term->count,
1334 'description' => $term->description,
1335 'link' => get_term_link($term->term_id, 'post_tag'),
1336 'name' => $term->name,
1337 'slug' => $term->slug,
1338 'taxonomy' => $term->taxonomy,
1339 'meta' => array()
1340 );
1341
1342 $tags = $this->get_tags();
1343 if ($wrap_response) $data['tags'] = json_encode($tags['data']);
1344 }
1345
1346 return $wrap_response ? $this->_response($data) : $data;
1347 } else {
1348 $error = array(
1349 'message' => $result->get_error_message()
1350 );
1351
1352 return $wrap_response ? $this->_generic_error_response('post_add_tag_failed', $error) : $error;
1353 }
1354 }
1355
1356 /**
1357 * Assigns tags to a certain post object
1358 *
1359 * @param int $post_id The ID of the post object
1360 * @param array $tag_ids A collection of tag IDs to assign to the post object
1361 * @return void
1362 */
1363 protected function assign_tag_to_post($post_id, $tag_ids) {
1364 if (!empty($tag_ids)) {
1365 // Making sure that we have the correct type to use and we
1366 // don't have any redundant IDs before saving.
1367 $tag_ids = array_unique(array_map('intval', $tag_ids));
1368
1369 // Attach (new) tags to post
1370 wp_set_object_terms($post_id, $tag_ids, 'post_tag');
1371 } else {
1372 wp_set_object_terms($post_id, null, 'post_tag');
1373 }
1374 }
1375
1376 /**
1377 * Pre-validates data before running the save process
1378 *
1379 * @param WP_Post $post The post object to validate
1380 * @param array $params An array of data that serves as parameters for the given request
1381 *
1382 * @return array|void
1383 */
1384 private function pre_validation($post, $params) {
1385 if (empty($post) || empty($params)) return;
1386
1387 if (!empty($params['password'])) {
1388 if (!empty($params['sticky'])) {
1389 return $this->_generic_error_response('post_save_failed', array(
1390 'message' => __('A post can not be sticky and have a password.'),
1391 'args' => $params
1392 ));
1393 }
1394
1395 if (!isset($params['sticky']) && is_sticky($post->ID)) {
1396 return $this->_generic_error_response('post_save_failed', array(
1397 'message' => __('A sticky post can not be password protected.'),
1398 'args' => $params
1399 ));
1400 }
1401 }
1402
1403 if (!empty($params['sticky'])) {
1404 if (!isset($params['password']) && post_password_required($post->ID)) {
1405 return $this->_generic_error_response('post_save_failed', array(
1406 'message' => __('A password protected post can not be set to sticky.'),
1407 'args' => $params
1408 ));
1409 }
1410 }
1411 }
1412
1413 /**
1414 * Saves or updates post/page information based from the submitted data
1415 *
1416 * @param array $params An array of data that serves as parameters for the given request
1417 * @return array
1418 */
1419 public function save($params) {
1420 global $updraftcentral_host_plugin;
1421
1422 $validation_fields = array(
1423 'post' => array('publish_posts', 'edit_posts', 'delete_posts'),
1424 'page' => array('publish_pages', 'edit_pages', 'delete_pages')
1425 );
1426
1427 if (!isset($validation_fields[$this->post_type])) return $this->_generic_error_response('unsupported_type_on_save_post');
1428
1429 $error = $this->_validate_capabilities($validation_fields[$this->post_type]);
1430 if (!empty($error)) return $error;
1431
1432 if (!empty($params['id']) || !empty($params['new'])) {
1433 $args = array();
1434
1435 if (!empty($params['id'])) {
1436 $post = get_post($params['id']);
1437 if (!empty($post)) {
1438 $result = $this->pre_validation($post, $params);
1439 if (isset($result['response']) && 'rpcerror' == $result['response']) {
1440 return $result;
1441 }
1442 }
1443 }
1444
1445 // post_content
1446 if (!empty($params['content']))
1447 $args['post_content'] = $params['content'];
1448
1449 // post_excerpt
1450 if (!empty($params['excerpt']))
1451 $args['post_excerpt'] = $params['excerpt'];
1452
1453 // menu_order
1454 if (isset($params['order']))
1455 $args['menu_order'] = (int) $params['order'];
1456
1457 // post_parent
1458 if (isset($params['parent'])) {
1459 $args['post_parent'] = empty($params['parent']) ? 0 : $params['parent'];
1460 }
1461
1462 // post_name
1463 if (!empty($params['slug']))
1464 $args['post_name'] = $params['slug'];
1465
1466 // post_status
1467 if (!empty($params['status'])) {
1468 $args['post_status'] = $params['status'];
1469 }
1470
1471 // post_title
1472 if (!empty($params['title']))
1473 $args['post_title'] = $params['title'];
1474
1475 // post_author
1476 if (!empty($params['author']))
1477 $args['post_author'] = $params['author'];
1478
1479 // comment_status
1480 if (!empty($params['comment_status']))
1481 $args['comment_status'] = $params['comment_status'];
1482
1483 // ping_status
1484 if (!empty($params['ping_status']))
1485 $args['ping_status'] = $params['ping_status'];
1486
1487 // visibility
1488 if (!empty($params['visibility'])) {
1489 switch ($params['visibility']) {
1490 case 'public':
1491 $args['post_status'] = 'publish';
1492 $args['post_password'] = '';
1493 break;
1494 case 'password':
1495 $args['post_status'] = 'publish';
1496 $args['post_password'] = $params['password'];
1497 break;
1498 case 'private':
1499 $args['post_status'] = 'private';
1500 $args['post_password'] = '';
1501 break;
1502 default:
1503 break;
1504 }
1505 } else {
1506 if (!empty($params['password'])) {
1507 $args['post_status'] = 'publish';
1508 $args['post_password'] = $params['password'];
1509 } elseif (isset($params['password']) && '' == $params['password']) {
1510 $args['post_status'] = 'publish';
1511 $args['post_password'] = '';
1512 }
1513 }
1514
1515 // post/publish date
1516 if (!empty($params['date'])) {
1517 $datetime = strtotime($params['date']);
1518 $post_date = date('Y-m-d H:i:s', $datetime);
1519
1520 $args['post_date'] = $post_date;
1521 $args['post_date_gmt'] = gmdate('Y-m-d H:i:s', $datetime);
1522
1523 // We only change the status to "future" based from the submitted date if the post status
1524 // is not empty and equal to 'publish' and the date is for the coming future.
1525 if (!empty($params['status']) && 'publish' == $params['status']) {
1526 if (strtotime($post_date) > strtotime(date('Y-m-d H:i:s'))) $args['post_status'] = 'future';
1527 }
1528 }
1529
1530 // Make sure we have a slug/post_name generated before insert/update
1531 if (empty($params['slug']) && !empty($params['title'])) {
1532 $args['post_name'] = sanitize_title_with_dashes($params['title']);
1533 }
1534
1535 if (!empty($params['new'])) {
1536 $args['post_type'] = $this->post_type;
1537 $post_id = wp_insert_post($args, true);
1538 } else {
1539 $args['ID'] = $params['id'];
1540 $args['post_modified'] = date('Y-m-d H:i:s');
1541 $args['post_modified_gmt'] = gmdate('Y-m-d H:i:s');
1542
1543 $post_id = wp_update_post($args, true);
1544 }
1545
1546 // We have successfully created/updated a post at this point, thus, we'll continue
1547 // with implementing the other requested processes and return the result.
1548 if (!is_wp_error($post_id)) {
1549 // sticky post
1550 if (isset($params['sticky'])) {
1551 $sticky = (bool) $params['sticky'];
1552 if ($sticky) {
1553 stick_post($post_id);
1554 } else {
1555 if (is_sticky($post_id)) {
1556 unstick_post($post_id);
1557 }
1558 }
1559 }
1560
1561 // template
1562 if (!empty($params['template'])) {
1563 update_post_meta($post_id, '_wp_page_template', $params['template']);
1564 }
1565
1566 // featured_media
1567 if (isset($params['featured_media'])) {
1568 if (!empty($params['featured_media'])) {
1569 $featured_media = (int) $params['featured_media'];
1570 $attach_continue = true;
1571
1572 $url = wp_get_attachment_url($featured_media);
1573 if (!empty($url) && !empty($params['featured_media_url']) && $url == $params['featured_media_url']) {
1574 set_post_thumbnail($post_id, $featured_media);
1575 update_post_meta($post_id, 'featured_media_updraftcentral', $params['featured_media']);
1576 $attach_continue = false;
1577 }
1578
1579 if ($attach_continue) {
1580 $featured_media_data = !empty($params['featured_media_data']) ? $params['featured_media_data'] : null;
1581 $media_id = $this->attach_remote_image($params['featured_media_url'], $featured_media_data, $post_id);
1582 if (!empty($media_id)) {
1583 // If we have a successful attachment then add reference to UC's media id
1584 update_post_meta($post_id, 'featured_media_updraftcentral', $params['featured_media']);
1585 }
1586 }
1587 } else {
1588 // Remove featured image.
1589 delete_post_meta($post_id, '_thumbnail_id');
1590 delete_post_meta($post_id, 'featured_media_updraftcentral');
1591 }
1592 }
1593
1594 // categories
1595 $categories_updated = false;
1596 if (!empty($params['categories'])) {
1597 $term_ids = array();
1598 foreach ($params['categories'] as $value) {
1599 $category = sanitize_text_field($value);
1600 $parent = 0;
1601
1602 if (false !== strpos($category, ':')) {
1603 list($parent, $category) = explode(':', $category);
1604 $result = $this->add_category(array('name' => $category, 'parent' => $parent), false);
1605
1606 if (!empty($result)) {
1607 array_push($term_ids, $result['id']);
1608 }
1609 } else {
1610 $term = get_term_by('id', $category, 'category');
1611 if (!empty($term)) {
1612 $term_id = $term->term_id;
1613 array_push($term_ids, $term_id);
1614 }
1615 }
1616 }
1617
1618 $this->assign_category_to_post($post_id, $term_ids);
1619 $categories_updated = true;
1620 }
1621
1622 // tags
1623 $tags_updated = false;
1624 if (!empty($params['tags'])) {
1625 $term_ids = array();
1626 foreach ($params['tags'] as $value) {
1627 $tag = sanitize_text_field($value);
1628 $field = is_numeric($tag) ? 'id' : 'name';
1629
1630 $term = get_term_by($field, $tag, 'post_tag');
1631 if (!empty($term)) {
1632 $term_id = $term->term_id;
1633 array_push($term_ids, $term_id);
1634 } else {
1635 $result = $this->add_tag(array('name' => $tag), false);
1636 if (!empty($result)) {
1637 array_push($term_ids, $result['id']);
1638 }
1639 }
1640 }
1641
1642 $this->assign_tag_to_post($post_id, $term_ids);
1643 $tags_updated = true;
1644 }
1645
1646 // Pulling any other relevant and additional information regarding
1647 // the post before returning it in the response.
1648 $postdata = $this->get_postdata($post_id);
1649
1650 if (!empty($params['new'])) {
1651 $timeout = !empty($params['timeout']) ? $params['timeout'] : 30;
1652 $postdata = array_merge($postdata, $this->get_preload_data($timeout, $this->post_type));
1653 } else {
1654 if ($categories_updated || $tags_updated) {
1655 $categories = $this->get_categories();
1656 $tags = $this->get_tags();
1657
1658 $postdata['preloaded'] = json_encode(array(
1659 'categories' => $categories['data'],
1660 'tags' => $tags['data']
1661 ));
1662 }
1663 }
1664
1665 $postdata['options'] = $this->get_options($this->post_type);
1666 return $this->_response($postdata);
1667 } else {
1668 // ERROR: error creating or updating post
1669 return $this->_generic_error_response('post_save_failed', array(
1670 'message' => $post_id->get_error_message(),
1671 'args' => $args
1672 ));
1673 }
1674 } else {
1675 // ERROR: no id parameter, invalid request
1676 return $this->_generic_error_response('post_invalid_request', array('message' => $updraftcentral_host_plugin->retrieve_show_message('parameters_missing')));
1677 }
1678 }
1679
1680 /**
1681 * Fetch and retrieves authors based from the submitted parameters
1682 *
1683 * @param array $params Containing all the needed information to filter the results of the current request
1684 * @return array
1685 */
1686 public function get_authors($params = array()) {
1687 global $updraftcentral_main;
1688
1689 // If expected parameters are empty or does not exists then set them to some default values
1690 $page = !empty($params['page']) ? (int) $params['page'] : 1;
1691 $per_page = !empty($params['per_page']) ? (int) $params['per_page'] : 15;
1692 $offset = ($page - 1) * $per_page;
1693 $who = !empty($params['who']) ? $params['who'] : 'authors';
1694 $order = !empty($params['order']) ? strtoupper($params['order']) : 'ASC';
1695 $orderby = !empty($params['orderby']) ? $params['orderby'] : 'display_name';
1696
1697 $get_user_params = array(
1698 'number' => $per_page,
1699 'paged' => $page,
1700 'offset' => $offset,
1701 'order' => $order,
1702 'orderby' => $orderby,
1703 );
1704
1705 // WP 5.9 deprecated the 'who' parameter and introduces the 'capability'
1706 // parameter, thus we'll be replacing the 'who' parameter in 5.9 or higher
1707 if (version_compare($updraftcentral_main->get_wordpress_version(), '5.9', '<')) {
1708 $get_user_params['who'] = $who;
1709 } else {
1710 $get_user_params['capability'] = array('edit_posts');
1711 }
1712
1713 $users = get_users($get_user_params);
1714
1715 $authors = array();
1716 $locale = get_locale();
1717
1718 foreach ($users as $user) {
1719 $data = array(
1720 'user' => json_encode($this->trim_object($user)),
1721 'misc' => array(
1722 'link' => get_author_posts_url($user->ID, $user->user_nicename),
1723 'locale' => function_exists('get_user_locale') ? get_user_locale($user) : $locale,
1724 'registered_date' => date('c', strtotime($user->user_registered)),
1725 )
1726 );
1727
1728 array_push($authors, $data);
1729 }
1730
1731 return $this->_response(array(
1732 'authors' => $authors
1733 ));
1734 }
1735
1736 /**
1737 * Fetch and retrieves parent pages based from the submitted parameters
1738 *
1739 * @param array $params Containing all the needed information to filter the results of the current request
1740 * @return array
1741 */
1742 public function get_parent_pages($params = array()) {
1743 // If expected parameters are empty or does not exists then set them to some default values
1744 $page = !empty($params['page']) ? (int) $params['page'] : 1;
1745 $per_page = !empty($params['per_page']) ? (int) $params['per_page'] : 100;
1746 $offset = ($page - 1) * $per_page;
1747 $exclude = !empty($params['exclude']) ? $params['exclude'] : array();
1748 $order = !empty($params['order']) ? strtoupper($params['order']) : 'ASC';
1749 $orderby = !empty($params['orderby']) ? $params['orderby'] : 'menu_order';
1750 $status = !empty($params['status']) ? $params['status'] : 'publish';
1751
1752 $args = array(
1753 'posts_per_page' => $per_page,
1754 'paged' => $page,
1755 'offset' => $offset,
1756 'post__not_in' => $exclude,
1757 'order' => $order,
1758 'orderby' => $orderby,
1759 'post_type' => 'page',
1760 'post_status' => $status,
1761 );
1762
1763 $query = new WP_Query($args);
1764 $posts = $query->posts;
1765
1766 $pages = array();
1767 if (!empty($posts)) {
1768 foreach ($posts as $post) {
1769 // Get additional information and merge with the response
1770 $postdata = $this->get_postdata($post, true);
1771 if (!empty($postdata)) array_push($pages, $this->trim_parent_info($postdata));
1772 }
1773 }
1774
1775 return $this->_response(array(
1776 'pages' => $pages
1777 ));
1778 }
1779
1780 /**
1781 * Trim down return data for parent pages
1782 *
1783 * @param array $postdata The array containing the data to process
1784 * @return array
1785 */
1786 protected function trim_parent_info($postdata) {
1787
1788 if (isset($postdata['post'])) {
1789 $post = json_decode($postdata['post']);
1790
1791 $page = new stdClass();
1792 $page->ID = $post->ID;
1793 $page->post_title = $post->post_title;
1794 $page->post_parent = $post->post_parent;
1795 $page->post_type = $post->post_type;
1796 $page->post_status = $post->post_status;
1797
1798 $postdata['post'] = json_encode($page);
1799 }
1800
1801 return $postdata;
1802 }
1803
1804 /**
1805 * Retrieves pages, templates, authors, categories and tags data that will be
1806 * used as options when displayed on the editor in UpdraftCentral
1807 *
1808 * @param string $type The type of the module that the current request is processing
1809 *
1810 * @return array
1811 */
1812 protected function get_options($type = 'post') {
1813 // Primarily used for editor consumption so we don't include trash here. Besides,
1814 // trash posts/pages aren't included as parent options.
1815 $parent_pages = $this->get_parent_pages();
1816 $pages = $parent_pages['data']['pages'];
1817
1818 // Add flexibility by letting users filter the default roles and add their own
1819 // custom page/post "author" role(s) if need be.
1820 $author_roles = apply_filters('updraftcentral_author_roles', array('administrator', 'editor', 'author', 'contributor'));
1821 $authors = get_users(array('role__in' => $author_roles));
1822
1823 if (!function_exists('get_page_templates')) {
1824 require_once(ABSPATH.'wp-admin/includes/theme.php');
1825 }
1826
1827 $templates = ('post' == $type) ? get_page_templates(null, 'post') : get_page_templates();
1828 $template_options = array();
1829 foreach ($templates as $template => $filename) {
1830 $item = array(
1831 'filename' => $filename,
1832 'template' => $template,
1833 );
1834 $template_options[] = $item;
1835 }
1836
1837 $page_options = array();
1838 foreach ($pages as $page_item) {
1839 if (isset($page_item['post'])) {
1840 $page = json_decode($page_item['post']);
1841 $item = array(
1842 'id' => $page->ID,
1843 'title' => $page->post_title,
1844 'parent' => $page->post_parent
1845 );
1846 $page_options[] = $item;
1847 }
1848 }
1849
1850 $author_options = array();
1851 foreach ($authors as $user) {
1852 $item = array(
1853 'id' => $user->ID,
1854 'name' => $user->display_name,
1855 );
1856 $author_options[] = $item;
1857 }
1858
1859 $response = array(
1860 'page' => $page_options,
1861 'author' => $author_options,
1862 'template' => $template_options,
1863 'date' => $this->get_date_options($type),
1864 );
1865
1866 if ('post' == $type) {
1867 $categories = get_categories(array('hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC'));
1868 $tags = get_tags(array('hide_empty' => false));
1869
1870 $category_options = array();
1871 foreach ($categories as $category) {
1872 $item = array(
1873 'id' => $category->term_id,
1874 'name' => $category->name,
1875 'parent' => $category->parent
1876 );
1877 $category_options[] = $item;
1878 }
1879
1880 $tag_options = array();
1881 foreach ($tags as $tag) {
1882 $item = array(
1883 'id' => $tag->term_id,
1884 'name' => $tag->name,
1885 );
1886 $tag_options[] = $item;
1887 }
1888
1889 $response['category'] = $category_options;
1890 $response['tag'] = $tag_options;
1891 }
1892
1893 return $response;
1894 }
1895
1896 /**
1897 * Changes the state/status of the given post based from the submitted action/request
1898 *
1899 * @param int $id The ID of the current page to work on
1900 * @param string $action The type of change that the current request is going to apply
1901 * @param string $type The type of the module that the current request is processing
1902 *
1903 * @return array
1904 */
1905 protected function apply_state($id, $action, $type = 'post') {
1906 if (empty($id)) return false;
1907
1908 $post = get_post($id);
1909 if (!empty($post)) {
1910 $previous_status = $post->post_status;
1911 $deleted = false;
1912
1913 switch ($action) {
1914 case 'draft':
1915 $args = array('ID' => $id, 'post_status' => 'draft');
1916 wp_update_post($args);
1917 break;
1918 case 'trash':
1919 wp_trash_post($id);
1920 break;
1921 case 'publish':
1922 $args = array('ID' => $id, 'post_status' => 'publish');
1923 wp_update_post($args);
1924 break;
1925 case 'restore':
1926 $args = array('ID' => $id, 'post_status' => 'pending');
1927 wp_update_post($args);
1928 break;
1929 case 'delete':
1930 $result = wp_delete_post($id, true);
1931 if (!empty($result)) $deleted = true;
1932 break;
1933 default:
1934 break;
1935 }
1936
1937 $postdata = $this->get_postdata($post);
1938 if (!empty($postdata) || $deleted) {
1939 $data = $deleted ? $id : $postdata;
1940 $result = array(
1941 'id' => $id,
1942 'previous_status' => $previous_status
1943 );
1944
1945 $result[$type] = $data;
1946 return $result;
1947 }
1948 }
1949
1950 return false;
1951 }
1952
1953 /**
1954 * Imports image from UpdraftCentral's page/post editor
1955 *
1956 * @param string $image_url The URL of the image to import
1957 * @param string $image_data The image data to save. If empty, image_url will be used to download the image
1958 * @param int $post_id The ID of the page where this image is to be attached
1959 *
1960 * @return integer
1961 */
1962 protected function attach_remote_image($image_url, $image_data, $post_id) {
1963 if (empty($image_url) || empty($post_id)) return;
1964
1965 $image = pathinfo($image_url);
1966 $image_name = $image['basename'];
1967 $upload_dir = wp_upload_dir();
1968
1969 if (empty($image_data)) {
1970 $response = wp_remote_get($image_url);
1971 if (!is_wp_error($response)) {
1972 $image_data = wp_remote_retrieve_body($response);
1973 }
1974 } else {
1975 $image_data = base64_decode($image_data);
1976 }
1977
1978 $media_id = 0;
1979 if (!empty($image_data)) {
1980 $unique_file_name = wp_unique_filename($upload_dir['path'], $image_name);
1981 $filename = basename($unique_file_name);
1982
1983 if (wp_mkdir_p($upload_dir['path'])) {
1984 $file = $upload_dir['path'] . '/' . $filename;
1985 } else {
1986 $file = $upload_dir['basedir'] . '/' . $filename;
1987 }
1988
1989 file_put_contents($file, $image_data);
1990 $wp_filetype = wp_check_filetype($filename, null);
1991
1992 $attachment = array(
1993 'post_mime_type' => $wp_filetype['type'],
1994 'post_title' => sanitize_file_name($filename),
1995 'post_content' => '',
1996 'post_status' => 'inherit'
1997 );
1998
1999 $media_id = wp_insert_attachment($attachment, $file, $post_id);
2000 require_once(ABSPATH . 'wp-admin/includes/image.php');
2001
2002 $attach_data = wp_generate_attachment_metadata($media_id, $file);
2003 wp_update_attachment_metadata($media_id, $attach_data);
2004 set_post_thumbnail($post_id, $media_id);
2005 }
2006
2007 return $media_id;
2008 }
2009
2010 /**
2011 * Checks whether we have the required fields submitted and the user has
2012 * the capabilities to execute the requested action
2013 *
2014 * @param array $capabilities The capabilities to check and validate
2015 *
2016 * @return array|void
2017 */
2018 protected function _validate_capabilities($capabilities) {
2019 foreach ($capabilities as $capability) {
2020 if (!current_user_can($capability)) return $this->_generic_error_response('insufficient_permission');
2021 }
2022 }
2023 }
2024