diff --git a/.gitattributes b/.gitattributes index a200a5a..771aace 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ .gitattributes export-ignore .github export-ignore -.gitignore export-ignore \ No newline at end of file +.gitignore export-ignore +phpcs.xml.dist export-ignore \ No newline at end of file diff --git a/.github/workflows/wpcs.yml b/.github/workflows/wpcs.yml new file mode 100644 index 0000000..aa3d41f --- /dev/null +++ b/.github/workflows/wpcs.yml @@ -0,0 +1,26 @@ +name: WPCS checks. + +on: [pull_request, push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + phpcs: + name: WPCS + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: WPCS checks + uses: 10up/wpcs-action@stable + with: + use_local_config: 'true' + enable_warnings: 'true' + extra_args: '-q -n --report-json=./phpcs.json' + - name: Update summary + run: | + npm i -g github:10up/phpcs-json-to-md + phpcs-json-to-md --path ./phpcs.json --output ./phpcs.md + cat phpcs.md >> $GITHUB_STEP_SUMMARY + if: always() diff --git a/README.md b/README.md index d7cb744..42bacf7 100755 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # ClassicPress Directory integrator. +[![ClassicPress Directory Coding Standard checks.](https://github.com/ClassicPress/classicpress-directory-integration/actions/workflows/cpcs.yml/badge.svg)](https://github.com/ClassicPress/classicpress-directory-integration/actions/workflows/cpcs.yml)[![WPCS checks.](https://github.com/ClassicPress/classicpress-directory-integration/actions/workflows/wpcs.yml/badge.svg)](https://github.com/ClassicPress/classicpress-directory-integration/actions/workflows/wpcs.yml) + ## Features - Plugins and themes from [ClassicPress Directory](https://directory.classicpress.net/) now can update as WP.org plugins. diff --git a/classes/PluginInstall.class.php b/classes/PluginInstall.class.php deleted file mode 100644 index 697f989..0000000 --- a/classes/PluginInstall.class.php +++ /dev/null @@ -1,485 +0,0 @@ -page) { - return; - } - wp_enqueue_style( 'classicpress-directory-integration-css', plugins_url( '../styles/directory-integration.css', __FILE__ ), []) ; - } - - public function scripts($hook) - { - if ($hook !== $this->page) { - return; - } - wp_enqueue_script( 'classicpress-directory-integration-js', plugins_url( '../scripts/directory-integration.js', __FILE__ ), array( 'wp-i18n' ), false, true ); - wp_set_script_translations( 'classicpress-directory-integration-js', 'classicpress-directory-integration', plugin_dir_path( 'classicpress-directory-integration' ) . 'languages' ); - } - - public function create_menu() - { - if (!current_user_can('install_plugins')) { - return; - } - - $this->page = add_submenu_page( - 'plugins.php', - esc_html__('Install ClassicPress Plugins', 'classicpress-directory-integration'), - esc_html__('Install CP Plugins', 'classicpress-directory-integration'), - 'install_plugins', - 'classicpress-directory-integration-plugin-install', - [$this, 'render_menu'], - 2 - ); - - add_action('load-' . $this->page, [$this, 'activate_action']); - add_action('load-' . $this->page, [$this, 'install_action']); - } - - public function rename_menu() { - global $submenu; - foreach ( $submenu['plugins.php'] as $key => $value ) { - if($value[2] !== 'plugin-install.php') { - continue; - } - $submenu['plugins.php'][$key][0] = esc_html__('Install WP Plugins', 'classicpress-directory-integration'); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - } - } - - // Get all installed ClassicPress plugin - // This function is different from the one in PluginUpdate class - // and considers a plugin from the dir not only if it has UpdateURI - // but also if it has RequiresCP. - private function get_local_cp_plugins() - { - - if ($this->local_cp_plugins !== false) { - return $this->local_cp_plugins; - } - - $all_plugins = get_plugins(); - $cp_plugins = []; - foreach ($all_plugins as $slug => $plugin) { - if (!array_key_exists('UpdateURI', $plugin) && !array_key_exists('RequiresCP', $plugin)) { - continue; - } - if (strpos($plugin['UpdateURI'], \CLASSICPRESS_DIRECTORY_INTEGRATION_URL) !== 0 && !array_key_exists('RequiresCP', $plugin)) { - continue; - } - $cp_plugins[dirname($slug)] = [ - 'WPSlug' => $slug, - 'Name' => $plugin['Name'], - 'Version' => $plugin['Version'], - 'PluginURI' => array_key_exists('PluginURI', $plugin) ? $plugin['PluginURI'] : null, - 'Active' => is_plugin_active($slug), - ]; - } - - $this->local_cp_plugins = $cp_plugins; - return $this->local_cp_plugins; - } - - // Validate and sanitize args for quering the ClassicPress Directory - public static function sanitize_args($args) - { - foreach ($args as $key => $value) { - $sanitized = false; - switch ($key) { - case 'per_page': - case 'page': - $args[$key] = (int) $value; - $sanitized = true; - break; - case 'byslug': - $args[$key] = preg_replace('[^A-Za-z0-9\-_]', '', $value); - $sanitized = true; - break; - case 'search': - $args[$key] = sanitize_text_field($value); - $sanitized = true; - break; - case '_fields': - $args[$key] = preg_replace('[^A-Za-z0-9\-_,]', '', $value); - $sanitized = true; - break; - } - if ($sanitized) { - continue; - } - unset($args[$key]); - } - return $args; - } - - // Query the ClassicPress Directory - public static function do_directory_request($args = [], $type = 'plugins') - { - $result['success'] = false; - - if (!in_array($type, ['plugins', 'themes'])) { - $result['error'] = $type . ' is not a supported type'; - return $result; - } - - $args = self::sanitize_args($args); - $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL . $type; - $endpoint = add_query_arg($args, $endpoint); - - $response = wp_remote_get($endpoint, ['user-agent' => classicpress_user_agent()]); - - if (is_wp_error($response)) { - $result['error'] = rtrim(implode(',', $response->get_error_messages()), '.'); - return $result; - } - - $e = wp_remote_retrieve_response_code($response); - if ($e !== 200) { - $result['error'] = $response['response']['message'].'.'; - $result['code'] = $response['response']['code']; - if (!isset($response['body']) || !json_validate($response['body'])) { - return $result; - } - $api_message = json_decode($response['body'], true); - if(!isset($api_message['message'])) { - return $result; - } - $result['error'] .= ' '.$api_message['message']; - return $result; - } - - if (!isset($response['headers'])) { - $result['error'] = 'No headers found'; - return $result; - } - - $headers = $response['headers']->getAll(); - if (!isset($headers['x-wp-total']) || !isset($headers['x-wp-totalpages'])) { - $result['error'] = 'No pagination headers found'; - return $result; - } - - $data_from_dir = json_decode(wp_remote_retrieve_body($response), true); - if ($data_from_dir === null) { - $result['error'] = 'Failed decoding response'; - return $result; - } - - $result['success'] = true; - $result['total-pages'] = $headers['x-wp-totalpages']; - $result['total-items'] = $headers['x-wp-total']; - $result['response'] = $data_from_dir; - - return $result; - } - - // Enqueue a notice - private function add_notice($message, $failure = false) - { - $other_notices = get_transient('cpdi_pi_notices'); - $notice = $other_notices === false ? '' : $other_notices; - $failure_style = $failure ? 'notice-error' : 'notice-success'; - $notice .= '
'; - $notice .= '

' . esc_html($message) . '

'; - $notice .= '
'; - set_transient('cpdi_pi_notices', $notice, \HOUR_IN_SECONDS); - } - - // Display notices - private function display_notices() - { - $notices = get_transient('cpdi_pi_notices'); - if ($notices === false) { - return; - } - // This contains html formatted from 'add_notice' function that uses 'esc_html'. - echo $notices; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - delete_transient('cpdi_pi_notices'); - } - - // Deal with activation requests - public function activate_action() - { - - // Load local plugins information - $local_cp_plugins = $this->get_local_cp_plugins(); - - // Security checks - if (!isset($_GET['action'])) { - return; - } - if ($_GET['action'] !== 'activate') { - return; - } - if (!check_admin_referer('activate', '_cpdi')) { - return; - } - if (!current_user_can('activate_plugins')) { - return; - } - if (!isset($_REQUEST['slug'])) { - return; - } - // Check if plugin slug is proper - $slug = sanitize_key(wp_unslash($_REQUEST['slug'])); - if (!array_key_exists($slug, $local_cp_plugins)) { - return; - } - - // Activate plugin - $result = activate_plugin($local_cp_plugins[$slug]['WPSlug']); - - if ($result !== null) { - // Translators: %1$s is the plugin name. - $message = sprintf(esc_html__('Error activating %1$s.', 'classicpress-directory-integration'), $local_cp_plugins[$slug]['Name']); - $this->add_notice($message, true); - } else { - // Translators: %1$s is the plugin name. - $message = sprintf(esc_html__('%1$s activated.', 'classicpress-directory-integration'), $local_cp_plugins[$slug]['Name']); - $this->add_notice($message, false); - } - - $sendback = remove_query_arg(['action', 'slug', '_cpdi'], wp_get_referer()); - wp_safe_redirect($sendback); - exit; - } - - // Deal with installation requests - public function install_action() - { - - // Security checks - if (!isset($_GET['action'])) { - return; - } - if ($_GET['action'] !== 'install') { - return; - } - if (!check_admin_referer('install', '_cpdi')) { - return; - } - if (!current_user_can('install_plugins')) { - return; - } - if (!isset($_REQUEST['slug'])) { - return; - } - // Check if plugin slug is proper - $slug = sanitize_key(wp_unslash($_REQUEST['slug'])); - - // Get github release file - $args = [ - 'byslug' => $slug, - '_fields' => 'meta,title', - ]; - $response = $this->do_directory_request($args, 'plugins'); - if (!$response['success'] || !isset($response['response'][0]['meta']['download_link'])) { - // Translators: %1$s is the plugin name. - $message = sprintf(esc_html__('API error.', 'classicpress-directory-integration'), $local_cp_plugins[$slug]['Name']); - $this->add_notice($message, true); - $sendback = remove_query_arg(['action', 'slug', '_cpdi'], wp_get_referer()); - wp_safe_redirect($sendback); - exit; - } - - $installation_url = $response['response'][0]['meta']['download_link']; - $plugin_name = $response['response'][0]['title']['rendered']; - - // Install plugin - $skin = new PluginInstallSkin(['type' => 'plugin']); - $upgrader = new \Plugin_Upgrader($skin); - $response = $upgrader->install($installation_url); - - if ($response !== true) { - // Translators: %1$s is the plugin name. - $message = sprintf(esc_html__('Error installing %1$s.', 'classicpress-directory-integration'), $plugin_name); - $this->add_notice($message, true); - } else { - // Translators: %1$s is the plugin name. - $message = sprintf(esc_html__('%1$s installed.', 'classicpress-directory-integration'), $plugin_name); - $this->add_notice($message, false); - } - - $sendback = remove_query_arg(['action', 'slug', '_cpdi'], wp_get_referer()); - wp_safe_redirect($sendback); - exit; - } - - // Render "Install CP plugins" menu - public function render_menu() - { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh - // Load local plugins information - $local_cp_plugins = $this->get_local_cp_plugins(); - - // Set age number if empty - // We check nonces only on activations and installations. - // In this function nothing is modified. - $page = isset($_REQUEST['getpage']) ? (int) $_REQUEST['getpage'] : 1; //phpcs:ignore WordPress.Security.NonceVerification.Recommended - - // Query the directory - $args = [ - 'per_page' => 12, - 'page' => $page, - ]; - - if (isset($_REQUEST['searchfor'])) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended - $args['search'] = sanitize_text_field(wp_unslash($_REQUEST['searchfor'])); //phpcs:ignore WordPress.Security.NonceVerification.Recommended - } - // Reset paginations for new searches - $searching = $args['search'] ?? ''; - if (isset($_REQUEST['searchingfor']) && $_REQUEST['searchingfor'] !== $searching) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended - $args['page'] = 1; - } - - $result = $this->do_directory_request($args); - if ($result['success'] === false) { - // Query failed, display errors and exit. - $this->add_notice(esc_html($result['error']).' ('.esc_html($result['code']).').', true); - } - - // Set up variables - $plugins = $result['response'] ?? []; - $pages = $result['total-pages'] ?? 0; - - if ($plugins === []) { - $this->add_notice(esc_html__('No plugins found.', 'classicpress-directory-integration'), true); - } - - // Display notices - $this->display_notices(); -?> - -
-

-

- - -
-
- -
-
-
- -
-
- ' : ''; - $vetted_article = $plugin['meta']['cpcs_status'] === 'passing' ? ' vetted-plugin' : ''; - $markdown_contents = self::get_markdown_contents( $content, '
', '
' ); - foreach ( $markdown_contents as $markdown_content ) { - $content = str_replace( '
' . $markdown_content . '
', $markdown_content, $content ); - } - ?> -
-
-

-
%1$s.', 'classicpress-directory-integration'), $plugin['meta']['developer_name']), ['b' => []]); ?>
-
-
-
-
-
-
-
- - 'install', 'slug' => $slug]), 'install', '_cpdi')) . '" class="button install-now">' . esc_html__('Install', 'classicpress-directory-integration') . ''; - } - if (array_key_exists($slug, $local_cp_plugins) && $local_cp_plugins[$slug]['Active']) { - echo '' . esc_html__('Active', 'classicpress-directory-integration') . ''; - } - if (array_key_exists($slug, $local_cp_plugins) && !$local_cp_plugins[$slug]['Active']) { - echo '' . esc_html__('Activate', 'classicpress-directory-integration') . ''; - } - ?> -
-
-
- -
- - - - -
-
- -get_directory_data(true); - $slug = dirname($args->slug); - if (!array_key_exists($slug, $dir_data)) { - return $result; - } - - // Query the directory - $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL.'plugins?byslug='.$slug; - $response = wp_remote_get($endpoint, ['user-agent' => classicpress_user_agent(true)]); - - if (is_wp_error($response) || empty($response['response']) || wp_remote_retrieve_response_code($response) !== 200) { - return false; - } - - $data_from_dir = json_decode(wp_remote_retrieve_body($response), true); - $data = $data_from_dir[0]; - - $result = [ - 'active_installs' => (int) $data['meta']['active_installations'], - 'author' => $data['meta']['developer_name'], - 'banners' => $this->get_plugin_images('banner', $slug), - 'description' => 'false', - 'icons' => $this->get_plugin_images('icon', $slug), - 'name' => $data['title']['rendered'], - 'requires_php' => $data['meta']['requires_php'], - 'screenshots' => $this->get_plugin_images('screenshot', $slug), - 'sections' => [ - 'description' => $data['content']['rendered'], - //'faq' => 'frequently asked questions', - //'installation' => 'installation', - //'screenshots' => 'screenshots', - //'reviews' => 'reviews', - //'other_notes' => 'other notes', - //'changelog' => 'changelog', - ], - 'short_description' => $data['excerpt']['rendered'], - 'slug' => null, // null so we don't point to WP.org - 'tags' => explode(',', $data['meta']['category_names']), - 'version' => $data['meta']['current_version'], - //'added' => true, // date - //'author_block_count' => true, // int - //'author_block_rating' => true, // int - //'author_profile' => true, // url - //'compatibility' => false, // empty array? - //'contributors' => true, // array( array( [profile], [avatar], [display_name] ) - //'donate_link' => true, // url - //'download_link' => true, // url - //'downloaded' => false, // int - //'homepage' => true, // url - //'last_updated' => true, // datetime - //'num_ratings' => 14, // int how many ratings - //'rating' => 50, // int rating x 100 - //'ratings' =>[ // unuseful? - // 5 => 10, - // 4 => 4, - // 3 => 0, - // 2 => 0, - // 1 => 0, - //], // array( [5..0] ) - //'requires' => true, // version string - //'support_threads_resolved' => true, // int - //'support_threads' => true, // int - //'tested' => true, // version string - //'versions' => true, // array( [version] url ) - ]; - - return (object) $result; - - } - - private function get_plugin_images($type, $plugin) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh - $images = []; - - if (empty($plugin)) { - return $images; - } - - if (!in_array($type, ['icon', 'banner', 'screenshot'], true)) { - return $images; - } - - /** - * Filters the folder where we search for icons and banners. - * - * The filtered path is relative to the plugin's directory - * - * Example: - * add_filter( - * 'cpdi_images_folder_' . basename( __DIR__ ), - * function ( $source ) { - * return '/assets/images'; - * } - * ); - * - * - * @param string $source Original folder path. - */ - $folder = apply_filters("cpdi_images_folder_{$plugin}", '/images'); - $image_path = untrailingslashit(WP_PLUGIN_DIR).'/'.$plugin.$folder; - $image_url = untrailingslashit(WP_PLUGIN_URL).'/'.$plugin.$folder; - - $image_qualities = [ - 'icon' => ['default', '1x', '2x'], - 'banner' => ['default', 'low', 'high'], - ]; - - $image_dimensions = [ - 'icon' => ['default' => '128', '1x' => '128', '2x' => '256'], - 'banner' => ['default' => '772x250', 'low' => '772x250', 'high' => '1544x500'], - ]; - - if ($type === 'icon' || $type === 'banner') { - if (file_exists($image_path.'/'.$type.'.svg')) { - foreach ($image_qualities[$type] as $key) { - $images[$key] = $image_url.'/'.$type.'.svg'; - } - } else { - foreach (['jpg', 'png'] as $ext) { - $all_keys = $image_qualities[$type]; - $last_key = array_pop($all_keys); - $middle_key = array_pop($all_keys); - if (file_exists($image_path.'/'.$type.'-'.$image_dimensions[$type][$middle_key].'.'.$ext)) { - foreach ($image_qualities[$type] as $key) { - $images[$key] = $image_url.'/'.$type.'-'.$image_dimensions[$type][$middle_key].'.'.$ext; - } - } - if (file_exists($image_path.'/'.$type.'-'.$image_dimensions[$type][$last_key].'.'.$ext)) { // phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed - $images[$last_key] = $image_url.'/'.$type.'-'.$image_dimensions[$type][$last_key].'.'.$ext; - } - } - } - - return $images; - } - - if ($type === 'screenshot') { - - if (file_exists($image_path)) { - - $dir_contents = scandir($image_path); - - foreach ($dir_contents as $name) { - if (strpos(strtolower($name), 'screenshot') === 0) { // phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed - $start = strpos($name, '-') + 1; - $for = strpos($name, '.') - $start; - $screenshot_number = substr($name, $start, $for); - $images[$screenshot_number] = $image_url.'/'.$name; - } - } - - ksort($images); - - } - - } - - return $images; - - } - - public function after_plugin_row($plugin_file, $plugin_data, $status) { - - $slug = dirname($plugin_file); - $plugins = $this->get_cp_plugins(); - - if (!array_key_exists($slug, $plugins)) { - return; - } - - $dir_data = $this->get_directory_data(); - - if (!array_key_exists($slug, $dir_data)) { - return; - } - - $data = $dir_data[$slug]; - $plugin = $plugins[$slug]; - - if (version_compare($plugin['Version'], $data['Version']) >= 0) { - // No updates available - return false; - } - - $message = ''; - if (version_compare(classicpress_version(), $data['RequiresCP']) === -1) { - // Higher CP version required - // Translators: %1$s is the plugin latest version. %2$s is the ClassicPress version required by the plugin. - $message .= sprintf (esc_html__('This plugin has not updated to version %1$s because it needs ClassicPress %2$s.', 'classicpress-directory-integration'), esc_html($data['Version']), esc_html($data['RequiresCP'])); - } - if (version_compare(phpversion(), $data['RequiresPHP']) === -1) { - if ($message !== '') { - $message .= ' '; - } - // Translators: %1$s is the plugin latest version. %2$s is the PHP version required by the plugin. - $message .= sprintf (esc_html__('This plugin has not updated to version %1$s because it needs PHP %2$s.', 'classicpress-directory-integration'), esc_html($data['Version']), esc_html($data['RequiresPHP'])); - } - - if ($message === '') { - return; - } - - echo '

