if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
class UpdraftCentral_Posts_Commands extends UpdraftCentral_Commands {
protected $switched = false;
protected $post_type = 'post';
* Function that gets called before every action
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
* link to udrpc_action main function in class UpdraftCentral_Listener
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.
// Here we assign the current blog_id to a variable $blog_id
$blog_id = get_current_blog_id();
if (!empty($data['site_id'])) $blog_id = $data['site_id'];
if (function_exists('switch_to_blog') && is_multisite() && $blog_id) {
$this->switched = switch_to_blog($blog_id);
* Function that gets called after every action
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
* link to udrpc_action main function in class UpdraftCentral_Listener
public function _post_action($command, $data, $extra_info) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// Here, we're restoring to the current (default) blog before we switched
if ($this->switched) restore_current_blog();
* Returns the keys and fields names that are associated to a particular module type
* @param string $type The type of the module that the current request is processing
private function get_state_fields_by_type($type) {
'validation_fields' => array('publish_posts', 'edit_posts', 'delete_posts'),
'count_key' => 'posts_count',
'error_key' => 'post_state_change_failed'
'validation_fields' => array('publish_pages', 'edit_pages', 'delete_pages'),
'count_key' => 'pages_count',
'error_key' => 'page_state_change_failed'
if (!isset($state_fields[$type])) return array();
return $state_fields[$type];
* Fetch and retrieves posts based from the submitted parameters
* @param array $params Containing all the needed information to filter the results of the current request
public function get($params) {
$state_fields = $this->get_state_fields_by_type($this->post_type);
if (empty($state_fields)) return $this->_generic_error_response('unsupported_type_on_get_posts');
$error = $this->_validate_capabilities($state_fields['validation_fields']);
if (!empty($error)) return $error;
// check paged parameter; if empty set to defaults
$paged = !empty($params['paged']) ? (int) $params['paged'] : 1;
$numberposts = !empty($params['numberposts']) ? (int) $params['numberposts'] : 10;
$offset = ($paged - 1) * $numberposts;
'posts_per_page' => $numberposts,
'post_type' => $this->post_type,
'post_status' => 'publish,private,draft,pending,future',
if (!empty($params['keyword'])) {
$args['s'] = $params['keyword'];
if ('post' == $this->post_type) {
if (!empty($params['category'])) {
$args['cat'] = (int) $params['category'];
if (!empty($params['date'])) {
list($monthnum, $year) = explode(':', $params['date']);
$args['monthnum'] = $monthnum;
if (!empty($params['status']) && 'all' !== $params['status']) {
$args['post_status'] = $params['status'];
$query = new WP_Query($args);
$count_posts = (int) $query->found_posts;
$page_count = absint($count_posts / $numberposts);
$remainder = absint($count_posts % $numberposts);
$page_count = ($remainder > 0) ? ++$page_count : $page_count;
'results' => $count_posts,
'items_from' => (($paged * $numberposts) - $numberposts) + 1,
'items_to' => ($paged == $page_count) ? $count_posts : $paged * $numberposts,
foreach ($result as $post) {
// Pulling any other relevant and additional information regarding
// the post before returning it in the response.
$postdata = $this->get_postdata($post, false);
array_push($posts, $postdata);
$state_fields['items_key'] => $posts,
'options' => $this->get_options($this->post_type),
$state_fields['count_key'] => $this->get_post_status_counts($this->post_type)
// Load any additional information if preload parameter is set. Will only be
// requested on initial load of items in UpdraftCentral.
if (isset($params['preload']) && $params['preload']) {
$timeout = !empty($params['timeout']) ? $params['timeout'] : 30;
$response = array_merge($response, $this->get_preload_data($timeout, $this->post_type));
return $this->_response($response);
* Extracts public properties from complex object and return a simple
* object (stdClass) that contains the public properties of the original object.
* @param object $obj Any type of complex objects that needs converting (e.g. WP_Taxonomy, WP_Term or WP_User)
protected function trim_object($obj) {
// To preserve the object's accessibility through its properties we recreate
// the object using the stdClass and fill it with the public properties
// that will be extracted from the original object ($obj).
$newObj = new stdClass();
// Making sure that we only extract those publicly accessible properties excluding
// the private, protected, static ones and methods.
$props = get_object_vars($obj);
foreach ($props as $key => $value) {
$newObj->{$key} = $value;
* Retrieves information that will be preloaded in UC for quick and easy access
* when editing a certain page or post
* @param int $timeout The user-defined timeout from UpdraftCentral
* @param string $type The type of the module that the current request is processing
protected function get_preload_data($timeout, $type = 'post') {
global $updraftcentral_host_plugin, $updraftcentral_main;
if (!function_exists('get_page_templates')) {
require_once(ABSPATH.'wp-admin/includes/theme.php');
$templates = ('post' == $type) ? get_page_templates(null, 'post') : get_page_templates();
if (!empty($templates)) {
$templates = array_flip($templates);
if (!isset($templates['default'])) {
$templates['default'] = $updraftcentral_host_plugin->retrieve_show_message('default_template');
// Preloading elements saves time and avoid unnecessary round trips to fetch
// these information individually.
$authors = $this->get_authors();
$parent_pages = $this->get_parent_pages();
'authors' => $authors['data']['authors'],
'parent_pages' => $parent_pages['data']['pages'],
'templates' => $templates,
'editor_styles' => $this->get_editor_styles($timeout),
'wp_version' => $updraftcentral_main->get_wordpress_version()
$categories = $this->get_categories();
$tags = $this->get_tags();
$data['taxonomies'] = $this->get_taxonomies();
$data['categories'] = $categories['data'];
$data['tags'] = $tags['data'];
'preloaded' => json_encode($data)
* Extract content from the given css path
* @param string $style CSS file path
* @param int $timeout The user-defined timeout from UpdraftCentral
protected function extract_css_content($style, $timeout) {
if (1 === preg_match('~^(https?:)?//~i', $style)) {
$response = wp_remote_get($style, array('timeout' => $timeout));
if (!is_wp_error($response)) {
$result = trim(wp_remote_retrieve_body($response));
if (!empty($result)) $content = $result;
// Editor styles that resides in "css/dist"
if (false !== ($pos = stripos($style, 'css/dist'))) {
$file = ABSPATH.WPINC.substr_replace($style, '/', 0, $pos);
// Styles that resides in "wp-content/themes" (coming from $editor_styles global var)
$file = get_theme_file_path($style);
$is_valid = (function_exists('is_file')) ? is_file($file) : file_exists($file);
$result = trim(file_get_contents($file));
if (!empty($result)) $content = $result;
return $this->filter_url($content);
* Convert URL entries contained in the CSS content to absolute URLs
* @param string $content The content of the CSS file
protected function filter_url($content) {
// Replace with valid URL (absolute)
preg_match_all('~url\((.+?)\)~i', $content, $all_matches);
if (!empty($all_matches) && isset($all_matches[1])) {
$urls = array_unique($all_matches[1]);
foreach ($urls as $url) {
$url = str_replace('"', '', $url);
if (false !== strpos($url, 'data:')) continue;
if (1 !== preg_match('~^(https?:)?//~i', $url)) {
if (1 === preg_match('~(plugins|themes)~i', $url, $matches)) {
if (false !== ($pos = stripos($url, $matches[1]))) {
if (!function_exists('content_url')) {
require_once ABSPATH.WPINC.'/link-template.php';
$absolute_url = rtrim(content_url(), '/').substr_replace($url, '/', 0, $pos);
$content = str_replace($url, $absolute_url, $content);
$path = preg_replace('~(\.+\/)~', '', $url);
$dirpath = trailingslashit(get_stylesheet_directory());
if (!file_exists($dirpath.$url)) $path = $this->resolve_path($path);
$absolute_url = (!empty($path)) ? trailingslashit(get_stylesheet_directory_uri()).ltrim($path, '/') : '';
$content = str_replace($url, $absolute_url, $content);
* Resolve URL to its actual absolute path
* @param string $path Some relative path to check
protected function resolve_path($path) {
$dir = trailingslashit(get_stylesheet_directory());
// Some relative paths declared within the css file (e.g. only has '../fonts/etc/', called deep down from a subfolder) where parent
// subfolder is not articulated needs to be resolve further to get its actual absolute path. Using glob will pinpoint its actual location
// rather than iterating through a series of sublevels just to find the actual file.
$result = str_replace($dir, '', glob($dir.'{,*/}{'.$path.'}', GLOB_BRACE));
if (!empty($result)) return $result[0];
* Retrieve the editor styles/assets to be use by UpdraftCentral when editing a post
* @param int $timeout The user-defined timeout from UpdraftCentral
protected function get_editor_styles($timeout) {
global $editor_styles, $wp_styles;
$editing_styles = $loaded = array();
$required = array('css/dist/editor/style.css', 'css/dist/block-library/style.css', 'css/dist/block-library/theme.css');
foreach ($required as $style) {
$editing_styles[] = array('css' => $this->extract_css_content($style, $timeout), 'inline' => '');
do_action('enqueue_block_editor_assets');
do_action('enqueue_block_assets');
// Checking for editor styles support since styles make vary from theme to theme
foreach ($editor_styles as $style) {
if (false !== array_search($style, $loaded)) continue;
$editing_styles[] = array('css' => $this->extract_css_content($style, $timeout), 'inline' => '');
foreach ($wp_styles->queue as $handle) {
$style = $wp_styles->registered[$handle]->src;
if (false !== array_search($style, $loaded)) continue;
$inline = $wp_styles->print_inline_style($handle, false);
$editing_styles[] = array(
'css' => $this->extract_css_content($style, $timeout),
'inline' => (!$inline) ? '' : $inline
$editing_styles[] = array('css' => $this->extract_css_content('/style.css', $timeout), 'inline' => '');
* Retrieves the total number of items found under each post statuses
* @param string $type The type of the module that the current request is processing
protected function get_post_status_counts($type = 'post') {
$posts = wp_count_posts($type);
$publish = (int) $posts->publish;
$private = (int) $posts->private;
$draft = (int) $posts->draft;
$pending = (int) $posts->pending;
$future = (int) $posts->future;
$trash = (int) $posts->trash;
// We exclude "trash" from the overall total as WP doesn't actually
// consider or include it in the total count.
$all = $publish + $private + $draft + $pending + $future;
* Retrieves a collection of formatted dates found for the given post statuses.
* It will be used as options for the date filter when managing the posts in UpdraftCentral.
* @param string $type The type of the module that the current request is processing
protected function get_date_options($type = 'post') {
$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");
* Make sure that we have the required fields to use in UpdraftCentral for
* displaying the categories and tags sections. Add if missing.
* @param object $item Taxonomy item to check
protected function map_tax($item) {
$taxs = array('category' => 'categories', 'post_tag' => 'tags');
if (array_key_exists($item->name, $taxs)) {
if (!isset($item->show_in_rest)) $item->show_in_rest = true;
if (!isset($item->rest_base)) $item->rest_base = $taxs[$item->name];
* Fetch and retrieves available taxonomies for this site and some capabilities specific
* to tags and categories when managing them.
protected function get_taxonomies() {
$taxonomies = get_taxonomies(array(), 'objects');
$taxonomies = array_map(array($this, 'map_tax'), $taxonomies);
'taxonomies' => $taxonomies,
'current_user_cap' => array(
'manage_categories' => current_user_can('manage_categories'),
'edit_categories' => current_user_can('edit_categories'),
'delete_categories' => current_user_can('delete_categories'),
'assign_categories' => current_user_can('assign_categories'),
'manage_post_tags' => current_user_can('manage_post_tags'),
'edit_post_tags' => current_user_can('edit_post_tags'),
'delete_post_tags' => current_user_can('delete_post_tags'),
'assign_post_tags' => current_user_can('assign_post_tags'),
* Fetch and retrieves categories based from the submitted parameters
* @param array $query Containing all the needed information to filter the results of the current request
public function get_categories($query = array()) {
$page = !empty($query['page']) ? (int) $query['page'] : 1;
$items_per_page = !empty($query['per_page']) ? (int) $query['per_page'] : 100;
$offset = ($page - 1) * $items_per_page;
$order = !empty($query['order']) ? $query['order'] : 'asc';
$orderby = !empty($query['orderby']) ? $query['orderby'] : 'name';