From 67eb9a97d7a9830f6ddc684bef0f1991c92305b2 Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Fri, 1 Jul 2022 16:56:07 +0700 Subject: [PATCH 01/14] 01-07-2022 ### 1.3.0 [Added] Plugin Settings Page (under Admin > Settings > Manage CP Repos) [Added] Setting to add custom GitHub Repositories of Orgs and/or Users. [Added] Setting to store Personal GitHub Token, which increases the API Limits to 5k hourly instead of 60. [Added] Verified Orgs (_not users_) are pre-selected. A PR can be used to add new Orgs to the vetted list. [Added] Fundations to read remote readme, README, (both in md or txt) files. Currently used ony for below [Fixed] item. [Fixed] Problem where plugins with foldername/distinct-filename.php AND a unguessable Plugin Title could not be managed. [Improved] Make drastically less calls to the GitHub API by re-using already queried data as much as possible. --- README.txt | 21 +- admin/class-cp-plgn-drctry-admin.php | 53 +++++ admin/class-cp-plgn-drctry-github.php | 214 ++++++++++++++++-- admin/class-cp-plgn-drctry-settings.php | 285 ++++++++++++++++++++++++ admin/css/cp-plgn-drctry-select2.css | 35 +++ admin/js/cp-plgn-drctry-select2.js | 46 ++++ admin/partials/github-orgs.txt | 6 + changelog.txt | 9 + includes/class-cp-plgn-drctry.php | 17 ++ readme.md | 30 ++- tukutoi-cp-directory-integration.php | 4 +- 11 files changed, 681 insertions(+), 39 deletions(-) create mode 100644 admin/class-cp-plgn-drctry-settings.php create mode 100644 admin/css/cp-plgn-drctry-select2.css create mode 100644 admin/js/cp-plgn-drctry-select2.js create mode 100644 admin/partials/github-orgs.txt diff --git a/README.txt b/README.txt index 64c34c4..f76e1c7 100644 --- a/README.txt +++ b/README.txt @@ -4,7 +4,7 @@ Donate link: https://paypal.me/tukutoi Tags: directory, plugins Requires at least: 1.0.0 Tested up to: 4.9.15 -Stable tag: 1.2.0 +Stable tag: 1.3.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -16,7 +16,7 @@ Install and activate like any other plugin. Navigate to Dashboard > Plugins > Manage CP Plugins and start managing ClassicPress Plugins. You can install, activate, deactivate, update, delete, and also search Plugins all from within the same screen. -The Directory results are cached locally for fast performance, and you can refresh the local cache on the click of a button. +The results are cached locally for fast performance, and you can refresh the local cache on the click of a button. It has a pagination and a total plugins display to navigate (15 plugins a time) through the assets. A "more info" will display all information known to ClassicPress about the plugin and developer. @@ -27,11 +27,18 @@ The plugin requires wp_remote_get and file_put_contents to work properly on the It is possible to manage plugins that are not listed in the ClassicPress Directory with this plugin as well. The conditions for this to work are: -- the GitHub stored Plugin MUST have a tag `classicpress-plugin` -- the GitHub Repository MUST have a valid Release tag named witha SemVer release version (like `1.0.0`) and Public Release with a manually uploaded Release Asset in Zip Format. This ZIP MUST be uploaded to the release section for `Attach binaries by dropping them here or selecting them.` -- currently only plugins stored by the TukuToi Organization are available - in the next release, a setting will be offered to end users in order to register any organziation or user. +- the GitHub stored Plugin MUST have a tag `classicpress-plugin`. +- the GitHub Repository MUST have a valid Release tag named with a SemVer release version (like `1.0.0`) and Public Release with a manually uploaded Release Asset in Zip Format. This ZIP MUST be uploaded to the release section for `Attach binaries by dropping them here or selecting them.` + +By default, there is a _vetted list_ of _Organizations_ added to the plugin. If a Developer wants to appear on said list, +they can submit a PR to the `github-orgs.txt` File of this Plugin, by adding their Guthub Organization data to the JSON array. +The Organization AND the PR initiator will be reviewed both by the author of this plugin as well the ClassicPress Plugin Review Team. +Only after careful assessment the Developer will be added to the Verified List of Organizations, and thus appear pre-selected in the Repositories queried by this plugin. + +Other, non verified Repositories (both users and orgs) can still be added easily by an end user in the dedicated Settings page (Dashboard > Settings > Manage CP Repos). == Disclaimers == -- The plugin does not take any responsibility for Plugins downloaded from the ClassicPress Directory or GitHub. +- The plugin does not take any responsibility for Plugins downloaded from the ClassicPress Directory or GitHub, not even if verified Organization's software. - The ClassicPress Plugin Repository is not always well maintained by the Developers who list their plugins. They forget often to bump the Version Number of their Plugins. This means, you *might* not see an update, even if there is one, or you might see an update to a certain version and get an update to a much higher version. -- If a GitHub stored plugin is not following above (MUST) clauses, it will not be possible for this plugin to find, pull or else manage such repos. \ No newline at end of file +- If a GitHub stored plugin is not following above (MUST) clauses, it will not be possible for this plugin to find, pull or else manage such repos. +- If you run into GitHub API Limits (it is not so generous) you should create a Personal Authentication Token as shown [here](https://docs.github.com/en/enterprise-server@3.4/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). You should only give this Token "read" rights, no post or edit rights. You should _never_ share this Token with anyone. You should then store this Token on the setting for it under Dashboard > Settings > Manage CP Repos. This will bump your GitHub API limits to 5000 per hours (which is far enough). \ No newline at end of file diff --git a/admin/class-cp-plgn-drctry-admin.php b/admin/class-cp-plgn-drctry-admin.php index 1760c6a..7e91d75 100644 --- a/admin/class-cp-plgn-drctry-admin.php +++ b/admin/class-cp-plgn-drctry-admin.php @@ -72,6 +72,7 @@ public function __construct( $plugin_name, $plugin_prefix, $version ) { $this->plugin_prefix = $plugin_prefix; $this->version = $version; $this->cp_dir = new Cp_Plgn_Drctry_Cp_Dir( $plugin_name, $plugin_prefix, $version ); + $this->cp_dir_options = new Cp_Plgn_Drctry_Settings( $plugin_name, $plugin_prefix, $version ); } @@ -85,6 +86,9 @@ public function enqueue_styles( $hook_suffix ) { if ( 'plugins_page_cp-plugins' === $hook_suffix ) { wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/cp-plgn-drctry-admin.css', array(), $this->version, 'all' ); + } elseif ( 'settings_page_cp_dir_opts' === $hook_suffix ) { + wp_enqueue_style( 'select2', 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css', array(), '4.1.0-rc.0', 'all' ); + wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/cp-plgn-drctry-select2.css', array( 'select2' ), $this->version, 'all' ); } } @@ -109,6 +113,9 @@ public function enqueue_scripts( $hook_suffix ) { 'nonce' => wp_create_nonce( 'updates' ), ) ); + } elseif ( 'settings_page_cp_dir_opts' === $hook_suffix ) { + wp_enqueue_script( 'select2', 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js', array( 'jquery' ), '4.1.0-rc.0', false ); + wp_enqueue_script( $this->plugin_name . '-select2', plugin_dir_url( __FILE__ ) . 'js/cp-plgn-drctry-select2.js', array( 'select2' ), $this->version, false ); } } @@ -274,6 +281,19 @@ public function add_plugins_list() { 3 ); + /** + * @since 1.3.0 + */ + add_submenu_page( + 'options-general.php', + esc_html__( 'ClassicPress Repositories', 'cp-plgn-drctry' ), + esc_html__( 'Manage CP Repos', 'cp-plgn-drctry' ), + 'manage_options', + 'cp_dir_opts', + array( $this, 'dir_settings_render' ), + 3 + ); + } /** @@ -291,4 +311,37 @@ public function render() { +
+

+
+ +
+
+ plugin_name = $plugin_name; $this->plugin_prefix = $plugin_prefix; $this->version = $version; - $this->git_org = 'tukutoi'; - $this->git_url = 'https://api.github.com/orgs/' . $this->git_org . '/repos'; - $this->tukutoi_plugin_names = array( - 'tukutoi-template-builder' => 'TukuToi Template Builder', - 'tukutoi-shortcodes' => 'TukuToi ShortCodes', - 'tukutoi-search-and-filter' => 'TukuToi Search and Filter', - 'tukutoi-cp-directory-integration' => 'CP Plugin Directory', - ); + $this->options = get_option( 'cp_dir_opts_options', array( 'cp_dir_opts_exteranal_org_repos' => $this->vetted_orgs() ) ); + } + + /** + * If available, get GitHub Auth Token. + * + * @since 1.3.0 + */ + private function set_auth() { + + $auth = array(); + if ( ! empty( $this->options ) && isset( $this->options['cp_dir_opts_section_github_token'] ) && ! empty( $this->options['cp_dir_opts_section_github_token'] ) ) { + $auth = array( + 'headers' => array( + 'Authorization' => 'token ' . esc_html( $this->options['cp_dir_opts_section_github_token'] ), + ), + ); + } + return $auth; } /** @@ -101,10 +112,62 @@ public function __construct( $plugin_name, $plugin_prefix, $version ) { */ public function get_git_plugins() { - return $this->build_git_plugins_object(); + $git_plugins = array(); + + if ( ! empty( $this->options ) ) { + if ( isset( $this->options['cp_dir_opts_exteranal_org_repos'] ) + && ! empty( $this->options['cp_dir_opts_exteranal_org_repos'] ) + ) { + foreach ( $this->options['cp_dir_opts_exteranal_org_repos'] as $org ) { + $org_url = 'https://api.github.com/orgs/' . $org . '/repos'; + $git_plugins = array_merge( $git_plugins, $this->build_git_plugins_object( $org_url ) ); + } + } + if ( isset( $this->options['cp_dir_opts_exteranal_user_repos'] ) + && ! empty( $this->options['cp_dir_opts_exteranal_user_repos'] ) + ) { + foreach ( $this->options['cp_dir_opts_exteranal_user_repos'] as $user ) { + $user_url = 'https://api.github.com/users/' . $user . '/repos'; + $git_plugins = array_merge( $git_plugins, $this->build_git_plugins_object( $user_url ) ); + } + } + } + + return $git_plugins; } + /** + * @since 1.3.0 + */ + private function vetted_orgs() { + $orgs = json_decode( $this->get_file_contents( '/partials/github-orgs.txt' ) ); + $_orgs = array(); + foreach ( $orgs as $org ) { + $_orgs[] = $org->slug; + } + + return $_orgs; + } + + /** + * CP Way of getting File Contents. + */ + private function get_file_contents( $file ) { + + global $wp_filesystem; + + if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) { + include_once ABSPATH . 'wp-admin/includes/file.php'; + $creds = request_filesystem_credentials( site_url() ); + wp_filesystem( $creds ); + } + + $contents = $wp_filesystem->get_contents( __DIR__ . $file ); + + return $contents; + + } /** * Get Plugins stored on Git. * @@ -112,15 +175,19 @@ public function get_git_plugins() { * * @return array $git_plugins A CP API Compatible array of plugin data. */ - private function build_git_plugins_object() { + private function build_git_plugins_object( $url ) { $git_plugins = array(); $data = array(); - $repos = wp_remote_get( $this->git_url ); + $repos = wp_remote_get( $url, $this->set_auth() ); + $_data = array( + 'developers' => array(), + ); + if ( wp_remote_retrieve_response_code( $repos ) === 200 ) { $repos = json_decode( wp_remote_retrieve_body( $repos ) ); } else { - echo '

' . esc_html__( 'We could not reach the GitHub Repositories API. It is possible you reached the limits of the GitHub Repositories API.', 'cp-plgn-drctry' ) . '

'; + echo '

' . esc_html__( 'We could not reach the GitHub Repositories API. It is possible you reached the limits of the GitHub Repositories API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ) . '

'; error_log( print_r( $repos, true ) ); return $git_plugins; } @@ -130,18 +197,21 @@ private function build_git_plugins_object() { if ( in_array( 'classicpress-plugin', $repo_object->topics ) ) { $release_data = $this->get_git_release_data( $repo_object->releases_url, $repo_object->name ); - $data['name'] = $this->tukutoi_plugin_names[ $repo_object->name ]; + $data['name'] = $this->get_readme_name( $repo_object->name, $repo_object->owner->login ); $data['description'] = $repo_object->description; $data['downloads'] = $release_data['count']; $data['changelog'] = $release_data['changelog']; - $data['developer'] = (object) array( - 'name' => 'TukuToi', - 'slug' => 'tukutoi', - 'web_url' => 'https://www.tukutoi.com', - 'username' => '', - 'website' => 'https://www.tukutoi.com', - 'published_at' => '', - ); + + /** + * Avoid hitting the API again if the Developer is already stored in a previous instance. + */ + if ( ! array_key_exists( $repo_object->owner->login, $_data['developers'] ) ) { + $data['developer'] = $this->get_git_dev_info( $repo_object->owner->login, $repo_object->owner->type ); + } else { + $data['developer'] = $_data['developers'][ $repo_object->owner->login ]; + } + $_data['developers'][ $repo_object->owner->login ] = $data['developer']; + $data['slug'] = $repo_object->name; $data['web_url'] = $repo_object->html_url; $data['minimum_wp_version'] = '4.9.15'; @@ -167,6 +237,104 @@ private function build_git_plugins_object() { } + /** + * Get Title (first line of .md or .txt, upper or lower case, after # or ==) + * + * @param string $item The repository slug/name. + * @param string $login The repo owner name. + * + * @since 1.3.0 + */ + private function get_readme_name( $item, $login ) { + + $title = esc_html__( 'No Title Found. ErrNo: CP-GH-249', 'cp-plgn-drctry' ); + + $first_line = strtok( $this->get_readme_data( $item, $login ), "\n" ); + + if ( str_contains( $this->variation, '.md' ) ) { + $title = sanitize_text_field( trim( str_replace( '#', '', $first_line ) ) ); + } elseif ( str_contains( $this->variation, '.txt' ) ) { + $title = sanitize_text_field( trim( str_replace( '==', '', $first_line ) ) ); + } + + return $title; + + } + + /** + * Get readme data (.md or .txt, upper or lower case) + * + * @param string $item The repository slug/name. + * @param string $login The repo owner name. + * + * @since 1.3.0 + */ + private function get_readme_data( $item, $login ) { + + $data = ''; + $readme_variations = array( 'README.md', 'readme.md', 'README.txt', 'readme.txt' ); + + foreach ( $readme_variations as $variation ) { + $readme = wp_remote_get( 'https://raw.githubusercontent.com/' . $login . '/' . $item . '/main/' . $variation ); + if ( wp_remote_retrieve_response_code( $readme ) !== 404 ) { + $this->variation = $variation; + break; + } + } + + if ( wp_remote_retrieve_response_code( $readme ) === 200 ) { + $data = wp_remote_retrieve_body( $readme ); + } else { + echo '

' . sprintf( esc_html__( 'We could not find a readme .md or .txt file for the Repository %$1. This can result in incomplete data. You should report this issue to the Developer (%$2)', 'cp-plgn-drctry' ), esc_html( $item ), esc_html( $login ) ) . '

'; + error_log( print_r( $readme, true ) ); + return $readme; + } + + return $data; + + } + + /** + * Get developer info from remote. + * + * @param string $login The Github "slug". + * @param string $type The Github domain type. + */ + private function get_git_dev_info( $login, $type ) { + + $dev_array = array( + 'name' => '', + 'slug' => '', + 'web_url' => '', + 'username' => '', + 'website' => '', + 'published_at' => '', + ); + + $_type = 'Organization' === $type ? 'orgs' : 'users'; + $dev = wp_remote_get( 'https://api.github.com/' . $_type . '/' . $login, $this->set_auth() ); + + if ( wp_remote_retrieve_response_code( $dev ) === 200 ) { + $dev = json_decode( wp_remote_retrieve_body( $dev ) ); + } else { + echo '

' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the GitHub %$1 "%$2". It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

'; + error_log( print_r( $dev, true ) ); + return $dev_array; + } + + $dev_array = array( + 'name' => $dev->name, + 'slug' => strtolower( $dev->login ), + 'web_url' => $dev->url, + 'username' => '', + 'website' => $dev->blog, + 'published_at' => $dev->created_at, + ); + + return (object) $dev_array; + + } + /** * Get Release Data from GitHub * @@ -184,12 +352,12 @@ private function get_git_release_data( $release_url, $repo_name ) { ); $url = str_replace( '{/id}', '/latest', $release_url ); - $release = wp_remote_get( $url ); + $release = wp_remote_get( $url, $this->set_auth() ); if ( wp_remote_retrieve_response_code( $release ) === 200 ) { $release = json_decode( wp_remote_retrieve_body( $release ) ); } else { // translators: %s: Name of remote GitHub Directory. - echo '

' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the repository "%s". It is possible you reached the limits of the GitHub Releases API.', 'cp-plgn-drctry' ), esc_html( $repo_name ) ) . '

'; + echo '

' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the repository "%s". It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $repo_name ) ) . '

'; error_log( print_r( $release, true ) ); return $release_data; } diff --git a/admin/class-cp-plgn-drctry-settings.php b/admin/class-cp-plgn-drctry-settings.php new file mode 100644 index 0000000..b4b12b5 --- /dev/null +++ b/admin/class-cp-plgn-drctry-settings.php @@ -0,0 +1,285 @@ + + */ +class Cp_Plgn_Drctry_Settings { + + /** + * The ID of this plugin. + * + * @since 1.3.0 + * @access private + * @var string $plugin_name The ID of this plugin. + */ + private $plugin_name; + + /** + * The unique prefix of this plugin. + * + * @since 1.3.0 + * @access private + * @var string $plugin_prefix The string used to uniquely prefix technical functions of this plugin. + */ + private $plugin_prefix; + + /** + * The version of this plugin. + * + * @since 1.3.0 + * @access private + * @var string $version The current version of this plugin. + */ + private $version; + + /** + * Initialize the class and set its properties. + * + * @since 1.3.0 + * @param string $plugin_name The name of this plugin. + * @param string $plugin_prefix The unique prefix of this plugin. + * @param string $version The version of this plugin. + */ + public function __construct( $plugin_name, $plugin_prefix, $version ) { + + $this->plugin_name = $plugin_name; + $this->plugin_prefix = $plugin_prefix; + $this->version = $version; + $this->vetted_orgs = $this->vetted_orgs(); + $this->verified_badge = 'classicpress-logo-feather-gradient-on-transparent'; + + } + + private function vetted_orgs() { + $orgs = json_decode( $this->get_file_contents( '/partials/github-orgs.txt' ) ); + $_orgs = array(); + foreach ( $orgs as $org ) { + $_orgs[] = $org->slug; + } + + return $_orgs; + } + + /** + * Custom Options and settings. + */ + public function settings_init() { + + // Register a new setting for "cp_dir_opts" page. + register_setting( 'cp_dir_opts', 'cp_dir_opts_options' ); + + // Register a new section in the "cp_dir_opts" page. + add_settings_section( + 'cp_dir_opts_section_external_repos', + __( 'External ClassicPress Repositories', 'cp-plgn-drctry' ), + array( $this, 'external_repos_cb' ), + 'cp_dir_opts' + ); + + // Register a new section in the "cp_dir_opts" page. + add_settings_section( + 'cp_dir_opts_section_github_token', + __( 'Your personal GitHub Token', 'cp-plgn-drctry' ), + array( $this, 'github_token_cb' ), + 'cp_dir_opts' + ); + + // Register a new field in the "cp_dir_opts_section_external_repos" section, inside the "cp_dir_opts" page. + add_settings_field( + 'cp_dir_opts_exteranal_org_repos', // As of WP 4.6 this value is used only internally. + // Use $args' label_for to populate the id inside the callback. + __( 'External Organization Repositories', 'cp-plgn-drctry' ), + array( $this, 'external_org_repos_select_cb' ), + 'cp_dir_opts', + 'cp_dir_opts_section_external_repos', + array( + 'label_for' => 'cp_dir_opts_exteranal_org_repos', + 'class' => 'cp_dir_opts_row', + 'cp_dir_opts_custom_data' => 'custom', + ) + ); + + add_settings_field( + 'cp_dir_opts_exteranal_user_repos', // As of WP 4.6 this value is used only internally. + // Use $args' label_for to populate the id inside the callback. + __( 'External Users Repositories', 'cp-plgn-drctry' ), + array( $this, 'external_user_repos_select_cb' ), + 'cp_dir_opts', + 'cp_dir_opts_section_external_repos', + array( + 'label_for' => 'cp_dir_opts_exteranal_user_repos', + 'class' => 'cp_dir_opts_row', + 'cp_dir_opts_custom_data' => 'custom', + ) + ); + + add_settings_field( + 'cp_dir_opts_section_github_token', // As of WP 4.6 this value is used only internally. + // Use $args' label_for to populate the id inside the callback. + __( 'Your personal Github Token', 'cp-plgn-drctry' ), + array( $this, 'github_token_input_cb' ), + 'cp_dir_opts', + 'cp_dir_opts_section_github_token', + array( + 'label_for' => 'cp_dir_opts_section_github_token', + 'class' => 'cp_dir_opts_row', + 'cp_dir_opts_custom_data' => 'custom', + ) + ); + } + + /** + * External repos section callback function. + * + * @param array $args The settings array, defining title, id, callback. + */ + public function external_repos_cb( $args ) { + ?> +

+ +

+ $this->vetted_orgs() ) ); + $_options = ''; + if ( false !== $options && ! empty( $options ) ) { + $orgs = $options[ $args['label_for'] ]; + foreach ( $orgs as $org ) { + $locked = in_array( $org, $this->vetted_orgs() ) ? 'locked="locked"' : ''; + $_options .= ''; + } + } + ?> + +

