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.
+[](https://github.com/ClassicPress/classicpress-directory-integration/actions/workflows/cpcs.yml)[](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 );
- }
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-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 );
- }
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-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 );
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 );
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 @@
-