'; - echo esc_html($message).'

'; - - } - - // Force a refresh of local ClassicPress directory data - public function refresh_cp_directory_data() { - $this->get_directory_data(true); - } - - // Get all installed ClassicPress plugin - private function get_cp_plugins() { - - if ($this->cp_plugins !== false) { - return $this->cp_plugins; - } - - $all_plugins = get_plugins(); - $cp_plugins = []; - foreach ($all_plugins as $slug => $plugin) { - if (!array_key_exists('UpdateURI', $plugin)) { - continue; - } - if (strpos($plugin['UpdateURI'], \CLASSICPRESS_DIRECTORY_INTEGRATION_URL) !== 0) { - continue; - } - $cp_plugins[dirname($slug)] = [ - 'WPSlug' => $slug, - 'Version' => $plugin['Version'], - 'RequiresPHP' => array_key_exists('RequiresPHP', $plugin) ? $plugin['RequiresPHP'] : null, - 'RequiresCP' => array_key_exists('RequiresCP', $plugin) ? $plugin['RequiresCP'] : null, - 'PluginURI' => array_key_exists('PluginURI', $plugin) ? $plugin['PluginURI'] : null, - ]; - } - - $this->cp_plugins = $cp_plugins; - return $this->cp_plugins; - - } - - // Get data from the directory for all installed ClassicPress plugin - private function get_directory_data($force = false) { - - // Try to get stored data - if (!$force && $this->cp_plugins_directory_data !== false) { - // We have it in memory - return $this->cp_plugins_directory_data; - } - $this->cp_plugins_directory_data = get_transient('cpdi_directory_data_plugins'); - if (!$force && $this->cp_plugins_directory_data !== false) { - // We have it in transient - return $this->cp_plugins_directory_data; - } - - // Query the directory - $plugins = $this->get_cp_plugins(); - $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL.'plugins?byslug='.implode(',', array_keys($plugins)).'&_fields=meta'; - $response = wp_remote_get($endpoint, ['user-agent' => classicpress_user_agent(true)]); - - if (is_wp_error($response) || empty($response['response']) || wp_remote_retrieve_response_code($response) !== 200) { - return []; - } - - $data_from_dir = json_decode(wp_remote_retrieve_body($response), true); - $data = []; - - foreach ($data_from_dir as $single_data) { - $data[$single_data['meta']['slug']] = [ - 'Download' => $single_data['meta']['download_link'], - 'Version' => $single_data['meta']['current_version'], - 'RequiresPHP' => $single_data['meta']['requires_php'], - 'RequiresCP' => $single_data['meta']['requires_cp'], - 'active_installs' => $single_data['meta']['active_installations'], - ]; - } - - $this->cp_plugins_directory_data = $data; - set_transient('cpdi_directory_data_plugins', $this->cp_plugins_directory_data, 3 * HOUR_IN_SECONDS); - return $this->cp_plugins_directory_data; - - } - - // Filter to trigger updates using Update URI header - public function update_uri_filter($update, $plugin_data, $plugin_file, $locales) { - - // https://developer.wordpress.org/reference/hooks/update_plugins_hostname/ - - // Get the slug from Update URI - if (preg_match('/plugins\?byslug=(.*)/', $plugin_data['UpdateURI'], $matches) !== 1) { - return false; - } - - // Check if the slug matches plugin file - if (!isset($matches[1]) || dirname($plugin_file) !== $matches[1]) { - return false; - } - $slug = $matches[1]; - - // Check if we have that plugin in installed ones - $plugins = $this->get_cp_plugins(); - if (!array_key_exists($slug, $plugins)) { - return false; - } - - // Check if we have that plugin in directory ones - $dir_data = $this->get_directory_data(); - if (!array_key_exists($slug, $dir_data)) { - return false; - } - - $plugin = $plugins[$slug]; - $data = $dir_data[$slug]; - - if (version_compare($plugin['Version'], $data['Version']) >= 0) { - // No updates available - return false; - } - if (version_compare(classicpress_version(), $data['RequiresCP']) === -1) { - // Higher CP version required - return false; - } - if (version_compare(phpversion(), $data['RequiresPHP']) === -1) { - // Higher PHP version required - return false; - } - - $update = [ - 'slug' => $plugin_file, - 'version' => $data['Version'], - 'package' => $data['Download'], - 'requires_php' => $data['RequiresPHP'], - 'requires_cp' => $data['RequiresCP'], - 'banners' => $this->get_plugin_images('banner', $slug), - 'icons' => $this->get_plugin_images('icon', $slug), - - ]; - - return $update; - - } - -} diff --git a/classes/ThemeInstall.class.php b/classes/ThemeInstall.class.php deleted file mode 100644 index b1a93d1..0000000 --- a/classes/ThemeInstall.class.php +++ /dev/null @@ -1,482 +0,0 @@ -page) { - return; - } - wp_enqueue_style( 'classicpress-directory-integration-css', plugins_url( '../styles/directory-integration.css', __FILE__ ), [] ); - } - - public function scripts($hook) - { - if ($hook !== $this->page) { - return; - } - wp_enqueue_script( 'classicpress-directory-integration-js', plugins_url( '../scripts/directory-integration.js', __FILE__ ), array( 'wp-i18n' ), false, true ); - wp_set_script_translations( 'classicpress-directory-integration-js', 'classicpress-directory-integration', plugin_dir_path( 'classicpress-directory-integration' ) . 'languages' ); - } - - public function create_menu() - { - if (!current_user_can('install_plugins')) { - return; - } - - $this->page = add_submenu_page( - 'themes.php', - esc_html__('ClassicPress Themes', 'classicpress-directory-integration'), - esc_html__('Install CP Themes', 'classicpress-directory-integration'), - 'install_themes', - 'classicpress-directory-integration-theme-install', - [$this, 'render_menu'], - 2 - ); - - add_action('load-' . $this->page, [$this, 'activate_action']); - add_action('load-' . $this->page, [$this, 'install_action']); - } - - public function rename_menu() { - global $submenu; - foreach ( $submenu['themes.php'] as $key => $value ) { - if($value[2] !== 'theme-install.php') { - continue; - } - $submenu['themes.php'][$key][0] = esc_html__('Install WP Themes', 'classicpress-directory-integration'); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - } - } - - // Get all installed ClassicPress Themes - // This function is different from the one in ThemeUpdate class - // and considers a theme from the dir not only if it has UpdateURI - // but also if it have RequiresCP. - private function get_local_cp_themes() - { - - if ($this->local_cp_themes !== false) { - return $this->local_cp_themes; - } - - $all_themes = wp_get_themes(); - $cp_themes = []; - foreach($all_themes as $slug => $inner){ - $cp_themes[($slug)] = [ - 'WPSlug' => $slug, - 'Name' => $inner->get( 'Name' ), - 'Version' => $inner->get( 'Version' ), - 'ThemeURI' => $inner->get( 'ThemeURI' ), - 'Active' => get_template(), - ]; - } - $this->local_cp_themes = $cp_themes; - return $this->local_cp_themes; - - } - - // Validate and sanitize args for quering the ClassicPress Directory - public static function sanitize_args($args) - { - foreach ($args as $key => $value) { - $sanitized = false; - switch ($key) { - case 'per_page': - case 'page': - $args[$key] = (int) $value; - $sanitized = true; - break; - case 'byslug': - $args[$key] = preg_replace('[^A-Za-z0-9\-_]', '', $value); - $sanitized = true; - break; - case 'search': - $args[$key] = sanitize_text_field($value); - $sanitized = true; - break; - case '_fields': - $args[$key] = preg_replace('[^A-Za-z0-9\-_,]', '', $value); - $sanitized = true; - break; - } - if ($sanitized) { - continue; - } - unset($args[$key]); - } - return $args; - } - - // Query the ClassicPress Directory - public static function do_directory_request($args = [], $type = 'themes') - { - $result['success'] = false; - - if (!in_array($type, ['themes', 'themes'])) { - $result['error'] = $type . ' is not a supported type'; - return $result; - } - - $args = self::sanitize_args($args); - $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL . $type; - $endpoint = add_query_arg($args, $endpoint); - - $response = wp_remote_get($endpoint, ['user-agent' => classicpress_user_agent()]); - - if (is_wp_error($response)) { - $result['error'] = rtrim(implode(',', $response->get_error_messages()), '.'); - return $result; - } - - $e = wp_remote_retrieve_response_code($response); - if ($e !== 200) { - $result['error'] = $response['response']['message'].'.'; - $result['code'] = $response['response']['code']; - if (!isset($response['body']) || !json_validate($response['body'])) { - return $result; - } - $api_message = json_decode($response['body'], true); - if(!isset($api_message['message'])) { - return $result; - } - $result['error'] .= ' '.$api_message['message']; - return $result; - } - - if (!isset($response['headers'])) { - $result['error'] = 'No headers found'; - return $result; - } - - $headers = $response['headers']->getAll(); - if (!isset($headers['x-wp-total']) || !isset($headers['x-wp-totalpages'])) { - $result['error'] = 'No pagination headers found'; - return $result; - } - - $data_from_dir = json_decode(wp_remote_retrieve_body($response), true); - if ($data_from_dir === null) { - $result['error'] = 'Failed decoding response'; - return $result; - } - - $result['success'] = true; - $result['total-pages'] = $headers['x-wp-totalpages']; - $result['total-items'] = $headers['x-wp-total']; - $result['response'] = $data_from_dir; - - return $result; - } - - // Enqueue a notice - private function add_notice($message, $failure = false) - { - $other_notices = get_transient('cpdi_ti_notices'); - $notice = $other_notices === false ? '' : $other_notices; - $failure_style = $failure ? 'notice-error' : 'notice-success'; - $notice .= '
'; - $notice .= '