+ verified_badge );// phpcs:ignore + ?> +

+ ' . esc_html( $org ) . ''; + } + } + ?> + +

+ +

+ + get_contents( __DIR__ . $file ); + + return $contents; + + } + +} diff --git a/admin/css/cp-plgn-drctry-select2.css b/admin/css/cp-plgn-drctry-select2.css new file mode 100644 index 0000000..95600c6 --- /dev/null +++ b/admin/css/cp-plgn-drctry-select2.css @@ -0,0 +1,35 @@ +/* remove X from locked tag */ +.locked-tag .select2-selection__choice__remove{ + display: none!important; +} + +/* I suggest to hide all selected tags from drop down list */ +.select2-results__option[aria-selected="true"]{ + display: none; +} +.cp-dir-verified-contributor-badge { + background-color: transparent; + border: none; + border-right: 1px solid #aaa; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + color: #999; + padding: 0 4px; + position: absolute; + left: 0; + top: 0; + width:30px; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__display { + padding-left: 15px; + line-height: 2 !important; +} +.select2-selection__choice { + height:30px; +} +.select2-selection__choice__remove { + font-size: 1.4em !important; +} +.cp-dir-verified-contributor-badge-description { + max-width: 30px; +} \ No newline at end of file diff --git a/admin/js/cp-plgn-drctry-select2.js b/admin/js/cp-plgn-drctry-select2.js new file mode 100644 index 0000000..cdbb645 --- /dev/null +++ b/admin/js/cp-plgn-drctry-select2.js @@ -0,0 +1,46 @@ +/** + * Admin side jQuery. + * Adds script to install plugin, + * show spinner on ajax and + * catch the exception of no mailer being on the computer of user. + */ + +(function( $ ) { + 'use strict'; + + var die = function( msg ) { + throw new Error( msg ); + } + + $( document ).on( + 'ready', + function() { + $('.cp-dir-select2').select2({ + tags: true, + width: '100%', + placeholder: "Type the name of a new Organization or User, then hit enter to add the new item to the list", + templateSelection : function (tag, container){ + // here we are finding option element of tag and + // if it has property 'locked' we will add class 'locked-tag' + // to be able to style element in select + var $option = $('.cp-dir-select2 option[value="'+tag.id+'"]'); + + if ($option.attr('locked')){ + $(container).find("button").replaceWith(''); + //$(container).addClass('locked-tag'); + tag.locked = true; + } + return tag.text; + }, + }) + .on('select2:unselecting', function(e){ + // before removing tag we check option element of tag and + // if it has property 'locked' we will create error to prevent all select2 functionality + if ($(e.params.args.data.element).attr('locked')) { + e.preventDefault(); + } + }); + } + ); + +})( jQuery ); diff --git a/admin/partials/github-orgs.txt b/admin/partials/github-orgs.txt new file mode 100644 index 0000000..9b43b07 --- /dev/null +++ b/admin/partials/github-orgs.txt @@ -0,0 +1,6 @@ +[ + { + "slug":"tukutoi", + "name":"TukuToi" + } +] \ No newline at end of file diff --git a/changelog.txt b/changelog.txt index 582f6c9..8e316e5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,12 @@ += 1.3.0 = +[Added] Plugin Settings Page (under Admin > Settings > Manage CP Repos) +[Added] Setting to add custom GitHub Repositories of Orgs and/or Users. +[Added] Setting to store Personal GitHub Token, which increases the API Limits to 5k hourly instead of 60. +[Added] Verified Orgs (_not users_) are pre-selected. A PR can be used to add new Orgs to the vetted list. +[Added] Fundations to read remote readme, README, (both in md or txt) files. Currently used ony for below [Fixed] item. +[Fixed] Problem where plugins with foldername/distinct-filename.php AND a unguessable Plugin Title could not be managed. +[Improved] Make drastically less calls to the GitHub API by re-using already queried data as much as possible. + = 1.2.0 = [Added] GitHub Repo Sync for (TukuToi) Plugins [Added] Total Page Number on pagination diff --git a/includes/class-cp-plgn-drctry.php b/includes/class-cp-plgn-drctry.php index 90789d9..b06ef87 100644 --- a/includes/class-cp-plgn-drctry.php +++ b/includes/class-cp-plgn-drctry.php @@ -140,6 +140,11 @@ private function load_dependencies() { */ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-cp-plgn-drctry-github.php'; + /** + * The class responsible for Plugin settings. + */ + require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-cp-plgn-drctry-settings.php'; + $this->loader = new Cp_Plgn_Drctry_Loader(); } @@ -190,6 +195,18 @@ private function define_admin_hooks() { $this->loader->add_action( 'wp_ajax_delete-plugin', $plugin_admin, 'delete_cp_plugin' ); } + /** + * Add Settings Screen. + * + * @since 1.3.0 + */ + if ( current_user_can( 'manage_options' ) + && is_user_logged_in() + && is_admin() + ) { + $cp_dir_options = new Cp_Plgn_Drctry_Settings( $this->get_plugin_name(), $this->get_plugin_prefix(), $this->get_version() ); + add_action( 'admin_init', array( $cp_dir_options, 'settings_init' ) ); + } } diff --git a/readme.md b/readme.md index d609788..79c4e6c 100644 --- a/readme.md +++ b/readme.md @@ -11,28 +11,44 @@ Install and activate like any other plugin. Navigate to Dashboard > Plugins > Manage CP Plugins and start managing ClassicPress Plugins. You can install, activate, deactivate, update, delete, and also search Plugins all from within the same screen. -The Directory results are cached locally for fast performance, and you can refresh the local cache on the click of a button. +The results are cached locally for fast performance, and you can refresh the local cache on the click of a button. It has a pagination and a total plugins display to navigate (15 plugins a time) through the assets. A "more info" will display all information known to ClassicPress about the plugin and developer. The plugin requires wp_remote_get and file_put_contents to work properly on the server. -### Plugins not listed in the ClassicPress Directory +## Plugins not listed in the ClassicPress Directory It is possible to manage plugins that are not listed in the ClassicPress Directory with this plugin as well. The conditions for this to work are: -- the GitHub stored Plugin MUST have a tag `classicpress-plugin` -- the GitHub Repository MUST have a valid Release tag named witha SemVer release version (like `1.0.0`) and Public Release with a manually uploaded Release Asset in Zip Format. This ZIP MUST be uploaded to the release section for `Attach binaries by dropping them here or selecting them.` -- currently only plugins stored by the TukuToi Organization are available - in the next release, a setting will be offered to end users in order to register any organziation or user. +- the GitHub stored Plugin MUST have a tag `classicpress-plugin`. +- the GitHub Repository MUST have a valid Release tag named with a SemVer release version (like `1.0.0`) and Public Release with a manually uploaded Release Asset in Zip Format. This ZIP MUST be uploaded to the release section for `Attach binaries by dropping them here or selecting them.` -### Disclaimers -- The plugin does not take any responsibility for Plugins downloaded from the ClassicPress Directory or GitHub. +By default, there is a _vetted list_ of _Organizations_ added to the plugin. If a Developer wants to appear on said list, +they can submit a PR to the `github-orgs.txt` File of this Plugin, by adding their Guthub Organization data to the JSON array. +The Organization AND the PR initiator will be reviewed both by the author of this plugin as well the ClassicPress Plugin Review Team. +Only after careful assessment the Developer will be added to the Verified List of Organizations, and thus appear pre-selected in the Repositories queried by this plugin. + +Other, non verified Repositories (both users and orgs) can still be added easily by an end user in the dedicated Settings page (Dashboard > Settings > Manage CP Repos). + +## Disclaimers +- The plugin does not take any responsibility for Plugins downloaded from the ClassicPress Directory or GitHub, not even if verified Organization's software. - The ClassicPress Plugin Repository is not always well maintained by the Developers who list their plugins. They forget often to bump the Version Number of their Plugins. This means, you *might* not see an update, even if there is one, or you might see an update to a certain version and get an update to a much higher version. - If a GitHub stored plugin is not following above (MUST) clauses, it will not be possible for this plugin to find, pull or else manage such repos. +- If you run into GitHub API Limits (it is not so generous) you should create a Personal Authentication Token as shown [here](https://docs.github.com/en/enterprise-server@3.4/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). You should only give this Token "read" rights, no post or edit rights. You should _never_ share this Token with anyone. You should then store this Token on the setting for it under Dashboard > Settings > Manage CP Repos. This will bump your GitHub API limits to 5000 per hours (which is far enough). ## Changelog +### 1.3.0 +[Added] Plugin Settings Page (under Admin > Settings > Manage CP Repos) +[Added] Setting to add custom GitHub Repositories of Orgs and/or Users. +[Added] Setting to store Personal GitHub Token, which increases the API Limits to 5k hourly instead of 60. +[Added] Verified Orgs (_not users_) are pre-selected. A PR can be used to add new Orgs to the vetted list. +[Added] Fundations to read remote readme, README, (both in md or txt) files. Currently used ony for below [Fixed] item. +[Fixed] Problem where plugins with foldername/distinct-filename.php AND a unguessable Plugin Title could not be managed. +[Improved] Make drastically less calls to the GitHub API by re-using already queried data as much as possible. + ### 1.2.0 [Added] GitHub Repo Sync for (TukuToi) Plugins [Added] Total Page Number on pagination diff --git a/tukutoi-cp-directory-integration.php b/tukutoi-cp-directory-integration.php index f52d013..188552e 100644 --- a/tukutoi-cp-directory-integration.php +++ b/tukutoi-cp-directory-integration.php @@ -15,7 +15,7 @@ * Plugin Name: CP Plugin Directory * Plugin URI: https://www.tukutoi.com/ * Description: Integrates the ClassicPress Plugin Directory and Plugins stored on GitHub (tagged with classicpress-plugin) into the ClassicPress Admin Interface. - * Version: 1.2.0 + * Version: 1.3.0 * Author: bedas * Requires at least: 4.9.15 * Requires PHP: 7.0.0 @@ -37,7 +37,7 @@ * Start at version 1.0.0 and use SemVer - https://semver.org * Rename this for your plugin and update it as you release new versions. */ -define( 'CP_PLGN_DRCTRY_VERSION', '1.2.0' ); +define( 'CP_PLGN_DRCTRY_VERSION', '1.3.0' ); /** * Define the Plugin basename From fcf574a41ba76d14ea8511ca87d47e3c99a21a72 Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Fri, 1 Jul 2022 18:04:23 +0700 Subject: [PATCH 02/14] 01-07-2022 [Fixed] Using str_contains is PHP 8 only --- .gitignore | 2 +- admin/class-cp-plgn-drctry-github.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9bea433..6aa2a85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ - +cp-plugins.txt .DS_Store diff --git a/admin/class-cp-plgn-drctry-github.php b/admin/class-cp-plgn-drctry-github.php index c55d3bc..eb3bfdb 100644 --- a/admin/class-cp-plgn-drctry-github.php +++ b/admin/class-cp-plgn-drctry-github.php @@ -251,9 +251,9 @@ private function get_readme_name( $item, $login ) { $first_line = strtok( $this->get_readme_data( $item, $login ), "\n" ); - if ( str_contains( $this->variation, '.md' ) ) { + if ( strpos( $this->variation, '.md' ) !== false ) { $title = sanitize_text_field( trim( str_replace( '#', '', $first_line ) ) ); - } elseif ( str_contains( $this->variation, '.txt' ) ) { + } elseif ( strpos( $this->variation, '.txt' ) !== false ) { $title = sanitize_text_field( trim( str_replace( '==', '', $first_line ) ) ); } From fb4fc79c007b23debc8186da8448e54762d73f43 Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Fri, 1 Jul 2022 19:36:40 +0700 Subject: [PATCH 03/14] 01-07-2022 [Fixed] Error messages where inadequate, and stopping the sync, when a plugin misses _all_ readme variants --- admin/class-cp-plgn-drctry-github.php | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/admin/class-cp-plgn-drctry-github.php b/admin/class-cp-plgn-drctry-github.php index eb3bfdb..7cda0d4 100644 --- a/admin/class-cp-plgn-drctry-github.php +++ b/admin/class-cp-plgn-drctry-github.php @@ -248,7 +248,6 @@ private function build_git_plugins_object( $url ) { private function get_readme_name( $item, $login ) { $title = esc_html__( 'No Title Found. ErrNo: CP-GH-249', 'cp-plgn-drctry' ); - $first_line = strtok( $this->get_readme_data( $item, $login ), "\n" ); if ( strpos( $this->variation, '.md' ) !== false ) { @@ -285,9 +284,9 @@ private function get_readme_data( $item, $login ) { if ( wp_remote_retrieve_response_code( $readme ) === 200 ) { $data = wp_remote_retrieve_body( $readme ); } else { - echo '

' . sprintf( esc_html__( 'We could not find a readme .md or .txt file for the Repository %$1. This can result in incomplete data. You should report this issue to the Developer (%$2)', 'cp-plgn-drctry' ), esc_html( $item ), esc_html( $login ) ) . '

'; + echo '

' . esc_html__( 'We could not find a readme .md or .txt file for the Repository. This can result in incomplete data. You should report this issue to the Developer', 'cp-plgn-drctry' ) . '

'; error_log( print_r( $readme, true ) ); - return $readme; + return $data; } return $data; @@ -317,7 +316,7 @@ private function get_git_dev_info( $login, $type ) { if ( wp_remote_retrieve_response_code( $dev ) === 200 ) { $dev = json_decode( wp_remote_retrieve_body( $dev ) ); } else { - echo '

' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the GitHub %$1 "%$2". It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

'; + echo '

' . sprintf( esc_html__( 'We could not reach the GitHub User/Org API for the GitHub %$1 "%$2". It is possible you reached the limits of the GitHub User/Org API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

'; error_log( print_r( $dev, true ) ); return $dev_array; } @@ -355,19 +354,21 @@ private function get_git_release_data( $release_url, $repo_name ) { $release = wp_remote_get( $url, $this->set_auth() ); if ( wp_remote_retrieve_response_code( $release ) === 200 ) { $release = json_decode( wp_remote_retrieve_body( $release ) ); + $release_data['version'] = $release->tag_name; + $release_data['download_link'] = $release->assets[0]->browser_download_url; + $release_data['count'] = $release->assets[0]->download_count; + $release_data['changelog'] = $release->body; + $release_data['updated_at'] = $release->assets[0]->updated_at; + } elseif ( wp_remote_retrieve_response_code( $release ) === 404 ) { + // translators: %s: Name of remote GitHub Directory. + echo '

' . sprintf( esc_html__( 'It does not seem that the Repository "%s" follows best practices. We could not find any Release for it on GitHub.', 'cp-plgn-drctry' ), $repo_name ) . '

'; + error_log( print_r( $release, true ) ); } else { // translators: %s: Name of remote GitHub Directory. echo '

' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the repository "%s". It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $repo_name ) ) . '

'; error_log( print_r( $release, true ) ); - return $release_data; } - $release_data['version'] = $release->tag_name; - $release_data['download_link'] = $release->assets[0]->browser_download_url; - $release_data['count'] = $release->assets[0]->download_count; - $release_data['changelog'] = $release->body; - $release_data['updated_at'] = $release->assets[0]->updated_at; - return $release_data; } From 11db62c90c5b6eeda94134cf5105fff62529a3ef Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Fri, 1 Jul 2022 20:02:30 +0700 Subject: [PATCH 04/14] 01-07-2022 [Changed] Listen to `default_branch` instead of hardcoded branch when getting raw readme [Fixed] Placeholders for sprintf() where misconfigured. --- admin/class-cp-plgn-drctry-github.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/admin/class-cp-plgn-drctry-github.php b/admin/class-cp-plgn-drctry-github.php index 7cda0d4..9024f59 100644 --- a/admin/class-cp-plgn-drctry-github.php +++ b/admin/class-cp-plgn-drctry-github.php @@ -197,7 +197,7 @@ private function build_git_plugins_object( $url ) { if ( in_array( 'classicpress-plugin', $repo_object->topics ) ) { $release_data = $this->get_git_release_data( $repo_object->releases_url, $repo_object->name ); - $data['name'] = $this->get_readme_name( $repo_object->name, $repo_object->owner->login ); + $data['name'] = $this->get_readme_name( $repo_object->name, $repo_object->owner->login, $repo_object->default_branch ); $data['description'] = $repo_object->description; $data['downloads'] = $release_data['count']; $data['changelog'] = $release_data['changelog']; @@ -245,10 +245,10 @@ private function build_git_plugins_object( $url ) { * * @since 1.3.0 */ - private function get_readme_name( $item, $login ) { + private function get_readme_name( $item, $login, $branch ) { $title = esc_html__( 'No Title Found. ErrNo: CP-GH-249', 'cp-plgn-drctry' ); - $first_line = strtok( $this->get_readme_data( $item, $login ), "\n" ); + $first_line = strtok( $this->get_readme_data( $item, $login, $branch ), "\n" ); if ( strpos( $this->variation, '.md' ) !== false ) { $title = sanitize_text_field( trim( str_replace( '#', '', $first_line ) ) ); @@ -268,13 +268,13 @@ private function get_readme_name( $item, $login ) { * * @since 1.3.0 */ - private function get_readme_data( $item, $login ) { + private function get_readme_data( $item, $login, $branch ) { $data = ''; $readme_variations = array( 'README.md', 'readme.md', 'README.txt', 'readme.txt' ); foreach ( $readme_variations as $variation ) { - $readme = wp_remote_get( 'https://raw.githubusercontent.com/' . $login . '/' . $item . '/main/' . $variation ); + $readme = wp_remote_get( 'https://raw.githubusercontent.com/' . $login . '/' . $item . '/' . $branch . '/' . $variation ); if ( wp_remote_retrieve_response_code( $readme ) !== 404 ) { $this->variation = $variation; break; @@ -284,7 +284,7 @@ private function get_readme_data( $item, $login ) { if ( wp_remote_retrieve_response_code( $readme ) === 200 ) { $data = wp_remote_retrieve_body( $readme ); } else { - echo '

' . esc_html__( 'We could not find a readme .md or .txt file for the Repository. This can result in incomplete data. You should report this issue to the Developer', 'cp-plgn-drctry' ) . '

'; + echo '

' . sprintf( esc_html__( 'We could not find a readme .md or .txt file for the Repository "%1$s". This can result in incomplete data. You should report this issue to %2$s (The Developer)', 'cp-plgn-drctry' ), $item, $login ) . '

'; error_log( print_r( $readme, true ) ); return $data; } @@ -316,7 +316,7 @@ private function get_git_dev_info( $login, $type ) { if ( wp_remote_retrieve_response_code( $dev ) === 200 ) { $dev = json_decode( wp_remote_retrieve_body( $dev ) ); } else { - echo '

' . sprintf( esc_html__( 'We could not reach the GitHub User/Org API for the GitHub %$1 "%$2". It is possible you reached the limits of the GitHub User/Org API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

'; + echo '

' . sprintf( esc_html__( 'We could not reach the GitHub User/Org API for the GitHub %1$s "%2$s". It is possible you reached the limits of the GitHub User/Org API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

'; error_log( print_r( $dev, true ) ); return $dev_array; } From e0377fc8054eb6e755bd0e30aac76e901736637e Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Sun, 3 Jul 2022 11:25:12 +0700 Subject: [PATCH 05/14] 03-07-2022 [Improved] Refactor [Added] Single repo setting [Fixed] Several bugs with existing integrations --- README.txt | 9 +- admin/class-cp-plgn-drctry-admin.php | 195 +------- admin/class-cp-plgn-drctry-cp-api.php | 86 ++++ admin/class-cp-plgn-drctry-cp-dir.php | 454 ------------------ admin/class-cp-plgn-drctry-cp-plugins-dir.php | 265 ++++++++++ admin/class-cp-plgn-drctry-fx.php | 338 +++++++++++++ admin/class-cp-plgn-drctry-github.php | 413 ++++++++-------- admin/class-cp-plgn-drctry-plugin-fx.php | 293 +++++++++++ admin/class-cp-plgn-drctry-settings.php | 149 ++++-- ...elect2.css => cp-plgn-drctry-settings.css} | 0 ...-select2.js => cp-plgn-drctry-settings.js} | 0 .../partials/cp-plgn-drctry-admin-display.php | 6 +- changelog.txt | 3 +- includes/class-cp-plgn-drctry.php | 35 +- readme.md | 11 +- tukutoi-cp-directory-integration.php | 2 +- 16 files changed, 1369 insertions(+), 890 deletions(-) create mode 100644 admin/class-cp-plgn-drctry-cp-api.php delete mode 100644 admin/class-cp-plgn-drctry-cp-dir.php create mode 100644 admin/class-cp-plgn-drctry-cp-plugins-dir.php create mode 100644 admin/class-cp-plgn-drctry-fx.php create mode 100644 admin/class-cp-plgn-drctry-plugin-fx.php rename admin/css/{cp-plgn-drctry-select2.css => cp-plgn-drctry-settings.css} (100%) rename admin/js/{cp-plgn-drctry-select2.js => cp-plgn-drctry-settings.js} (100%) diff --git a/README.txt b/README.txt index f76e1c7..a581376 100644 --- a/README.txt +++ b/README.txt @@ -1,4 +1,4 @@ -=== ClassicPress Plugin Directory === +=== ClassicPress Directory Integration === Contributors: bedas Donate link: https://paypal.me/tukutoi Tags: directory, plugins @@ -28,14 +28,17 @@ The plugin requires wp_remote_get and file_put_contents to work properly on the It is possible to manage plugins that are not listed in the ClassicPress Directory with this plugin as well. The conditions for this to work are: - the GitHub stored Plugin MUST have a tag `classicpress-plugin`. -- the GitHub Repository MUST have a valid Release tag named with a SemVer release version (like `1.0.0`) and Public Release with a manually uploaded Release Asset in Zip Format. This ZIP MUST be uploaded to the release section for `Attach binaries by dropping them here or selecting them.` +- the GitHub Repository MUST have a valid Release tag named with a SemVer release version (like `1.0.0`) . +- the release MUST have a manually uploaded Zip Asset uploaded to the release section for `Attach binaries by dropping them here or selecting them.` holding the plugin. +- the repository MUST have EITHER OR BOTH a readme.txt OR readme.md (can be all uppercase too). The readme.txt is prioritized and MUST follow the WordPress readme.txt rules. The readme.md file is used only as backup, and if used, MUST have at least one line featuring `# Plugin Name Here`. +- the repository MUST be public. By default, there is a _vetted list_ of _Organizations_ added to the plugin. If a Developer wants to appear on said list, they can submit a PR to the `github-orgs.txt` File of this Plugin, by adding their Guthub Organization data to the JSON array. The Organization AND the PR initiator will be reviewed both by the author of this plugin as well the ClassicPress Plugin Review Team. Only after careful assessment the Developer will be added to the Verified List of Organizations, and thus appear pre-selected in the Repositories queried by this plugin. -Other, non verified Repositories (both users and orgs) can still be added easily by an end user in the dedicated Settings page (Dashboard > Settings > Manage CP Repos). +Other, non verified Repositories (both users and orgs) can still be added by an end user in the dedicated Settings page (Dashboard > Settings > Manage CP Repos). == Disclaimers == - The plugin does not take any responsibility for Plugins downloaded from the ClassicPress Directory or GitHub, not even if verified Organization's software. diff --git a/admin/class-cp-plgn-drctry-admin.php b/admin/class-cp-plgn-drctry-admin.php index 7e91d75..213bbec 100644 --- a/admin/class-cp-plgn-drctry-admin.php +++ b/admin/class-cp-plgn-drctry-admin.php @@ -1,6 +1,6 @@ plugin_name = $plugin_name; $this->plugin_prefix = $plugin_prefix; $this->version = $version; - $this->cp_dir = new Cp_Plgn_Drctry_Cp_Dir( $plugin_name, $plugin_prefix, $version ); - $this->cp_dir_options = new Cp_Plgn_Drctry_Settings( $plugin_name, $plugin_prefix, $version ); } @@ -85,10 +74,10 @@ public function __construct( $plugin_name, $plugin_prefix, $version ) { public function enqueue_styles( $hook_suffix ) { if ( 'plugins_page_cp-plugins' === $hook_suffix ) { - wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/cp-plgn-drctry-admin.css', array(), $this->version, 'all' ); + wp_enqueue_style( $this->plugin_prefix . 'plugins', plugin_dir_url( __FILE__ ) . 'css/cp-plgn-drctry-admin.css', array(), $this->version, 'all' ); } elseif ( 'settings_page_cp_dir_opts' === $hook_suffix ) { wp_enqueue_style( 'select2', 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css', array(), '4.1.0-rc.0', 'all' ); - wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/cp-plgn-drctry-select2.css', array( 'select2' ), $this->version, 'all' ); + wp_enqueue_style( $this->plugin_prefix . 'settings', plugin_dir_url( __FILE__ ) . 'css/cp-plgn-drctry-settings.css', array( 'select2' ), $this->version, 'all' ); } } @@ -103,9 +92,9 @@ public function enqueue_scripts( $hook_suffix ) { if ( 'plugins_page_cp-plugins' === $hook_suffix ) { add_thickbox(); - wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/cp-plgn-drctry-admin.js', array( 'jquery' ), $this->version, false ); + wp_enqueue_script( $this->plugin_prefix . 'plugins', plugin_dir_url( __FILE__ ) . 'js/cp-plgn-drctry-admin.js', array( 'jquery' ), $this->version, false ); wp_localize_script( - $this->plugin_name, + $this->plugin_prefix . 'plugins', 'ajax_object', array( 'ajax_url' => esc_url( admin_url( 'admin-ajax.php' ) ), @@ -115,161 +104,17 @@ public function enqueue_scripts( $hook_suffix ) { ); } elseif ( 'settings_page_cp_dir_opts' === $hook_suffix ) { wp_enqueue_script( 'select2', 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js', array( 'jquery' ), '4.1.0-rc.0', false ); - wp_enqueue_script( $this->plugin_name . '-select2', plugin_dir_url( __FILE__ ) . 'js/cp-plgn-drctry-select2.js', array( 'select2' ), $this->version, false ); + wp_enqueue_script( $this->plugin_prefix . 'settings', plugin_dir_url( __FILE__ ) . 'js/cp-plgn-drctry-settings.js', array( 'select2' ), $this->version, false ); } } /** - * Install a Plugin. + * Add Menu Pages. * - * @since 1.1.3 Added overwrite_package argument - * @param bool $overwrite Whether to overwrite the plugin or not. Default False. - */ - public function install_cp_plugin( $overwrite = false ) { - - if ( ! isset( $_POST['_ajax_nonce'] ) - || empty( $_POST['_ajax_nonce'] ) - || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'updates' ) ) { - die( 'Invalid or missing Nonce!' ); - } - - if ( ! isset( $_POST['url'] ) ) { - wp_send_json( 'Something went wrong' ); - } - - /** - * We include Upgrader Class. - * - * @todo Check this path on EACH CP UPDATE. It might change! - */ - include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); - $upgrader = new Plugin_Upgrader(); - $response = $upgrader->install( esc_url_raw( wp_unslash( $_POST['url'] ) ), array( 'overwrite_package' => $overwrite ) ); - - wp_send_json( $response ); - - } - - /** - * Update a Plugin. - */ - public function update_cp_plugin() { - - if ( ! isset( $_POST['_ajax_nonce'] ) - || empty( $_POST['_ajax_nonce'] ) - || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'updates' ) ) { - die( 'Invalid or missing Nonce!' ); - } - - if ( ! isset( $_POST['slug'] ) ) { - wp_send_json( 'Something went wrong' ); - } - - /** - * We cannot use Upgrader Class, because CP has no way of - * selecting custom file URL. Only WP Can do that. - * - * We simply replace the plugin entirely. - * - * @since 1.0.0 Update Plugin - * @since 1.1.3 Update itself - */ - $this->install_cp_plugin( true ); - - } - - /** - * Delete a Plugin. - */ - public function delete_cp_plugin() { - - if ( ! isset( $_POST['_ajax_nonce'] ) - || empty( $_POST['_ajax_nonce'] ) - || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'updates' ) ) { - die( 'Invalid or missing Nonce!' ); - } - - if ( ! isset( $_POST['plugin'] ) ) { - wp_send_json( 'Something went wrong' ); - } - - /** - * This returns true on success, false if $Plugin is empty, - * null if creds are missing, WP Error on failure. - */ - $deleted = delete_plugins( array( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) ) ); - - if ( false === $deleted ) { - // creds are missing. - $deleted = 'The Plugin Slug is missing from delete_plugins() function.'; - } elseif ( null === $deleted ) { - $deleted = 'Filesystem Credentials are required. You are not allowed to perform this action.'; - } elseif ( is_wp_error( $deleted ) ) { - $deleted = 'There has been an error. Please check the error logs.'; - } elseif ( true !== $deleted ) { - $deleted = 'Unknown error occurred'; - } - - wp_send_json( $deleted ); - - } - - /** - * Deactivate a Plugin. + * @since 1.0.0 Add Plugins List Page. + * @since 1.3.0 Add Settings Page. */ - public function deactivate_cp_plugin() { - - if ( ! isset( $_POST['_ajax_nonce'] ) - || empty( $_POST['_ajax_nonce'] ) - || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'updates' ) ) { - die( 'Invalid or missing Nonce!' ); - } - - if ( ! isset( $_POST['slug'] ) ) { - wp_send_json( 'Something went wrong' ); - } - - /** - * This function does not return anything. - * We have no way of knowing whether the plugin was deactivated or not. - * We however reload the page in JS after this operation, so the new status will tell. - */ - deactivate_plugins( sanitize_text_field( wp_unslash( $_POST['slug'] ) ), true ); - - wp_send_json( 'Plugin Possibly Updated' ); - - } - - /** - * Activate a Plugin. - */ - public function activate_cp_plugin() { - - if ( ! isset( $_POST['_ajax_nonce'] ) - || empty( $_POST['_ajax_nonce'] ) - || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'updates' ) ) { - die( 'Invalid or missing Nonce!' ); - } - - if ( ! isset( $_POST['slug'] ) ) { - wp_send_json( 'Something went wrong' ); - } - - /** - * The function returns a WP error if something went wrong, - * null otherwise. - */ - $activated = activate_plugin( sanitize_text_field( wp_unslash( $_POST['slug'] ) ) ); - - wp_send_json( $activated ); - - } - - /** - * Creates the submenu item and calls on the Submenu Page object to render - * the actual contents of the page. - */ - public function add_plugins_list() { + public function add_menu_pages() { add_submenu_page( 'plugins.php', @@ -281,9 +126,6 @@ public function add_plugins_list() { 3 ); - /** - * @since 1.3.0 - */ add_submenu_page( 'options-general.php', esc_html__( 'ClassicPress Repositories', 'cp-plgn-drctry' ), @@ -297,7 +139,7 @@ public function add_plugins_list() { } /** - * Render the admin page. + * Render the plugins list page. */ public function render() { ?> @@ -306,13 +148,16 @@ public function render() {

- cp_dir->list_plugins(); ?> + plugin_name, $this->plugin_prefix, $this->version ); + $cp_dir->list_plugins(); + ?> + */ +trait Cp_Plgn_Drctry_Cp_Api { + + private function get_cp_pages() { + + $pages = $this->get_remote_decoded_body( $this->cp_dir_url ); + + if ( false !== $pages + && 404 !== $pages + ) { + + /** + * On the first API page, the first meta:links link is null + * This is because there is no "previous page" on the first page. + * The last meta:links link is the "next" page, which we already + * have in the meta:links as well. Thus, we remove first and last + * to get all actual pages of the API. + */ + array_shift( $pages->meta->links ); + array_pop( $pages->meta->links ); + $pages = $pages->meta->links; + + } else { + + echo '

' . esc_html__( 'We could not reach the ClassicPress Directory API. It is possible you reached the limits of the ClassicPress Directory API.', 'cp-plgn-drctry' ) . '

'; + $pages = array(); + + } + + return $pages; + + } + /** + * Retrieve all plugins from the CP API. + * + * @return array $all_cp_plugins An array of all plugins objects. + */ + private function get_cp_plugins() { + + $all_cp_plugins = array(); + $pages = $this->get_cp_pages(); + + // loop over each page and get each pages's data. + foreach ( $this->get_cp_pages() as $link ) { + + // Get current page's plugins. + $current_page_plugins = $this->get_remote_decoded_body( $link->url ); + + // Check response. + if ( false !== $current_page_plugins ) { + + // Merge plugins into main plugins array. + $all_cp_plugins = array_merge( $all_cp_plugins, $current_page_plugins->data ); + + } else { + + echo ''; + + } + } + + return $all_cp_plugins; + + } +} diff --git a/admin/class-cp-plgn-drctry-cp-dir.php b/admin/class-cp-plgn-drctry-cp-dir.php deleted file mode 100644 index 87d5747..0000000 --- a/admin/class-cp-plgn-drctry-cp-dir.php +++ /dev/null @@ -1,454 +0,0 @@ - - */ -class Cp_Plgn_Drctry_Cp_Dir { - - /** - * The ID of this plugin. - * - * @since 1.0.0 - * @access private - * @var string $plugin_name The ID of this plugin. - */ - private $plugin_name; - - /** - * The unique prefix of this plugin. - * - * @since 1.0.0 - * @access private - * @var string $plugin_prefix The string used to uniquely prefix technical functions of this plugin. - */ - private $plugin_prefix; - - /** - * The version of this plugin. - * - * @since 1.0.0 - * @access private - * @var string $version The current version of this plugin. - */ - private $version; - - /** - * Initialize the class and set its properties. - * - * @since 1.0.0 - * @param string $plugin_name The name of this plugin. - * @param string $plugin_prefix The unique prefix of this plugin. - * @param string $version The version of this plugin. - */ - public function __construct( $plugin_name, $plugin_prefix, $version ) { - - $this->plugin_name = $plugin_name; - $this->plugin_prefix = $plugin_prefix; - $this->version = $version; - - } - - /** - * Chunk plugins into "pages" and return as grid/list. - */ - public function list_plugins() { - - // All Plugins. - $plugins = $this->get_plugins(); - // If $plugins is empty, there was an error. Abort. - if ( empty( $plugins ) ) { - return ''; - } - - $has_update = $this->has_update( $plugins ); - // Maybe search. - $plugins = $this->search_plugins( $plugins ); - // Paginate them by 15. - $paginated = array_chunk( $plugins, 15, false ); - // Last page is amount of array keys - 1 (because of start at 0). - $last = count( $paginated ) - 1; - // Check if the current page is a paged URL. - $paged = 0; - if ( isset( $_GET['paged'] ) - && isset( $_GET['tkt_page_nonce'] ) - && wp_verify_nonce( sanitize_key( wp_unslash( $_GET['tkt_page_nonce'] ) ), 'tkt_page_nonce' ) - ) { - $paged = (int) $_GET['paged']; - } - // Build "prev" link "paged" value. - $prev = filter_var( - $paged - 1, - FILTER_VALIDATE_INT, - array( - 'options' => array( - 'default' => 0, - 'min_range' => 0, - 'max_range' => $last, - ), - ) - ); - // Build "next" link "paged" value. - $next = filter_var( - $paged + 1, - FILTER_VALIDATE_INT, - array( - 'options' => array( - 'default' => 0, - 'min_range' => 1, - 'max_range' => $last, - ), - ) - ); - // Get current chunk of plugins. - $current_plugins = $paginated[ $paged ]; - - // Render everything in HTML. - include( __DIR__ . '/partials/cp-plgn-drctry-admin-display.php' ); - - } - - /** - * CP Way of getting File Contents. - */ - private function get_file_contents() { - - global $wp_filesystem; - - if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) { - include_once ABSPATH . 'wp-admin/includes/file.php'; - $creds = request_filesystem_credentials( site_url() ); - wp_filesystem( $creds ); - } - - $contents = $wp_filesystem->get_contents( __DIR__ . '/partials/cp-plugins.txt' ); - - return $contents; - - } - - /** - * CP Way of putting File Contentes. - * - * @param mixed $contents The content to put. - */ - private function put_file_contents( $contents ) { - - global $wp_filesystem; - - if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) { - include_once ABSPATH . 'wp-admin/includes/file.php'; - $creds = request_filesystem_credentials( site_url() ); - wp_filesystem( $creds ); - } - - $wp_filesystem->put_contents( __DIR__ . '/partials/cp-plugins.txt', $contents ); - - } - - /** - * Retrieve all plugins and store in file for cache. - * - * @return array $plugins An array of all plugins objects. - */ - private function get_plugins() { - - $plugins = array(); - $all_plugins = array(); - - /** - * If cache not yet built or refreshing cache. - */ - - if ( 0 === filesize( __DIR__ . '/partials/cp-plugins.txt' ) - || ( - isset( $_GET['refresh'] ) - && isset( $_GET['tkt_nonce'] ) - && wp_verify_nonce( sanitize_key( wp_unslash( $_GET['tkt_nonce'] ) ), 'tkt-refresh-data' ) - && 1 === (int) $_GET['refresh'] - ) - ) { - - // Empty the cache if set. - if ( 0 !== filesize( __DIR__ . '/partials/cp-plugins.txt' ) ) { - $this->put_file_contents( '' ); - } - - // get github plugins. - $git_plugins = new Cp_Plgn_Drctry_GitHub( $this->plugin_name, $this->plugin_prefix, $this->version ); - - // get first page. - $plugins = wp_remote_get( 'https://directory.classicpress.net/api/plugins/' ); - - // Check response. - if ( wp_remote_retrieve_response_code( $plugins ) === 200 ) { - - // get first page body. - $plugins_body = wp_remote_retrieve_body( $plugins ); - $plugins_body = json_decode( $plugins_body ); - - /** - * On the first API page, the first meta:links link is null - * This is because there is no "previous page" on the first page. - * The last meta:links link is the "next" page, which we already - * have in the meta:links as well. Thus, we remove first and last - * to get all actual pages of the API. - */ - array_shift( $plugins_body->meta->links ); - array_pop( $plugins_body->meta->links ); - - // loop over each page and get each pages's data. - foreach ( $plugins_body->meta->links as $link ) { - - // Get current page's plugins. - $current_page_plugins = wp_remote_get( $link->url ); - - // Check response. - if ( wp_remote_retrieve_response_code( $current_page_plugins ) === 200 ) { - - // Get current page's body. - $current_page_plugins_body = wp_remote_retrieve_body( $current_page_plugins ); - $current_page_plugins_body = json_decode( $current_page_plugins_body ); - - // Merge plugins into main plugins array. - $all_plugins = array_merge( $all_plugins, $current_page_plugins_body->data ); - - } else { - - echo ''; - error_log( print_r( $current_page_plugins, true ) ); - - } - } - } else { - - echo '

' . esc_html__( 'We could not reach the ClassicPress Directory API. It is possible you reached the limits of the ClassicPress Directory API.', 'cp-plgn-drctry' ) . '

'; - error_log( print_r( $plugins, true ) ); - - } - - // insert git plugins here. - $plugins = array_merge( $all_plugins, $git_plugins->get_git_plugins() ); - // Re-encode all plugins to JSON. - if ( ! empty( $plugins ) ) { - $plugins = wp_json_encode( $plugins ); - } else { - $plugins = ''; - } - $this->put_file_contents( $plugins ); - - } - - // Get data from cache. - $plugins = $this->get_file_contents(); - // Decode data. - $plugins = json_decode( $plugins ); - // Return as array. - return $plugins; - - } - - /** - * Search through the cached plugins. - * - * @param array $plugins The Array of Plugin objects. - * @return array $plugins The Found Plugins (or all, if nothing found). - */ - private function search_plugins( $plugins ) { - if ( isset( $_GET['s'] ) - && isset( $_GET['tkt-src-nonce'] ) - && wp_verify_nonce( sanitize_key( wp_unslash( $_GET['tkt-src-nonce'] ) ), 'tkt-src-nonce' ) - ) { - $search_term = sanitize_text_field( wp_unslash( $_GET['s'] ) ); - foreach ( $plugins as $key => $plugin ) { - if ( stripos( $plugin->description, $search_term ) !== false - || stripos( $plugin->developer->name, $search_term ) !== false - || stripos( $plugin->name, $search_term ) !== false - ) { - $found_plugins[] = $plugins[ $key ]; - } - } - if ( ! empty( $found_plugins ) ) { - $plugins = $found_plugins; - } else { - // Nothing wrong with this, since it is hardcoded no need to escape. - echo ''; - } - } - - return $plugins; - } - - /** - * Create a simple search form. - */ - private function search_form() { - - ?> -
- - - $val ) { - if ( 'paged' !== $key - && 'refresh' !== $key - && 's' !== $key - ) { - echo ''; - } - } - } - wp_nonce_field( 'tkt-src-nonce', 'tkt-src-nonce', false ); - ?> -
- check_plugin_installed( $plugin ) ) { - - $current_installed_version = get_plugins()[ $this->plugin_slug( $plugin ) ]['Version']; - $remote_version = $plugin->current_version; - $has_update = version_compare( $current_installed_version, $remote_version ); - if ( -1 === $has_update ) { - $updates[ $this->plugin_slug( $plugin ) ] = array( $current_installed_version, $remote_version ); - } - } - } - - return $updates; - - } - - /** - * Helper function to check if plugin is installed. - * - * @param object $plugin The Current Plugin Object. - * @return bool $is_installed If the plugin is installed or not. - */ - private function check_plugin_installed( $plugin ) { - - $plugin_filename = str_replace( '.zip', '', basename( $plugin->download_link ) ); - $plugin_slug = $plugin_filename . '/' . $plugin_filename . '.php'; - $installed_plugins = get_plugins(); - - $is_installed = array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true ) || array_search( $plugin->name, array_column( $installed_plugins, 'Name' ) ) !== false; - - return $is_installed; - - } - - /** - * Check if plugin is active. - * - * @param object $plugin The Current Plugin Object. - * @return bool $is_active If the Plugin is active. - */ - private function check_plugin_active( $plugin ) { - - $is_active = is_plugin_active( $this->plugin_slug( $plugin ) ); - - return $is_active; - } - - /** - * Is Plugin active and installed. - * - * @param object $plugin The Current Plugin Object. - * @return string $plugin_slug The Plugin Slug. - */ - private function plugin_slug( $plugin ) { - - $is_active = false; - $plugin_filename = str_replace( '.zip', '', basename( $plugin->download_link ) ); - $plugin_slug = $plugin_filename . '/' . $plugin_filename . '.php'; - $is_active = is_plugin_active( $plugin_slug ); - - if ( false === $is_active ) { - /** - * Handle bad plugins. - * - * It could be that some bad practice was followed - * and the plugin-folder/name.php does not match the downloaded item. - * This is not best practice, - * but unfortunately WP has allowed it, - * so for backward(s compatibility) reasons we make sure that - * if folder/name are a mismatch we can still check on the plugin state. - * - * First, get all installed plugins. - * From that array, search if the current Plugin Name (from API) is - * within the installed plugins. - * If so, get the key of that active plugin from the array. - * Then, fetch the proper slug of that plugin from the keys array. - * Then, repopulate $plugin_slug for later usage too. - */ - $installed_plugins = get_plugins(); - $plugin_key = array_search( $plugin->name, array_column( $installed_plugins, 'Name' ) ); - $keys = array_keys( $installed_plugins ); - if ( false !== $plugin_key ) { - $plugin_slug = $keys[ $plugin_key ]; - } - } - - return $plugin_slug; - } - - /** - * Returns the more info content. - * - * @param object $plugin The Current Plugin Object. - * @return string $html The HTML to produce the more info content. - */ - private function more_info( $plugin ) { - - $html = ''; - - foreach ( $plugin as $prop => $value ) { - $html .= '

' . esc_html( $prop ) . '

'; - if ( is_object( $value ) ) { - foreach ( $value as $sub_prop => $sub_value ) { - $html .= '
  • ' . esc_html( $sub_prop ) . ': ' . esc_html( $sub_value ) . '
  • '; - } - } else { - $html .= '

    ' . esc_html( $value ) . '

    '; - } - } - - return $html; - - } - -} diff --git a/admin/class-cp-plgn-drctry-cp-plugins-dir.php b/admin/class-cp-plgn-drctry-cp-plugins-dir.php new file mode 100644 index 0000000..f432d71 --- /dev/null +++ b/admin/class-cp-plgn-drctry-cp-plugins-dir.php @@ -0,0 +1,265 @@ + + */ +class Cp_Plgn_Drctry_Cp_Plugins_Dir { + + /** + * Include arbitrary functionality for the Plugins list. + */ + use Cp_Plgn_Drctry_Fx, Cp_Plgn_Drctry_Cp_Api, Cp_Plgn_Drctry_GitHub; + + /** + * The ID of this plugin. + * + * @since 1.0.0 + * @access private + * @var string $plugin_name The ID of this plugin. + */ + private $plugin_name; + + /** + * The unique prefix of this plugin. + * + * @since 1.0.0 + * @access private + * @var string $plugin_prefix The string used to uniquely prefix technical functions of this plugin. + */ + private $plugin_prefix; + + /** + * The version of this plugin. + * + * @since 1.0.0 + * @access private + * @var string $version The current version of this plugin. + */ + private $version; + + /** + * Initialize the class and set its properties. + * + * @since 1.0.0 + * @param string $plugin_name The name of this plugin. + * @param string $plugin_prefix The unique prefix of this plugin. + * @param string $version The version of this plugin. + */ + public function __construct( $plugin_name, $plugin_prefix, $version ) { + + $this->plugin_name = $plugin_name; + $this->plugin_prefix = $plugin_prefix; + $this->version = $version; + $this->cp_dir_url = 'https://directory.classicpress.net/api/plugins/'; + $this->plugins_cache_file = __DIR__ . '/partials/cp-plugins.txt'; + $this->plugin_fx = new Cp_Plgn_Drctry_Plugin_Fx( $plugin_name, $plugin_prefix, $version ); + $this->options = get_option( 'cp_dir_opts_options', array( 'cp_dir_opts_exteranal_org_repos' => $this->vetted_orgs() ) ); + $this->readme_vars = array( + 'README.txt', + 'readme.txt', + 'README.md', + 'readme.md', + ); + $this->plugins_topic = 'classicpress-plugin'; + + } + + /** + * List all plugins in a paginated, searchable grid. + */ + public function list_plugins() { + + /** + * Abort early if no plugins cached. + */ + $plugins = $this->get_plugins(); + if ( empty( $plugins ) ) { + return ''; + } + + /** + * Define variables for Display conditions needed BEFORE pagination is done. + * + * @var array $has_update The array of Plugins that feature an update. + */ + $has_update = $this->plugin_fx->has_update( $plugins ); + + /** + * Apply search. + * + * @var array $plugins The array of found plugins. Returns ALL if nothing found. + */ + $plugins = $this->search_plugins( $plugins ); + + /** + * Paginate. + * + * @var array $paginated Array chunks of plugins. + * @var int $last The Last Page. + * @var int $paged The Current Page. + * @var int $prev The Previous Page. + * @var int $next The Next page. + * @var array $current_plugins Array chunk of current plugins. + */ + $paginated = $this->list_pagination( $plugins, 'paginated' ); + $last = $this->list_pagination( $plugins, 'last' ); + $paged = $this->list_pagination( $plugins, 'paged' ); + $prev = $this->list_pagination( $plugins, 'prev' ); + $next = $this->list_pagination( $plugins, 'next' ); + $current_plugins = $paginated[ $paged ]; + + // Render everything in HTML. + include( __DIR__ . '/partials/cp-plgn-drctry-admin-display.php' ); + + } + + /** + * Search through the cached plugins. + * + * @param array $plugins The Array of Plugin objects. + * @return array $plugins The Found Plugins (or nothing, if nothing found). + */ + private function search_plugins( $plugins ) { + + /** + * Reviewers: we do check the nonce, CPCS just does not recognise this custom function. + */ + if ( isset( $_GET['s'] )// phpcs:ignore. + && $this->validate_get_nonce( 'tkt-src-nonce', 'tkt-src-nonce' ) + ) { + + $search_term = sanitize_text_field( wp_unslash( $_GET['s'] ) );// phpcs:ignore. + + foreach ( $plugins as $key => $plugin ) { + + if ( stripos( $plugin->description, $search_term ) !== false + || stripos( $plugin->developer->name, $search_term ) !== false + || stripos( $plugin->name, $search_term ) !== false + ) { + + $found_plugins[] = $plugins[ $key ]; + + } + } + + if ( empty( $found_plugins ) ) { + + echo ''; + $plugins = array(); + + } else { + + $plugins = $found_plugins; + + } + } + + return $plugins; + } + + /** + * Maybe flush the cache. + * + * @param string $file The Cache file path. + */ + private function maybe_flush_cache( $file ) { + + /** + * If cache not yet built or refreshing cache. + * + * Reviewers: we do check nonce, CPCS just does not recognise this function. + */ + if ( + isset( $_GET['refresh'] )// phpcs:ignore. + && $this->validate_get_nonce( 'tkt_nonce', 'tkt-refresh-data' ) + && 1 === (int) $_GET['refresh']// phpcs:ignore. + ) { + /** + * Flush cache if not empty. + */ + $this->put_file_contents( '', $this->plugins_cache_file ); + } + + } + + /** + * Maybe populate cache file. + * + * @param string $file The Cache file path. + */ + private function maybe_populate_cache( $file ) { + + /** + * If cache not yet built or refreshing cache. + */ + if ( 0 === filesize( $file ) ) { + // Get plugins. + $git_plugins = $this->get_git_plugins(); + $cp_plugins = $this->get_cp_plugins(); + $all_plugins = array_merge( $cp_plugins, $git_plugins ); + // Populate cache. + $this->put_file_contents( $this->encode_to_json( $all_plugins ), $this->plugins_cache_file ); + } + + } + + /** + * Merge all Plugins from all APIs. + * + * @return array The array of all plugins objects. + */ + private function get_plugins() { + + // Maybe Flush cache. + $this->maybe_flush_cache( $this->plugins_cache_file ); + // Maybe Populate cache. + $this->maybe_populate_cache( $this->plugins_cache_file ); + // Get data from cache and Decode. + return json_decode( $this->get_file_contents( $this->plugins_cache_file ) ); + + } + + /** + * Returns the more info content. + * + * @param object $plugin The Current Plugin Object. + * @return string $html The HTML to produce the more info content. + */ + private function more_info( $plugin ) { + + $html = ''; + + foreach ( $plugin as $prop => $value ) { + $html .= '

    ' . esc_html( $prop ) . '

    '; + if ( is_object( $value ) ) { + foreach ( $value as $sub_prop => $sub_value ) { + $html .= '
  • ' . esc_html( $sub_prop ) . ': ' . esc_html( $sub_value ) . '
  • '; + } + } else { + $html .= '

    ' . esc_html( $value ) . '

    '; + } + } + + return $html; + + } + +} diff --git a/admin/class-cp-plgn-drctry-fx.php b/admin/class-cp-plgn-drctry-fx.php new file mode 100644 index 0000000..46880ba --- /dev/null +++ b/admin/class-cp-plgn-drctry-fx.php @@ -0,0 +1,338 @@ + + */ +trait Cp_Plgn_Drctry_Fx { + + /** + * Validates any POSTed nonce by key and nonce. + * + * @param string $key The POST key where nonce is passed. + * @param string $nonce The Nonce to validate (name). + * @param string $message The message to return on failure. + */ + private function validate_post_nonce( $key, $nonce, $message = 'Invalid or missing Nonce!' ) { + + if ( ! isset( $_POST[ sanitize_key( $key ) ] ) + || empty( $_POST[ sanitize_key( $key ) ] ) + || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ sanitize_key( $key ) ] ) ), sanitize_key( $nonce ) ) ) { + wp_send_json( esc_html( $message ) ); + } + + } + + /** + * Validates any GET nonce by key and nonce. + * + * @param string $key The GET key where nonce is passed. + * @param string $nonce The Nonce to validate (name). + */ + private function validate_get_nonce( $key, $nonce ) { + + if ( isset( $_GET[ sanitize_key( $key ) ] ) + && ! empty( $_GET[ sanitize_key( $key ) ] ) + && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ sanitize_key( $key ) ] ) ), sanitize_key( $nonce ) ) ) { + return true; + } + + return false; + + } + + /** + * Validates any POSTed nonce by key and nonce. + * + * @param string $key The POST key where nonce is passed. + * @param string $message The message to return on failure. + */ + private function maybe_send_json_failure( $key, $message = 'Something went wrong' ) { + + /** + * Reviewers: Nonce is verified with ($this) validate_get_nonce + * before using maybe_send_json_failure this plugin always uses validate_get_nonce + */ + if ( ! isset( $_POST[ sanitize_key( $key ) ] ) ) {// phpcs:ignore. + wp_send_json( esc_html( $message ) ); + } + + } + + /** + * Validates any POSTed nonce by key and nonce. + * + * @param string $key The POST key where nonce is passed. + * @param string $sanitization The Sanitization function to use. + */ + private function get_posted_data( $key, $sanitization ) { + + /** + * Reviewers: $_POST is set because checked with maybe_send_json_failure + */ + return $sanitization( wp_unslash( $_POST[ $key ] ) );// phpcs:ignore. + + } + + /** + * CP Way of getting File Contents. + * + * @param string $file The file path to get contents from. + */ + private function get_file_contents( $file ) { + + global $wp_filesystem; + + if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) { + include_once ABSPATH . 'wp-admin/includes/file.php'; + $creds = request_filesystem_credentials( site_url() ); + wp_filesystem( $creds ); + } + + $contents = $wp_filesystem->get_contents( $file ); + + return $contents; + + } + + /** + * CP Way of putting File Contentes. + * + * @param mixed $contents The content to put. + * @param string $file The file path to get contents from. + */ + private function put_file_contents( $contents, $file ) { + + global $wp_filesystem; + + if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) { + include_once ABSPATH . 'wp-admin/includes/file.php'; + $creds = request_filesystem_credentials( site_url() ); + wp_filesystem( $creds ); + } + + $wp_filesystem->put_contents( $file, $contents ); + + } + + /** + * Get Remote body json decoded. + * + * @param string $url Remote URL. + * @param array $header Array of headers to send to remote. Empty by default. + */ + private function get_remote_decoded_body( $url, $header = array() ) { + + $r = wp_remote_get( $url, $header ); + $rc = wp_remote_retrieve_response_code( $r ); + if ( 200 === $rc ) { + + return json_decode( wp_remote_retrieve_body( $r ) ); + + } elseif ( 404 === $rc ) { + + return $rc; + + } else { + + return false; + + } + + } + + /** + * Get Remote body json decoded. + * + * @param string $url Remote URL. + * @param array $header Array of headers to send to remote. Empty by default. + */ + private function get_remote_raw_body( $url, $header = array() ) { + + $r = wp_remote_get( $url, $header ); + $rc = wp_remote_retrieve_response_code( $r ); + if ( 200 === $rc ) { + + return wp_remote_retrieve_body( $r ); + + } elseif ( 404 === $rc ) { + + return $rc; + + } else { + + return false; + + } + + } + + /** + * Encode data to JSON or return empty string. + * + * @param mixed $data The Data to encode. + */ + private function encode_to_json( $data ) { + + // Encode all data to JSON or return empty string. + if ( ! empty( $data ) ) { + $data = wp_json_encode( $data ); + } else { + $data = ''; + } + + return $data; + + } + + /** + * Get a list of vetted orgs + * + * @return array $_orgs An array of vetted orgs. + */ + private function vetted_orgs() { + $orgs = json_decode( $this->get_file_contents( __DIR__ . '/partials/github-orgs.txt' ) ); + $_orgs = array(); + foreach ( $orgs as $org ) { + $_orgs[] = $org->slug; + } + + return $_orgs; + } + + /** + * Get string value between delimiters. + * + * @param string $str The string to scan. + * @param string $start_delimiter The start delimiter to look for. + * @param string $end_delimiter The end delimiter to look for. + * @return string The string between. + */ + private function get_content_between( $str, $start_delimiter, $end_delimiter ) { + + $contents = array(); + $start_delimiter_length = strlen( $start_delimiter ); + $end_delimiter_length = strlen( $end_delimiter ); + $start_from = 0; + $content_start = 0; + $content_end = 0; + + while ( false !== ( $content_start = strpos( $str, $start_delimiter, $start_from ) ) ) { + + $content_start += $start_delimiter_length; + $content_end = strpos( $str, $end_delimiter, $content_start ); + if ( false === $content_end ) { + + break; + + } + $contents[] = substr( $str, $content_start, $content_end - $content_start ); + $start_from = $content_end + $end_delimiter_length; + + } + + return $contents; + + } + + /** + * Create a paginated list out of an array of items + * + * @param array $data The data to paginate. + * @param string $return What part of the pagination assets to return. + */ + private function list_pagination( $data, $return ) { + + $paginated = array_chunk( $data, 15, false ); + // Last page is amount of array keys - 1 (because of start at 0). + $last = count( $paginated ) - 1; + $paged = 0; + /** + * Reviewers: we do check the nonce, CPCS just does not recognise this custom function. + */ + if ( isset( $_GET['paged'] ) // phpcs:ignore. + && $this->validate_get_nonce( 'tkt_page_nonce', 'tkt_page_nonce' ) + ) { + $paged = (int) $_GET['paged'];// phpcs:ignore. + } + // Build "prev" link "paged" value. + $prev = filter_var( + $paged - 1, + FILTER_VALIDATE_INT, + array( + 'options' => array( + 'default' => 0, + 'min_range' => 0, + 'max_range' => $last, + ), + ) + ); + // Build "next" link "paged" value. + $next = filter_var( + $paged + 1, + FILTER_VALIDATE_INT, + array( + 'options' => array( + 'default' => 0, + 'min_range' => 1, + 'max_range' => $last, + ), + ) + ); + + return $$return; + } + + /** + * Create a safe HTML search form input + */ + private function search_form() { + + ?> +
    + validate_get_nonce( 'tkt-src-nonce', 'tkt-src-nonce' ) + ) { + $query = sanitize_text_field( wp_unslash( $_GET['s'] ) );// phpcs:ignore. + } + ?> + + $val ) {// phpcs:ignore. + if ( 'paged' !== $key + && 'refresh' !== $key + && 's' !== $key + ) { + echo ''; + } + } + } + wp_nonce_field( 'tkt-src-nonce', 'tkt-src-nonce', false ); + ?> +
    + */ -class Cp_Plgn_Drctry_GitHub { - - /** - * The ID of this plugin. - * - * @since 1.2.0 - * @access private - * @var string $plugin_name The ID of this plugin. - */ - private $plugin_name; - - /** - * The unique prefix of this plugin. - * - * @since 1.2.0 - * @access private - * @var string $plugin_prefix The string used to uniquely prefix technical functions of this plugin. - */ - private $plugin_prefix; - - /** - * The version of this plugin. - * - * @since 1.2.0 - * @access private - * @var string $version The current version of this plugin. - */ - private $version; - - /** - * The Github Org - * - * @since 1.2.0 - * @access private - * @var string $git_org GitHub Organization. - */ - private $git_org; - /** - * The GitHub ORG URL - * - * @since 1.2.0 - * @access private - * @var string $git_url GitHub URL. - */ - private $git_url; - /** - * TukuToi Plugins seem to not follow best practices! - * - * @since 1.2.0 - * @access private - * @var array $tukutoi_plugin_names A mess that beda has cooked for himself. - */ - private $tukutoi_plugin_names; - - /** - * Initialize the class and set its properties. - * - * @since 1.2.0 - * @param string $plugin_name The name of this plugin. - * @param string $plugin_prefix The unique prefix of this plugin. - * @param string $version The version of this plugin. - */ - public function __construct( $plugin_name, $plugin_prefix, $version ) { - - $this->plugin_name = $plugin_name; - $this->plugin_prefix = $plugin_prefix; - $this->version = $version; - $this->options = get_option( 'cp_dir_opts_options', array( 'cp_dir_opts_exteranal_org_repos' => $this->vetted_orgs() ) ); - } +trait Cp_Plgn_Drctry_GitHub { /** * If available, get GitHub Auth Token. @@ -110,7 +47,7 @@ private function set_auth() { /** * Get all Git Plugins Public function. */ - public function get_git_plugins() { + private function get_git_plugins() { $git_plugins = array(); @@ -119,16 +56,26 @@ public function get_git_plugins() { && ! empty( $this->options['cp_dir_opts_exteranal_org_repos'] ) ) { foreach ( $this->options['cp_dir_opts_exteranal_org_repos'] as $org ) { - $org_url = 'https://api.github.com/orgs/' . $org . '/repos'; - $git_plugins = array_merge( $git_plugins, $this->build_git_plugins_object( $org_url ) ); + // $org_url = 'https://api.github.com/orgs/' . $org . '/repos'; + $org_url = 'https://api.github.com/search/repositories?q=topic:classicpress-plugin+org:' . $org; + $git_plugins = array_merge( $git_plugins, $this->get_git_repos( $org_url, $org, 'Organization' ) ); } } if ( isset( $this->options['cp_dir_opts_exteranal_user_repos'] ) && ! empty( $this->options['cp_dir_opts_exteranal_user_repos'] ) ) { foreach ( $this->options['cp_dir_opts_exteranal_user_repos'] as $user ) { - $user_url = 'https://api.github.com/users/' . $user . '/repos'; - $git_plugins = array_merge( $git_plugins, $this->build_git_plugins_object( $user_url ) ); + // $user_url = 'https://api.github.com/users/' . $user . '/repos'; + $user_url = 'https://api.github.com/search/repositories?q=topic:classicpress-plugin+user:' . $user; + $git_plugins = array_merge( $git_plugins, $this->get_git_repos( $user_url, $user, 'User' ) ); + } + } + if ( isset( $this->options['cp_dir_opts_exteranal_repos'] ) + && ! empty( $this->options['cp_dir_opts_exteranal_repos'] ) + ) { + foreach ( $this->options['cp_dir_opts_exteranal_repos'] as $repo ) { + $repo_url = 'https://api.github.com/repos/' . $repo; + $git_plugins = array_merge( $git_plugins, $this->get_git_repos( $repo_url, strtok( $repo, '/' ), 'Repository' ) ); } } } @@ -138,99 +85,89 @@ public function get_git_plugins() { } /** - * @since 1.3.0 - */ - private function vetted_orgs() { - $orgs = json_decode( $this->get_file_contents( '/partials/github-orgs.txt' ) ); - $_orgs = array(); - foreach ( $orgs as $org ) { - $_orgs[] = $org->slug; - } - - return $_orgs; - } - - /** - * CP Way of getting File Contents. + * Get all pages of the results found. + * + * @param array $response the WP Remote Get Response. + * @return int $last_page The last (amount of) page found. */ - private function get_file_contents( $file ) { + private function get_gh_pages( $response ) { - global $wp_filesystem; + $pages = wp_remote_retrieve_header( 'links', $response ); + $last_page = 0; - if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) { - include_once ABSPATH . 'wp-admin/includes/file.php'; - $creds = request_filesystem_credentials( site_url() ); - wp_filesystem( $creds ); + if ( ! empty( $pages->link ) ) { + $_links = explode( ',', $pages ); + $last_page = (int) rtrim( strtok( $link[1], '&page=' ), '>; rel="last"' ); } - $contents = $wp_filesystem->get_contents( __DIR__ . $file ); - - return $contents; + return $last_page; } + /** * Get Plugins stored on Git. * * Currently only supports TukuToi Org. * + * @param string $url The URL to get remote response from. + * @param string $name The name of the repository. + * @param string $domain the type of repository (org or name). * @return array $git_plugins A CP API Compatible array of plugin data. */ - private function build_git_plugins_object( $url ) { + private function get_git_repos( $url, $name, $domain ) { - $git_plugins = array(); + $all_git_plugins = array(); $data = array(); - $repos = wp_remote_get( $url, $this->set_auth() ); $_data = array( 'developers' => array(), ); + $repos = $this->get_remote_decoded_body( $url, $this->set_auth() ); + + if ( false !== $repos + && 404 !== $repos + ) { + + if ( 'Repository' !== $domain ) { + $pages = $this->get_gh_pages( $repos ); + $page = 0; - if ( wp_remote_retrieve_response_code( $repos ) === 200 ) { - $repos = json_decode( wp_remote_retrieve_body( $repos ) ); + while ( $page <= $pages ) { + $all_git_plugins = array_merge( $all_git_plugins, $this->build_git_plugins_objects( $repos, $_data ) ); + $repos = $this->get_remote_decoded_body( $url . '?page=' . $page + 1, $this->set_auth() ); + $page++; + } + } else { + if ( in_array( $this->plugins_topic, $repos->topics ) ) { + $all_git_plugins[] = $this->build_git_plugin_object( $repos, $_data ); + } + } + } elseif ( 404 === $repos ) { + // Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. + echo '

    ' . sprintf( esc_html__( 'We cannot find any %1$s by name "%2$s". Perhaps you made a typo when registering it the ClassicPress Repositories Settings, or the %1$s by name "%2$s" has been deleted from GitHub.', 'cp-plgn-drctry' ), esc_html( $domain ), esc_html( $name ) ) . '

    '; } else { - echo '

    ' . esc_html__( 'We could not reach the GitHub Repositories API. It is possible you reached the limits of the GitHub Repositories API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ) . '

    '; - error_log( print_r( $repos, true ) ); - return $git_plugins; + // Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. + echo '

    ' . sprintf( esc_html__( 'We could not fetch data for the %1$s "%2$s". It is possible you reached the limits of the GitHub Repositories API.', 'cp-plgn-drctry' ), esc_html( $domain ), esc_html( $name ) ) . '

    '; } - foreach ( $repos as $repo_object ) { + return $all_git_plugins; - if ( in_array( 'classicpress-plugin', $repo_object->topics ) ) { - $release_data = $this->get_git_release_data( $repo_object->releases_url, $repo_object->name ); + } - $data['name'] = $this->get_readme_name( $repo_object->name, $repo_object->owner->login, $repo_object->default_branch ); - $data['description'] = $repo_object->description; - $data['downloads'] = $release_data['count']; - $data['changelog'] = $release_data['changelog']; + /** + * Build a CP APi compatible array of objects of repository data. + * + * @param array $repos The repositories found by the query. + * @param array $_data Array placeholder to cache remote Developer. + * @return array $git_plugins An array of repository data Objects. + */ + private function build_git_plugins_objects( $repos, $_data ) { + + $git_plugins = array(); + + foreach ( $repos->items as $repo_object ) { + + $git_plugins[] = (object) $this->build_git_plugin_object( $repo_object, $_data ); - /** - * Avoid hitting the API again if the Developer is already stored in a previous instance. - */ - if ( ! array_key_exists( $repo_object->owner->login, $_data['developers'] ) ) { - $data['developer'] = $this->get_git_dev_info( $repo_object->owner->login, $repo_object->owner->type ); - } else { - $data['developer'] = $_data['developers'][ $repo_object->owner->login ]; - } - $_data['developers'][ $repo_object->owner->login ] = $data['developer']; - - $data['slug'] = $repo_object->name; - $data['web_url'] = $repo_object->html_url; - $data['minimum_wp_version'] = '4.9.15'; - $data['minimum_cp_version'] = '1.2.0'; - $data['current_version'] = $release_data['version']; - $data['latest_cp_compatible_version'] = ''; - $data['git_provider'] = 'GitHub'; - $data['repo_url'] = $repo_object->html_url; - $data['download_link'] = $release_data['download_link']; - $data['comment'] = ''; - $data['type'] = (object) array( - 'key' => 'CP', - 'value' => 0, - 'description' => 'Developed for ClassicPress', - ); - $data['published_at'] = $release_data['updated_at']; - - $git_plugins[] = (object) $data; - } } return $git_plugins; @@ -238,22 +175,72 @@ private function build_git_plugins_object( $url ) { } /** - * Get Title (first line of .md or .txt, upper or lower case, after # or ==) + * Build a CP APi compatible object of repository data. * - * @param string $item The repository slug/name. - * @param string $login The repo owner name. + * @param array $repo_object The repositories found by the query. + * @param array $_data Array placeholder to cache remote Developer. + * @return object $git_plugins An object of Repo data. + */ + private function build_git_plugin_object( $repo_object, $_data ) { + + $release_data = $this->get_git_release_data( $repo_object->releases_url, $repo_object->name, $repo_object->owner->login ); + $data['name'] = $this->get_readme_name( $repo_object->name, $repo_object->owner->login, $repo_object->default_branch ); + $data['description'] = $repo_object->description; + $data['downloads'] = $release_data['count']; + $data['changelog'] = $release_data['changelog']; + /** + * Avoid hitting the API again if the Developer is already stored in a previous instance. + */ + if ( ! array_key_exists( $repo_object->owner->login, $_data['developers'] ) ) { + $data['developer'] = $this->get_git_dev_info( $repo_object->owner->login, $repo_object->owner->type ); + } else { + $data['developer'] = $_data['developers'][ $repo_object->owner->login ]; + } + $_data['developers'][ $repo_object->owner->login ] = $data['developer']; + $data['slug'] = $repo_object->name; + $data['web_url'] = $repo_object->html_url; + $data['minimum_wp_version'] = false; + $data['minimum_cp_version'] = false; + $data['current_version'] = $release_data['version']; + $data['latest_cp_compatible_version'] = false; + $data['git_provider'] = 'GitHub'; + $data['repo_url'] = $repo_object->html_url; + $data['download_link'] = $release_data['download_link']; + $data['comment'] = false; + $data['type'] = (object) array( + 'key' => 'GH', + 'value' => 0, + 'description' => 'Pulled from GitHub', + ); + $data['published_at'] = $release_data['updated_at']; + + return (object) $data; + + } + + /** + * Get the "name" of the plugin - assumed to be first text following a '# ' in the readme. * * @since 1.3.0 + * @param string $item The repository slug/name. + * @param string $login The repo owner name. + * @param string $branch The default branch of the repository. + * @return string $title The "name" of this plugin. */ private function get_readme_name( $item, $login, $branch ) { - $title = esc_html__( 'No Title Found. ErrNo: CP-GH-249', 'cp-plgn-drctry' ); - $first_line = strtok( $this->get_readme_data( $item, $login, $branch ), "\n" ); + $title = esc_html__( 'No Title Found. You have to manage this Plugin manually.', 'cp-plgn-drctry' ); + $readme = $this->get_readme_data( $item, $login, $branch ); + $token = 'md' === pathinfo( $this->readme_var, PATHINFO_EXTENSION ) ? '#' : '==='; + + if ( '===' === $token ) { + $first_line = $this->get_content_between( $readme, $token, $token ); + } else { + $first_line = $this->get_content_between( $readme, $token, "\n" ); + } - if ( strpos( $this->variation, '.md' ) !== false ) { - $title = sanitize_text_field( trim( str_replace( '#', '', $first_line ) ) ); - } elseif ( strpos( $this->variation, '.txt' ) !== false ) { - $title = sanitize_text_field( trim( str_replace( '==', '', $first_line ) ) ); + if ( ! empty( $first_line ) ) { + $title = sanitize_text_field( trim( $first_line[0] ) ); } return $title; @@ -261,43 +248,61 @@ private function get_readme_name( $item, $login, $branch ) { } /** - * Get readme data (.md or .txt, upper or lower case) + * Get readme data * - * @param string $item The repository slug/name. - * @param string $login The repo owner name. + * Readme can be: + * README.md + * readme.md + * README.txt + * readme.txt * * @since 1.3.0 + * @param string $item The repository slug/name. + * @param string $login The repo owner name. + * @param string $branch The default branch of the repository. + * @return string $readme The Readme Content. */ private function get_readme_data( $item, $login, $branch ) { - $data = ''; - $readme_variations = array( 'README.md', 'readme.md', 'README.txt', 'readme.txt' ); + $readme = ''; + $has_readme = false; - foreach ( $readme_variations as $variation ) { - $readme = wp_remote_get( 'https://raw.githubusercontent.com/' . $login . '/' . $item . '/' . $branch . '/' . $variation ); - if ( wp_remote_retrieve_response_code( $readme ) !== 404 ) { - $this->variation = $variation; + foreach ( $this->readme_vars as $var ) { + + $readme = $this->get_remote_raw_body( 'https://raw.githubusercontent.com/' . $login . '/' . $item . '/' . $branch . '/' . $var ); + + /** + * This should make sure that we have at least some form of non-error string as response. + * It might be literally anything though, at this point. + */ + if ( false !== $readme + && is_string( $readme ) + && ! empty( $readme ) + ) { + $has_readme = true; + $this->readme_var = $var; break; + } } - if ( wp_remote_retrieve_response_code( $readme ) === 200 ) { - $data = wp_remote_retrieve_body( $readme ); - } else { - echo '

    ' . sprintf( esc_html__( 'We could not find a readme .md or .txt file for the Repository "%1$s". This can result in incomplete data. You should report this issue to %2$s (The Developer)', 'cp-plgn-drctry' ), $item, $login ) . '

    '; - error_log( print_r( $readme, true ) ); - return $data; + if ( false === $has_readme ) { + + // Translators: %1$s: name if repository, %2$s name of repository owner. + echo '

    ' . sprintf( esc_html__( 'We could not find a readme .md or .txt file for the Repository "%1$s". This can result in incomplete data. You should report this issue to %2$s (The Developer)', 'cp-plgn-drctry' ), esc_html( $item ), esc_html( $login ) ) . '

    '; + } - return $data; + return $readme; } /** - * Get developer info from remote. + * Get developer info from Github. * * @param string $login The Github "slug". * @param string $type The Github domain type. + * @return array $dev_array A CP API Compatible "developer" array. */ private function get_git_dev_info( $login, $type ) { @@ -311,24 +316,32 @@ private function get_git_dev_info( $login, $type ) { ); $_type = 'Organization' === $type ? 'orgs' : 'users'; - $dev = wp_remote_get( 'https://api.github.com/' . $_type . '/' . $login, $this->set_auth() ); + $dev = $this->get_remote_decoded_body( 'https://api.github.com/' . $_type . '/' . $login, $this->set_auth() ); + + if ( false !== $dev + && 404 !== $dev + ) { + + $dev_array = array( + 'name' => $dev->name, + 'slug' => strtolower( $dev->login ), + 'web_url' => $dev->html_url, + 'username' => '', + 'website' => $dev->blog, + 'published_at' => $dev->created_at, + ); + + } elseif ( 404 === $dev ) { + + // Translators: %1$s: type of repository, %2$s name of repository owner. + echo '

    ' . sprintf( esc_html__( 'We could not find the GitHub User/Org API for the GitHub %1$s "%2$s".', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; - if ( wp_remote_retrieve_response_code( $dev ) === 200 ) { - $dev = json_decode( wp_remote_retrieve_body( $dev ) ); } else { - echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub User/Org API for the GitHub %1$s "%2$s". It is possible you reached the limits of the GitHub User/Org API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; - error_log( print_r( $dev, true ) ); - return $dev_array; - } - $dev_array = array( - 'name' => $dev->name, - 'slug' => strtolower( $dev->login ), - 'web_url' => $dev->url, - 'username' => '', - 'website' => $dev->blog, - 'published_at' => $dev->created_at, - ); + // Translators: %1$s: type of repository, %2$s name of repository owner. + echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub User/Org API for the GitHub %1$s "%2$s". It is possible you reached the limits of the GitHub User/Org API.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; + + } return (object) $dev_array; @@ -337,10 +350,12 @@ private function get_git_dev_info( $login, $type ) { /** * Get Release Data from GitHub * - * @param string $release_url The Github API Releases URL. - * @return array $release_data An array with some Data from GitHub api about release. + * @param string $release_url The Github API Releases URL. + * @param string $repo_name The repository Name. + * @param string $owner The name of the repo owner. + * @return array $release_data A CP API Compatible "release" data array. */ - private function get_git_release_data( $release_url, $repo_name ) { + private function get_git_release_data( $release_url, $repo_name, $owner ) { $release_data = array( 'version' => '', @@ -351,22 +366,28 @@ private function get_git_release_data( $release_url, $repo_name ) { ); $url = str_replace( '{/id}', '/latest', $release_url ); - $release = wp_remote_get( $url, $this->set_auth() ); - if ( wp_remote_retrieve_response_code( $release ) === 200 ) { - $release = json_decode( wp_remote_retrieve_body( $release ) ); + $release = $this->get_remote_decoded_body( $url, $this->set_auth() ); + + if ( false !== $release + && 404 !== $release + ) { + $release_data['version'] = $release->tag_name; $release_data['download_link'] = $release->assets[0]->browser_download_url; $release_data['count'] = $release->assets[0]->download_count; $release_data['changelog'] = $release->body; $release_data['updated_at'] = $release->assets[0]->updated_at; - } elseif ( wp_remote_retrieve_response_code( $release ) === 404 ) { + + } elseif ( 404 === $release ) { + // translators: %s: Name of remote GitHub Directory. - echo '

    ' . sprintf( esc_html__( 'It does not seem that the Repository "%s" follows best practices. We could not find any Release for it on GitHub.', 'cp-plgn-drctry' ), $repo_name ) . '

    '; - error_log( print_r( $release, true ) ); + echo '

    ' . sprintf( esc_html__( 'It does not seem that the Repository "%1$s" by %2$s follows best practices. We could not find any Release for it on GitHub.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; + } else { + // translators: %s: Name of remote GitHub Directory. - echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the repository "%s". It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $repo_name ) ) . '

    '; - error_log( print_r( $release, true ) ); + echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the repository "%1$s" by %2$s. It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; + } return $release_data; diff --git a/admin/class-cp-plgn-drctry-plugin-fx.php b/admin/class-cp-plgn-drctry-plugin-fx.php new file mode 100644 index 0000000..808907b --- /dev/null +++ b/admin/class-cp-plgn-drctry-plugin-fx.php @@ -0,0 +1,293 @@ + + */ +class Cp_Plgn_Drctry_Plugin_Fx { + + /** + * Load Arbitrary Functions. + */ + use Cp_Plgn_Drctry_Fx; + + /** + * The ID of this plugin. + * + * @since 1.0.0 + * @access private + * @var string $plugin_name The ID of this plugin. + */ + private $plugin_name; + + /** + * The unique prefix of this plugin. + * + * @since 1.0.0 + * @access private + * @var string $plugin_prefix The string used to uniquely prefix technical functions of this plugin. + */ + private $plugin_prefix; + + /** + * The version of this plugin. + * + * @since 1.0.0 + * @access private + * @var string $version The current version of this plugin. + */ + private $version; + + /** + * Initialize the class and set its properties. + * + * @since 1.0.0 + * @param string $plugin_name The name of this plugin. + * @param string $plugin_prefix The unique prefix of this plugin. + * @param string $version The version of this plugin. + */ + public function __construct( $plugin_name, $plugin_prefix, $version ) { + + $this->plugin_name = $plugin_name; + $this->plugin_prefix = $plugin_prefix; + $this->version = $version; + + } + + /** + * Install a Plugin. + * + * @since 1.1.3 Added overwrite_package argument + * @param bool $overwrite Whether to overwrite the plugin or not. Default False. + */ + public function install_cp_plugin( $overwrite = false ) { + + $this->validate_post_nonce( '_ajax_nonce', 'updates' ); + $this->maybe_send_json_failure( 'url' ); + $plugin = $this->get_posted_data( 'url', 'esc_url_raw' ); + + /** + * We include Upgrader Class. + * + * @todo Check this path on EACH CP UPDATE. It might change! + */ + include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + $upgrader = new Plugin_Upgrader(); + $response = $upgrader->install( $plugin, array( 'overwrite_package' => $overwrite ) ); + + wp_send_json( $response ); + + } + + /** + * Activate a Plugin. + */ + public function activate_cp_plugin() { + + $this->validate_post_nonce( '_ajax_nonce', 'updates' ); + $this->maybe_send_json_failure( 'slug' ); + $plugin = $this->get_posted_data( 'slug', 'sanitize_text_field' ); + + /** + * The function returns a WP error if something went wrong, + * null otherwise. + */ + $activated = activate_plugin( $plugin ); + + wp_send_json( $activated ); + + } + + /** + * Deactivate a Plugin. + */ + public function deactivate_cp_plugin() { + + $this->validate_post_nonce( '_ajax_nonce', 'updates' ); + $this->maybe_send_json_failure( 'slug' ); + $plugin = $this->get_posted_data( 'slug', 'sanitize_text_field' ); + + /** + * This function does not return anything. + * We have no way of knowing whether the plugin was deactivated or not. + * We however reload the page in JS after this operation, so the new status will tell. + */ + deactivate_plugins( $plugin, true ); + + wp_send_json( 'Plugin Possibly Updated' ); + + } + + /** + * Delete a Plugin. + */ + public function delete_cp_plugin() { + + $this->validate_post_nonce( '_ajax_nonce', 'updates' ); + $this->maybe_send_json_failure( 'plugin' ); + $plugin = $this->get_posted_data( 'plugin', 'sanitize_text_field' ); + + /** + * This returns true on success, false if $Plugin is empty, + * null if creds are missing, WP Error on failure. + */ + $deleted = delete_plugins( array( $plugin ) ); + + if ( false === $deleted ) { + // creds are missing. + $deleted = esc_html__( 'The Plugin Slug is missing from delete_plugins() function.', 'cp-plgn-drctry' ); + } elseif ( null === $deleted ) { + $deleted = esc_html__( 'Filesystem Credentials are required. You are not allowed to perform this action.', 'cp-plgn-drctry' ); + } elseif ( is_wp_error( $deleted ) ) { + $deleted = esc_html__( 'There has been an error. Please check the error logs.', 'cp-plgn-drctry' ); + } elseif ( true !== $deleted ) { + $deleted = esc_html__( 'Unknown error occurred', 'cp-plgn-drctry' ); + } + + wp_send_json( $deleted ); + + } + + /** + * Update a Plugin. + */ + public function update_cp_plugin() { + + $this->validate_post_nonce( '_ajax_nonce', 'updates' ); + $this->maybe_send_json_failure( 'slug' ); + + /** + * We cannot use Upgrader Class, because CP has no way of + * selecting custom file URL. Only WP Can do that. + * + * We simply replace the plugin entirely. + * + * @since 1.0.0 Update Plugin + * @since 1.1.3 Update itself + */ + $this->install_cp_plugin( true ); + + } + + /** + * Helper function to check if plugin has update. + * + * @param object $plugins All Plugin Objects in array. + * @return bool $is_installed If the plugin is installed or not. + */ + public function has_update( $plugins ) { + + $updates = array(); + foreach ( $plugins as $plugin ) { + if ( $this->check_plugin_installed( $plugin ) ) { + + $current_installed_version = get_plugins()[ $this->plugin_slug( $plugin ) ]['Version']; + $remote_version = $plugin->current_version; + $has_update = version_compare( $current_installed_version, $remote_version ); + if ( -1 === $has_update ) { + $updates[ $this->plugin_slug( $plugin ) ] = array( $current_installed_version, $remote_version ); + } + } + } + + return $updates; + + } + + /** + * Helper function to check if plugin is installed. + * + * @param object $plugin The Current Plugin Object. + * @return bool $is_installed If the plugin is installed or not. + */ + public function check_plugin_installed( $plugin ) { + + $plugin_filename = str_replace( '.zip', '', basename( $plugin->download_link ) ); + $plugin_slug = $plugin_filename . '/' . $plugin_filename . '.php'; + $installed_plugins = get_plugins(); + + $is_installed = array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true ) || array_search( $plugin->name, array_column( $installed_plugins, 'Name' ) ) !== false; + + return $is_installed; + + } + + /** + * Check if plugin is active. + * + * @param object $plugin The Current Plugin Object. + * @return bool $is_active If the Plugin is active. + */ + public function check_plugin_active( $plugin ) { + + $is_active = is_plugin_active( $this->plugin_slug( $plugin ) ); + + return $is_active; + } + + /** + * Is Plugin active and installed. + * + * @param object $plugin The Current Plugin Object. + * @return string $plugin_slug The Plugin Slug. + */ + public function plugin_slug( $plugin ) { + + $is_active = false; + $plugin_filename = str_replace( '.zip', '', basename( $plugin->download_link ) ); + $plugin_slug = $plugin_filename . '/' . $plugin_filename . '.php'; + $is_active = is_plugin_active( $plugin_slug ); + + if ( false === $is_active ) { + /** + * Handle bad plugins. + * + * It could be that some bad practice was followed + * and the plugin-folder/name.php does not match the downloaded item. + * This is not best practice, + * but unfortunately WP has allowed it, + * so for backward(s compatibility) reasons we make sure that + * if folder/name are a mismatch we can still check on the plugin state. + * + * First, get all installed plugins. + * From that array, search if the current Plugin Name (from API) is + * within the installed plugins. + * If so, get the key of that active plugin from the array. + * Then, fetch the proper slug of that plugin from the keys array. + * Then, repopulate $plugin_slug for later usage too. + */ + $installed_plugins = get_plugins(); + $plugin_key = array_search( $plugin->name, array_column( $installed_plugins, 'Name' ) ); + $keys = array_keys( $installed_plugins ); + if ( false !== $plugin_key ) { + $plugin_slug = $keys[ $plugin_key ]; + } + } + + return $plugin_slug; + } + +} diff --git a/admin/class-cp-plgn-drctry-settings.php b/admin/class-cp-plgn-drctry-settings.php index b4b12b5..a94f02b 100644 --- a/admin/class-cp-plgn-drctry-settings.php +++ b/admin/class-cp-plgn-drctry-settings.php @@ -21,6 +21,11 @@ */ class Cp_Plgn_Drctry_Settings { + /** + * Include arbitrary functions + */ + use Cp_Plgn_Drctry_Fx; + /** * The ID of this plugin. * @@ -61,30 +66,32 @@ public function __construct( $plugin_name, $plugin_prefix, $version ) { $this->plugin_name = $plugin_name; $this->plugin_prefix = $plugin_prefix; $this->version = $version; - $this->vetted_orgs = $this->vetted_orgs(); $this->verified_badge = 'classicpress-logo-feather-gradient-on-transparent'; } - private function vetted_orgs() { - $orgs = json_decode( $this->get_file_contents( '/partials/github-orgs.txt' ) ); - $_orgs = array(); - foreach ( $orgs as $org ) { - $_orgs[] = $org->slug; - } - - return $_orgs; - } - /** - * Custom Options and settings. + * Custom Setting sections and fields. + * + * Registers a new setting: + * - `cp_dir_opts_options` + * + * Adds two sections: + * - `cp_dir_opts_section_external_repos` + * - `cp_dir_opts_section_github_token` + * Adds four setting fields: + * + * - `cp_dir_opts_exteranal_org_repos` + * - `cp_dir_opts_exteranal_user_repos` + * - `cp_dir_opts_exteranal_repos` + * - `cp_dir_opts_section_github_token` + * + * @since 1.3.0 */ public function settings_init() { - // Register a new setting for "cp_dir_opts" page. register_setting( 'cp_dir_opts', 'cp_dir_opts_options' ); - // Register a new section in the "cp_dir_opts" page. add_settings_section( 'cp_dir_opts_section_external_repos', __( 'External ClassicPress Repositories', 'cp-plgn-drctry' ), @@ -92,7 +99,6 @@ public function settings_init() { 'cp_dir_opts' ); - // Register a new section in the "cp_dir_opts" page. add_settings_section( 'cp_dir_opts_section_github_token', __( 'Your personal GitHub Token', 'cp-plgn-drctry' ), @@ -100,10 +106,8 @@ public function settings_init() { 'cp_dir_opts' ); - // Register a new field in the "cp_dir_opts_section_external_repos" section, inside the "cp_dir_opts" page. add_settings_field( 'cp_dir_opts_exteranal_org_repos', // As of WP 4.6 this value is used only internally. - // Use $args' label_for to populate the id inside the callback. __( 'External Organization Repositories', 'cp-plgn-drctry' ), array( $this, 'external_org_repos_select_cb' ), 'cp_dir_opts', @@ -116,8 +120,7 @@ public function settings_init() { ); add_settings_field( - 'cp_dir_opts_exteranal_user_repos', // As of WP 4.6 this value is used only internally. - // Use $args' label_for to populate the id inside the callback. + 'cp_dir_opts_exteranal_user_repos', __( 'External Users Repositories', 'cp-plgn-drctry' ), array( $this, 'external_user_repos_select_cb' ), 'cp_dir_opts', @@ -130,8 +133,20 @@ public function settings_init() { ); add_settings_field( - 'cp_dir_opts_section_github_token', // As of WP 4.6 this value is used only internally. - // Use $args' label_for to populate the id inside the callback. + 'cp_dir_opts_exteranal_repos', + __( 'External Single Repositories', 'cp-plgn-drctry' ), + array( $this, 'external_repos_select_cb' ), + 'cp_dir_opts', + 'cp_dir_opts_section_external_repos', + array( + 'label_for' => 'cp_dir_opts_exteranal_repos', + 'class' => 'cp_dir_opts_row', + 'cp_dir_opts_custom_data' => 'custom', + ) + ); + + add_settings_field( + 'cp_dir_opts_section_github_token', __( 'Your personal Github Token', 'cp-plgn-drctry' ), array( $this, 'github_token_input_cb' ), 'cp_dir_opts', @@ -142,21 +157,28 @@ public function settings_init() { 'cp_dir_opts_custom_data' => 'custom', ) ); + } /** - * External repos section callback function. + * External Repos section callback function. * * @param array $args The settings array, defining title, id, callback. */ public function external_repos_cb( $args ) { ?> -

    +
    +

    + +
    + +

    +
    $this->vetted_orgs() ) ); $_options = ''; - if ( false !== $options && ! empty( $options ) ) { + if ( false !== $options + && ! empty( $options ) + && isset( $options[ $args['label_for'] ] ) + ) { $orgs = $options[ $args['label_for'] ]; foreach ( $orgs as $org ) { $locked = in_array( $org, $this->vetted_orgs() ) ? 'locked="locked"' : ''; @@ -222,7 +247,10 @@ public function external_user_repos_select_cb( $args ) { // Get the value of the setting we've registered with register_setting(). $options = get_option( 'cp_dir_opts_options' ); $_options = ''; - if ( false !== $options && ! empty( $options ) ) { + if ( false !== $options + && ! empty( $options ) + && isset( $options[ $args['label_for'] ] ) + ) { $orgs = $options[ $args['label_for'] ]; foreach ( $orgs as $org ) { $_options .= ''; @@ -248,38 +276,65 @@ class="cp-dir-select2" ' . esc_html( $org ) . ''; + } + } + ?> + +

    + +

    + + + style="width: 100%;" + > get_contents( __DIR__ . $file ); - - return $contents; - - } - } diff --git a/admin/css/cp-plgn-drctry-select2.css b/admin/css/cp-plgn-drctry-settings.css similarity index 100% rename from admin/css/cp-plgn-drctry-select2.css rename to admin/css/cp-plgn-drctry-settings.css diff --git a/admin/js/cp-plgn-drctry-select2.js b/admin/js/cp-plgn-drctry-settings.js similarity index 100% rename from admin/js/cp-plgn-drctry-select2.js rename to admin/js/cp-plgn-drctry-settings.js diff --git a/admin/partials/cp-plgn-drctry-admin-display.php b/admin/partials/cp-plgn-drctry-admin-display.php index 82105b8..2b90315 100644 --- a/admin/partials/cp-plgn-drctry-admin-display.php +++ b/admin/partials/cp-plgn-drctry-admin-display.php @@ -53,9 +53,9 @@ check_plugin_installed( $single_plugin ); - $is_active = $this->check_plugin_active( $single_plugin ); - $plugin_slug = $this->plugin_slug( $single_plugin ); + $is_installed = $this->plugin_fx->check_plugin_installed( $single_plugin ); + $is_active = $this->plugin_fx->check_plugin_active( $single_plugin ); + $plugin_slug = $this->plugin_fx->plugin_slug( $single_plugin ); /** * Not all plugin developers have a forum profile. */ diff --git a/changelog.txt b/changelog.txt index 8e316e5..bfddb1d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,11 +1,12 @@ = 1.3.0 = [Added] Plugin Settings Page (under Admin > Settings > Manage CP Repos) -[Added] Setting to add custom GitHub Repositories of Orgs and/or Users. +[Added] Setting to add custom GitHub Repositories of Orgs, Users or single Repos. [Added] Setting to store Personal GitHub Token, which increases the API Limits to 5k hourly instead of 60. [Added] Verified Orgs (_not users_) are pre-selected. A PR can be used to add new Orgs to the vetted list. [Added] Fundations to read remote readme, README, (both in md or txt) files. Currently used ony for below [Fixed] item. [Fixed] Problem where plugins with foldername/distinct-filename.php AND a unguessable Plugin Title could not be managed. [Improved] Make drastically less calls to the GitHub API by re-using already queried data as much as possible. +[Improved] Refactored Plugin Code. = 1.2.0 = [Added] GitHub Repo Sync for (TukuToi) Plugins diff --git a/includes/class-cp-plgn-drctry.php b/includes/class-cp-plgn-drctry.php index b06ef87..9b5812f 100644 --- a/includes/class-cp-plgn-drctry.php +++ b/includes/class-cp-plgn-drctry.php @@ -125,15 +125,35 @@ private function load_dependencies() { */ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-cp-plgn-drctry-i18n.php'; + /** + * The trait responsible for providing all arbitrary actions used through the plugin. + */ + require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-cp-plgn-drctry-fx.php'; + /** * The class responsible for defining all actions that occur in the admin area. */ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-cp-plgn-drctry-admin.php'; + /** + * The class responsible for defining all actions used to manage plugins. + */ + require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-cp-plgn-drctry-plugin-fx.php'; + + /** + * The class responsible for getting all CP Dir API plugins. + */ + require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-cp-plgn-drctry-cp-api.php'; + + /** + * The class responsible for getting all CP Dir API plugins. + */ + require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-cp-plgn-drctry-github.php'; + /** * The class responsible for defining all CP Dir listing Features. */ - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-cp-plgn-drctry-cp-dir.php'; + require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-cp-plgn-drctry-cp-plugins-dir.php'; /** * The class responsible for getting GitHub Items. @@ -184,15 +204,16 @@ private function define_admin_hooks() { ) { $plugin_admin = new Cp_Plgn_Drctry_Admin( $this->get_plugin_name(), $this->get_plugin_prefix(), $this->get_version() ); + $plugin_manage = new Cp_Plgn_Drctry_Plugin_Fx( $this->get_plugin_name(), $this->get_plugin_prefix(), $this->get_version() ); $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' ); $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' ); - $this->loader->add_action( 'admin_menu', $plugin_admin, 'add_plugins_list' ); - $this->loader->add_action( 'wp_ajax_install_cp_plugin', $plugin_admin, 'install_cp_plugin' ); - $this->loader->add_action( 'wp_ajax_update_cp_plugin', $plugin_admin, 'update_cp_plugin' ); - $this->loader->add_action( 'wp_ajax_deactivate_cp_plugin', $plugin_admin, 'deactivate_cp_plugin' ); - $this->loader->add_action( 'wp_ajax_activate_cp_plugin', $plugin_admin, 'activate_cp_plugin' ); - $this->loader->add_action( 'wp_ajax_delete-plugin', $plugin_admin, 'delete_cp_plugin' ); + $this->loader->add_action( 'admin_menu', $plugin_admin, 'add_menu_pages' ); + $this->loader->add_action( 'wp_ajax_install_cp_plugin', $plugin_manage, 'install_cp_plugin' ); + $this->loader->add_action( 'wp_ajax_update_cp_plugin', $plugin_manage, 'update_cp_plugin' ); + $this->loader->add_action( 'wp_ajax_deactivate_cp_plugin', $plugin_manage, 'deactivate_cp_plugin' ); + $this->loader->add_action( 'wp_ajax_activate_cp_plugin', $plugin_manage, 'activate_cp_plugin' ); + $this->loader->add_action( 'wp_ajax_delete-plugin', $plugin_manage, 'delete_cp_plugin' ); } /** diff --git a/readme.md b/readme.md index 79c4e6c..d9607df 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,5 @@ -# ClassicPress Plugin Directory +# ClassicPress Directory Integration + [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=TukuToi_tukutoi-cp-directory-integration&metric=bugs)](https://sonarcloud.io/summary/new_code?id=TukuToi_tukutoi-cp-directory-integration) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=TukuToi_tukutoi-cp-directory-integration&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=TukuToi_tukutoi-cp-directory-integration) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=TukuToi_tukutoi-cp-directory-integration&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=TukuToi_tukutoi-cp-directory-integration) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=TukuToi_tukutoi-cp-directory-integration&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=TukuToi_tukutoi-cp-directory-integration) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=TukuToi_tukutoi-cp-directory-integration&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=TukuToi_tukutoi-cp-directory-integration) [![slack](https://img.shields.io/badge/Community%20%26%20Support-grey?style=for-the-badge&logo=slack&logoColor=white&label=slack&labelColor=4A154B)](https://tukutoi.slack.com/join/shared_invite/zt-1b1x1844z-_~~4pikNzssevxwnx3BqCA#/shared-invite/email) @@ -23,7 +24,10 @@ The plugin requires wp_remote_get and file_put_contents to work properly on the It is possible to manage plugins that are not listed in the ClassicPress Directory with this plugin as well. The conditions for this to work are: - the GitHub stored Plugin MUST have a tag `classicpress-plugin`. -- the GitHub Repository MUST have a valid Release tag named with a SemVer release version (like `1.0.0`) and Public Release with a manually uploaded Release Asset in Zip Format. This ZIP MUST be uploaded to the release section for `Attach binaries by dropping them here or selecting them.` +- the GitHub Repository MUST have a valid Release tag named with a SemVer release version (like `1.0.0`) . +- the release MUST have a manually uploaded Zip Asset uploaded to the release section for `Attach binaries by dropping them here or selecting them.` holding the plugin. +- the repository MUST have EITHER OR BOTH a readme.txt OR readme.md (can be all uppercase too). The readme.txt is prioritized and MUST follow the WordPress readme.txt rules. The readme.md file is used only as backup, and if used, MUST have at least one line featuring `# Plugin Name Here`. +- the repository MUST be public. By default, there is a _vetted list_ of _Organizations_ added to the plugin. If a Developer wants to appear on said list, they can submit a PR to the `github-orgs.txt` File of this Plugin, by adding their Guthub Organization data to the JSON array. @@ -42,12 +46,13 @@ Other, non verified Repositories (both users and orgs) can still be added easily ### 1.3.0 [Added] Plugin Settings Page (under Admin > Settings > Manage CP Repos) -[Added] Setting to add custom GitHub Repositories of Orgs and/or Users. +[Added] Setting to add custom GitHub Repositories of Orgs, Users or single Repos. [Added] Setting to store Personal GitHub Token, which increases the API Limits to 5k hourly instead of 60. [Added] Verified Orgs (_not users_) are pre-selected. A PR can be used to add new Orgs to the vetted list. [Added] Fundations to read remote readme, README, (both in md or txt) files. Currently used ony for below [Fixed] item. [Fixed] Problem where plugins with foldername/distinct-filename.php AND a unguessable Plugin Title could not be managed. [Improved] Make drastically less calls to the GitHub API by re-using already queried data as much as possible. +[Improved] Refactored Plugin Code. ### 1.2.0 [Added] GitHub Repo Sync for (TukuToi) Plugins diff --git a/tukutoi-cp-directory-integration.php b/tukutoi-cp-directory-integration.php index 188552e..6bd3f3c 100644 --- a/tukutoi-cp-directory-integration.php +++ b/tukutoi-cp-directory-integration.php @@ -12,7 +12,7 @@ * @package Cp_Plgn_Drctry * * @wordpress-plugin - * Plugin Name: CP Plugin Directory + * Plugin Name: ClassicPress Directory Integration * Plugin URI: https://www.tukutoi.com/ * Description: Integrates the ClassicPress Plugin Directory and Plugins stored on GitHub (tagged with classicpress-plugin) into the ClassicPress Admin Interface. * Version: 1.3.0 From 3cc46a8a3dfab1e6d79d4bf0d1a422d4db897927 Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Sun, 3 Jul 2022 14:11:26 +0700 Subject: [PATCH 06/14] 03-07-2022 [Changed] Do not fetch plugins anymore from GitHub if they have no release [Changed] Make notices dismissable --- admin/class-cp-plgn-drctry-cp-api.php | 2 +- admin/class-cp-plgn-drctry-github.php | 29 ++++++++++++--------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/admin/class-cp-plgn-drctry-cp-api.php b/admin/class-cp-plgn-drctry-cp-api.php index 7716a50..e12cf22 100644 --- a/admin/class-cp-plgn-drctry-cp-api.php +++ b/admin/class-cp-plgn-drctry-cp-api.php @@ -43,7 +43,7 @@ private function get_cp_pages() { } else { - echo '

    ' . esc_html__( 'We could not reach the ClassicPress Directory API. It is possible you reached the limits of the ClassicPress Directory API.', 'cp-plgn-drctry' ) . '

    '; + echo '

    ' . esc_html__( 'We could not reach the ClassicPress Directory API. It is possible you reached the limits of the ClassicPress Directory API.', 'cp-plgn-drctry' ) . '

    '; $pages = array(); } diff --git a/admin/class-cp-plgn-drctry-github.php b/admin/class-cp-plgn-drctry-github.php index b7f3c3c..5871fbd 100644 --- a/admin/class-cp-plgn-drctry-github.php +++ b/admin/class-cp-plgn-drctry-github.php @@ -143,10 +143,10 @@ private function get_git_repos( $url, $name, $domain ) { } } elseif ( 404 === $repos ) { // Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. - echo '

    ' . sprintf( esc_html__( 'We cannot find any %1$s by name "%2$s". Perhaps you made a typo when registering it the ClassicPress Repositories Settings, or the %1$s by name "%2$s" has been deleted from GitHub.', 'cp-plgn-drctry' ), esc_html( $domain ), esc_html( $name ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We cannot find any %1$s by name "%2$s". Perhaps you made a typo when registering it the ClassicPress Repositories Settings, or the %1$s by name "%2$s" has been deleted from GitHub.', 'cp-plgn-drctry' ), esc_html( $domain ), esc_html( $name ) ) . '

    '; } else { // Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. - echo '

    ' . sprintf( esc_html__( 'We could not fetch data for the %1$s "%2$s". It is possible you reached the limits of the GitHub Repositories API.', 'cp-plgn-drctry' ), esc_html( $domain ), esc_html( $name ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not fetch data for the %1$s "%2$s". It is possible you reached the limits of the GitHub Repositories API.', 'cp-plgn-drctry' ), esc_html( $domain ), esc_html( $name ) ) . '

    '; } return $all_git_plugins; @@ -166,11 +166,11 @@ private function build_git_plugins_objects( $repos, $_data ) { foreach ( $repos->items as $repo_object ) { - $git_plugins[] = (object) $this->build_git_plugin_object( $repo_object, $_data ); + $git_plugins[] = $this->build_git_plugin_object( $repo_object, $_data ); } - return $git_plugins; + return array_filter( $git_plugins ); } @@ -184,6 +184,9 @@ private function build_git_plugins_objects( $repos, $_data ) { private function build_git_plugin_object( $repo_object, $_data ) { $release_data = $this->get_git_release_data( $repo_object->releases_url, $repo_object->name, $repo_object->owner->login ); + if ( empty( $release_data ) ) { + return; // If the plugin has no releases, we exclude. + } $data['name'] = $this->get_readme_name( $repo_object->name, $repo_object->owner->login, $repo_object->default_branch ); $data['description'] = $repo_object->description; $data['downloads'] = $release_data['count']; @@ -289,7 +292,7 @@ private function get_readme_data( $item, $login, $branch ) { if ( false === $has_readme ) { // Translators: %1$s: name if repository, %2$s name of repository owner. - echo '

    ' . sprintf( esc_html__( 'We could not find a readme .md or .txt file for the Repository "%1$s". This can result in incomplete data. You should report this issue to %2$s (The Developer)', 'cp-plgn-drctry' ), esc_html( $item ), esc_html( $login ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not find a readme .md or .txt file for the Repository "%1$s". This can result in incomplete data. You should report this issue to %2$s (The Developer)', 'cp-plgn-drctry' ), esc_html( $item ), esc_html( $login ) ) . '

    '; } @@ -334,12 +337,12 @@ private function get_git_dev_info( $login, $type ) { } elseif ( 404 === $dev ) { // Translators: %1$s: type of repository, %2$s name of repository owner. - echo '

    ' . sprintf( esc_html__( 'We could not find the GitHub User/Org API for the GitHub %1$s "%2$s".', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not find the GitHub User/Org API for the GitHub %1$s "%2$s".', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; } else { // Translators: %1$s: type of repository, %2$s name of repository owner. - echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub User/Org API for the GitHub %1$s "%2$s". It is possible you reached the limits of the GitHub User/Org API.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub User/Org API for the GitHub %1$s "%2$s". It is possible you reached the limits of the GitHub User/Org API.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; } @@ -357,13 +360,7 @@ private function get_git_dev_info( $login, $type ) { */ private function get_git_release_data( $release_url, $repo_name, $owner ) { - $release_data = array( - 'version' => '', - 'download_link' => '', - 'count' => 0, - 'changelog' => '', - 'updated_at' => '', - ); + $release_data = array(); $url = str_replace( '{/id}', '/latest', $release_url ); $release = $this->get_remote_decoded_body( $url, $this->set_auth() ); @@ -381,12 +378,12 @@ private function get_git_release_data( $release_url, $repo_name, $owner ) { } elseif ( 404 === $release ) { // translators: %s: Name of remote GitHub Directory. - echo '

    ' . sprintf( esc_html__( 'It does not seem that the Repository "%1$s" by %2$s follows best practices. We could not find any Release for it on GitHub.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'It does not seem that the Repository "%1$s" by %2$s follows best practices. We could not find any Release for it on GitHub. We excluded it from this list, you can either handle it yourself by downloading it from GitHub, or/and contact the Author and ask to use proper Releases.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; } else { // translators: %s: Name of remote GitHub Directory. - echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the repository "%1$s" by %2$s. It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the repository "%1$s" by %2$s. It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; } From 7e1ed7600dde609ecbe416b27b113864644071ca Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Sun, 3 Jul 2022 18:34:45 +0700 Subject: [PATCH 07/14] 03-7-2022 [Added] Populated .pot file with strings [Fixed] Broken/nonsense localised string in admin-display.php (props @bahiirwa) --- .../partials/cp-plgn-drctry-admin-display.php | 3 +- languages/cp-plgn-drctry.pot | 0 .../tukutoi-cp-directory-integration.pot | 219 ++++++++++++++++++ 3 files changed, 221 insertions(+), 1 deletion(-) delete mode 100644 languages/cp-plgn-drctry.pot create mode 100644 languages/tukutoi-cp-directory-integration.pot diff --git a/admin/partials/cp-plgn-drctry-admin-display.php b/admin/partials/cp-plgn-drctry-admin-display.php index 2b90315..eeaa75f 100644 --- a/admin/partials/cp-plgn-drctry-admin-display.php +++ b/admin/partials/cp-plgn-drctry-admin-display.php @@ -19,7 +19,8 @@
    $plugin_versions ) { - echo '

    ' . esc_html( get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_name )['Name'] ) . ' should be updated to ' . esc_html( $plugin_versions[1] ) . '

    '; + // Translators: %1$s: Plugin Name, %2$s Plugin Version. + echo '

    ' . sprintf( esc_html__( ' %1$s should be updated to %2$s' ), esc_html( get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_name )['Name'] ), esc_html( $plugin_versions[1] ) ) . '

    '; } ?>
    diff --git a/languages/cp-plgn-drctry.pot b/languages/cp-plgn-drctry.pot deleted file mode 100644 index e69de29..0000000 diff --git a/languages/tukutoi-cp-directory-integration.pot b/languages/tukutoi-cp-directory-integration.pot new file mode 100644 index 0000000..4fa6a31 --- /dev/null +++ b/languages/tukutoi-cp-directory-integration.pot @@ -0,0 +1,219 @@ +# Copyright (C) 2022 bedas +# This file is distributed under the same license as the ClassicPress Directory Integration plugin. +msgid "" +msgstr "" +"Project-Id-Version: ClassicPress Directory Integration 1.3.0\n" +"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/tukutoi-cp-directory-integration\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2022-07-03T11:32:51+00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"X-Generator: WP-CLI 2.4.0\n" +"X-Domain: cp-plgn-drctry\n" + +#. Plugin Name of the plugin +msgid "ClassicPress Directory Integration" +msgstr "" + +#. Plugin URI of the plugin +#. Author URI of the plugin +msgid "https://www.tukutoi.com/" +msgstr "" + +#. Description of the plugin +msgid "Integrates the ClassicPress Plugin Directory and Plugins stored on GitHub (tagged with classicpress-plugin) into the ClassicPress Admin Interface." +msgstr "" + +#. Author of the plugin +msgid "bedas" +msgstr "" + +#: admin/class-cp-plgn-drctry-admin.php:121 +#: admin/class-cp-plgn-drctry-admin.php:148 +msgid "ClassicPress Plugins" +msgstr "" + +#: admin/class-cp-plgn-drctry-admin.php:122 +msgid "Manage CP Plugins" +msgstr "" + +#: admin/class-cp-plgn-drctry-admin.php:131 +#: admin/class-cp-plgn-drctry-admin.php:173 +msgid "ClassicPress Repositories" +msgstr "" + +#: admin/class-cp-plgn-drctry-admin.php:132 +msgid "Manage CP Repos" +msgstr "" + +#: admin/class-cp-plgn-drctry-admin.php:149 +msgid "Browse, Install and Activate ClassicPress Plugins" +msgstr "" + +#: admin/class-cp-plgn-drctry-cp-api.php:46 +msgid "We could not reach the ClassicPress Directory API. It is possible you reached the limits of the ClassicPress Directory API." +msgstr "" + +#: admin/class-cp-plgn-drctry-cp-api.php:78 +msgid "We could not reach some sub-page of the ClassicPress Directory API. It is possible you have reached the ClassicPress Directory API Limits." +msgstr "" + +#: admin/class-cp-plgn-drctry-cp-plugins-dir.php:165 +msgid "Nothing Found" +msgstr "" + +#. Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. +#: admin/class-cp-plgn-drctry-github.php:146 +msgid "We cannot find any %1$s by name \"%2$s\". Perhaps you made a typo when registering it the ClassicPress Repositories Settings, or the %1$s by name \"%2$s\" has been deleted from GitHub." +msgstr "" + +#. Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. +#: admin/class-cp-plgn-drctry-github.php:149 +msgid "We could not fetch data for the %1$s \"%2$s\". It is possible you reached the limits of the GitHub Repositories API." +msgstr "" + +#: admin/class-cp-plgn-drctry-github.php:235 +msgid "No Title Found. You have to manage this Plugin manually." +msgstr "" + +#. Translators: %1$s: name if repository, %2$s name of repository owner. +#: admin/class-cp-plgn-drctry-github.php:295 +msgid "We could not find a readme .md or .txt file for the Repository \"%1$s\". This can result in incomplete data. You should report this issue to %2$s (The Developer)" +msgstr "" + +#. Translators: %1$s: type of repository, %2$s name of repository owner. +#: admin/class-cp-plgn-drctry-github.php:340 +msgid "We could not find the GitHub User/Org API for the GitHub %1$s \"%2$s\"." +msgstr "" + +#. Translators: %1$s: type of repository, %2$s name of repository owner. +#: admin/class-cp-plgn-drctry-github.php:345 +msgid "We could not reach the GitHub User/Org API for the GitHub %1$s \"%2$s\". It is possible you reached the limits of the GitHub User/Org API." +msgstr "" + +#. translators: %s: Name of remote GitHub Directory. +#: admin/class-cp-plgn-drctry-github.php:381 +msgid "It does not seem that the Repository \"%1$s\" by %2$s follows best practices. We could not find any Release for it on GitHub. We excluded it from this list, you can either handle it yourself by downloading it from GitHub, or/and contact the Author and ask to use proper Releases." +msgstr "" + +#. translators: %s: Name of remote GitHub Directory. +#: admin/class-cp-plgn-drctry-github.php:386 +msgid "We could not reach the GitHub Releases API for the repository \"%1$s\" by %2$s. It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the \"Personal GitHub Token\" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight." +msgstr "" + +#: admin/class-cp-plgn-drctry-plugin-fx.php:161 +msgid "The Plugin Slug is missing from delete_plugins() function." +msgstr "" + +#: admin/class-cp-plgn-drctry-plugin-fx.php:163 +msgid "Filesystem Credentials are required. You are not allowed to perform this action." +msgstr "" + +#: admin/class-cp-plgn-drctry-plugin-fx.php:165 +msgid "There has been an error. Please check the error logs." +msgstr "" + +#: admin/class-cp-plgn-drctry-plugin-fx.php:167 +msgid "Unknown error occurred" +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:97 +msgid "External ClassicPress Repositories" +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:104 +msgid "Your personal GitHub Token" +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:111 +msgid "External Organization Repositories" +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:124 +msgid "External Users Repositories" +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:137 +msgid "External Single Repositories" +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:150 +msgid "Your personal Github Token" +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:172 +msgid "Add external (GitHub) Organizations or Users from which you want to pull Themes or Plugins from." +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:174 +msgid "The integration will automatically scan the external Repositories by the \"classicpress-plugin\" topic when listing Plugins, and \"classicpress-theme\" when listing Themes." +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:187 +msgid "Add your personal GitHub Token to avoid API locking." +msgstr "" + +#. translators: %s: SVG Badge/Logo. +#: admin/class-cp-plgn-drctry-settings.php:234 +msgid "Search and select additional Repositories managed by GitHub Organizations. Organizations with a %s badge cannot be removed, and have been vetted by the ClassicPress Community. Other Organizations not featuring the badge are not vetted by the community." +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:274 +msgid "Search and select additional Repositories managed by GitHub Users. Caution: users are not always vetted by the community." +msgstr "" + +#: admin/class-cp-plgn-drctry-settings.php:313 +msgid "Add additional Single Repositories from GitHub. You must save these in the OWNER/REPO format" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:32 +msgid "Refresh List" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:36 +msgid "Reset" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:38 +msgid "Plugins list navigation" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:40 +#: admin/partials/cp-plgn-drctry-admin-display.php:146 +msgid "Plugins found" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:42 +#: admin/partials/cp-plgn-drctry-admin-display.php:148 +msgid "First page" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:43 +#: admin/partials/cp-plgn-drctry-admin-display.php:149 +msgid "Previous page" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:45 +#: admin/partials/cp-plgn-drctry-admin-display.php:151 +msgid "Next page" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:46 +#: admin/partials/cp-plgn-drctry-admin-display.php:152 +msgid "Last page" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:119 +msgid "Contact the Developer" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:120 +msgid "Report this Plugin" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:126 +msgid "Read More on GitHub" +msgstr "" From ad42a62b5afe9807d0f4b9eac97485e1c3601ee5 Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Mon, 4 Jul 2022 11:36:09 +0700 Subject: [PATCH 08/14] 04-07-2022 [Fixed] Several Translation strings [Added] Localised Select2 Placeholder --- admin/class-cp-plgn-drctry-admin.php | 9 +++- admin/class-cp-plgn-drctry-cp-api.php | 4 +- admin/class-cp-plgn-drctry-cp-plugins-dir.php | 54 +++++++++++++++++++ admin/class-cp-plgn-drctry-fx.php | 2 +- admin/class-cp-plgn-drctry-github.php | 12 ++--- admin/class-cp-plgn-drctry-plugin-fx.php | 1 + admin/class-cp-plgn-drctry-settings.php | 27 ++++++---- admin/js/cp-plgn-drctry-settings.js | 54 ++++++++++--------- .../partials/cp-plgn-drctry-admin-display.php | 19 ++++--- 9 files changed, 131 insertions(+), 51 deletions(-) diff --git a/admin/class-cp-plgn-drctry-admin.php b/admin/class-cp-plgn-drctry-admin.php index 213bbec..2680e3a 100644 --- a/admin/class-cp-plgn-drctry-admin.php +++ b/admin/class-cp-plgn-drctry-admin.php @@ -105,6 +105,13 @@ public function enqueue_scripts( $hook_suffix ) { } elseif ( 'settings_page_cp_dir_opts' === $hook_suffix ) { wp_enqueue_script( 'select2', 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js', array( 'jquery' ), '4.1.0-rc.0', false ); wp_enqueue_script( $this->plugin_prefix . 'settings', plugin_dir_url( __FILE__ ) . 'js/cp-plgn-drctry-settings.js', array( 'select2' ), $this->version, false ); + wp_localize_script( + $this->plugin_prefix . 'settings', + 'settings_object', + array( + 'placeholder' => esc_html_( 'Type and press return to add a new Item.', 'cp-plgn-drctry' ), + ) + ); } } @@ -146,7 +153,7 @@ public function render() {

    -

    +

    plugin_name, $this->plugin_prefix, $this->version ); diff --git a/admin/class-cp-plgn-drctry-cp-api.php b/admin/class-cp-plgn-drctry-cp-api.php index e12cf22..94a3acc 100644 --- a/admin/class-cp-plgn-drctry-cp-api.php +++ b/admin/class-cp-plgn-drctry-cp-api.php @@ -43,7 +43,7 @@ private function get_cp_pages() { } else { - echo '

    ' . esc_html__( 'We could not reach the ClassicPress Directory API. It is possible you reached the limits of the ClassicPress Directory API.', 'cp-plgn-drctry' ) . '

    '; + echo '

    ' . esc_html__( 'We could not reach the ClassicPress API. It is possible you reached the limits of the ClassicPress API.', 'cp-plgn-drctry' ) . '

    '; $pages = array(); } @@ -75,7 +75,7 @@ private function get_cp_plugins() { } else { - echo ''; + echo ''; } } diff --git a/admin/class-cp-plgn-drctry-cp-plugins-dir.php b/admin/class-cp-plgn-drctry-cp-plugins-dir.php index f432d71..2dc2f83 100644 --- a/admin/class-cp-plgn-drctry-cp-plugins-dir.php +++ b/admin/class-cp-plgn-drctry-cp-plugins-dir.php @@ -55,6 +55,60 @@ class Cp_Plgn_Drctry_Cp_Plugins_Dir { */ private $version; + /** + * The ClassicPress API URL. + * + * @since 1.3.0 + * @access private + * @var string $cp_dir_url The URL used by ClassicPress to present its API. + */ + private $cp_dir_url; + + /** + * The Plugins Cache File path. + * + * @since 1.3.0 + * @access private + * @var string $plugins_cache_file The File used by this plugin to store the Plugins in a cache. + */ + private $plugins_cache_file; + + /** + * The Instance of the Plugin Functionality. + * + * @since 1.3.0 + * @access private + * @var object $plugin_fx The instance of the Cp_Plgn_Drctry_Plugin_Fx() Class handling plugins. + */ + private $plugin_fx; + + /** + * The Options of this Plugin. + * + * @since 1.3.0 + * @access private + * @var array $options The options stored by the user for this plugin. + */ + private $options; + + /** + * The Variations of readmes supported. + * + * @since 1.3.0 + * @access private + * @var array $readme_vars The different variations of readme supported by the plugin. + */ + private $readme_vars; + + /** + * The Topics searched for. + * + * @since 1.3.0 + * @access private + * @var string $plugins_topic The Topic searched for in the Github repos. + */ + private $plugins_topic; + /** * Initialize the class and set its properties. * diff --git a/admin/class-cp-plgn-drctry-fx.php b/admin/class-cp-plgn-drctry-fx.php index 46880ba..f9fecbe 100644 --- a/admin/class-cp-plgn-drctry-fx.php +++ b/admin/class-cp-plgn-drctry-fx.php @@ -316,7 +316,7 @@ private function search_form() { $query = sanitize_text_field( wp_unslash( $_GET['s'] ) );// phpcs:ignore. } ?> - + $val ) {// phpcs:ignore. diff --git a/admin/class-cp-plgn-drctry-github.php b/admin/class-cp-plgn-drctry-github.php index 5871fbd..d08f983 100644 --- a/admin/class-cp-plgn-drctry-github.php +++ b/admin/class-cp-plgn-drctry-github.php @@ -143,7 +143,7 @@ private function get_git_repos( $url, $name, $domain ) { } } elseif ( 404 === $repos ) { // Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. - echo '

    ' . sprintf( esc_html__( 'We cannot find any %1$s by name "%2$s". Perhaps you made a typo when registering it the ClassicPress Repositories Settings, or the %1$s by name "%2$s" has been deleted from GitHub.', 'cp-plgn-drctry' ), esc_html( $domain ), esc_html( $name ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We cannot find any %1$s by name "%2$s". Perhaps you made a typo when registering it in the ClassicPress Repositories Settings, or the %1$s by name "%2$s" has been deleted from GitHub.', 'cp-plgn-drctry' ), esc_html( $domain ), esc_html( $name ) ) . '

    '; } else { // Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. echo '

    ' . sprintf( esc_html__( 'We could not fetch data for the %1$s "%2$s". It is possible you reached the limits of the GitHub Repositories API.', 'cp-plgn-drctry' ), esc_html( $domain ), esc_html( $name ) ) . '

    '; @@ -292,7 +292,7 @@ private function get_readme_data( $item, $login, $branch ) { if ( false === $has_readme ) { // Translators: %1$s: name if repository, %2$s name of repository owner. - echo '

    ' . sprintf( esc_html__( 'We could not find a readme .md or .txt file for the Repository "%1$s". This can result in incomplete data. You should report this issue to %2$s (The Developer)', 'cp-plgn-drctry' ), esc_html( $item ), esc_html( $login ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not find a readme (.md or .txt) file for the Repository "%1$s". This can result in incomplete data. You should report this issue to the repository owner "%2$s"', 'cp-plgn-drctry' ), esc_html( $item ), esc_html( $login ) ) . '

    '; } @@ -337,12 +337,12 @@ private function get_git_dev_info( $login, $type ) { } elseif ( 404 === $dev ) { // Translators: %1$s: type of repository, %2$s name of repository owner. - echo '

    ' . sprintf( esc_html__( 'We could not find the GitHub User/Org API for the GitHub %1$s "%2$s".', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not find any information about the owner of the %1$s with name "%2$s".', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; } else { // Translators: %1$s: type of repository, %2$s name of repository owner. - echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub User/Org API for the GitHub %1$s "%2$s". It is possible you reached the limits of the GitHub User/Org API.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub API for the GitHub %1$s "%2$s". It is possible you reached the limits of the GitHub API.', 'cp-plgn-drctry' ), esc_html( $type ), esc_html( $login ) ) . '

    '; } @@ -378,12 +378,12 @@ private function get_git_release_data( $release_url, $repo_name, $owner ) { } elseif ( 404 === $release ) { // translators: %s: Name of remote GitHub Directory. - echo '

    ' . sprintf( esc_html__( 'It does not seem that the Repository "%1$s" by %2$s follows best practices. We could not find any Release for it on GitHub. We excluded it from this list, you can either handle it yourself by downloading it from GitHub, or/and contact the Author and ask to use proper Releases.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not find any Public Release for the "%1$s" Repository on GitHub. We excluded it from this list. You can either download the code yourself from GitHub, or/and contact the owner "%2$s" and ask them to provide Public Releases.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; } else { // translators: %s: Name of remote GitHub Directory. - echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub Releases API for the repository "%1$s" by %2$s. It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the "Personal GitHub Token" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; + echo '

    ' . sprintf( esc_html__( 'We could not reach the GitHub API for the GitHub Repository "%1$s" by %2$s. It is possible you reached the limits of the GitHub API.', 'cp-plgn-drctry' ), esc_html( $repo_name ), esc_html( $owner ) ) . '

    '; } diff --git a/admin/class-cp-plgn-drctry-plugin-fx.php b/admin/class-cp-plgn-drctry-plugin-fx.php index 808907b..9895b13 100644 --- a/admin/class-cp-plgn-drctry-plugin-fx.php +++ b/admin/class-cp-plgn-drctry-plugin-fx.php @@ -137,6 +137,7 @@ public function deactivate_cp_plugin() { */ deactivate_plugins( $plugin, true ); + // This string is never seen by anyone, so it does not need to be translated nor escaped. wp_send_json( 'Plugin Possibly Updated' ); } diff --git a/admin/class-cp-plgn-drctry-settings.php b/admin/class-cp-plgn-drctry-settings.php index a94f02b..55951a4 100644 --- a/admin/class-cp-plgn-drctry-settings.php +++ b/admin/class-cp-plgn-drctry-settings.php @@ -53,6 +53,15 @@ class Cp_Plgn_Drctry_Settings { */ private $version; + /** + * The Badge for verified Repos. + * + * @since 1.3.0 + * @access private + * @var string $version Badge used for verified Orgs from GitHub. + */ + private $verified_badge; + /** * Initialize the class and set its properties. * @@ -108,7 +117,7 @@ public function settings_init() { add_settings_field( 'cp_dir_opts_exteranal_org_repos', // As of WP 4.6 this value is used only internally. - __( 'External Organization Repositories', 'cp-plgn-drctry' ), + __( 'GitHub Organizations', 'cp-plgn-drctry' ), array( $this, 'external_org_repos_select_cb' ), 'cp_dir_opts', 'cp_dir_opts_section_external_repos', @@ -121,7 +130,7 @@ public function settings_init() { add_settings_field( 'cp_dir_opts_exteranal_user_repos', - __( 'External Users Repositories', 'cp-plgn-drctry' ), + __( 'GitHub Users', 'cp-plgn-drctry' ), array( $this, 'external_user_repos_select_cb' ), 'cp_dir_opts', 'cp_dir_opts_section_external_repos', @@ -134,7 +143,7 @@ public function settings_init() { add_settings_field( 'cp_dir_opts_exteranal_repos', - __( 'External Single Repositories', 'cp-plgn-drctry' ), + __( 'Single GitHub Repositories', 'cp-plgn-drctry' ), array( $this, 'external_repos_select_cb' ), 'cp_dir_opts', 'cp_dir_opts_section_external_repos', @@ -169,7 +178,7 @@ public function external_repos_cb( $args ) { ?>

    - +

    @@ -184,7 +193,7 @@ public function external_repos_cb( $args ) { */ public function github_token_cb( $args ) { ?> -

    +

    verified_badge );// phpcs:ignore + // translators: %s: Verified Organizations Badge. + printf( esc_html__( 'Add GitHub Organizations by typing their exact Name, then press return/enter on your keyboard. Organizations with a %s badge cannot be removed, and have been vetted by the ClassicPress Community. Other Organizations not featuring the badge are not vetted by the community.', 'cp-plgn-drctry' ), $this->verified_badge );// phpcs:ignore ?>

    - +

    - +

    '); - //$(container).addClass('locked-tag'); - tag.locked = true; - } - return tag.text; - }, - }) - .on('select2:unselecting', function(e){ - // before removing tag we check option element of tag and - // if it has property 'locked' we will create error to prevent all select2 functionality - if ($(e.params.args.data.element).attr('locked')) { - e.preventDefault(); - } - }); + if ($option.attr( 'locked' )) { + $( container ).find( "button" ).replaceWith( '' ); + // $(container).addClass('locked-tag'); + tag.locked = true; + } + return tag.text; + }, + } + ) + .on( + 'select2:unselecting', + function(e){ + // before removing tag we check option element of tag and + // if it has property 'locked' we will create error to prevent all select2 functionality. + if ($( e.params.args.data.element ).attr( 'locked' )) { + e.preventDefault(); + } + } + ); } ); diff --git a/admin/partials/cp-plgn-drctry-admin-display.php b/admin/partials/cp-plgn-drctry-admin-display.php index eeaa75f..c187e26 100644 --- a/admin/partials/cp-plgn-drctry-admin-display.php +++ b/admin/partials/cp-plgn-drctry-admin-display.php @@ -76,22 +76,27 @@ - Install Now + - Activate Now + - Update Now - current_version ) ); ?> + + + current_version ) ); + ?> + - Deactivate Now + @@ -100,13 +105,13 @@ ?>
  • - Delete +
  • - More Details +
  • From b0075705b5b6c74e5f1ba2ecd7c295987bb07ee7 Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Mon, 4 Jul 2022 11:52:26 +0700 Subject: [PATCH 09/14] 04-07-2022 [Changed] Readme --- README.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.txt b/README.txt index a581376..eb2d6e7 100644 --- a/README.txt +++ b/README.txt @@ -30,7 +30,8 @@ The conditions for this to work are: - the GitHub stored Plugin MUST have a tag `classicpress-plugin`. - the GitHub Repository MUST have a valid Release tag named with a SemVer release version (like `1.0.0`) . - the release MUST have a manually uploaded Zip Asset uploaded to the release section for `Attach binaries by dropping them here or selecting them.` holding the plugin. -- the repository MUST have EITHER OR BOTH a readme.txt OR readme.md (can be all uppercase too). The readme.txt is prioritized and MUST follow the WordPress readme.txt rules. The readme.md file is used only as backup, and if used, MUST have at least one line featuring `# Plugin Name Here`. +- the repository MUST have EITHER OR BOTH a readme.txt OR readme.md (can be all uppercase too). The readme.txt is prioritized and MUST follow the WordPress readme.txt rules with the EXCEPTION that the first line MUST match the plugin name from the plugin main file. +- The readme.md file is used only as backup, and if used, MUST have at least one line featuring `# Plugin Name Here`. - the repository MUST be public. By default, there is a _vetted list_ of _Organizations_ added to the plugin. If a Developer wants to appear on said list, From 8f531f69c96016ce8878e772f3167c15d091b5be Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Mon, 4 Jul 2022 11:58:08 +0700 Subject: [PATCH 10/14] 04-07-2022 [Updated] Translation POT File --- .../tukutoi-cp-directory-integration.pot | 147 +++++++++++------- 1 file changed, 88 insertions(+), 59 deletions(-) diff --git a/languages/tukutoi-cp-directory-integration.pot b/languages/tukutoi-cp-directory-integration.pot index 4fa6a31..5c86920 100644 --- a/languages/tukutoi-cp-directory-integration.pot +++ b/languages/tukutoi-cp-directory-integration.pot @@ -1,15 +1,15 @@ -# Copyright (C) 2022 bedas +# Copyright (C) 2022 TukuToi # This file is distributed under the same license as the ClassicPress Directory Integration plugin. msgid "" msgstr "" "Project-Id-Version: ClassicPress Directory Integration 1.3.0\n" -"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/tukutoi-cp-directory-integration\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" +"Report-Msgid-Bugs-To: https://github.com/TukuToi/tukutoi-cp-directory-integration/issues\n" +"Last-Translator: Beda Schmid \n" +"Language-Team: i18n \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2022-07-03T11:32:51+00:00\n" +"POT-Creation-Date: 2022-07-04T04:53:28+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.4.0\n" "X-Domain: cp-plgn-drctry\n" @@ -27,47 +27,47 @@ msgstr "" msgid "Integrates the ClassicPress Plugin Directory and Plugins stored on GitHub (tagged with classicpress-plugin) into the ClassicPress Admin Interface." msgstr "" -#. Author of the plugin -msgid "bedas" -msgstr "" - -#: admin/class-cp-plgn-drctry-admin.php:121 -#: admin/class-cp-plgn-drctry-admin.php:148 +#: admin/class-cp-plgn-drctry-admin.php:128 +#: admin/class-cp-plgn-drctry-admin.php:155 msgid "ClassicPress Plugins" msgstr "" -#: admin/class-cp-plgn-drctry-admin.php:122 +#: admin/class-cp-plgn-drctry-admin.php:129 msgid "Manage CP Plugins" msgstr "" -#: admin/class-cp-plgn-drctry-admin.php:131 -#: admin/class-cp-plgn-drctry-admin.php:173 +#: admin/class-cp-plgn-drctry-admin.php:138 +#: admin/class-cp-plgn-drctry-admin.php:180 msgid "ClassicPress Repositories" msgstr "" -#: admin/class-cp-plgn-drctry-admin.php:132 +#: admin/class-cp-plgn-drctry-admin.php:139 msgid "Manage CP Repos" msgstr "" -#: admin/class-cp-plgn-drctry-admin.php:149 -msgid "Browse, Install and Activate ClassicPress Plugins" +#: admin/class-cp-plgn-drctry-admin.php:156 +msgid "Browse, Install, Activate, Deactivate, Update and Delete ClassicPress Plugins" msgstr "" #: admin/class-cp-plgn-drctry-cp-api.php:46 -msgid "We could not reach the ClassicPress Directory API. It is possible you reached the limits of the ClassicPress Directory API." +msgid "We could not reach the ClassicPress API. It is possible you reached the limits of the ClassicPress API." msgstr "" #: admin/class-cp-plgn-drctry-cp-api.php:78 -msgid "We could not reach some sub-page of the ClassicPress Directory API. It is possible you have reached the ClassicPress Directory API Limits." +msgid "We could not reach sume SubPage of the ClassicPress API. It is possible you reached the limits of the ClassicPress API." msgstr "" -#: admin/class-cp-plgn-drctry-cp-plugins-dir.php:165 +#: admin/class-cp-plgn-drctry-cp-plugins-dir.php:219 msgid "Nothing Found" msgstr "" +#: admin/class-cp-plgn-drctry-fx.php:319 +msgid "Press Return To Search..." +msgstr "" + #. Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. #: admin/class-cp-plgn-drctry-github.php:146 -msgid "We cannot find any %1$s by name \"%2$s\". Perhaps you made a typo when registering it the ClassicPress Repositories Settings, or the %1$s by name \"%2$s\" has been deleted from GitHub." +msgid "We cannot find any %1$s by name \"%2$s\". Perhaps you made a typo when registering it in the ClassicPress Repositories Settings, or the %1$s by name \"%2$s\" has been deleted from GitHub." msgstr "" #. Translators: %1$s: type of GitHub account (org or user), %2$s: name of account. @@ -81,92 +81,92 @@ msgstr "" #. Translators: %1$s: name if repository, %2$s name of repository owner. #: admin/class-cp-plgn-drctry-github.php:295 -msgid "We could not find a readme .md or .txt file for the Repository \"%1$s\". This can result in incomplete data. You should report this issue to %2$s (The Developer)" +msgid "We could not find a readme (.md or .txt) file for the Repository \"%1$s\". This can result in incomplete data. You should report this issue to the repository owner \"%2$s\"" msgstr "" #. Translators: %1$s: type of repository, %2$s name of repository owner. #: admin/class-cp-plgn-drctry-github.php:340 -msgid "We could not find the GitHub User/Org API for the GitHub %1$s \"%2$s\"." +msgid "We could not find any information about the owner of the %1$s with name \"%2$s\"." msgstr "" #. Translators: %1$s: type of repository, %2$s name of repository owner. #: admin/class-cp-plgn-drctry-github.php:345 -msgid "We could not reach the GitHub User/Org API for the GitHub %1$s \"%2$s\". It is possible you reached the limits of the GitHub User/Org API." +msgid "We could not reach the GitHub API for the GitHub %1$s \"%2$s\". It is possible you reached the limits of the GitHub API." msgstr "" #. translators: %s: Name of remote GitHub Directory. #: admin/class-cp-plgn-drctry-github.php:381 -msgid "It does not seem that the Repository \"%1$s\" by %2$s follows best practices. We could not find any Release for it on GitHub. We excluded it from this list, you can either handle it yourself by downloading it from GitHub, or/and contact the Author and ask to use proper Releases." +msgid "We could not find any Public Release for the \"%1$s\" Repository on GitHub. We excluded it from this list. You can either download the code yourself from GitHub, or/and contact the owner \"%2$s\" and ask them to provide Public Releases." msgstr "" #. translators: %s: Name of remote GitHub Directory. #: admin/class-cp-plgn-drctry-github.php:386 -msgid "We could not reach the GitHub Releases API for the repository \"%1$s\" by %2$s. It is possible you reached the limits of the GitHub Releases API. We reccommend creating a GitHub Personal Token, then add it to the \"Personal GitHub Token\" setting in the Settings > Manage CP Repos menu. If you already di that, you reached 5000 hourly requests, which likely indicates that ClassicPress went viral overnight." +msgid "We could not reach the GitHub API for the GitHub Repository \"%1$s\" by %2$s. It is possible you reached the limits of the GitHub API." msgstr "" -#: admin/class-cp-plgn-drctry-plugin-fx.php:161 +#: admin/class-cp-plgn-drctry-plugin-fx.php:162 msgid "The Plugin Slug is missing from delete_plugins() function." msgstr "" -#: admin/class-cp-plgn-drctry-plugin-fx.php:163 +#: admin/class-cp-plgn-drctry-plugin-fx.php:164 msgid "Filesystem Credentials are required. You are not allowed to perform this action." msgstr "" -#: admin/class-cp-plgn-drctry-plugin-fx.php:165 +#: admin/class-cp-plgn-drctry-plugin-fx.php:166 msgid "There has been an error. Please check the error logs." msgstr "" -#: admin/class-cp-plgn-drctry-plugin-fx.php:167 +#: admin/class-cp-plgn-drctry-plugin-fx.php:168 msgid "Unknown error occurred" msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:97 +#: admin/class-cp-plgn-drctry-settings.php:106 msgid "External ClassicPress Repositories" msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:104 +#: admin/class-cp-plgn-drctry-settings.php:113 msgid "Your personal GitHub Token" msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:111 -msgid "External Organization Repositories" +#: admin/class-cp-plgn-drctry-settings.php:120 +msgid "GitHub Organizations" msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:124 -msgid "External Users Repositories" +#: admin/class-cp-plgn-drctry-settings.php:133 +msgid "GitHub Users" msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:137 -msgid "External Single Repositories" +#: admin/class-cp-plgn-drctry-settings.php:146 +msgid "Single GitHub Repositories" msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:150 +#: admin/class-cp-plgn-drctry-settings.php:159 msgid "Your personal Github Token" msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:172 -msgid "Add external (GitHub) Organizations or Users from which you want to pull Themes or Plugins from." +#: admin/class-cp-plgn-drctry-settings.php:181 +msgid "Add GitHub Organizations or Users from which you want to pull Repositories." msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:174 +#: admin/class-cp-plgn-drctry-settings.php:183 msgid "The integration will automatically scan the external Repositories by the \"classicpress-plugin\" topic when listing Plugins, and \"classicpress-theme\" when listing Themes." msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:187 -msgid "Add your personal GitHub Token to avoid API locking." +#: admin/class-cp-plgn-drctry-settings.php:196 +msgid "Add your personal GitHub Token to avoid API limit exhaustions." msgstr "" -#. translators: %s: SVG Badge/Logo. -#: admin/class-cp-plgn-drctry-settings.php:234 -msgid "Search and select additional Repositories managed by GitHub Organizations. Organizations with a %s badge cannot be removed, and have been vetted by the ClassicPress Community. Other Organizations not featuring the badge are not vetted by the community." +#. translators: %s: Verified Organizations Badge. +#: admin/class-cp-plgn-drctry-settings.php:243 +msgid "Add GitHub Organizations by typing their exact Name, then press return/enter on your keyboard. Organizations with a %s badge cannot be removed, and have been vetted by the ClassicPress Community. Other Organizations not featuring the badge are not vetted by the community." msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:274 -msgid "Search and select additional Repositories managed by GitHub Users. Caution: users are not always vetted by the community." +#: admin/class-cp-plgn-drctry-settings.php:283 +msgid "Add GitHub Users by typing their exact Name, then press return/enter on your keyboard. Caution: Users are not vetted by the community. Only Organizations can be vetted." msgstr "" -#: admin/class-cp-plgn-drctry-settings.php:313 -msgid "Add additional Single Repositories from GitHub. You must save these in the OWNER/REPO format" +#: admin/class-cp-plgn-drctry-settings.php:322 +msgid "Add Single GitHub Repositories by typing their exact Name in the OWNER/REPOSITORY format, then press return/enter on your keyboard." msgstr "" #: admin/partials/cp-plgn-drctry-admin-display.php:32 @@ -182,38 +182,67 @@ msgid "Plugins list navigation" msgstr "" #: admin/partials/cp-plgn-drctry-admin-display.php:40 -#: admin/partials/cp-plgn-drctry-admin-display.php:146 +#: admin/partials/cp-plgn-drctry-admin-display.php:151 msgid "Plugins found" msgstr "" #: admin/partials/cp-plgn-drctry-admin-display.php:42 -#: admin/partials/cp-plgn-drctry-admin-display.php:148 +#: admin/partials/cp-plgn-drctry-admin-display.php:153 msgid "First page" msgstr "" #: admin/partials/cp-plgn-drctry-admin-display.php:43 -#: admin/partials/cp-plgn-drctry-admin-display.php:149 +#: admin/partials/cp-plgn-drctry-admin-display.php:154 msgid "Previous page" msgstr "" #: admin/partials/cp-plgn-drctry-admin-display.php:45 -#: admin/partials/cp-plgn-drctry-admin-display.php:151 +#: admin/partials/cp-plgn-drctry-admin-display.php:156 msgid "Next page" msgstr "" #: admin/partials/cp-plgn-drctry-admin-display.php:46 -#: admin/partials/cp-plgn-drctry-admin-display.php:152 +#: admin/partials/cp-plgn-drctry-admin-display.php:157 msgid "Last page" msgstr "" -#: admin/partials/cp-plgn-drctry-admin-display.php:119 +#: admin/partials/cp-plgn-drctry-admin-display.php:79 +msgid "Install Now" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:83 +msgid "Activate Now" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:89 +msgid "Update Now" +msgstr "" + +#. Translators: %1$s: old version number, %2$s: new version number. +#: admin/partials/cp-plgn-drctry-admin-display.php:93 +msgid "From v%1$s to v%2$s" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:99 +msgid "Deactivate Now" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:108 +msgid "Delete" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:114 +msgid "More Details" +msgstr "" + +#: admin/partials/cp-plgn-drctry-admin-display.php:124 msgid "Contact the Developer" msgstr "" -#: admin/partials/cp-plgn-drctry-admin-display.php:120 +#: admin/partials/cp-plgn-drctry-admin-display.php:125 msgid "Report this Plugin" msgstr "" -#: admin/partials/cp-plgn-drctry-admin-display.php:126 +#: admin/partials/cp-plgn-drctry-admin-display.php:131 msgid "Read More on GitHub" msgstr "" From a76595dbb3f474f7948e43656b63c37125ba4ee6 Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Mon, 4 Jul 2022 15:31:09 +0700 Subject: [PATCH 11/14] 04-07-2022 [Changed] Execute +/- before . explicitly. --- admin/class-cp-plgn-drctry-github.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/class-cp-plgn-drctry-github.php b/admin/class-cp-plgn-drctry-github.php index d08f983..c10c45e 100644 --- a/admin/class-cp-plgn-drctry-github.php +++ b/admin/class-cp-plgn-drctry-github.php @@ -133,7 +133,7 @@ private function get_git_repos( $url, $name, $domain ) { while ( $page <= $pages ) { $all_git_plugins = array_merge( $all_git_plugins, $this->build_git_plugins_objects( $repos, $_data ) ); - $repos = $this->get_remote_decoded_body( $url . '?page=' . $page + 1, $this->set_auth() ); + $repos = $this->get_remote_decoded_body( $url . '?page=' . ( $page + 1 ), $this->set_auth() ); $page++; } } else { From 6013fe40141c00323bb81f0a594afc58ce8bcbab Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Mon, 4 Jul 2022 18:10:51 +0700 Subject: [PATCH 12/14] 04-07-2022 [Added] add .files to gitattributes to ignore on ZIP --- .gitattributes | 2 ++ tukutoi-cp-directory-integration.zip | Bin 0 -> 56700 bytes 2 files changed, 2 insertions(+) create mode 100644 tukutoi-cp-directory-integration.zip diff --git a/.gitattributes b/.gitattributes index dfe0770..dde3060 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ # Auto detect text files and perform LF normalization * text=auto +.gitattributes export-ignore +.gitignore export-ignore \ No newline at end of file diff --git a/tukutoi-cp-directory-integration.zip b/tukutoi-cp-directory-integration.zip new file mode 100644 index 0000000000000000000000000000000000000000..9156cdb9a9ba7f57acf9ece9471b83eb2efc785a GIT binary patch literal 56700 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 #F&oR}3= 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 Date: Mon, 4 Jul 2022 18:15:33 +0700 Subject: [PATCH 13/14] Delete tukutoi-cp-directory-integration.zip --- tukutoi-cp-directory-integration.zip | Bin 56700 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tukutoi-cp-directory-integration.zip diff --git a/tukutoi-cp-directory-integration.zip b/tukutoi-cp-directory-integration.zip deleted file mode 100644 index 9156cdb9a9ba7f57acf9ece9471b83eb2efc785a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56700 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 #F&oR}3= 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 Date: Mon, 4 Jul 2022 18:16:06 +0700 Subject: [PATCH 14/14] Update .gitattributes --- .gitattributes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index dde3060..a2b0f8b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ # Auto detect text files and perform LF normalization * text=auto .gitattributes export-ignore -.gitignore export-ignore \ No newline at end of file +.gitignore export-ignore +.git export-ignore \ No newline at end of file