diff --git a/.gitattributes b/.gitattributes
index dfe0770..a2b0f8b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,5 @@
# Auto detect text files and perform LF normalization
* text=auto
+.gitattributes export-ignore
+.gitignore export-ignore
+.git export-ignore
\ No newline at end of file
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/README.txt b/README.txt
index 64c34c4..eb2d6e7 100644
--- a/README.txt
+++ b/README.txt
@@ -1,10 +1,10 @@
-=== ClassicPress Plugin Directory ===
+=== ClassicPress Directory Integration ===
Contributors: bedas
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,22 @@ 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`) .
+- 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 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,
+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 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..2680e3a 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 );
}
@@ -84,7 +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_prefix . 'settings', plugin_dir_url( __FILE__ ) . 'css/cp-plgn-drctry-settings.css', array( 'select2' ), $this->version, 'all' );
}
}
@@ -99,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' ) ),
@@ -109,160 +102,26 @@ 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_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' ),
+ )
+ );
}
}
/**
- * 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.
+ * @since 1.0.0 Add Plugins List Page.
+ * @since 1.3.0 Add Settings Page.
*/
- 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.
- */
- 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',
@@ -274,19 +133,65 @@ public function add_plugins_list() {
3
);
+ 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
+ );
+
}
/**
- * Render the admin page.
+ * Render the plugins list page.
*/
public function render() {
?>
+ */
+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 API. It is possible you reached the limits of the ClassicPress 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() {
-
- ?>
-
- 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 .= '
';
+ }
+ }
+
+ 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..f9fecbe
--- /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() {
+
+ ?>
+
+
*/
-class Cp_Plgn_Drctry_GitHub {
+trait Cp_Plgn_Drctry_GitHub {
/**
- * The ID of this plugin.
+ * If available, get GitHub Auth Token.
*
- * @since 1.2.0
- * @access private
- * @var string $plugin_name The ID of this plugin.
+ * @since 1.3.0
*/
- private $plugin_name;
+ 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;
+ }
/**
- * 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.
+ * Get all Git Plugins Public function.
*/
- private $plugin_prefix;
+ private function get_git_plugins() {
+
+ $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';
+ $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';
+ $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' ) );
+ }
+ }
+ }
+
+ return $git_plugins;
+
+ }
/**
- * The version of this plugin.
+ * Get all pages of the results found.
*
- * @since 1.2.0
- * @access private
- * @var string $version The current version of this plugin.
+ * @param array $response the WP Remote Get Response.
+ * @return int $last_page The last (amount of) page found.
*/
- private $version;
+ private function get_gh_pages( $response ) {
+
+ $pages = wp_remote_retrieve_header( 'links', $response );
+ $last_page = 0;
+
+ if ( ! empty( $pages->link ) ) {
+ $_links = explode( ',', $pages );
+ $last_page = (int) rtrim( strtok( $link[1], '&page=' ), '>; rel="last"' );
+ }
+
+ return $last_page;
+
+ }
/**
- * The Github Org
+ * Get Plugins stored on Git.
*
- * @since 1.2.0
- * @access private
- * @var string $git_org GitHub Organization.
- */
- private $git_org;
- /**
- * The GitHub ORG URL
+ * Currently only supports TukuToi Org.
*
- * @since 1.2.0
- * @access private
- * @var string $git_url GitHub URL.
+ * @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 $git_url;
+ private function get_git_repos( $url, $name, $domain ) {
+
+ $all_git_plugins = array();
+ $data = array();
+ $_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;
+
+ 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 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 ) ) . '
';
+ }
+
+ return $all_git_plugins;
+
+ }
+
/**
- * TukuToi Plugins seem to not follow best practices!
+ * Build a CP APi compatible array of objects of repository data.
*
- * @since 1.2.0
- * @access private
- * @var array $tukutoi_plugin_names A mess that beda has cooked for himself.
+ * @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 $tukutoi_plugin_names;
+ private function build_git_plugins_objects( $repos, $_data ) {
+
+ $git_plugins = array();
+
+ foreach ( $repos->items as $repo_object ) {
+
+ $git_plugins[] = $this->build_git_plugin_object( $repo_object, $_data );
+
+ }
+
+ return array_filter( $git_plugins );
+
+ }
/**
- * Initialize the class and set its properties.
+ * Build a CP APi compatible object of repository data.
*
- * @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.
+ * @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.
*/
- public function __construct( $plugin_name, $plugin_prefix, $version ) {
-
- $this->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',
+ 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'];
+ $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 all Git Plugins Public function.
+ * 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.
*/
- public function get_git_plugins() {
+ private function get_readme_name( $item, $login, $branch ) {
+
+ $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" );
+ }
- return $this->build_git_plugins_object();
+ if ( ! empty( $first_line ) ) {
+ $title = sanitize_text_field( trim( $first_line[0] ) );
+ }
+
+ return $title;
}
/**
- * Get Plugins stored on Git.
+ * Get readme data
*
- * Currently only supports TukuToi Org.
+ * Readme can be:
+ * README.md
+ * readme.md
+ * README.txt
+ * readme.txt
*
- * @return array $git_plugins A CP API Compatible array of plugin data.
+ * @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 build_git_plugins_object() {
+ private function get_readme_data( $item, $login, $branch ) {
- $git_plugins = array();
- $data = array();
- $repos = wp_remote_get( $this->git_url );
- 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' ) . '
' . 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 ) ) . '
';
+
+ }
+
+ return $readme;
}
/**
- * Get Release Data from GitHub
+ * Get developer info 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 $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_release_data( $release_url, $repo_name ) {
-
- $release_data = array(
- 'version' => '',
- 'download_link' => '',
- 'count' => 0,
- 'changelog' => '',
- 'updated_at' => '',
+ 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 = $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 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 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 ) ) . '
';
+
+ }
+
+ return (object) $dev_array;
+
+ }
+
+ /**
+ * Get Release Data from GitHub
+ *
+ * @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, $owner ) {
+
+ $release_data = array();
+
$url = str_replace( '{/id}', '/latest', $release_url );
- $release = wp_remote_get( $url );
- 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 ( 404 === $release ) {
+
+ // translators: %s: Name of remote GitHub Directory.
+ 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 "%s". It is possible you reached the limits of the GitHub Releases API.', 'cp-plgn-drctry' ), esc_html( $repo_name ) ) . '
' . 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 ) ) . '
';
- $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;
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..9895b13
--- /dev/null
+++ b/admin/class-cp-plgn-drctry-plugin-fx.php
@@ -0,0 +1,294 @@
+
+ */
+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 );
+
+ // This string is never seen by anyone, so it does not need to be translated nor escaped.
+ 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
new file mode 100644
index 0000000..55951a4
--- /dev/null
+++ b/admin/class-cp-plgn-drctry-settings.php
@@ -0,0 +1,349 @@
+
+ */
+class Cp_Plgn_Drctry_Settings {
+
+ /**
+ * Include arbitrary functions
+ */
+ use Cp_Plgn_Drctry_Fx;
+
+ /**
+ * 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;
+
+ /**
+ * 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.
+ *
+ * @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->verified_badge = '';
+
+ }
+
+ /**
+ * 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_setting( 'cp_dir_opts', 'cp_dir_opts_options' );
+
+ add_settings_section(
+ 'cp_dir_opts_section_external_repos',
+ __( 'External ClassicPress Repositories', 'cp-plgn-drctry' ),
+ array( $this, 'external_repos_cb' ),
+ 'cp_dir_opts'
+ );
+
+ add_settings_section(
+ 'cp_dir_opts_section_github_token',
+ __( 'Your personal GitHub Token', 'cp-plgn-drctry' ),
+ array( $this, 'github_token_cb' ),
+ 'cp_dir_opts'
+ );
+
+ add_settings_field(
+ 'cp_dir_opts_exteranal_org_repos', // As of WP 4.6 this value is used only internally.
+ __( 'GitHub Organizations', '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',
+ __( 'GitHub Users', '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_exteranal_repos',
+ __( 'Single GitHub 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',
+ '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 ) {
+ ?>
+
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..bfddb1d 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,13 @@
+= 1.3.0 =
+[Added] Plugin Settings Page (under Admin > Settings > Manage CP Repos)
+[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
[Added] Total Page Number on pagination
diff --git a/includes/class-cp-plgn-drctry.php b/includes/class-cp-plgn-drctry.php
index 90789d9..9b5812f 100644
--- a/includes/class-cp-plgn-drctry.php
+++ b/includes/class-cp-plgn-drctry.php
@@ -125,21 +125,46 @@ 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.
*/
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();
}
@@ -179,17 +204,30 @@ 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' );
}
+ /**
+ * 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/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..5c86920
--- /dev/null
+++ b/languages/tukutoi-cp-directory-integration.pot
@@ -0,0 +1,248 @@
+# 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://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-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"
+
+#. 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 ""
+
+#: 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:129
+msgid "Manage CP Plugins"
+msgstr ""
+
+#: 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:139
+msgid "Manage CP Repos"
+msgstr ""
+
+#: 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 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 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: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 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.
+#: 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 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 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 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 "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 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:162
+msgid "The Plugin Slug is missing from delete_plugins() function."
+msgstr ""
+
+#: 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:166
+msgid "There has been an error. Please check the error logs."
+msgstr ""
+
+#: admin/class-cp-plgn-drctry-plugin-fx.php:168
+msgid "Unknown error occurred"
+msgstr ""
+
+#: admin/class-cp-plgn-drctry-settings.php:106
+msgid "External ClassicPress Repositories"
+msgstr ""
+
+#: admin/class-cp-plgn-drctry-settings.php:113
+msgid "Your personal GitHub Token"
+msgstr ""
+
+#: admin/class-cp-plgn-drctry-settings.php:120
+msgid "GitHub Organizations"
+msgstr ""
+
+#: admin/class-cp-plgn-drctry-settings.php:133
+msgid "GitHub Users"
+msgstr ""
+
+#: admin/class-cp-plgn-drctry-settings.php:146
+msgid "Single GitHub Repositories"
+msgstr ""
+
+#: admin/class-cp-plgn-drctry-settings.php:159
+msgid "Your personal Github Token"
+msgstr ""
+
+#: 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: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:196
+msgid "Add your personal GitHub Token to avoid API limit exhaustions."
+msgstr ""
+
+#. 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: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: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
+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:151
+msgid "Plugins found"
+msgstr ""
+
+#: admin/partials/cp-plgn-drctry-admin-display.php:42
+#: 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:154
+msgid "Previous page"
+msgstr ""
+
+#: admin/partials/cp-plgn-drctry-admin-display.php:45
+#: 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:157
+msgid "Last page"
+msgstr ""
+
+#: 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:125
+msgid "Report this Plugin"
+msgstr ""
+
+#: admin/partials/cp-plgn-drctry-admin-display.php:131
+msgid "Read More on GitHub"
+msgstr ""
diff --git a/readme.md b/readme.md
index d609788..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)
@@ -11,28 +12,48 @@ 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`) .
+- 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.
-### Disclaimers
-- The plugin does not take any responsibility for Plugins downloaded from the ClassicPress Directory or GitHub.
+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, 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
[Added] Total Page Number on pagination
diff --git a/tukutoi-cp-directory-integration.php b/tukutoi-cp-directory-integration.php
index f52d013..6bd3f3c 100644
--- a/tukutoi-cp-directory-integration.php
+++ b/tukutoi-cp-directory-integration.php
@@ -12,10 +12,10 @@
* @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.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