' . esc_html($message) . '

'; - $notice .= '
'; - set_transient('cpdi_ti_notices', $notice, \HOUR_IN_SECONDS); - } - - // Display notices - private function display_notices() - { - $notices = get_transient('cpdi_ti_notices'); - if ($notices === false) { - return; - } - // This contains html formatted from 'add_notice' function that uses 'esc_html'. - echo $notices; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - delete_transient('cpdi_ti_notices'); - } - - // Deal with activation requests - public function activate_action() - { - - // Load local themes information - $local_cp_themes = $this->get_local_cp_themes(); - - // Security checks - if (!isset($_GET['action'])) { - return; - } - if ($_GET['action'] !== 'activate') { - return; - } - if (!check_admin_referer('activate', '_cpdi')) { - return; - } - if (!current_user_can('install_themes')) { - return; - } - if (!isset($_REQUEST['slug'])) { - return; - } - - // Check if theme slug is proper - $slug = sanitize_key(wp_unslash($_REQUEST['slug'])); - - if (!array_key_exists($slug, $local_cp_themes)) { - return; - } - - // Activate Theme - $result = switch_theme($local_cp_themes[$slug]['WPSlug']); - - if ($result !== null) { - // Translators: %1$s is the theme name. - $message = sprintf(esc_html__('Error activating %1$s.', 'classicpress-directory-integration'), $local_cp_themes[$slug]['Name']); - $this->add_notice($message, true); - } else { - // Translators: %1$s is the theme name. - $message = sprintf(esc_html__('%1$s activated.', 'classicpress-directory-integration'), $local_cp_themes[$slug]['Name']); - $this->add_notice($message, false); - } - - $sendback = remove_query_arg(['action', 'slug', '_cpdi'], wp_get_referer()); - wp_safe_redirect($sendback); - exit; - } - - // Deal with installation requests - public function install_action() - { - - // Security checks - if (!isset($_GET['action'])) { - return; - } - if ($_GET['action'] !== 'install') { - return; - } - if (!check_admin_referer('install', '_cpdi')) { - return; - } - if (!current_user_can('install_themes')) { - return; - } - if (!isset($_REQUEST['slug'])) { - return; - } - // Check if theme slug is proper - $slug = sanitize_key(wp_unslash($_REQUEST['slug'])); - - // Get github release file - $args = [ - 'byslug' => $slug, - '_fields' => 'meta,title', - ]; - $response = $this->do_directory_request($args, 'themes'); - - if (!$response['success'] || !isset($response['response'][0]['meta']['download_link'])) { - // Translators: %1$s is the theme name. - $message = sprintf(esc_html__('API error for theme %1$s.', 'classicpress-directory-integration'), $local_cp_themes[$slug]['Name']); - $this->add_notice($message, true); - $sendback = remove_query_arg(['action', 'slug', '_cpdi'], wp_get_referer()); - wp_safe_redirect($sendback); - exit; - } - - $installation_url = $response['response'][0]['meta']['download_link']; - $theme_name = $response['response'][0]['title']['rendered']; - - // Install Theme - $skin = new ThemeInstallSkin(['type' => 'theme']); - $upgrader = new \Theme_Upgrader($skin); - $response = $upgrader->install($installation_url); - - if ($response !== true) { - // Translators: %1$s is the theme name. - $message = sprintf(esc_html__('Error installing %1$s.', 'classicpress-directory-integration'), $theme_name); - $this->add_notice($message, true); - } else { - // Translators: %1$s is the theme name. - $message = sprintf(esc_html__('%1$s installed.', 'classicpress-directory-integration'), $theme_name); - $this->add_notice($message, false); - } - - $sendback = remove_query_arg(['action', 'slug', '_cpdi'], wp_get_referer()); - wp_safe_redirect($sendback); - exit; - } - - // Render "CP Themes" menu - public function render_menu() - { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh - // Load local themes information - $local_cp_themes = $this->get_local_cp_themes(); - - // Set age number if empty - // We check nonces only on activations and installations. - // In this function nothing is modified. - $page = isset($_REQUEST['getpage']) ? (int) $_REQUEST['getpage'] : 1; //phpcs:ignore WordPress.Security.NonceVerification.Recommended - - // Query the directory - $args = [ - 'per_page' => 12, - 'page' => $page, - ]; - - if (isset($_REQUEST['searchfor'])) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended - $args['search'] = sanitize_text_field(wp_unslash($_REQUEST['searchfor'])); //phpcs:ignore WordPress.Security.NonceVerification.Recommended - } - // Reset paginations for new searches - $searching = $args['search'] ?? ''; - if (isset($_REQUEST['searchingfor']) && $_REQUEST['searchingfor'] !== $searching) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended - $args['page'] = 1; - } - - $result = $this->do_directory_request($args); - if ($result['success'] === false) { - // Query failed, display errors and exit. - $this->add_notice(esc_html($result['error']).' ('.esc_html($result['code']).').', true); - } - - // Set up variables - $themes = $result['response'] ?? []; - $pages = $result['total-pages'] ?? 0; - - if ($themes === []) { - $this->add_notice(esc_html__('No themes found.', 'classicpress-directory-integration'), true); - } - - // Display notices - $this->display_notices(); -?> - -
-

