hub2wp/includes/class-h2wp-admin-ajax.php
2026-02-28 01:29:08 +01:00

470 lines
17 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles AJAX requests for the hub2wp plugin.
*/
class H2WP_Admin_Ajax {
/**
* Clean any buffered output started after the given level.
*
* @param int $buffer_level Buffer level to return to.
* @return void
*/
private function clean_ajax_buffers( $buffer_level ) {
while ( ob_get_level() > $buffer_level ) {
ob_end_clean();
}
}
/**
* Normalize repository type from request.
*
* @return string plugin|theme
*/
private function get_repo_type_from_request() {
$repo_type = isset( $_POST['repo_type'] ) ? sanitize_key( wp_unslash( $_POST['repo_type'] ) ) : 'plugin';
return in_array( $repo_type, array( 'plugin', 'theme' ), true ) ? $repo_type : 'plugin';
}
/**
* Check capability for the current repository type.
*
* @param string $repo_type Repository type.
* @return bool
*/
private function can_manage_repo_type( $repo_type ) {
$cap = ( 'theme' === $repo_type ) ? 'install_themes' : 'install_plugins';
return current_user_can( $cap );
}
/**
* Constructor.
*/
public function __construct() {
add_action( 'wp_ajax_h2wp_get_plugin_details', array( $this, 'get_plugin_details' ) );
add_action( 'wp_ajax_h2wp_check_compatibility', array( $this, 'check_compatibility' ) );
add_action( 'wp_ajax_h2wp_get_changelog', array( $this, 'get_changelog' ) );
add_action( 'wp_ajax_h2wp_install_plugin', array( $this, 'install_plugin' ) );
add_action( 'wp_ajax_h2wp_activate_plugin', array( $this, 'activate_plugin' ) );
}
/**
* Handle AJAX request to get plugin details.
*/
public function get_plugin_details() {
// Check nonce.
check_ajax_referer( 'h2wp_plugin_details_nonce', 'nonce' );
$repo_type = $this->get_repo_type_from_request();
// Check user capabilities.
if ( ! $this->can_manage_repo_type( $repo_type ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'hub2wp' ) ) );
}
// Get and sanitize parameters.
$owner = isset( $_POST['owner'] ) ? sanitize_text_field( wp_unslash( $_POST['owner'] ) ) : '';
$repo = isset( $_POST['repo'] ) ? sanitize_text_field( wp_unslash( $_POST['repo'] ) ) : '';
if ( empty( $owner ) || empty( $repo ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid parameters.', 'hub2wp' ) ) );
}
// Get access token from settings.
$access_token = H2WP_Settings::get_access_token();
$api = new H2WP_GitHub_API( $access_token );
// Fetch data.
$repo_details = $api->get_repo_details( $owner, $repo );
if ( is_wp_error( $repo_details ) ) {
wp_send_json_error( array( 'message' => $repo_details->get_error_message() ) );
}
$readme_html = $api->get_readme_html( $owner, $repo );
if ( is_wp_error( $readme_html ) ) {
$readme_html = __( 'No README available.', 'hub2wp' );
}
$readme_html = $this->strip_plugin_headers( $readme_html );
$og_image = $api->get_og_image( $owner, $repo );
if ( is_wp_error( $og_image ) ) {
$og_image = $repo_details['owner']['avatar_url'];
}
$last_updated = '';
if ( isset( $repo_details['pushed_at'] ) ) {
$last_updated = sprintf(
/* translators: %s: human-readable time difference */
__( '%s ago', 'hub2wp' ),
human_time_diff( strtotime( $repo_details['pushed_at'] ) )
);
}
// Let's try to use the pushed_at date of the default_branch
if ( isset( $repo_details['default_branch'] ) ) {
$branch_details = $api->get_branch_details( $owner, $repo, $repo_details['default_branch'] );
if ( ! is_wp_error( $branch_details ) && isset( $branch_details['commit']['commit']['author']['date'] ) ) {
$last_updated = sprintf(
/* translators: %s: human-readable time difference */
__( '%s ago', 'hub2wp' ),
human_time_diff( strtotime( $branch_details['commit']['commit']['author']['date'] ) )
);
}
}
// Prepare data.
$data = array(
'name' => isset( $repo_details['name'] ) ? $repo_details['name'] : '',
'display_name' => isset( $repo_details['name'] ) ? ucwords( str_replace( array( '-', 'wp', 'wordpress', 'seo' ), array( ' ', 'WP', 'WordPress', 'SEO' ), $repo_details['name'] ) ) : '',
'owner' => isset( $repo_details['owner']['login'] ) ? sanitize_text_field( $repo_details['owner']['login'] ) : '',
'repo' => isset( $repo_details['name'] ) ? sanitize_text_field( $repo_details['name'] ) : '',
'description' => isset( $repo_details['description'] ) ? $repo_details['description'] : '',
'readme' => $readme_html,
'stargazers' => isset( $repo_details['stargazers_count'] ) ? number_format_i18n( $repo_details['stargazers_count'] ) : '0',
'forks' => isset( $repo_details['forks_count'] ) ? number_format_i18n( $repo_details['forks_count'] ) : '0',
'watchers' => $api->get_watchers_count( $owner, $repo ),
'open_issues' => isset( $repo_details['open_issues_count'] ) ? number_format_i18n( $repo_details['open_issues_count'] ) : '0',
'html_url' => isset( $repo_details['html_url'] ) ? esc_url_raw( $repo_details['html_url'] ) : '',
'homepage' => isset( $repo_details['homepage'] ) ? esc_url_raw( $repo_details['homepage'] ) : '',
'og_image' => esc_url_raw( $og_image ),
'owner_avatar_url' => isset( $repo_details['owner']['avatar_url'] ) ? esc_url_raw( $repo_details['owner']['avatar_url'] ) : '',
'author' => isset( $repo_details['owner']['login'] ) ? sanitize_text_field( $repo_details['owner']['login'] ) : '',
'author_url' => isset( $repo_details['owner']['html_url'] ) ? esc_url_raw( $repo_details['owner']['html_url'] ) : '',
'updated_at' => $last_updated,
'topics' => isset( $repo_details['topics'] ) ? $this->extract_topics( $repo_details['topics'], $repo_type ) : array(),
'is_installed' => H2WP_Admin_Page::is_repo_installed( $owner, $repo, $repo_type ),
'repo_type' => $repo_type,
);
wp_send_json_success( $data );
}
/**
* Try to strip plugin headers like "Contributors: x", "Donate link: Y", "Tags: Z", etc. from the readme because they already appear in the sidebar.
*
* @param string $readme Readme HTML.
* @return string Filtered readme HTML.
*/
private function strip_plugin_headers( $readme ) {
$skip = array(
'contributors',
'donate link',
'tags',
'requires at least',
'tested up to',
'stable tag',
'requires php',
'license',
'license uri',
);
$lines = explode( "\n", $readme );
$filtered_lines = array();
$header_section = true;
foreach ( $lines as $index => $line ) {
// Only check first x lines for headers.
if ( $index >= 40 ) {
$header_section = false;
}
if ( $header_section ) {
$skip_line = false;
foreach ( $skip as $header ) {
if ( stripos( $line, $header . ':' ) === 0 ) {
$skip_line = true;
break;
}
}
if ( $skip_line ) {
continue;
}
}
$filtered_lines[] = $line;
}
return implode( "\n", $filtered_lines );
}
/**
* Handle AJAX request to install a plugin.
*/
public function install_plugin() {
// Check nonce.
check_ajax_referer( 'h2wp_plugin_details_nonce', 'nonce' );
$repo_type = $this->get_repo_type_from_request();
// Check user capabilities.
if ( ! $this->can_manage_repo_type( $repo_type ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'hub2wp' ) ) );
}
// Get and sanitize parameters.
$owner = isset( $_POST['owner'] ) ? sanitize_text_field( wp_unslash( $_POST['owner'] ) ) : '';
$repo = isset( $_POST['repo'] ) ? sanitize_text_field( wp_unslash( $_POST['repo'] ) ) : '';
if ( empty( $owner ) || empty( $repo ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid parameters.', 'hub2wp' ) ) );
}
ob_start();
// Check if plugin is compatible.
$api = new H2WP_GitHub_API( H2WP_Settings::get_access_token() );
$compatibility = $api->check_compatibility( $owner, $repo, $repo_type );
if ( ! $compatibility['is_compatible'] ) {
$this->clean_ajax_buffers( 0 );
wp_send_json_error( array( 'message' => $compatibility['reason'] ) );
}
$download_url = $api->get_download_url( $owner, $repo );
// Install the plugin. Pass the access token so private-repo zips can be
// downloaded with an Authorization header (the upgrader's built-in
// download_url() never sends auth headers).
$installer = new H2WP_Plugin_Installer();
$result = ( 'theme' === $repo_type )
? $installer->install_theme( $download_url, H2WP_Settings::get_access_token() )
: $installer->install_plugin( $download_url, H2WP_Settings::get_access_token() );
if ( is_wp_error( $result ) ) {
$this->clean_ajax_buffers( 0 );
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
if ( 'theme' === $repo_type ) {
$theme_data = $installer->theme_data;
$theme_data['owner'] = $owner;
$theme_data['repo'] = $repo;
$theme_data['repo_type'] = 'theme';
$theme_data['stylesheet'] = $this->find_theme_stylesheet( $theme_data );
$h2wp_themes = get_option( 'h2wp_themes', array() );
$repo_key = $owner . '/' . $repo;
$existing = isset( $h2wp_themes[ $repo_key ] ) ? $h2wp_themes[ $repo_key ] : array();
$h2wp_themes[ $repo_key ] = array_merge( $existing, $theme_data );
$h2wp_themes[ $repo_key ]['last_checked'] = time();
$h2wp_themes[ $repo_key ]['last_updated'] = time();
update_option( 'h2wp_themes', $h2wp_themes, false );
$template = ! empty( $theme_data['template'] ) ? $theme_data['template'] : $theme_data['stylesheet'];
$theme_data['activate_url'] = add_query_arg(
array(
'action' => 'activate',
'stylesheet' => $theme_data['stylesheet'],
'template' => $template,
'_wpnonce' => wp_create_nonce( 'switch-theme_' . $theme_data['stylesheet'] ),
),
admin_url( 'themes.php' )
);
$this->clean_ajax_buffers( 0 );
wp_send_json_success( $theme_data );
}
$plugin_data = $installer->plugin_data;
$plugin_data['owner'] = $owner;
$plugin_data['repo'] = $repo;
$plugin_data['repo_type'] = 'plugin';
$plugin_data['plugin_file'] = $this->find_plugin_file( $plugin_data );
// Store plugin data in the h2wp_plugins option.
$h2wp_plugins = get_option( 'h2wp_plugins', array() );
$repo_key = $owner . '/' . $repo;
// Preserve existing fields (e.g. 'private' flag set when manually monitoring the repo).
$existing = isset( $h2wp_plugins[ $repo_key ] ) ? $h2wp_plugins[ $repo_key ] : array();
$h2wp_plugins[ $repo_key ] = array_merge( $existing, $plugin_data );
$h2wp_plugins[ $repo_key ]['last_checked'] = time();
$h2wp_plugins[ $repo_key ]['last_updated'] = time();
update_option( 'h2wp_plugins', $h2wp_plugins, false );
$plugin_data['activate_url'] = add_query_arg( array(
'action' => 'activate',
'plugin' => $plugin_data['plugin_file'],
'_wpnonce' => wp_create_nonce( 'activate-plugin_' . $plugin_data['plugin_file'] ),
), admin_url( 'plugins.php' ) );
$this->clean_ajax_buffers( 0 );
wp_send_json_success( $plugin_data );
}
/**
* Handle AJAX request to check plugin compatibility.
*/
public function check_compatibility() {
// Check nonce.
check_ajax_referer( 'h2wp_plugin_details_nonce', 'nonce' );
$repo_type = $this->get_repo_type_from_request();
// Check user capabilities.
if ( ! $this->can_manage_repo_type( $repo_type ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'hub2wp' ) ) );
}
// Get and sanitize parameters.
$owner = isset( $_POST['owner'] ) ? sanitize_text_field( wp_unslash( $_POST['owner'] ) ) : '';
$repo = isset( $_POST['repo'] ) ? sanitize_text_field( wp_unslash( $_POST['repo'] ) ) : '';
if ( empty( $owner ) || empty( $repo ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid parameters.', 'hub2wp' ) ) );
}
// Get access token from settings.
$access_token = H2WP_Settings::get_access_token();
$api = new H2WP_GitHub_API( $access_token );
// Check compatibility.
$compatibility = $api->check_compatibility( $owner, $repo, $repo_type );
wp_send_json_success( array( 'is_compatible' => $compatibility['is_compatible'], 'reason' => $compatibility['reason'], 'headers' => ! empty( $compatibility['headers'] ) ? $compatibility['headers'] : array() ) );
}
/**
* Handle AJAX request to activate a plugin.
*/
public function activate_plugin() {
// Check nonce.
check_ajax_referer( 'h2wp_plugin_details_nonce', 'nonce' );
// Check user capabilities.
if ( ! current_user_can( 'activate_plugins' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'hub2wp' ) ) );
}
// Get and sanitize parameters.
$plugin_file = isset( $_POST['plugin_file'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_file'] ) ) : '';
if ( empty( $plugin_file ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid parameters.', 'hub2wp' ) ) );
}
// Activate the plugin.
activate_plugin( $plugin_file );
wp_send_json_success();
}
/**
* Extract topics from repo details.
*
* @param array $topics Topics array.
* @return array Filtered topics.
*/
private function extract_topics( $topics, $repo_type = 'plugin' ) {
$base_url = ( 'theme' === $repo_type )
? admin_url( 'themes.php?page=h2wp-theme-browser' )
: admin_url( 'plugins.php?page=h2wp-plugin-browser' );
return array_values( array_filter( array_map( function( $topic ) use ( $repo_type, $base_url ) {
$skip = ( 'theme' === $repo_type )
? array( 'wordpress-theme', 'wordpress-themes', 'wordpress', 'theme', 'wp-theme', 'wp' )
: array( 'wordpress-plugin', 'wordpress-plugins', 'wordpress', 'plugin', 'wp-plugin', 'wp' );
if ( in_array( strtolower( $topic ), $skip ) ) {
return;
}
return array(
'name' => $topic,
'url' => add_query_arg( 'tag', $topic, $base_url ),
);
}, $topics ) ) );
}
/**
* Find the plugin file in the plugin directory.
*
* @param array $plugin_data Plugin data.
* @return string|bool Plugin file path or false if not found.
*/
private function find_plugin_file( $plugin_data ) {
$plugin_file = false;
$plugins = get_plugins();
foreach ( $plugins as $file => $data ) {
if ( $data['Name'] === $plugin_data['name'] && $data['Author'] === $plugin_data['author'] ) {
$plugin_file = $file;
break;
}
}
return $plugin_file;
}
/**
* Find the installed stylesheet slug for a theme.
*
* @param array $theme_data Theme data.
* @return string|bool
*/
private function find_theme_stylesheet( $theme_data ) {
if ( ! empty( $theme_data['stylesheet'] ) ) {
return $theme_data['stylesheet'];
}
$themes = wp_get_themes();
foreach ( $themes as $stylesheet => $theme ) {
if ( $theme->get( 'Name' ) === $theme_data['name'] ) {
return $stylesheet;
}
}
return false;
}
/**
* Handle AJAX request to get changelog.
*/
public function get_changelog() {
// Check nonce
check_ajax_referer( 'h2wp_plugin_details_nonce', 'nonce' );
$repo_type = $this->get_repo_type_from_request();
// Check user capabilities
if ( ! $this->can_manage_repo_type( $repo_type ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'hub2wp' ) ) );
}
// Get and sanitize parameters
$owner = isset( $_POST['owner'] ) ? sanitize_text_field( wp_unslash( $_POST['owner'] ) ) : '';
$repo = isset( $_POST['repo'] ) ? sanitize_text_field( wp_unslash( $_POST['repo'] ) ) : '';
if ( empty( $owner ) || empty( $repo ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid parameters.', 'hub2wp' ) ) );
}
// Get access token from settings
$access_token = H2WP_Settings::get_access_token();
$api = new H2WP_GitHub_API( $access_token );
// Fetch changelog
$changelog = $api->get_changelog( $owner, $repo );
if ( is_wp_error( $changelog ) ) {
wp_send_json_error( array( 'message' => $changelog->get_error_message() ) );
}
if ( empty( $changelog ) ) {
wp_send_json_error( array( 'message' => __( 'No changelog available.', 'hub2wp' ) ) );
}
$changelog_html = '<ul class="h2wp-changelog">';
foreach ( $changelog as $release ) {
$changelog_html .= '<li>';
$changelog_html .= '<h4>' . esc_html( $release['version'] ) . ( $release['title'] ? ' (' . esc_html( $release['title'] ) . ')' : '' ) . '</h4>';
$changelog_html .= '<p><strong>' . __( 'Released:', 'hub2wp' ) . '</strong> ' . date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $release['date'] ) ) . '</p>';
$changelog_html .= '<p>' . nl2br( $release['description'] ) . '</p>';
$changelog_html .= '<p><a href="' . $release['url'] . '" target="_blank">' . __( 'View on GitHub', 'hub2wp' ) . '</a></p>';
$changelog_html .= '</li>';
}
$changelog_html .= '</ul>';
wp_send_json_success( array( 'changelog_html' => $changelog_html ) );
}
}