/ string A piece of code used to test regexes for find-replace settings for first load of the category HTML '_category_find_replace_raw_html', // array An array including what to find and with what to replace for raw response content of category pages '_category_find_replace_first_load', // array An array including what to find and with what to replace for category HTML '_category_find_replace_element_attributes',// array An array including what to find and with what to replace for specified elements' specified attributes '_category_exchange_element_attributes', // array An array including selectors of elements and the attributes whose values should be exchanged '_category_remove_element_attributes', // array An array including selectors of elements and comma-separated attributes that should be removed from the element '_category_find_replace_element_html', // array An array including what to find and with what to replace for specified elements' HTML '_test_url_category', // string Holds a test URL for the user to conduct tests on category pages '_category_notify_empty_value_selectors', // array CSS selectors to be used to notify the user via email when one of the selector's value is empty/not found // Post tab '_test_url_post', // string Holds a test URL for the user to conduct tests on post pages '_post_title_selectors', // array Selector for post title '_post_excerpt_selectors', // array Selectors for the post summary '_post_content_selectors', // array Selectors for the post content '_post_category_name_selectors', // array CSS selectors with attributes that find category names '_post_category_add_all_found_category_names',// bool When checked, category names found by all CSS selectors will be added '_post_category_name_separators', // array Separators that will be used to separate category names in a single string '_post_category_add_hierarchical', // bool True if categories found by a single selector will be added hierarchically '_post_category_do_not_add_category_in_map', // bool True if the category defined in the category map should not be added when there is at least one category found by CSS selectors '_post_date_selectors', // array Selectors for the post date '_test_find_replace_date', // string A date which is used to conduct find-replace test '_post_find_replace_date', // array An array including what to find and with what to replace for dates '_post_date_add_minutes', // int How many minutes that should be added to the final date '_post_custom_content_shortcode_selectors', // array An array holding selectors with custom attributes and customly-defined shortcodes '_post_tag_selectors', // array Selectors for post tag '_post_slug_selectors', // array Selectors for post slug '_post_paginate', // bool If the original post is paginated, paginate it in WP as well '_post_next_page_url_selectors', // array Next page selectors for the post if it is paginated '_post_next_page_all_pages_url_selectors', // array Sometimes the post page does not have next page URL. Instead, it has all page URLs in one place. '_post_is_list_type', // bool Whether or not the post is created as a list '_post_list_item_starts_after_selectors', // array CSS selectors to understand first list items' start position '_post_list_title_selectors', // array Title selectors for the list-type post '_post_list_content_selectors', // array Content selectors for the list-type post '_post_list_item_number_selectors', // array Selectors for list item numbers '_post_list_item_auto_number', // bool True if item numbers can be set automatically, if item's number does not exist '_post_list_insert_reversed', // bool True to insert the list items in reverse order '_post_meta_keywords', // bool Whether or not to save meta keywords '_post_meta_keywords_as_tags', // bool True if meta keywords should be inserted as tags '_post_meta_description', // bool Whether or not to save meta description '_post_unnecessary_element_selectors', // array Selectors for the elements to be removed from the content '_post_save_all_images_in_content', // bool Whether or not to save all images in post content as media '_post_save_images_as_media', // bool Whether or not to upload post images to WP '_post_save_images_as_gallery', // bool Whether or not to save to-be-specified images as gallery '_post_gallery_image_selectors', // array Selectors with attributes for image URLs in the HTML of the page '_post_save_images_as_woocommerce_gallery', // bool True if the gallery images should be saved as the value of post meta key that is used to store the gallery for WooCommerce products '_post_image_selectors', // array Selectors for image URLs in the post '_test_find_replace_image_urls', // string An image URL which is used to conduct find-replace test '_post_find_replace_image_urls', // array An array including what to find and with what to replace for image URLs '_post_save_thumbnails_if_not_exist', // bool True if a thumbnail image should be saved from a post page, if no thumbnail is found in category page. '_post_thumbnail_selectors', // array CSS selectors for thumbnail images in post page '_test_find_replace_thumbnail_url', // string An image URL which is used to conduct find-replace test '_post_find_replace_thumbnail_url', // array An array including what to find and with what to replace for thumbnail URL '_post_custom_meta_selectors', // array An array for selectors with attribute and their meta properties, such as meta key, and whether it is multiple or not '_post_custom_meta', // array An array containing custom post meta keys and their values. '_post_custom_taxonomy_selectors', // array An array for selectors with attribute and their meta properties, such as meta key, and whether it is multiple or not '_post_custom_taxonomy', // array An array containing custom post taxonomy names and their values. '_post_notify_empty_value_selectors', // array CSS selectors to be used to notify the user via email when one of the selector's value is empty/not found // Templates tab '_post_template_main', // string Main template for the post '_post_template_title', // string Title template for the post '_post_template_excerpt', // string Excerpt template for the post '_post_template_list_item', // string List item template for the post '_post_template_gallery_item', // string Gallery item template for a single image '_post_remove_links_from_short_codes', // bool True if the template should be cleared from URLs '_post_convert_iframes_to_short_code', // bool True if the iframes in the post template should be converted to a short code '_post_convert_scripts_to_short_code', // bool True if the scripts in the post template should be converted to a short code '_test_find_replace', // string A piece of code used to test RegExes '_post_find_replace_template', // array An array including what to find and with what to replace for template '_post_find_replace_title', // array An array including what to find and with what to replace for title '_post_find_replace_excerpt', // array An array including what to find and with what to replace for excerpt '_post_find_replace_tags', // array An array including what to find and with what to replace for tags '_post_find_replace_meta_keywords', // array An array including what to find and with what to replace for meta keywords '_post_find_replace_meta_description', // array An array including what to find and with what to replace for meta description '_post_find_replace_custom_shortcodes', // array An array including what to find and with what to replace for the data of custom short codes '_test_find_replace_first_load', // string A piece of code used to test regexes for find-replace settings for first load of the post HTML '_post_find_replace_raw_html', // array An array including what to find and with what to replace for raw response content of post pages '_post_find_replace_first_load', // array An array including what to find and with what to replace for post HTML '_post_find_replace_element_attributes', // array An array including what to find and with what to replace for specified elements' specified attributes '_post_exchange_element_attributes', // array An array including selectors of elements and the attributes whose values should be exchanged '_post_remove_element_attributes', // array An array including selectors of elements and comma-separated attributes that should be removed from the element '_post_find_replace_element_html', // array An array including what to find and with what to replace for specified elements' HTML '_post_find_replace_custom_meta', // array An array including what to find and with what to replace for specified meta keys '_post_find_replace_custom_short_code', // array An array including what to find and with what to replace for specified custom short codes '_template_unnecessary_element_selectors', // array Selectors for the elements to be removed from the template // Notes tab '_notes', // string A setting for the user to keep notes about the site (this is rich text editor). '_notes_simple', // string A setting for the user to keep simple (not formatted) notes about the site. (just textarea) // Others '_dev_tools_state', // string A serialized array containing the state of DEV tools for this post ]; /** * @var array A key-value pair where keys are post meta keys defined in {@link $metaKeys} and the values are their * default values. */ private $metaKeyDefaults = [ '_fix_tabs' => ['on'], '_fix_content_navigation' => ['on'], ]; /** * @var array Meta keys used to keep track of the CRON jobs */ private $cronMetaKeys = [ /* Keys for URL-collecting CRON event */ '_cron_last_checked_at', // date Date of last URL collection '_cron_last_checked_category_url', // string URL (or URL part, just how the user saves it as) of the last checked category '_cron_last_checked_category_next_page_url',// string Next page URL for the last checked category (basically, next page to crawl) '_cron_no_new_url_inserted_count', // int Number of pages crawled with no new URL insertion in a row. E.g. Page 1 - none, // Page 2 - none, Page 3 - none => this value will be 3 '_cron_crawled_page_count', // int Holds how many pages crawled before /* Keys for post-crawling CRON event */ '_cron_last_crawled_at', // date Date of last post crawl '_cron_last_crawled_url_id', // int Stores ID of the last crawled URL from urls table '_cron_post_next_page_url', // string Stores next page URL for a paginated post '_cron_post_next_page_urls', // array Stores next page URLs as a serialized array for a paginated post. This is used if the post has // all of the next pages together. '_cron_post_draft_id', // int Stores the ID of the draft post. A draft post is a post created if target post is paginated. New // content is appended to that post's content. After all pages are crawled, the draft is published. /* Keys for post-recrawling CRON event */ '_cron_recrawl_last_crawled_at', // date Date of last post recrawl '_cron_recrawl_last_crawled_url_id', // int Stores ID of the last recrawled URL from urls table '_cron_recrawl_post_next_page_url', // string Stores next page URL for a paginated post '_cron_recrawl_post_next_page_urls', // array Stores next page URLs as a serialized array for a paginated post. This is used if the post has // all of the next pages together. '_cron_recrawl_post_draft_id', // int Stores the ID of the draft post. A draft post is a post created if target post is paginated. New // content is appended to that post's content. After all pages are recrawled, the draft is published. ]; /** @var array Meta keys used to store a string value (not array). These are very important for importing/exporting * settings successfully. */ private $singleMetaKeys = [ // SITE SETTINGS // Main '_active', '_active_recrawling', '_active_post_deleting', '_active_translation', '_main_page_url', '_do_not_use_general_settings', '_cache_test_url_responses', '_fix_tabs', '_fix_content_navigation', // Category '_category_list_page_url', '_category_collect_in_reverse_order', '_test_find_replace_thumbnail_url_cat', '_test_find_replace_first_load_cat', '_category_post_save_thumbnails', '_category_post_is_link_before_thumbnail', '_test_url_category', // Post '_test_url_post', '_post_category_add_all_found_category_names', '_post_category_add_hierarchical', '_post_category_do_not_add_category_in_map', '_test_find_replace_date', '_post_date_add_minutes', '_post_paginate', '_post_is_list_type', '_post_list_item_auto_number', '_post_list_insert_reversed', '_post_meta_keywords', '_post_meta_keywords_as_tags', '_post_meta_description', '_post_save_all_images_in_content', '_post_save_images_as_media', '_post_save_images_as_gallery', '_post_save_images_as_woocommerce_gallery', '_test_find_replace_image_urls', '_test_find_replace_thumbnail_url', '_post_save_thumbnails_if_not_exist', // Templates '_post_template_main', '_post_template_title', '_post_template_excerpt', '_post_template_list_item', '_post_template_gallery_item', '_post_remove_links_from_short_codes', '_post_convert_iframes_to_short_code', '_post_convert_scripts_to_short_code', '_test_find_replace', '_test_find_replace_first_load', '_notes', '_notes_simple', '_dev_tools_state', // GENERAL SETTINGS '_wpcc_make_sure_encoding_utf8', '_wpcc_convert_charset_to_utf8', '_wpcc_http_user_agent', '_wpcc_http_accept', '_wpcc_http_allow_cookies', '_wpcc_use_proxy', '_wpcc_connection_timeout', '_wpcc_test_url_proxy', '_wpcc_proxies', '_wpcc_proxy_try_limit', '_wpcc_proxy_randomize', '_wpcc_is_notification_active', '_wpcc_notification_email_interval_for_site', '_wpcc_no_new_url_page_trial_limit', '_wpcc_max_page_count_per_category', '_wpcc_run_count_url_collection', '_wpcc_run_count_post_crawl', '_wpcc_run_count_post_recrawl', '_wpcc_max_recrawl_count', '_wpcc_min_time_between_two_recrawls_in_min', '_wpcc_recrawl_posts_newer_than_in_min', '_wpcc_delete_posts_older_than_in_min', '_wpcc_max_post_count_per_post_delete_event', '_wpcc_is_delete_post_attachments', '_wpcc_allow_comments', '_wpcc_post_status', '_wpcc_post_type', '_wpcc_post_author', '_wpcc_post_tag_limit', '_wpcc_post_password', '_wpcc_is_translation_active', '_wpcc_selected_translation_service', '_wpcc_translation_google_translate_from', '_wpcc_translation_google_translate_to', '_wpcc_translation_google_translate_project_id', '_wpcc_translation_google_translate_api_key', '_wpcc_translation_google_translate_test', '_wpcc_translation_microsoft_translate_from', '_wpcc_translation_microsoft_translate_to', '_wpcc_translation_microsoft_translate_client_secret', '_wpcc_translation_microsoft_translate_test', // CRON '_cron_last_checked_at', '_cron_last_checked_category_url', '_cron_last_checked_category_next_page_url', '_cron_no_new_url_inserted_count', '_cron_crawled_page_count', '_cron_last_crawled_at', '_cron_last_crawled_url_id', '_cron_post_next_page_url', '_cron_post_draft_id', '_cron_recrawl_last_crawled_at', '_cron_recrawl_last_crawled_url_id', '_cron_recrawl_post_next_page_url', '_cron_recrawl_post_draft_id', '_cron_last_deleted_at', ]; private $editorButtonsMain; private $editorButtonsTitle; private $editorButtonsExcerpt; private $editorButtonsList; private $editorButtonsGallery; private $editorButtonsOptionsBoxTemplates; private $allPredefinedShortCodes = []; /** @var null|array Holds count of saved URLs and URLs in queue for each site */ private static $urlCounts = null; public function __construct() { add_action('plugins_loaded', function() { // Initialize the meta keys when the plugins are loaded $this->initMetaKeys(); }, 999); // Execute this as late as possible since we want the registered post detail factories add their own meta keys as well // Create post type $this->createCustomPostType(); // Create pageActionKey JS variable, which can be used when making AJAX requests as action variable add_action('admin_print_scripts', function() { // Print the script only if we are on a site page. $screen = get_current_screen(); if($screen && $screen->base == 'post' && $screen->post_type == Constants::$POST_TYPE) { echo " "; } }); // Register ajax url for site list add_action('wp_ajax_wcc_site_list', function() { if(!check_admin_referer('wcc-site-list', Constants::$NONCE_NAME)) wp_die("Nonce is invalid."); if(!isset($_POST["data"])) wp_die(_wpcc("Data does not exist in your request. The request should include 'data'")); if(!isset($_POST["post_id"])) wp_die(_wpcc("Post ID does not exist in your request. The request should have 'post_id'.")); if(!current_user_can(Constants::$ALLOWED_USER_CAPABILITY)) wp_die("You are not allowed for this."); // We'll return JSON response. header('Content-Type: application/json'); echo Factory::postService()->postSiteList($_POST["post_id"], $_POST["data"]); wp_die(); }); // Register ajax url for tests add_action('wp_ajax_wcc_test', function () { if(!check_admin_referer('wcc-settings-metabox', Constants::$NONCE_NAME)) wp_die(); if(!isset($_POST["data"])) wp_die(_wpcc("Data does not exist in your request. The request should include 'data'")); // We'll return JSON response. header('Content-Type: application/json'); $data = $_POST["data"]; // Show the test results if(isset($data["testType"]) && $testType = $data["testType"]) { $result = Test::respondToTestRequest($data); if($result !== null) { echo $result; } } else if(isset($data["requestType"]) && $requestType = $data["requestType"]) { $result = Settings::respondToAjaxRequest($data); if($result !== null) { echo $result; } // If there is a command } else if(isset($data["cmd"]) && $cmd = $data["cmd"]) { switch($cmd) { case "saveDevToolsState": if($data["postId"]) { $result = Utils::savePostMeta($data["postId"], '_dev_tools_state', json_encode($data["state"])); echo $result ? 1 : 0; } break; case "loadGeneralSettings": case "clearGeneralSettings": $isPostPage = true; $isOption = true; $settings = $cmd == "clearGeneralSettings" ? [] : Settings::getAllGeneralSettings(); $view = Utils::view('general-settings.settings') ->with(Settings::getSettingsPageVariables(false)) ->with(compact("isPostPage", "isOption", "settings")) ->render(); // HTML attributes with JSON values cause the attributes not to be rendered properly by browser. // So, let's replace single quotes of the JSON-valued attributes with double quotes. Also, double // quotes in JSON string are escaped as ". Let's unescape them as well. After all this, the // HTML will be valid. $view = str_replace('="{', "='{", $view); $view = str_replace('}"', "}'", $view); $view = str_replace(""", '"', $view); $response = json_encode([ "view" => $view ]); echo $response; break; case "invalidate_url_response_cache": $url = Utils::array_get($data, "url"); $result = $url ? ResponseCache::getInstance()->delete("GET", $url) : false; echo $result ? 1 : 0; break; case "invalidate_all_url_response_caches": $result = ResponseCache::getInstance()->deleteAll(); echo $result ? 1 : 0; break; case "saveSiteSettings": echo $this->quickSaveSettings($data); break; } } wp_die(); }); } /** * Initializes meta keys used by the plugin * @since 1.8.0 */ private function initMetaKeys() { // Combine meta keys for the post and keys for general settings. By this way, the user will be able to save options // for those keys. This is because the request is checked for $metaKeys. // First, remove the setting used for activating scheduling. Each site already has an "active" setting. $generalSettings = Factory::generalSettingsController()->settings; unset($generalSettings[array_search('_wpcc_is_scheduling_active', $generalSettings)]); $this->metaKeys = array_merge($this->metaKeys, $generalSettings); // Add the meta keys of the registered post details $this->metaKeys = PostDetailsService::getInstance()->addAllSettingsMetaKeys($this->metaKeys); $this->metaKeyDefaults = PostDetailsService::getInstance()->addAllSettingsMetaKeyDefaults($this->metaKeyDefaults); $this->singleMetaKeys = PostDetailsService::getInstance()->addAllSingleSettingsMetaKeys($this->singleMetaKeys); /* * ALLOW MODIFICATION OF META KEYS WITH FILTERS */ /** * Modify meta keys that are used to save site settings. * * @param array $metaKeys * * @since 1.6.3 * @return array Modified meta keys */ $this->metaKeys = apply_filters('wpcc/post/settings/meta-keys', $this->metaKeys); /** * Modify meta key defaults. * * @param array $metaKeyDefaults * * @since 1.8.0 * @return array Modified meta key defaults */ $this->metaKeyDefaults = apply_filters('wpcc/post/settings/meta-key-defaults', $this->metaKeyDefaults); /** * Modify CRON meta keys that are used to save information about CRON events. * * @param array $cronMetaKeys * * @since 1.6.3 * @return array Modified CRON meta keys */ $this->cronMetaKeys = apply_filters('wpcc/post/settings/cron-meta-keys', $this->cronMetaKeys); /** * Modify single meta keys. These keys can only be used to store a single value. So, they cannot store serialized * array etc. They can only store a single value. Indicating if a meta key is single or not has a vital importance * when showing already-saved settings in the form item fields and importing/exporting settings. Hence, if a meta * key you added to 'metaKeys' stores a single value, you have to make sure that you added that meta key among * 'singleMetaKeys' as well. * * @param array $singleMetaKeys * * @since 1.6.3 * @return array Modified single meta keys */ $this->singleMetaKeys = apply_filters('wpcc/post/settings/single-meta-keys', $this->singleMetaKeys); } /** * Handles AJAX requests made from site list page * @param int $postId ID of the site to be updated * @param array $data * @return string JSON */ public function postSiteList($postId, $data) { if(!Factory::wptslmClient()->isUserCool()) { $key = isset($data["_active"]) ? '_active' : '_active_recrawling'; return json_encode([ "data" => $data, $key => $data[$key] == "true" ? false : true, ]); } // Save the data $results = []; if(isset($data["_active"])) { $results["_active"] = Utils::savePostMeta($postId, "_active", $data["_active"] == "true" ? true : false, true); } if(isset($data["_active_recrawling"])) { $results["_active_recrawling"] = Utils::savePostMeta($postId, "_active_recrawling", $data["_active_recrawling"] == "true" ? true : false, true); } if(isset($data["_active_post_deleting"])) { $results["_active_post_deleting"] = Utils::savePostMeta($postId, "_active_post_deleting", $data["_active_post_deleting"] == "true" ? true : false, true); } $results["data"] = $data; $results["post_id"] = $postId; return json_encode($results); } /** * Prepares and returns HTML for site settings meta box * @return string HTML */ public function getSettingsMetaBox() { if(!current_user_can(Constants::$ALLOWED_USER_CAPABILITY)) return ''; global $post; // Set Tiny MCE settings so that it allows custom HTML codes and keeps them unchanged add_filter('tiny_mce_before_init', function($settings) { // Disable autop to keep all valid HTML elements $settings['wpautop'] = false; // Don't remove line breaks $settings['remove_linebreaks'] = false; // Format the HTML $settings['apply_source_formatting'] = true; // Convert newline characters to BR $settings['convert_newlines_to_brs'] = true; // Don't remove redundant BR $settings['remove_redundant_brs'] = false; // Pass back to WordPress return $settings; }); $settings = get_post_meta($post->ID); // Set the defaults only if there are no settings. if (!$settings) $settings = $this->metaKeyDefaults; $settingsImpl = new SettingsImpl($settings, static::getSingleMetaKeys()); // Create view variables $viewVars = array_merge([ 'postId' => $post->ID, 'settings' => $settings, 'settingsForExport' => base64_encode(serialize($this->getSettingsForExport($settings))), 'categories' => Utils::getCategories($settingsImpl), 'buttonsMain' => $this->getEditorButtonsMain(), 'buttonsTitle' => $this->getEditorButtonsTitle(), 'buttonsExcerpt' => $this->getEditorButtonsExcerpt(), 'buttonsList' => $this->getEditorButtonsList(), 'buttonsGallery' => $this->getEditorButtonsGallery(), 'buttonsOptionsBoxTemplates' => $this->getEditorButtonsOptionsBoxTemplates(), 'buttonsFileOptionsBoxTemplates'=> FileOptionsBoxApplier::getShortCodeButtons(), 'optionsBoxConfigs' => $this->getOptionsBoxConfigs($settingsImpl) ], Settings::getSettingsPageVariables(false)); // Add post detail settings if there are any $postDetailSettingsViews = PostDetailsService::getInstance()->getSettingsViews($settingsImpl, $viewVars); $viewVars['postDetailSettingsViews'] = $postDetailSettingsViews; return Utils::view('site-settings/main')->with($viewVars)->render(); } /** * Creates options box configurations for specific settings. * * @param SettingsImpl $postSettings * @return array A key-value pair. The keys are meta keys of the settings. The values are arrays storing the * configuration for the options box for that setting. * @since 1.8.0 */ private function getOptionsBoxConfigs($postSettings) { $configs = [ // Category post URL selectors '_category_post_link_selectors' => OptionsBoxConfiguration::init() ->addTabOption(OptionsBoxTab::TEMPLATES, TemplatesTabOptions::ALLOWED_SHORT_CODES, [ ShortCodeName::WCC_ITEM ])->get(), // Category next page selectors '_category_next_page_selectors' => OptionsBoxConfiguration::init() ->addTabOption(OptionsBoxTab::TEMPLATES, TemplatesTabOptions::ALLOWED_SHORT_CODES, [ ShortCodeName::WCC_ITEM ])->get(), // Featured image selectors '_post_thumbnail_selectors' => OptionsBoxConfiguration::init() ->setType(OptionsBoxType::FILE) ->get(), // Gallery image selectors '_post_gallery_image_selectors' => OptionsBoxConfiguration::init() ->setType(OptionsBoxType::FILE) ->get(), // Image selectors '_post_image_selectors' => OptionsBoxConfiguration::init() ->setType(OptionsBoxType::FILE) ->get(), ]; // Get the configurations of registered post details $configs = array_merge($configs, PostDetailsService::getInstance()->getOptionsBoxConfigs($postSettings)); return $configs; } /** * Prepares and returns HTML for site notes meta box * @return string HTML */ public function getNotesMetaBox() { if(!current_user_can(Constants::$ALLOWED_USER_CAPABILITY)) return ''; global $post; $notesSimple = get_post_meta($post->ID, '_notes_simple'); return Utils::view('site-settings/meta-box-notes')->with([ 'notesSimple' => $notesSimple ]); } /** * Handles HTTP POST requests made by create/edit page (where site settings meta box is) * * @param int $postId * @param WP_Post $postAfter * @param WP_Post $postBefore */ public function postSettingsMetaBox($postId, $postAfter, $postBefore) { if(!Factory::wptslmClient()->isUserCool()) return; if(!current_user_can(Constants::$ALLOWED_USER_CAPABILITY)) return; // If the nonce does not exist in the request or the request is not made from admin page, abort. if(!isset($_POST["action"]) || !$_POST["action"] == 'wcc_tools') { // Allow requests made from Tools if (!isset($_POST[Constants::$NONCE_NAME]) || !check_admin_referer('wcc-settings-metabox', Constants::$NONCE_NAME)) return; } // Do not run if the post is moved to trash. if ($postAfter->post_status == 'trash') return; // Do not run if the post is restored. if ($postBefore->post_status == 'trash') return; $this->saveSettings($postId, $_POST); } /** * Saves settings from AJAX data that contains serialized form values. * * @param array $data AJAX data * @return string JSON * @since 1.8.0 */ private function quickSaveSettings($data) { $postId = Utils::array_get($data, "postId"); $serializedSettings = Utils::array_get($data, "settings"); if (!$serializedSettings) { return json_encode([ "success" => false, "message" => _wpcc("Settings do not exist in the data.") ]); } if (!$postId) { return json_encode([ "success" => false, "message" => _wpcc("Post ID does not exist.") ]); } // Prepare the serialized settings string // parse_str function escapes special characters. However, it cannot escape special characters that are // URL-encoded. Therefore, we need to escape them manually. urldecode function does not do the job either. It // behaves the same for some reason. // A backslash is URL-encoded and it needs to be escaped. Here, we replace a backslash, whose URL-encoded // equivalent is %5C, with double backslash, which is %5C%5C. $serializedSettings = str_replace('%5C', '%5C%5C', $serializedSettings); // Parse the serialized value to an array $settings = []; parse_str($serializedSettings, $settings); // Remove URL hash since it is only needed when the page is updated after saving. Here, the settings are saved // via AJAX. So, no update. if (isset($settings['url_hash'])) unset($settings['url_hash']); // Save the settings $result = $this->saveSettings($postId, $settings); // Add export option's value $result["settingsForExport"] = base64_encode(serialize($this->getSettingsForExport(get_post_meta($postId)))); return json_encode($result); } /** * @param int $postId * @param array $settings Settings retrieved from form. $_POST can be directly supplied. The values must be slashed * because WP's post meta saving function requires slashed data. * @return array * @since 1.8.0 */ private function saveSettings($postId, $settings) { $data = $settings; $success = true; $message = ''; $queryParams = []; if(isset($data["url_hash"])) $queryParams["url_hash"] = $data["url_hash"]; // Check if the user wants to import the settings if(isset($data["_post_import_settings"]) && !empty($data["_post_import_settings"])) { // User wants to import the settings. Parse them and replace data variable with the imported settings. $serializedSettings = base64_decode($data["_post_import_settings"]); if($serializedSettings && is_serialized($serializedSettings)) { $settings = unserialize($serializedSettings); // When saving the data with update_post_meta or a similar function, WordPress first unslashes it. // So, we need to slash the values of the array using wp_slash. This does not matter when normally saving // the settings. Because, WordPress automatically slashes the values taken from $_POST. $data = Utils::arrayDeepSlash($settings); } } // Check if the category map is the same as before $categoryMapBefore = get_post_meta($postId, '_category_map', true); if(is_array($categoryMapBefore)) $categoryMapBefore = array_values($categoryMapBefore); if(isset($data['_category_map'])) { $categoryMapCurrent = array_values($data['_category_map']); // If category map is changed, then delete all of the unsaved URLs belonging to this site. Because, it is // not possible to know which URL is for which category, since we do not store category URLs in the table. if($categoryMapBefore !== $categoryMapCurrent) { Factory::databaseService()->deleteUrlsBySiteIdAndSavedStatus($postId, false); // Also reset (deleting does the job) the CRON meta values for this site $cronMetaKeys = $this->cronMetaKeys; unset($cronMetaKeys[array_search('_cron_last_crawled_at', $cronMetaKeys)]); unset($cronMetaKeys[array_search('_cron_last_checked_at', $cronMetaKeys)]); unset($cronMetaKeys[array_search('_cron_recrawl_last_crawled_at', $cronMetaKeys)]); unset($cronMetaKeys[array_search('_cron_recrawl_last_checked_at', $cronMetaKeys)]); foreach($cronMetaKeys as $key) { delete_post_meta($postId, $key); } } } $keys = $this->metaKeys; // Validate password fields $validate = Utils::validatePasswordInput($data, $keys, get_post_meta($postId, '_wpcc_post_password', true)); if(!$validate["success"]) { // Not valid. $message = $validate["message"] . ' ' . _wpcc('Settings are updated, but password could not be changed.'); $success = false; } // Save options foreach ($data as $key => $value) { if (in_array($key, $this->metaKeys)) { if(is_array($value)) $value = array_values($value); Utils::savePostMeta($postId, $key, $value, true); // Remove the key, since it is saved. unset($keys[array_search($key, $keys)]); } } // Delete the metas which are not set foreach($keys as $key) delete_post_meta($postId, $key); // Update notice option. This option is used to show notices on site (custom post) page. if(!$success) { update_option('_wpcc_site_notice', $message, true); Utils::savePostMeta($postId, '_wpcc_site_query_params', false); } else { update_option('_wpcc_site_notice', false, true); Utils::savePostMeta($postId, '_wpcc_site_query_params', $queryParams); } return [ "message" => $message, "success" => $success ]; } /** * Prepares and returns an array for exporting settings. * * @param $settings * @return array */ private function getSettingsForExport($settings) { foreach($settings as $key => &$mSetting) { // If current key is not in our meta keys, remove it from the array. We should export only related settings. // Otherwise, we have to deal with this when importing. if(!in_array($key, $this->metaKeys)) { unset($settings[$key]); continue; } $mSetting = $this->getUnserialized($mSetting); // Set single meta key values as string if(in_array($key, $this->singleMetaKeys) && is_array($mSetting) && !empty($mSetting)) { $mSetting = array_values($mSetting)[0]; } } return $settings; } /** * Checks a parameter if it should be unserialized, and if so, does so. If the parameter has serialized values inside, * those will be unserialized as well. Hence, at the end, there will be no serialized strings inside the value. * * @param mixed $metaValue The value to be unserialized * @return mixed Unserialized value */ private function getUnserialized($metaValue) { $val = (!empty($metaValue) && isset($metaValue[0])) ? $metaValue[0] : $metaValue; return is_serialized($val) ? $this->getUnserialized(unserialize($val)) : $metaValue; } /** * Creates custom post types, attaches events to be fired when post is saved, makes necessary changes and so on. */ public function createCustomPostType() { // Add custom post type and configure it add_action('init', function () { $labels = array( 'name' => _wpcc('Sites'), 'singular_name' => _wpcc('Site'), 'menu_name' => _wpcc('Content Crawler'), 'name_admin_bar' => _wpcc('Content Crawler Site'), 'add_new' => _wpcc('Add New'), 'add_new_item' => _wpcc('Add New Site'), 'new_item' => _wpcc('New Site'), 'edit_item' => _wpcc('Edit Site'), 'view_item' => _wpcc('View Site'), 'all_items' => _wpcc('All Sites'), 'search_items' => _wpcc('Search Sites'), 'parent_item_colon' => _wpcc('Parent Sites:'), 'not_found' => _wpcc('No sites found.'), 'not_found_in_trash' => _wpcc('No sites found in Trash.') ); $args = array( 'public' => false, 'labels' => $labels, 'description' => _wpcc('A custom post type which stores sites to be crawled'), 'menu_icon' => 'dashicons-tickets-alt', 'show_ui' => true, 'show_in_admin_bar' => true, 'show_in_menu' => true, 'supports' => [] ); register_post_type(Constants::$POST_TYPE, $args); // Remove text editor remove_post_type_support(Constants::$POST_TYPE, 'editor'); }); // Set columns add_filter(sprintf('manage_%s_posts_columns', Constants::$POST_TYPE), function($columns) { unset($columns["date"]); $newColumns = [ "author" => _wpcc("Author"), "active" => _wpcc("Active for scheduling"), "active_recrawling" => _wpcc("Active for recrawling"), "active_post_deleting" => _wpcc("Active for deleting"), "counts" => _wpcc("Counts"), "last_checked" => _wpcc("Last URL Collection"), "last_crawled" => _wpcc("Last Post Crawl"), "last_recrawled" => _wpcc("Last Post Recrawl"), "last_deleted" => _wpcc("Last Post Delete"), "date" => __("Date") ]; return array_merge($columns, $newColumns); }); // Set sortable columns add_filter(sprintf('manage_edit-%s_sortable_columns', Constants::$POST_TYPE), function($columns) { $columns['active'] = 'active'; $columns['active_recrawling'] = 'active_recrawling'; $columns['active_post_deleting'] = 'active_post_deleting'; $columns['last_checked'] = 'last_checked'; $columns['last_crawled'] = 'last_crawled'; $columns['last_recrawled'] = 'last_recrawled'; $columns['last_deleted'] = 'last_deleted'; return $columns; }); // Sort the columns when the user wants it add_action("load-edit.php", function() { add_filter('request', function($vars) { if (isset($vars['post_type']) && $vars['post_type'] == Constants::$POST_TYPE) { if (isset($vars['orderby'])) { $metaKey = $orderBy = null; switch($vars['orderby']) { case 'active': $metaKey = '_active'; $orderBy = 'meta_value'; break; case 'active_recrawling': $metaKey = '_active_recrawling'; $orderBy = 'meta_value'; break; case 'active_post_deleting': $metaKey = '_active_post_deleting'; $orderBy = 'meta_value'; break; case 'last_checked': $metaKey = '_cron_last_checked_at'; $orderBy = 'meta_value'; break; case 'last_crawled': $metaKey = '_cron_last_crawled_at'; $orderBy = 'meta_value'; break; case 'last_recrawled': $metaKey = '_cron_recrawl_last_crawled_at'; $orderBy = 'meta_value'; break; case 'last_deleted': $metaKey = Factory::schedulingService()->metaKeyCronLastDeleted; $orderBy = 'meta_value'; break; } // Merge the query vars with custom variables. if($metaKey !== null && $orderBy !== null) { $vars = array_merge($vars, [ 'meta_key' => $metaKey, 'orderby' => $orderBy ]); } } } return $vars; }); }); // Set column contents add_filter(sprintf('manage_%s_posts_custom_column', Constants::$POST_TYPE), function($columnName, $postId) { // dd($columnName); if($columnName == 'active') { $active = get_post_meta($postId, '_active', true); echo ''; } else if($columnName == 'active_recrawling') { $active = get_post_meta($postId, '_active_recrawling', true); echo ''; } else if($columnName == 'active_post_deleting') { $active = get_post_meta($postId, '_active_post_deleting', true); echo ''; } else if($columnName == 'counts') { $allCounts = Factory::postService()->getUrlTableCounts(); if(!isset($allCounts[$postId])) { echo "-"; } else { $counts = $allCounts[$postId]; $s = '%1$s: %2$d'; echo sprintf($s, _wpcc("Queue"), $counts["count_queue"]) . "
" . sprintf($s, _wpcc("Saved"), $counts["count_saved"]) . "
" . sprintf($s, _wpcc("Updated"), $counts["count_updated"]) . "
" . sprintf($s, _wpcc("Deleted"), $counts["count_deleted"]) . "
" . sprintf($s, _wpcc("Other"), $counts["count_other"]) . "
" . sprintf($s, _wpcc("Total"), $counts["count_total"]) ; } } else if($columnName == 'last_checked') { $date = get_post_meta($postId, '_cron_last_checked_at', true); echo Utils::getDateFormatted($date); } else if($columnName == 'last_crawled') { $date = get_post_meta($postId, '_cron_last_crawled_at', true); echo Utils::getDateFormatted($date); } else if($columnName == 'last_recrawled') { $date = get_post_meta($postId, '_cron_recrawl_last_crawled_at', true); echo Utils::getDateFormatted($date); } else if($columnName == 'last_deleted') { $date = get_post_meta($postId, Factory::schedulingService()->metaKeyCronLastDeleted, true); echo Utils::getDateFormatted($date); } }, 10, 2); // Remove quick edit button add_filter('post_row_actions', function ($actions) { $currentScreen = get_current_screen(); if(!isset($currentScreen->post_type) || $currentScreen->post_type != Constants::$POST_TYPE) return $actions; unset($actions['inline hide-if-no-js']); return $actions; }, 10, 1); // Set interaction messages add_filter('post_updated_messages', function ($messages) { $post = get_post(); $messages[Constants::$POST_TYPE] = array( 0 => '', 1 => _wpcc('Site updated.'), 2 => _wpcc('Custom field updated.'), 3 => _wpcc('Custom field deleted.'), 4 => _wpcc('Site updated.'), 5 => isset($_GET['revision']) ? sprintf(_wpcc('Site restored to revision from %s'), wp_post_revision_title((int)$_GET['revision'], false)) : false, 6 => _wpcc('Site published.'), 7 => _wpcc('Site saved.'), 8 => _wpcc('Site submitted.'), 9 => sprintf( _wpcc('Site scheduled for: %1$s.'), date_i18n('M j, Y @ G:i', strtotime($post->post_date)) ), 10 => _wpcc('Site draft updated.'), ); return $messages; }); add_filter('enter_title_here', function($title) { if(get_current_screen()->post_type == Constants::$POST_TYPE) { $title = _wpcc('Enter site name here'); } return $title; }); // Create help tabs add_filter('admin_head', function () { $screen = get_current_screen(); // Stop if we are not in the custom post type screen we created. if (!isset($screen->post_type) || $screen->post_type != Constants::$POST_TYPE) return; // $basics = array( // 'id' => 'wcc_site_basics', // 'title' => 'Site Basics', // 'content' => 'Basic content for help tab here' // ); // // $formatting = array( // 'id' => 'wcc_site_formatting', // 'title' => 'Site Formatting', // 'content' => 'Content for help tab here' // ); // // $screen->add_help_tab($basics); // $screen->add_help_tab($formatting); // ADD NONCE // This will add the nonce after "All" link above the table (near "Published" link). This is the best // place I can come up with. add_filter('views_' . $screen->id, function($views) { $views['all'] = $views['all'] . wp_nonce_field('wcc-site-list', Constants::$NONCE_NAME); return $views; }); }); // Add the meta box. It will hold all settings. add_action('add_meta_boxes', function () { add_meta_box( Constants::$SITE_SETTINGS_META_BOX_ID, _wpcc('Settings'), function () { echo Factory::postService()->getSettingsMetaBox(); }, Constants::$POST_TYPE, 'normal', 'high' ); // Also add a meta box for keeping simple notes. add_meta_box( Constants::$SITE_SETTINGS_NOTES_META_BOX_ID, _wpcc('Simple Notes'), function() { echo Factory::postService()->getNotesMetaBox(); }, Constants::$POST_TYPE, 'side' ); }); // Add a class to the meta box to be able to differentiate it from other meta boxes. In this case, we want // the meta box not sortable, because WYSIWYG editor does not like being moved around, and the meta box will // have several WYSIWYG editors inside. add_filter(sprintf('postbox_classes_%s_%s', Constants::$POST_TYPE, Constants::$SITE_SETTINGS_META_BOX_ID), function($classes) { $classes[] = 'not-sortable'; return $classes; } ); // Add styles and scripts for post settings add_action('admin_enqueue_scripts', function ($hook) { // Check if we are on the custom post page. global $post; $valid = ($hook == 'post-new.php' && isset($_GET["post_type"]) && $_GET["post_type"] == Constants::$POST_TYPE) || ($hook == 'post.php' && $post && $post->post_type == Constants::$POST_TYPE); if(!$valid) return; Factory::assetManager()->addPostSettings(); $settings = $post && isset($post->ID) ? get_post_meta($post->ID) : []; // Add assets of the registered post details PostDetailsService::getInstance()->addSiteSettingsAssets($settings); Factory::assetManager()->addTooltip(); Factory::assetManager()->addClipboard(); Factory::assetManager()->addDevTools(); Factory::assetManager()->addOptionsBox(); Factory::assetManager()->addAnimate(); }); // Add styles and scripts for site list add_action('admin_enqueue_scripts', function($hook) { // Check if we are on the site list page $valid = $hook == 'edit.php' && isset($_GET["post_type"]) && $_GET["post_type"] == Constants::$POST_TYPE; if(!$valid) return; Factory::assetManager()->addPostList(); }); // Save options when the post is saved add_action('post_updated', function($postId, $postAfter, $postBefore) { Factory::postService()->postSettingsMetaBox($postId, $postAfter, $postBefore); }, 10, 3); // Delete all URLs when the site is permanently deleted add_action('admin_init', function() { add_action('delete_post', function($postId) { global $post_type; if ($post_type != Constants::$POST_TYPE) return; Factory::databaseService()->deleteUrlsBySiteId($postId); }); }); // Show notices when there is an error add_action('admin_notices', function() { $message = get_option('_wpcc_site_notice'); if($message) { echo Utils::view('partials/alert')->with([ 'message' => $message, 'type' => 'error' ])->render(); update_option('_wpcc_site_notice', false); } }); } /** * Get counts of URLs grouped by site ID and whether they are saved or not. * * @return array An array with keys being site IDs and values being an array containing post counts. Each value * array has count_saved, count_updated, count_queue, count_deleted, count_other, count_total. * These values are either integer or null. */ public function getUrlTableCounts() { // If it is already found before, return it. if(static::$urlCounts) return static::$urlCounts; // Find URL counts global $wpdb; $tableUrls = Factory::databaseService()->getDbTableUrlsName(); $query = "SELECT t_total.post_id, count_saved, count_updated, count_queue, count_deleted, (IFNULL(count_total, 0) - IFNULL(count_saved, 0) - IFNULL(count_queue, 0) - IFNULL(count_deleted, 0)) as count_other, count_total FROM (SELECT post_id, count(*) as count_total FROM {$tableUrls} GROUP BY post_id) t_total LEFT JOIN ( SELECT post_id, count(*) as count_queue FROM {$tableUrls} WHERE saved_post_id IS NULL AND is_saved = FALSE GROUP BY post_id) t_queue ON t_total.post_id = t_queue.post_id LEFT JOIN ( SELECT post_id, count(*) as count_saved FROM {$tableUrls} WHERE saved_post_id IS NOT NULL AND is_saved = TRUE GROUP BY post_id) t_saved ON t_total.post_id = t_saved.post_id LEFT JOIN ( SELECT post_id, count(*) as count_updated FROM {$tableUrls} WHERE saved_post_id IS NOT NULL AND is_saved = TRUE AND update_count > 0 GROUP BY post_id) t_updated ON t_total.post_id = t_updated.post_id LEFT JOIN ( SELECT post_id, count(*) as count_deleted FROM {$tableUrls} WHERE saved_post_id IS NULL AND deleted_at IS NOT NULL GROUP BY post_id) t_deleted ON t_total.post_id = t_deleted.post_id"; $results = $wpdb->get_results($query, ARRAY_A); $data = []; foreach($results as $result) { // Get post id from current result $currentPostId = $result["post_id"]; // Unset the post id unset($result["post_id"]); // Add the result to the data under post ID key. $data[$currentPostId] = $result; } static::$urlCounts = $data; return static::$urlCounts; } /* * EDITOR BUTTONS */ private function getEditorButtonsMain() { if(!$this->editorButtonsMain) $this->editorButtonsMain = [ $this->createButtonInfo(ShortCodeName::WCC_MAIN_TITLE, _wpcc("Prepared post title"), true), $this->createButtonInfo(ShortCodeName::WCC_MAIN_EXCERPT, _wpcc("Prepared post excerpt"), true), $this->createButtonInfo(ShortCodeName::WCC_MAIN_CONTENT, _wpcc("Main post content")), $this->createButtonInfo(ShortCodeName::WCC_MAIN_LIST, _wpcc("List items")), $this->createButtonInfo(ShortCodeName::WCC_MAIN_GALLERY, _wpcc("Gallery items")), $this->createButtonInfo(ShortCodeName::WCC_SOURCE_URL, sprintf(_wpcc('Full URL of the target page. You can use this to reference the source page. E.g. Source'), '[' . ShortCodeName::WCC_SOURCE_URL .']')), ]; return $this->editorButtonsMain; } private function getEditorButtonsTitle() { if(!$this->editorButtonsTitle) $this->editorButtonsTitle = [ $this->createButtonInfo(ShortCodeName::WCC_MAIN_TITLE, _wpcc("Original post title"), true), ]; return $this->editorButtonsTitle; } private function getEditorButtonsExcerpt() { if(!$this->editorButtonsExcerpt) $this->editorButtonsExcerpt = [ $this->createButtonInfo(ShortCodeName::WCC_MAIN_TITLE, _wpcc("Prepared post title"), true), $this->createButtonInfo(ShortCodeName::WCC_MAIN_EXCERPT, _wpcc("Original post excerpt"), true), ]; return $this->editorButtonsExcerpt; } private function getEditorButtonsList() { if(!$this->editorButtonsList) $this->editorButtonsList = [ $this->createButtonInfo(ShortCodeName::WCC_LIST_ITEM_TITLE, _wpcc("List item title")), $this->createButtonInfo(ShortCodeName::WCC_LIST_ITEM_CONTENT, _wpcc("List item content")), $this->createButtonInfo(ShortCodeName::WCC_LIST_ITEM_POSITION, _wpcc("The position of the item.")), ]; return $this->editorButtonsList; } private function getEditorButtonsGallery() { if(!$this->editorButtonsGallery) $this->editorButtonsGallery = [ $this->createButtonInfo(ShortCodeName::WCC_GALLERY_ITEM_URL, _wpcc("Gallery item URL")) ]; return $this->editorButtonsGallery; } private function getEditorButtonsOptionsBoxTemplates() { if (!$this->editorButtonsOptionsBoxTemplates) { $this->editorButtonsOptionsBoxTemplates = array_merge([ $this->createButtonInfo(ShortCodeName::WCC_ITEM, _wpcc("Found item")) ], $this->getEditorButtonsMain()); } return $this->editorButtonsOptionsBoxTemplates; } /** * @param string $code Short code without square brackets * @param string $description Description for what the short code does * @param bool $fresh True if a fresh instance should be returned. Otherwise, if the code created before, * the previously-created instance will be returned. * @return ShortCodeButton Short code button */ private function createButtonInfo($code, $description = '', $fresh = false) { return ShortCodeButton::getShortCodeButton($code, $description, $fresh); } /** * Get an array of all predefined short codes * @return array An array of short codes with square brackets */ public function getPredefinedShortCodes() { if(!$this->allPredefinedShortCodes) { $combinedButtons = array_merge( $this->getEditorButtonsMain(), $this->getEditorButtonsTitle(), $this->getEditorButtonsExcerpt(), $this->getEditorButtonsList(), $this->getEditorButtonsGallery() ); $result = []; foreach ($combinedButtons as $btn) { /** @var ShortCodeButton $btn */ $result[] = $btn->getCodeWithBrackets(); } $this->allPredefinedShortCodes = $result; } return $this->allPredefinedShortCodes; } /* * */ /** * Get single meta keys * * @return array An array of keys */ public function getSingleMetaKeys() { return $this->singleMetaKeys; } }