-

- - -
-
- -
-
-
- -
-
- ' : ''; - $vetted_article = $theme['meta']['cpcs_status'] === 'passing' ? ' vetted-theme' : ''; - $markdown_contents = self::get_markdown_contents( $content, '
', '
' ); - foreach ( $markdown_contents as $markdown_content ) { - $content = str_replace( '
' . $markdown_content . '
', $markdown_content, $content ); - } - ?> -
-
-

-
%1$s.', 'classicpress-directory-integration'), $theme['meta']['developer_name']), ['b' => []]); ?>
-
-
-
-
-
-
-
- - 'install', 'slug' => $slug]), 'install', '_cpdi')) . '" class="button install-now">' . esc_html__('Install', 'classicpress-directory-integration') . ''; - } - if ( array_key_exists($slug, $local_cp_themes) && ($local_cp_themes[$slug]['Active'] == $slug ) ) { - echo '' . esc_html__('Active', 'classicpress-directory-integration') . ''; - } - if ( array_key_exists($slug, $local_cp_themes) && ($local_cp_themes[$slug]['Active'] != $slug ) ) { - echo '' . esc_html__('Activate', 'classicpress-directory-integration') . ''; - } - ?> -
-
-
- -
- - - - -
-
- -cp_themes !== false) { - return $this->cp_themes; - } - - $all_themes = wp_get_themes(); - $cp_themes = []; - foreach ($all_themes as $slug => $theme) { - if ($theme->display('UpdateURI') === '') { - continue; - } - if (strpos($theme->display('UpdateURI'), \CLASSICPRESS_DIRECTORY_INTEGRATION_URL) !== 0) { - continue; - } - $cp_themes[$slug] = [ - 'WPSlug' => $slug, - 'Version' => $theme->display('UpdateURI'), - 'RequiresPHP' => $theme->display('RequiresPHP'), - 'RequiresCP' => $theme->display('RequiresCP'), - 'PluginURI' => $theme->display('PluginURI'), - ]; - } - - $this->cp_themes = $cp_themes; - return $this->cp_themes; - - } - - // Get data from the directory for all installed ClassicPress themes - private function get_directory_data($force = false) { - - // Try to get stored data - if (!$force && $this->cp_themes_directory_data !== false) { - // We have it in memory - return $this->cp_themes_directory_data; - } - $this->cp_themes_directory_data = get_transient('cpdi_directory_data_themes'); - if (!$force && $this->cp_themes_directory_data !== false) { - // We have it in transient - return $this->cp_themes_directory_data; - } - - // Query the directory - $themes = $this->get_cp_themes(); - $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL.'themes?byslug='.implode(',', array_keys($themes)).'&_fields=meta'; - $response = wp_remote_get($endpoint, ['user-agent' => classicpress_user_agent(true)]); - - if (is_wp_error($response) || empty($response['response']) || wp_remote_retrieve_response_code($response) !== 200) { - return []; - } - - $data_from_dir = json_decode(wp_remote_retrieve_body($response), true); - $data = []; - - foreach ($data_from_dir as $single_data) { - $data[$single_data['meta']['slug']] = [ - 'Download' => $single_data['meta']['download_link'], - 'Version' => $single_data['meta']['current_version'], - 'RequiresPHP' => $single_data['meta']['requires_php'], - 'RequiresCP' => $single_data['meta']['requires_cp'], - 'active_installs' => $single_data['meta']['active_installations'], - ]; - } - - $this->cp_themes_directory_data = $data; - set_transient('cpdi_directory_data_themes', $this->cp_themes_directory_data, 3 * HOUR_IN_SECONDS); - return $this->cp_themes_directory_data; - - } - - // Filter to trigger updates using Update URI header - public function update_uri_filter($update, $theme_data, $theme_stylesheet, $locales) { - - // https://developer.wordpress.org/reference/hooks/update_themes_hostname/ - - // Get the slug from Update URI - if (preg_match('/themes\?byslug=(.*)/', $theme_data['UpdateURI'], $matches) !== 1) { - return false; - } - - // Check if the slug matches theme dir - if (!isset($matches[1]) || $theme_stylesheet !== $matches[1]) { - return false; - } - $slug = $matches[1]; - - // Check if we have that theme in installed ones - $themes = $this->get_cp_themes(); - - if (!array_key_exists($slug, $themes)) { - return false; - } - - // Check if we have that theme in directory ones - $dir_data = $this->get_directory_data(); - if (!array_key_exists($slug, $dir_data)) { - return false; - } - - $theme = $themes[$slug]; - $data = $dir_data[$slug]; - - if (version_compare($theme['Version'], $data['Version']) >= 0) { - // No updates available - return false; - } - if (version_compare(classicpress_version(), $theme['RequiresCP']) === -1) { - // Higher CP version required - return false; - } - if (version_compare(phpversion(), $theme['RequiresPHP']) === -1) { - // Higher PHP version required - return false; - } - - $update = [ - 'slug' => $theme_stylesheet, - 'version' => $data['Version'], - 'package' => $data['Download'], - 'requires_php' => $data['RequiresPHP'], - 'requires_cp' => $data['RequiresCP'], - 'url' => 'https://'.wp_parse_url(\CLASSICPRESS_DIRECTORY_INTEGRATION_URL, PHP_URL_HOST).'/themes/'.$theme_stylesheet, - ]; - - return $update; - - } - -} diff --git a/classes/WPCLI.class.php b/classes/WPCLI.class.php deleted file mode 100644 index 6288a65..0000000 --- a/classes/WPCLI.class.php +++ /dev/null @@ -1,58 +0,0 @@ -page ) { + return; + } + wp_enqueue_style( 'classicpress-directory-integration-css', plugins_url( '../styles/directory-integration.css', __FILE__ ), array() ); + } + + public function scripts( $hook ) { + if ( $hook !== $this->page ) { + return; + } + wp_enqueue_script( 'classicpress-directory-integration-js', plugins_url( '../scripts/directory-integration.js', __FILE__ ), array( 'wp-i18n' ), false, true ); + wp_set_script_translations( 'classicpress-directory-integration-js', 'classicpress-directory-integration', plugin_dir_path( 'classicpress-directory-integration' ) . 'languages' ); + } + + public function create_menu() { + if ( ! current_user_can( 'install_plugins' ) ) { + return; + } + + $this->page = add_submenu_page( + 'plugins.php', + esc_html__( 'Install ClassicPress Plugins', 'classicpress-directory-integration' ), + esc_html__( 'Install CP Plugins', 'classicpress-directory-integration' ), + 'install_plugins', + 'classicpress-directory-integration-plugin-install', + array( $this, 'render_menu' ), + 2 + ); + + add_action( 'load-' . $this->page, array( $this, 'activate_action' ) ); + add_action( 'load-' . $this->page, array( $this, 'install_action' ) ); + } + + public function rename_menu() { + global $submenu; + foreach ( $submenu['plugins.php'] as $key => $value ) { + if ( $value[2] !== 'plugin-install.php' ) { + continue; + } + $submenu['plugins.php'][ $key ][0] = esc_html__( 'Install WP Plugins', 'classicpress-directory-integration' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + } + } + + // Get all installed ClassicPress plugin + // This function is different from the one in PluginUpdate class + // and considers a plugin from the dir not only if it has UpdateURI + // but also if it has RequiresCP. + private function get_local_cp_plugins() { + + if ( $this->local_cp_plugins !== false ) { + return $this->local_cp_plugins; + } + + $all_plugins = get_plugins(); + $cp_plugins = array(); + foreach ( $all_plugins as $slug => $plugin ) { + if ( ! array_key_exists( 'UpdateURI', $plugin ) && ! array_key_exists( 'RequiresCP', $plugin ) ) { + continue; + } + if ( strpos( $plugin['UpdateURI'], \CLASSICPRESS_DIRECTORY_INTEGRATION_URL ) !== 0 && ! array_key_exists( 'RequiresCP', $plugin ) ) { + continue; + } + $cp_plugins[ dirname( $slug ) ] = array( + 'WPSlug' => $slug, + 'Name' => $plugin['Name'], + 'Version' => $plugin['Version'], + 'PluginURI' => array_key_exists( 'PluginURI', $plugin ) ? $plugin['PluginURI'] : null, + 'Active' => is_plugin_active( $slug ), + ); + } + + $this->local_cp_plugins = $cp_plugins; + return $this->local_cp_plugins; + } + + // Validate and sanitize args for quering the ClassicPress Directory + public static function sanitize_args( $args ) { + foreach ( $args as $key => $value ) { + $sanitized = false; + switch ( $key ) { + case 'per_page': + case 'page': + $args[ $key ] = (int) $value; + $sanitized = true; + break; + case 'byslug': + $args[ $key ] = preg_replace( '[^A-Za-z0-9\-_]', '', $value ); + $sanitized = true; + break; + case 'search': + $args[ $key ] = sanitize_text_field( $value ); + $sanitized = true; + break; + case '_fields': + $args[ $key ] = preg_replace( '[^A-Za-z0-9\-_,]', '', $value ); + $sanitized = true; + break; + } + if ( $sanitized ) { + continue; + } + unset( $args[ $key ] ); + } + return $args; + } + + // Query the ClassicPress Directory + public static function do_directory_request( $args = array(), $type = 'plugins' ) { + $result['success'] = false; + + if ( ! in_array( $type, array( 'plugins', 'themes' ) ) ) { + $result['error'] = $type . ' is not a supported type'; + return $result; + } + + $args = self::sanitize_args( $args ); + $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL . $type; + $endpoint = add_query_arg( $args, $endpoint ); + + $response = wp_remote_get( $endpoint, array( 'user-agent' => classicpress_user_agent() ) ); + + if ( is_wp_error( $response ) ) { + $result['error'] = rtrim( implode( ',', $response->get_error_messages() ), '.' ); + return $result; + } + + $e = wp_remote_retrieve_response_code( $response ); + if ( $e !== 200 ) { + $result['error'] = $response['response']['message'] . '.'; + $result['code'] = $response['response']['code']; + if ( ! isset( $response['body'] ) || ! json_validate( $response['body'] ) ) { + return $result; + } + $api_message = json_decode( $response['body'], true ); + if ( ! isset( $api_message['message'] ) ) { + return $result; + } + $result['error'] .= ' ' . $api_message['message']; + return $result; + } + + if ( ! isset( $response['headers'] ) ) { + $result['error'] = 'No headers found'; + return $result; + } + + $headers = $response['headers']->getAll(); + if ( ! isset( $headers['x-wp-total'] ) || ! isset( $headers['x-wp-totalpages'] ) ) { + $result['error'] = 'No pagination headers found'; + return $result; + } + + $data_from_dir = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( $data_from_dir === null ) { + $result['error'] = 'Failed decoding response'; + return $result; + } + + $result['success'] = true; + $result['total-pages'] = $headers['x-wp-totalpages']; + $result['total-items'] = $headers['x-wp-total']; + $result['response'] = $data_from_dir; + + return $result; + } + + // Enqueue a notice + private function add_notice( $message, $failure = false ) { + $other_notices = get_transient( 'cpdi_pi_notices' ); + $notice = $other_notices === false ? '' : $other_notices; + $failure_style = $failure ? 'notice-error' : 'notice-success'; + $notice .= '
'; + $notice .= '

' . esc_html( $message ) . '

'; + $notice .= '
'; + set_transient( 'cpdi_pi_notices', $notice, \HOUR_IN_SECONDS ); + } + + // Display notices + private function display_notices() { + $notices = get_transient( 'cpdi_pi_notices' ); + if ( $notices === false ) { + return; + } + // This contains html formatted from 'add_notice' function that uses 'esc_html'. + echo $notices; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + delete_transient( 'cpdi_pi_notices' ); + } + + // Deal with activation requests + public function activate_action() { + + // Load local plugins information + $local_cp_plugins = $this->get_local_cp_plugins(); + + // Security checks + if ( ! isset( $_GET['action'] ) ) { + return; + } + if ( $_GET['action'] !== 'activate' ) { + return; + } + if ( ! check_admin_referer( 'activate', '_cpdi' ) ) { + return; + } + if ( ! current_user_can( 'activate_plugins' ) ) { + return; + } + if ( ! isset( $_REQUEST['slug'] ) ) { + return; + } + // Check if plugin slug is proper + $slug = sanitize_key( wp_unslash( $_REQUEST['slug'] ) ); + if ( ! array_key_exists( $slug, $local_cp_plugins ) ) { + return; + } + + // Activate plugin + $result = activate_plugin( $local_cp_plugins[ $slug ]['WPSlug'] ); + + if ( $result !== null ) { + // Translators: %1$s is the plugin name. + $message = sprintf( esc_html__( 'Error activating %1$s.', 'classicpress-directory-integration' ), $local_cp_plugins[ $slug ]['Name'] ); + $this->add_notice( $message, true ); + } else { + // Translators: %1$s is the plugin name. + $message = sprintf( esc_html__( '%1$s activated.', 'classicpress-directory-integration' ), $local_cp_plugins[ $slug ]['Name'] ); + $this->add_notice( $message, false ); + } + + $sendback = remove_query_arg( array( 'action', 'slug', '_cpdi' ), wp_get_referer() ); + wp_safe_redirect( $sendback ); + exit; + } + + // Deal with installation requests + public function install_action() { + + // Security checks + if ( ! isset( $_GET['action'] ) ) { + return; + } + if ( $_GET['action'] !== 'install' ) { + return; + } + if ( ! check_admin_referer( 'install', '_cpdi' ) ) { + return; + } + if ( ! current_user_can( 'install_plugins' ) ) { + return; + } + if ( ! isset( $_REQUEST['slug'] ) ) { + return; + } + // Check if plugin slug is proper + $slug = sanitize_key( wp_unslash( $_REQUEST['slug'] ) ); + + // Get github release file + $args = array( + 'byslug' => $slug, + '_fields' => 'meta,title', + ); + $response = $this->do_directory_request( $args, 'plugins' ); + if ( ! $response['success'] || ! isset( $response['response'][0]['meta']['download_link'] ) ) { + // Translators: %1$s is the plugin name. + $message = sprintf( esc_html__( 'API error.', 'classicpress-directory-integration' ), $local_cp_plugins[ $slug ]['Name'] ); + $this->add_notice( $message, true ); + $sendback = remove_query_arg( array( 'action', 'slug', '_cpdi' ), wp_get_referer() ); + wp_safe_redirect( $sendback ); + exit; + } + + $installation_url = $response['response'][0]['meta']['download_link']; + $plugin_name = $response['response'][0]['title']['rendered']; + + // Install plugin + $skin = new PluginInstallSkin( array( 'type' => 'plugin' ) ); + $upgrader = new \Plugin_Upgrader( $skin ); + $response = $upgrader->install( $installation_url ); + + if ( $response !== true ) { + // Translators: %1$s is the plugin name. + $message = sprintf( esc_html__( 'Error installing %1$s.', 'classicpress-directory-integration' ), $plugin_name ); + $this->add_notice( $message, true ); + } else { + // Translators: %1$s is the plugin name. + $message = sprintf( esc_html__( '%1$s installed.', 'classicpress-directory-integration' ), $plugin_name ); + $this->add_notice( $message, false ); + } + + $sendback = remove_query_arg( array( 'action', 'slug', '_cpdi' ), wp_get_referer() ); + wp_safe_redirect( $sendback ); + exit; + } + + // Render "Install CP plugins" menu + public function render_menu() { + // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh + // Load local plugins information + $local_cp_plugins = $this->get_local_cp_plugins(); + + // Set age number if empty + // We check nonces only on activations and installations. + // In this function nothing is modified. + $page = isset( $_REQUEST['getpage'] ) ? (int) $_REQUEST['getpage'] : 1; //phpcs:ignore WordPress.Security.NonceVerification.Recommended + + // Query the directory + $args = array( + 'per_page' => 12, + 'page' => $page, + ); + + if ( isset( $_REQUEST['searchfor'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['searchfor'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + // Reset paginations for new searches + $searching = $args['search'] ?? ''; + if ( isset( $_REQUEST['searchingfor'] ) && $_REQUEST['searchingfor'] !== $searching ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + $args['page'] = 1; + } + + $result = $this->do_directory_request( $args ); + if ( $result['success'] === false ) { + // Query failed, display errors and exit. + $this->add_notice( esc_html( $result['error'] ) . ' (' . esc_html( $result['code'] ) . ').', true ); + } + + // Set up variables + $plugins = $result['response'] ?? array(); + $pages = $result['total-pages'] ?? 0; + + if ( $plugins === array() ) { + $this->add_notice( esc_html__( 'No plugins found.', 'classicpress-directory-integration' ), true ); + } + + // Display notices + $this->display_notices(); + ?> + +
+

+

+ + +
+
+ +
+
+
+ +
+
+ ' : ''; + $vetted_article = $plugin['meta']['cpcs_status'] === 'passing' ? ' vetted-plugin' : ''; + $markdown_contents = self::get_markdown_contents( $content, '
', '
' ); + foreach ( $markdown_contents as $markdown_content ) { + $content = str_replace( '
' . $markdown_content . '
', $markdown_content, $content ); + } + ?> +
+
+

+ +

+
%1$s.', 'classicpress-directory-integration' ), $plugin['meta']['developer_name'] ), array( 'b' => array() ) ); ?>
+
+
+
+
+
+
+
+ + 'install', + 'slug' => $slug, + ) + ), + 'install', + '_cpdi' + ) + ) . '" class="button install-now">' . esc_html__( 'Install', 'classicpress-directory-integration' ) . ''; + } + if ( array_key_exists( $slug, $local_cp_plugins ) && $local_cp_plugins[ $slug ]['Active'] ) { + echo '' . esc_html__( 'Active', 'classicpress-directory-integration' ) . ''; + } + if ( array_key_exists( $slug, $local_cp_plugins ) && ! $local_cp_plugins[ $slug ]['Active'] ) { + echo '' . esc_html__( 'Activate', 'classicpress-directory-integration' ) . ''; + } + ?> +
+
+
+ +
+ + + + +
+
+ + get_directory_data( true ); + $slug = dirname( $args->slug ); + if ( ! array_key_exists( $slug, $dir_data ) ) { + return $result; + } + + // Query the directory + $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL . 'plugins?byslug=' . $slug; + $response = wp_remote_get( $endpoint, array( 'user-agent' => classicpress_user_agent( true ) ) ); + + if ( is_wp_error( $response ) || empty( $response['response'] ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { + return false; + } + + $data_from_dir = json_decode( wp_remote_retrieve_body( $response ), true ); + $data = $data_from_dir[0]; + + $result = array( + 'active_installs' => (int) $data['meta']['active_installations'], + 'author' => $data['meta']['developer_name'], + 'banners' => $this->get_plugin_images( 'banner', $slug ), + 'description' => 'false', + 'icons' => $this->get_plugin_images( 'icon', $slug ), + 'name' => $data['title']['rendered'], + 'requires_php' => $data['meta']['requires_php'], + 'screenshots' => $this->get_plugin_images( 'screenshot', $slug ), + 'sections' => array( + 'description' => $data['content']['rendered'], + //'faq' => 'frequently asked questions', + //'installation' => 'installation', + //'screenshots' => 'screenshots', + //'reviews' => 'reviews', + //'other_notes' => 'other notes', + //'changelog' => 'changelog', + ), + 'short_description' => $data['excerpt']['rendered'], + 'slug' => null, // null so we don't point to WP.org + 'tags' => explode( ',', $data['meta']['category_names'] ), + 'version' => $data['meta']['current_version'], + //'added' => true, // date + //'author_block_count' => true, // int + //'author_block_rating' => true, // int + //'author_profile' => true, // url + //'compatibility' => false, // empty array? + //'contributors' => true, // array( array( [profile], [avatar], [display_name] ) + //'donate_link' => true, // url + //'download_link' => true, // url + //'downloaded' => false, // int + //'homepage' => true, // url + //'last_updated' => true, // datetime + //'num_ratings' => 14, // int how many ratings + //'rating' => 50, // int rating x 100 + //'ratings' =>[ // unuseful? + // 5 => 10, + // 4 => 4, + // 3 => 0, + // 2 => 0, + // 1 => 0, + //], // array( [5..0] ) + //'requires' => true, // version string + //'support_threads_resolved' => true, // int + //'support_threads' => true, // int + //'tested' => true, // version string + //'versions' => true, // array( [version] url ) + ); + + return (object) $result; + } + + private function get_plugin_images( $type, $plugin ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh + $images = array(); + + if ( empty( $plugin ) ) { + return $images; + } + + if ( ! in_array( $type, array( 'icon', 'banner', 'screenshot' ), true ) ) { + return $images; + } + + /** + * Filters the folder where we search for icons and banners. + * + * The filtered path is relative to the plugin's directory + * + * Example: + * add_filter( + * 'cpdi_images_folder_' . basename( __DIR__ ), + * function ( $source ) { + * return '/assets/images'; + * } + * ); + * + * + * @param string $source Original folder path. + */ + $folder = apply_filters( "cpdi_images_folder_{$plugin}", '/images' ); + $image_path = untrailingslashit( WP_PLUGIN_DIR ) . '/' . $plugin . $folder; + $image_url = untrailingslashit( WP_PLUGIN_URL ) . '/' . $plugin . $folder; + + $image_qualities = array( + 'icon' => array( 'default', '1x', '2x' ), + 'banner' => array( 'default', 'low', 'high' ), + ); + + $image_dimensions = array( + 'icon' => array( + 'default' => '128', + '1x' => '128', + '2x' => '256', + ), + 'banner' => array( + 'default' => '772x250', + 'low' => '772x250', + 'high' => '1544x500', + ), + ); + + if ( $type === 'icon' || $type === 'banner' ) { + if ( file_exists( $image_path . '/' . $type . '.svg' ) ) { + foreach ( $image_qualities[ $type ] as $key ) { + $images[ $key ] = $image_url . '/' . $type . '.svg'; + } + } else { + foreach ( array( 'jpg', 'png' ) as $ext ) { + $all_keys = $image_qualities[ $type ]; + $last_key = array_pop( $all_keys ); + $middle_key = array_pop( $all_keys ); + if ( file_exists( $image_path . '/' . $type . '-' . $image_dimensions[ $type ][ $middle_key ] . '.' . $ext ) ) { + foreach ( $image_qualities[ $type ] as $key ) { + $images[ $key ] = $image_url . '/' . $type . '-' . $image_dimensions[ $type ][ $middle_key ] . '.' . $ext; + } + } + if ( file_exists( $image_path . '/' . $type . '-' . $image_dimensions[ $type ][ $last_key ] . '.' . $ext ) ) { // phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed + $images[ $last_key ] = $image_url . '/' . $type . '-' . $image_dimensions[ $type ][ $last_key ] . '.' . $ext; + } + } + } + + return $images; + } + + if ( $type === 'screenshot' ) { + + if ( file_exists( $image_path ) ) { + + $dir_contents = scandir( $image_path ); + + foreach ( $dir_contents as $name ) { + if ( strpos( strtolower( $name ), 'screenshot' ) === 0 ) { // phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed + $start = strpos( $name, '-' ) + 1; + $for = strpos( $name, '.' ) - $start; + $screenshot_number = substr( $name, $start, $for ); + $images[ $screenshot_number ] = $image_url . '/' . $name; + } + } + + ksort( $images ); + + } + } + + return $images; + } + + public function after_plugin_row( $plugin_file, $plugin_data, $status ) { + + $slug = dirname( $plugin_file ); + $plugins = $this->get_cp_plugins(); + + if ( ! array_key_exists( $slug, $plugins ) ) { + return; + } + + $dir_data = $this->get_directory_data(); + + if ( ! array_key_exists( $slug, $dir_data ) ) { + return; + } + + $data = $dir_data[ $slug ]; + $plugin = $plugins[ $slug ]; + + if ( version_compare( $plugin['Version'], $data['Version'] ) >= 0 ) { + // No updates available + return false; + } + + $message = ''; + if ( version_compare( classicpress_version(), $data['RequiresCP'] ) === -1 ) { + // Higher CP version required + // Translators: %1$s is the plugin latest version. %2$s is the ClassicPress version required by the plugin. + $message .= sprintf( esc_html__( 'This plugin has not updated to version %1$s because it needs ClassicPress %2$s.', 'classicpress-directory-integration' ), esc_html( $data['Version'] ), esc_html( $data['RequiresCP'] ) ); + } + if ( version_compare( phpversion(), $data['RequiresPHP'] ) === -1 ) { + if ( $message !== '' ) { + $message .= ' '; + } + // Translators: %1$s is the plugin latest version. %2$s is the PHP version required by the plugin. + $message .= sprintf( esc_html__( 'This plugin has not updated to version %1$s because it needs PHP %2$s.', 'classicpress-directory-integration' ), esc_html( $data['Version'] ), esc_html( $data['RequiresPHP'] ) ); + } + + if ( $message === '' ) { + return; + } + + echo '

'; + echo esc_html( $message ) . '

'; + } + + // Force a refresh of local ClassicPress directory data + public function refresh_cp_directory_data() { + $this->get_directory_data( true ); + } + + // Get all installed ClassicPress plugin + private function get_cp_plugins() { + + if ( $this->cp_plugins !== false ) { + return $this->cp_plugins; + } + + $all_plugins = get_plugins(); + $cp_plugins = array(); + foreach ( $all_plugins as $slug => $plugin ) { + if ( ! array_key_exists( 'UpdateURI', $plugin ) ) { + continue; + } + if ( strpos( $plugin['UpdateURI'], \CLASSICPRESS_DIRECTORY_INTEGRATION_URL ) !== 0 ) { + continue; + } + $cp_plugins[ dirname( $slug ) ] = array( + 'WPSlug' => $slug, + 'Version' => $plugin['Version'], + 'RequiresPHP' => array_key_exists( 'RequiresPHP', $plugin ) ? $plugin['RequiresPHP'] : null, + 'RequiresCP' => array_key_exists( 'RequiresCP', $plugin ) ? $plugin['RequiresCP'] : null, + 'PluginURI' => array_key_exists( 'PluginURI', $plugin ) ? $plugin['PluginURI'] : null, + ); + } + + $this->cp_plugins = $cp_plugins; + return $this->cp_plugins; + } + + // Get data from the directory for all installed ClassicPress plugin + private function get_directory_data( $force = false ) { + + // Try to get stored data + if ( ! $force && $this->cp_plugins_directory_data !== false ) { + // We have it in memory + return $this->cp_plugins_directory_data; + } + $this->cp_plugins_directory_data = get_transient( 'cpdi_directory_data_plugins' ); + if ( ! $force && $this->cp_plugins_directory_data !== false ) { + // We have it in transient + return $this->cp_plugins_directory_data; + } + + // Query the directory + $plugins = $this->get_cp_plugins(); + $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL . 'plugins?byslug=' . implode( ',', array_keys( $plugins ) ) . '&_fields=meta'; + $response = wp_remote_get( $endpoint, array( 'user-agent' => classicpress_user_agent( true ) ) ); + + if ( is_wp_error( $response ) || empty( $response['response'] ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { + return array(); + } + + $data_from_dir = json_decode( wp_remote_retrieve_body( $response ), true ); + $data = array(); + + foreach ( $data_from_dir as $single_data ) { + $data[ $single_data['meta']['slug'] ] = array( + 'Download' => $single_data['meta']['download_link'], + 'Version' => $single_data['meta']['current_version'], + 'RequiresPHP' => $single_data['meta']['requires_php'], + 'RequiresCP' => $single_data['meta']['requires_cp'], + 'active_installs' => $single_data['meta']['active_installations'], + ); + } + + $this->cp_plugins_directory_data = $data; + set_transient( 'cpdi_directory_data_plugins', $this->cp_plugins_directory_data, 3 * HOUR_IN_SECONDS ); + return $this->cp_plugins_directory_data; + } + + // Filter to trigger updates using Update URI header + public function update_uri_filter( $update, $plugin_data, $plugin_file, $locales ) { + + // https://developer.wordpress.org/reference/hooks/update_plugins_hostname/ + + // Get the slug from Update URI + if ( preg_match( '/plugins\?byslug=(.*)/', $plugin_data['UpdateURI'], $matches ) !== 1 ) { + return false; + } + + // Check if the slug matches plugin file + if ( ! isset( $matches[1] ) || dirname( $plugin_file ) !== $matches[1] ) { + return false; + } + $slug = $matches[1]; + + // Check if we have that plugin in installed ones + $plugins = $this->get_cp_plugins(); + if ( ! array_key_exists( $slug, $plugins ) ) { + return false; + } + + // Check if we have that plugin in directory ones + $dir_data = $this->get_directory_data(); + if ( ! array_key_exists( $slug, $dir_data ) ) { + return false; + } + + $plugin = $plugins[ $slug ]; + $data = $dir_data[ $slug ]; + + if ( version_compare( $plugin['Version'], $data['Version'] ) >= 0 ) { + // No updates available + return false; + } + if ( version_compare( classicpress_version(), $data['RequiresCP'] ) === -1 ) { + // Higher CP version required + return false; + } + if ( version_compare( phpversion(), $data['RequiresPHP'] ) === -1 ) { + // Higher PHP version required + return false; + } + + $update = array( + 'slug' => $plugin_file, + 'version' => $data['Version'], + 'package' => $data['Download'], + 'requires_php' => $data['RequiresPHP'], + 'requires_cp' => $data['RequiresCP'], + 'banners' => $this->get_plugin_images( 'banner', $slug ), + 'icons' => $this->get_plugin_images( 'icon', $slug ), + + ); + + return $update; + } +} diff --git a/classes/class-theme-install.php b/classes/class-theme-install.php new file mode 100644 index 0000000..38b2b6d --- /dev/null +++ b/classes/class-theme-install.php @@ -0,0 +1,488 @@ +page ) { + return; + } + wp_enqueue_style( 'classicpress-directory-integration-css', plugins_url( '../styles/directory-integration.css', __FILE__ ), array() ); + } + + public function scripts( $hook ) { + if ( $hook !== $this->page ) { + return; + } + wp_enqueue_script( 'classicpress-directory-integration-js', plugins_url( '../scripts/directory-integration.js', __FILE__ ), array( 'wp-i18n' ), false, true ); + wp_set_script_translations( 'classicpress-directory-integration-js', 'classicpress-directory-integration', plugin_dir_path( 'classicpress-directory-integration' ) . 'languages' ); + } + + public function create_menu() { + if ( ! current_user_can( 'install_plugins' ) ) { + return; + } + + $this->page = add_submenu_page( + 'themes.php', + esc_html__( 'ClassicPress Themes', 'classicpress-directory-integration' ), + esc_html__( 'Install CP Themes', 'classicpress-directory-integration' ), + 'install_themes', + 'classicpress-directory-integration-theme-install', + array( $this, 'render_menu' ), + 2 + ); + + add_action( 'load-' . $this->page, array( $this, 'activate_action' ) ); + add_action( 'load-' . $this->page, array( $this, 'install_action' ) ); + } + + public function rename_menu() { + global $submenu; + foreach ( $submenu['themes.php'] as $key => $value ) { + if ( $value[2] !== 'theme-install.php' ) { + continue; + } + $submenu['themes.php'][ $key ][0] = esc_html__( 'Install WP Themes', 'classicpress-directory-integration' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + } + } + + // Get all installed ClassicPress Themes + // This function is different from the one in ThemeUpdate class + // and considers a theme from the dir not only if it has UpdateURI + // but also if it have RequiresCP. + private function get_local_cp_themes() { + + if ( $this->local_cp_themes !== false ) { + return $this->local_cp_themes; + } + + $all_themes = wp_get_themes(); + $cp_themes = array(); + foreach ( $all_themes as $slug => $inner ) { + $cp_themes[ ( $slug ) ] = array( + 'WPSlug' => $slug, + 'Name' => $inner->get( 'Name' ), + 'Version' => $inner->get( 'Version' ), + 'ThemeURI' => $inner->get( 'ThemeURI' ), + 'Active' => get_template(), + ); + } + $this->local_cp_themes = $cp_themes; + return $this->local_cp_themes; + } + + // Validate and sanitize args for quering the ClassicPress Directory + public static function sanitize_args( $args ) { + foreach ( $args as $key => $value ) { + $sanitized = false; + switch ( $key ) { + case 'per_page': + case 'page': + $args[ $key ] = (int) $value; + $sanitized = true; + break; + case 'byslug': + $args[ $key ] = preg_replace( '[^A-Za-z0-9\-_]', '', $value ); + $sanitized = true; + break; + case 'search': + $args[ $key ] = sanitize_text_field( $value ); + $sanitized = true; + break; + case '_fields': + $args[ $key ] = preg_replace( '[^A-Za-z0-9\-_,]', '', $value ); + $sanitized = true; + break; + } + if ( $sanitized ) { + continue; + } + unset( $args[ $key ] ); + } + return $args; + } + + // Query the ClassicPress Directory + public static function do_directory_request( $args = array(), $type = 'themes' ) { + $result['success'] = false; + + if ( ! in_array( $type, array( 'themes', 'themes' ) ) ) { + $result['error'] = $type . ' is not a supported type'; + return $result; + } + + $args = self::sanitize_args( $args ); + $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL . $type; + $endpoint = add_query_arg( $args, $endpoint ); + + $response = wp_remote_get( $endpoint, array( 'user-agent' => classicpress_user_agent() ) ); + + if ( is_wp_error( $response ) ) { + $result['error'] = rtrim( implode( ',', $response->get_error_messages() ), '.' ); + return $result; + } + + $e = wp_remote_retrieve_response_code( $response ); + if ( $e !== 200 ) { + $result['error'] = $response['response']['message'] . '.'; + $result['code'] = $response['response']['code']; + if ( ! isset( $response['body'] ) || ! json_validate( $response['body'] ) ) { + return $result; + } + $api_message = json_decode( $response['body'], true ); + if ( ! isset( $api_message['message'] ) ) { + return $result; + } + $result['error'] .= ' ' . $api_message['message']; + return $result; + } + + if ( ! isset( $response['headers'] ) ) { + $result['error'] = 'No headers found'; + return $result; + } + + $headers = $response['headers']->getAll(); + if ( ! isset( $headers['x-wp-total'] ) || ! isset( $headers['x-wp-totalpages'] ) ) { + $result['error'] = 'No pagination headers found'; + return $result; + } + + $data_from_dir = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( $data_from_dir === null ) { + $result['error'] = 'Failed decoding response'; + return $result; + } + + $result['success'] = true; + $result['total-pages'] = $headers['x-wp-totalpages']; + $result['total-items'] = $headers['x-wp-total']; + $result['response'] = $data_from_dir; + + return $result; + } + + // Enqueue a notice + private function add_notice( $message, $failure = false ) { + $other_notices = get_transient( 'cpdi_ti_notices' ); + $notice = $other_notices === false ? '' : $other_notices; + $failure_style = $failure ? 'notice-error' : 'notice-success'; + $notice .= '
'; + $notice .= '

' . esc_html( $message ) . '

'; + $notice .= '
'; + set_transient( 'cpdi_ti_notices', $notice, \HOUR_IN_SECONDS ); + } + + // Display notices + private function display_notices() { + $notices = get_transient( 'cpdi_ti_notices' ); + if ( $notices === false ) { + return; + } + // This contains html formatted from 'add_notice' function that uses 'esc_html'. + echo $notices; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + delete_transient( 'cpdi_ti_notices' ); + } + + // Deal with activation requests + public function activate_action() { + + // Load local themes information + $local_cp_themes = $this->get_local_cp_themes(); + + // Security checks + if ( ! isset( $_GET['action'] ) ) { + return; + } + if ( $_GET['action'] !== 'activate' ) { + return; + } + if ( ! check_admin_referer( 'activate', '_cpdi' ) ) { + return; + } + if ( ! current_user_can( 'install_themes' ) ) { + return; + } + if ( ! isset( $_REQUEST['slug'] ) ) { + return; + } + + // Check if theme slug is proper + $slug = sanitize_key( wp_unslash( $_REQUEST['slug'] ) ); + + if ( ! array_key_exists( $slug, $local_cp_themes ) ) { + return; + } + + // Activate Theme + $result = switch_theme( $local_cp_themes[ $slug ]['WPSlug'] ); + + if ( $result !== null ) { + // Translators: %1$s is the theme name. + $message = sprintf( esc_html__( 'Error activating %1$s.', 'classicpress-directory-integration' ), $local_cp_themes[ $slug ]['Name'] ); + $this->add_notice( $message, true ); + } else { + // Translators: %1$s is the theme name. + $message = sprintf( esc_html__( '%1$s activated.', 'classicpress-directory-integration' ), $local_cp_themes[ $slug ]['Name'] ); + $this->add_notice( $message, false ); + } + + $sendback = remove_query_arg( array( 'action', 'slug', '_cpdi' ), wp_get_referer() ); + wp_safe_redirect( $sendback ); + exit; + } + + // Deal with installation requests + public function install_action() { + + // Security checks + if ( ! isset( $_GET['action'] ) ) { + return; + } + if ( $_GET['action'] !== 'install' ) { + return; + } + if ( ! check_admin_referer( 'install', '_cpdi' ) ) { + return; + } + if ( ! current_user_can( 'install_themes' ) ) { + return; + } + if ( ! isset( $_REQUEST['slug'] ) ) { + return; + } + // Check if theme slug is proper + $slug = sanitize_key( wp_unslash( $_REQUEST['slug'] ) ); + + // Get github release file + $args = array( + 'byslug' => $slug, + '_fields' => 'meta,title', + ); + $response = $this->do_directory_request( $args, 'themes' ); + + if ( ! $response['success'] || ! isset( $response['response'][0]['meta']['download_link'] ) ) { + // Translators: %1$s is the theme name. + $message = sprintf( esc_html__( 'API error for theme %1$s.', 'classicpress-directory-integration' ), $local_cp_themes[ $slug ]['Name'] ); + $this->add_notice( $message, true ); + $sendback = remove_query_arg( array( 'action', 'slug', '_cpdi' ), wp_get_referer() ); + wp_safe_redirect( $sendback ); + exit; + } + + $installation_url = $response['response'][0]['meta']['download_link']; + $theme_name = $response['response'][0]['title']['rendered']; + + // Install Theme + $skin = new ThemeInstallSkin( array( 'type' => 'theme' ) ); + $upgrader = new \Theme_Upgrader( $skin ); + $response = $upgrader->install( $installation_url ); + + if ( $response !== true ) { + // Translators: %1$s is the theme name. + $message = sprintf( esc_html__( 'Error installing %1$s.', 'classicpress-directory-integration' ), $theme_name ); + $this->add_notice( $message, true ); + } else { + // Translators: %1$s is the theme name. + $message = sprintf( esc_html__( '%1$s installed.', 'classicpress-directory-integration' ), $theme_name ); + $this->add_notice( $message, false ); + } + + $sendback = remove_query_arg( array( 'action', 'slug', '_cpdi' ), wp_get_referer() ); + wp_safe_redirect( $sendback ); + exit; + } + + // Render "CP Themes" menu + public function render_menu() { + // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh + // Load local themes information + $local_cp_themes = $this->get_local_cp_themes(); + + // Set age number if empty + // We check nonces only on activations and installations. + // In this function nothing is modified. + $page = isset( $_REQUEST['getpage'] ) ? (int) $_REQUEST['getpage'] : 1; //phpcs:ignore WordPress.Security.NonceVerification.Recommended + + // Query the directory + $args = array( + 'per_page' => 12, + 'page' => $page, + ); + + if ( isset( $_REQUEST['searchfor'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['searchfor'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + // Reset paginations for new searches + $searching = $args['search'] ?? ''; + if ( isset( $_REQUEST['searchingfor'] ) && $_REQUEST['searchingfor'] !== $searching ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + $args['page'] = 1; + } + + $result = $this->do_directory_request( $args ); + if ( $result['success'] === false ) { + // Query failed, display errors and exit. + $this->add_notice( esc_html( $result['error'] ) . ' (' . esc_html( $result['code'] ) . ').', true ); + } + + // Set up variables + $themes = $result['response'] ?? array(); + $pages = $result['total-pages'] ?? 0; + + if ( $themes === array() ) { + $this->add_notice( esc_html__( 'No themes found.', 'classicpress-directory-integration' ), true ); + } + + // Display notices + $this->display_notices(); + ?> + +
+

+

+ + +
+
+ +
+
+
+ +
+
+ ' : ''; + $vetted_article = $theme['meta']['cpcs_status'] === 'passing' ? ' vetted-theme' : ''; + $markdown_contents = self::get_markdown_contents( $content, '
', '
' ); + foreach ( $markdown_contents as $markdown_content ) { + $content = str_replace( '
' . $markdown_content . '
', $markdown_content, $content ); + } + ?> +
+
+

+ +

+
%1$s.', 'classicpress-directory-integration' ), $theme['meta']['developer_name'] ), array( 'b' => array() ) ); ?>
+
+
+
+
+
+
+
+ + 'install', + 'slug' => $slug, + ) + ), + 'install', + '_cpdi' + ) + ) . '" class="button install-now">' . esc_html__( 'Install', 'classicpress-directory-integration' ) . ''; + } + if ( array_key_exists( $slug, $local_cp_themes ) && ( $local_cp_themes[ $slug ]['Active'] == $slug ) ) { + echo '' . esc_html__( 'Active', 'classicpress-directory-integration' ) . ''; + } + if ( array_key_exists( $slug, $local_cp_themes ) && ( $local_cp_themes[ $slug ]['Active'] != $slug ) ) { + echo '' . esc_html__( 'Activate', 'classicpress-directory-integration' ) . ''; + } + ?> +
+
+
+ +
+ + + + +
+
+ + cp_themes !== false ) { + return $this->cp_themes; + } + + $all_themes = wp_get_themes(); + $cp_themes = array(); + foreach ( $all_themes as $slug => $theme ) { + if ( $theme->display( 'UpdateURI' ) === '' ) { + continue; + } + if ( strpos( $theme->display( 'UpdateURI' ), \CLASSICPRESS_DIRECTORY_INTEGRATION_URL ) !== 0 ) { + continue; + } + $cp_themes[ $slug ] = array( + 'WPSlug' => $slug, + 'Version' => $theme->display( 'UpdateURI' ), + 'RequiresPHP' => $theme->display( 'RequiresPHP' ), + 'RequiresCP' => $theme->display( 'RequiresCP' ), + 'PluginURI' => $theme->display( 'PluginURI' ), + ); + } + + $this->cp_themes = $cp_themes; + return $this->cp_themes; + } + + // Get data from the directory for all installed ClassicPress themes + private function get_directory_data( $force = false ) { + + // Try to get stored data + if ( ! $force && $this->cp_themes_directory_data !== false ) { + // We have it in memory + return $this->cp_themes_directory_data; + } + $this->cp_themes_directory_data = get_transient( 'cpdi_directory_data_themes' ); + if ( ! $force && $this->cp_themes_directory_data !== false ) { + // We have it in transient + return $this->cp_themes_directory_data; + } + + // Query the directory + $themes = $this->get_cp_themes(); + $endpoint = \CLASSICPRESS_DIRECTORY_INTEGRATION_URL . 'themes?byslug=' . implode( ',', array_keys( $themes ) ) . '&_fields=meta'; + $response = wp_remote_get( $endpoint, array( 'user-agent' => classicpress_user_agent( true ) ) ); + + if ( is_wp_error( $response ) || empty( $response['response'] ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { + return array(); + } + + $data_from_dir = json_decode( wp_remote_retrieve_body( $response ), true ); + $data = array(); + + foreach ( $data_from_dir as $single_data ) { + $data[ $single_data['meta']['slug'] ] = array( + 'Download' => $single_data['meta']['download_link'], + 'Version' => $single_data['meta']['current_version'], + 'RequiresPHP' => $single_data['meta']['requires_php'], + 'RequiresCP' => $single_data['meta']['requires_cp'], + 'active_installs' => $single_data['meta']['active_installations'], + ); + } + + $this->cp_themes_directory_data = $data; + set_transient( 'cpdi_directory_data_themes', $this->cp_themes_directory_data, 3 * HOUR_IN_SECONDS ); + return $this->cp_themes_directory_data; + } + + // Filter to trigger updates using Update URI header + public function update_uri_filter( $update, $theme_data, $theme_stylesheet, $locales ) { + + // https://developer.wordpress.org/reference/hooks/update_themes_hostname/ + + // Get the slug from Update URI + if ( preg_match( '/themes\?byslug=(.*)/', $theme_data['UpdateURI'], $matches ) !== 1 ) { + return false; + } + + // Check if the slug matches theme dir + if ( ! isset( $matches[1] ) || $theme_stylesheet !== $matches[1] ) { + return false; + } + $slug = $matches[1]; + + // Check if we have that theme in installed ones + $themes = $this->get_cp_themes(); + + if ( ! array_key_exists( $slug, $themes ) ) { + return false; + } + + // Check if we have that theme in directory ones + $dir_data = $this->get_directory_data(); + if ( ! array_key_exists( $slug, $dir_data ) ) { + return false; + } + + $theme = $themes[ $slug ]; + $data = $dir_data[ $slug ]; + + if ( version_compare( $theme['Version'], $data['Version'] ) >= 0 ) { + // No updates available + return false; + } + if ( version_compare( classicpress_version(), $theme['RequiresCP'] ) === -1 ) { + // Higher CP version required + return false; + } + if ( version_compare( phpversion(), $theme['RequiresPHP'] ) === -1 ) { + // Higher PHP version required + return false; + } + + $update = array( + 'slug' => $theme_stylesheet, + 'version' => $data['Version'], + 'package' => $data['Download'], + 'requires_php' => $data['RequiresPHP'], + 'requires_cp' => $data['RequiresCP'], + 'url' => 'https://' . wp_parse_url( \CLASSICPRESS_DIRECTORY_INTEGRATION_URL, PHP_URL_HOST ) . '/themes/' . $theme_stylesheet, + ); + + return $update; + } +} diff --git a/classes/class-wpcli.php b/classes/class-wpcli.php new file mode 100644 index 0000000..a3daa35 --- /dev/null +++ b/classes/class-wpcli.php @@ -0,0 +1,58 @@ +

%2$s

', esc_attr($class), esc_html($message)); -} - -const DB_VERSION = 1; - -// Load non namespaced constants and functions -require_once 'includes/constants.php'; -require_once 'includes/functions.php'; - -// Load Helpers trait. -require_once 'classes/Helpers.trait.php'; - -// Load Plugin Update functionality class. -require_once 'classes/PluginUpdate.class.php'; -$plugin_update = new PluginUpdate(); - -// Load Plugin Install functionality class. -require_once 'classes/PluginInstall.class.php'; -$plugin_install = new PluginInstall(); - -// Load Theme Update functionality class. -require_once 'classes/ThemeUpdate.class.php'; -$theme_update = new ThemeUpdate(); - -// Load Theme Install functionality class. -require_once 'classes/ThemeInstall.class.php'; -$theme_install = new ThemeInstall(); - -// Register text domain -function register_text_domain() { - load_plugin_textdomain('classicpress-directory-integration', false, dirname(plugin_basename(__FILE__)).'/languages'); -} -add_action('plugins_loaded', '\ClassicPress\Directory\register_text_domain'); - -// Add commands to WP-CLI -require_once 'classes/WPCLI.class.php'; -if (defined('WP_CLI') && WP_CLI) { - \WP_CLI::add_command('cpdi', '\ClassicPress\Directory\CPDICLI'); -} +

%2$s

', esc_attr( $class ), esc_html( $message ) ); +} + +const DB_VERSION = 1; + +// Load non namespaced constants and functions +require_once 'includes/constants.php'; +require_once 'includes/functions.php'; + +// Load Helpers trait. +require_once 'classes/trait-helpers.php'; + +// Load Plugin Update functionality class. +require_once 'classes/class-plugin-update.php'; +$plugin_update = new PluginUpdate(); + +// Load Plugin Install functionality class. +require_once 'classes/class-plugin-install.php'; +$plugin_install = new PluginInstall(); + +// Load Theme Update functionality class. +require_once 'classes/class-theme-update.php'; +$theme_update = new ThemeUpdate(); + +// Load Theme Install functionality class. +require_once 'classes/class-theme-install.php'; +$theme_install = new ThemeInstall(); + +// Register text domain +function register_text_domain() { + load_plugin_textdomain( 'classicpress-directory-integration', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); +} +add_action( 'plugins_loaded', '\ClassicPress\Directory\register_text_domain' ); + +// Add commands to WP-CLI +require_once 'classes/class-wpcli.php'; +if ( defined( 'WP_CLI' ) && WP_CLI ) { + \WP_CLI::add_command( 'cpdi', '\ClassicPress\Directory\CPDICLI' ); +} diff --git a/images/index.php b/images/index.php index 6fd199e..b3d9bbc 100755 --- a/images/index.php +++ b/images/index.php @@ -1 +1 @@ - + + Apply WordPress Coding Standards to all Core files + + + + + + + + + + + + + + + + + + + + ./ + + + + + + + warning + + + warning + + + warning + + + warning + + + warning + + + warning + + + warning + + + warning + + + warning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + warning + + + warning + + + warning + + + + + + * + + + diff --git a/scripts/index.php b/scripts/index.php index 6fd199e..b3d9bbc 100755 --- a/scripts/index.php +++ b/scripts/index.php @@ -1 +1 @@ -