Initial commit: Atomaste website

This commit is contained in:
2025-12-10 12:17:30 -05:00
commit 0b9e5d1605
19260 changed files with 5206382 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
<?php
namespace Hostinger;
defined( 'ABSPATH' ) || exit;
class Activator {
public const INSTALLATION_OPTION_NAME = 'hts_new_installation';
/**
* @return void
*/
public static function activate(): void {
$options = new DefaultOptions();
$options->add_options();
self::update_installation_state_on_activation();
}
/**
* Saves installation state.
*
* @return void
*/
public static function update_installation_state_on_activation(): void {
$installation_state = get_option( self::INSTALLATION_OPTION_NAME, false );
if ( $installation_state !== 'old' ) {
add_option( self::INSTALLATION_OPTION_NAME, 'new' );
}
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace Hostinger\Admin;
use Hostinger\Admin\Menu;
use Hostinger\LlmsTxtGenerator\LlmsTxtFileHelper;
use Hostinger\WpMenuManager\Menus;
use Hostinger\Helper;
use Hostinger\WpHelper\Utils;
defined( 'ABSPATH' ) || exit;
/**
* Class Hostinger_Admin_Assets
*
* Handles the enqueueing of styles and scripts for the Hostinger admin pages.
*/
class Assets {
/**
* @var Helper Instance of the Hostinger_Helper class.
*/
private Helper $helper;
/**
* @var LlmsTxtFileHelper
*/
private LlmsTxtFileHelper $llms_txt_file_helper;
/**
* @var Utils
*/
private Utils $utils;
public function __construct() {
$this->llms_txt_file_helper = new LlmsTxtFileHelper();
$this->helper = new Helper();
$this->utils = new Utils();
$admin_path = parse_url( admin_url(), PHP_URL_PATH );
// Load assets only on Hostinger admin pages.
if ( $this->utils->isThisPage( $admin_path . 'admin.php?page=' . Menu::MENU_SLUG ) || $this->utils->isThisPage( $admin_path . 'admin.php?page=' . Menus::MENU_SLUG ) ) {
add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
}
if ( is_admin() ) {
add_action( 'admin_enqueue_scripts', array( $this, 'global_styles' ) );
}
}
/**
* Enqueues styles for the Hostinger admin pages.
*/
public function admin_styles(): void {
// Vue frontend styles.
wp_enqueue_style(
'hostinger_tools_styles',
HOSTINGER_VUE_ASSETS_URL . '/main.css',
array(),
HOSTINGER_VERSION
);
// Plugin rating styles.
wp_enqueue_style(
'hostinger_rating_styles',
HOSTINGER_ASSETS_URL . '/css/plugin-rating.min.css',
array(),
HOSTINGER_VERSION
);
}
public function global_styles(): void {
wp_enqueue_style(
'hostinger_tools_global_styles',
HOSTINGER_ASSETS_URL . 'css/hostinger-global.min.css',
array(),
HOSTINGER_VERSION
);
}
/**
* Enqueues scripts for the Hostinger admin pages.
*/
public function admin_scripts(): void {
global $wp_version;
wp_enqueue_script(
'hostinger_tools_main_scripts',
HOSTINGER_VUE_ASSETS_URL . '/main.js',
array(
'jquery',
'wp-i18n',
),
HOSTINGER_VERSION,
false
);
wp_localize_script(
'hostinger_tools_main_scripts',
'hostinger_tools_data',
array(
'home_url' => home_url(),
'site_url' => get_site_url(),
'plugin_url' => $this->helper->get_hostinger_plugin_url(),
'asset_url' => HOSTINGER_PLUGIN_URL,
'hplatform' => ! empty( $_SERVER['H_PLATFORM'] ) ? 1 : 0,
'edit_site_url' => $this->helper->get_edit_site_url(),
'llmstxt_file_url' => $this->llms_txt_file_helper->get_llmstxt_file_url(),
'llmstxt_file_user_generated' => $this->llms_txt_file_helper->is_user_generated_file(),
'translations' => array(
'routes_tools' => __( 'Tools', 'hostinger' ),
'hostinger_tools_open_guide' => __( 'Open guide', 'hostinger' ),
'hostinger_tools_preview_site' => __( 'Preview site', 'hostinger' ),
'hostinger_tools_edit_site' => __( 'Edit site', 'hostinger' ),
'hostinger_tools_disable_public_access' => __( 'Disable public access to the site (WordPress admins will still be able to access)', 'hostinger' ),
'hostinger_tools_skip_link_maintenance_mode' => __( 'Skip-link that bypasses the maintenance mode', 'hostinger' ),
'hostinger_tools_reset_link' => __( 'Reset link', 'hostinger' ),
'hostinger_tools_disable_xml_rpc' => __( 'Disable XML-RPC', 'hostinger' ),
'hostinger_tools_xml_rpc_description' => __( 'XML-RPC allows apps to connect to your WordPress site, but might expose your site\'s security. Disable this feature if you don\'t need it', 'hostinger' ),
'hostinger_tools_disable_authentication_password' => __( 'Disable application passwords', 'hostinger' ),
'hostinger_tools_authentication_password_description' => __( 'WordPress application passwords allow users to authenticate API requests without using their main login credentials, allowing for third-party integrations.', 'hostinger' ),
'hostinger_tools_force_https' => __( 'Force HTTPS', 'hostinger' ),
'hostinger_tools_force_https_description' => __( 'Redirects all HTTP URLs to HTTPS sites', 'hostinger' ),
'hostinger_tools_force_www' => __( 'Force WWW', 'hostinger' ),
'hostinger_tools_force_www_description' => __( 'Redirects all WWW URLs to non-WWW ones', 'hostinger' ),
'hostinger_tools_force_www_description_not_available' => __( 'WWW and non-WWW domain records are not pointing to the same host. Redirect not possible.', 'hostinger' ),
'hostinger_tools_php_version' => __( 'PHP version', 'hostinger' ),
'hostinger_tools_wordpress_version' => __( 'WordPress version', 'hostinger' ),
'hostinger_tools_php_version_description' => __( 'Various updates and fixes available in the newest version.', 'hostinger' ),
'hostinger_tools_running_latest_version' => __( 'Running the latest version', 'hostinger' ),
'hostinger_tools_update' => __( 'Update', 'hostinger' ),
'hostinger_tools_update_to' => __( 'Update to', 'hostinger' ),
'hostinger_tools_update_to_recommended' => __( 'is recommended', 'hostinger' ),
'hostinger_tools_update_to_wordpress_version_description' => __( 'For improved security, ensure you use the latest version of WordPress', 'hostinger' ),
'hostinger_tools_maintenance' => __( 'Maintenance', 'hostinger' ),
'hostinger_tools_preview_my_website' => __( 'Preview my website', 'hostinger' ),
'hostinger_tools_security' => __( 'Security', 'hostinger' ),
'hostinger_tools_redirects' => __( 'Redirects', 'hostinger' ),
'hostinger_tools_ai' => __( 'AI Tools', 'hostinger' ),
'hostinger_tools_llms' => __( 'LLM Optimization', 'hostinger' ),
'hostinger_tools_enable_llms_txt' => __( 'Create LLMs.txt file', 'hostinger' ),
'hostinger_tools_llms_txt_description' => __( 'Let AI explore, understand, and interact with your WordPress site.', 'hostinger' ),
'hostinger_tools_optin_mcp' => __( 'Web2Agent', 'hostinger' ),
'hostinger_tools_optin_mcp_description' => __( 'Make your website easier for AI tools to understand. Website content updates will be tracked to keep the AI discovery service up to date.', 'hostinger' ),
'hostinger_tools_llms_txt_learn_more' => __( 'Learn more', 'hostinger' ),
'hostinger_tools_llms_txt_check_validity' => __( 'Check validity', 'hostinger' ),
'hostinger_tools_llms_txt_llmstxt' => __( 'LLMS.txt', 'hostinger' ),
'hostinger_tools_llms_txt_external_file_found' => __( 'An external LLMs.txt file was found. Switching on the toggle will replace it with a new one.', 'hostinger' ),
'hostinger_tools_llms_txt_modal_title' => __( 'Create new LLMs.txt file?', 'hostinger' ),
'hostinger_tools_llms_txt_modal_description' => __( 'This will replace the existing LLMs.txt file with a new one by Hostinger Tools. The original file cant be restored.', 'hostinger' ),
'hostinger_tools_llms_txt_modal_cancel' => __( 'Cancel', 'hostinger' ),
'hostinger_tools_llms_txt_modal_create_file' => __( 'Create file', 'hostinger' ),
'hostinger_tools_maintenance_mode' => __( 'Maintenance mode', 'hostinger' ),
'hostinger_tools_bypass_link' => __( 'Bypass link', 'hostinger' ),
'xml_security_modal_description' => __( ' Turning on XML-RPC might make your site less secure. Do you want to proceed?', 'hostinger' ),
'xml_security_modal_title' => __( 'Disclaimer', 'hostinger' ),
'xml_security_modal_cancel' => __( 'Cancel', 'hostinger' ),
'xml_security_modal_proceed_anyway' => __( 'Proceed anyway', 'hostinger' ),
'bypass_link_reset_modal_title' => __( 'Bypass link reset', 'hostinger' ),
'bypass_link_reset_modal_description' => __( 'This will invalidate the currently generated link in use. This action cannot be undone, are you sure you want to proceed?', 'hostinger' ),
'bypass_link_reset_modal_cancel' => __( 'Cancel', 'hostinger' ),
'bypass_link_reset_modal_reset_link' => __( 'Reset link', 'hostinger' ),
'bypass_link_reset_success' => __( 'Link has been reset', 'hostinger' ),
'hostinger_tools_settings_updated' => __( 'Your settings have been updated', 'hostinger' ),
'hostinger_tools_settings_error' => __( 'It was an error updating your settings', 'hostinger' ),
'hostinger_tools_mcp_choice' => __( 'Allow Kodee to manage your site', 'hostinger' ),
'hostinger_tools_mcp_description' => __( 'Let Kodee manage your site on your behalf. This allows Kodee to perform actions like creating pages or updating settings. We will install and pre-configure the WordPress MCP plugin for you.', 'hostinger' ),
'hostinger_tools_copied_successfully' => __( 'Copied successfully', 'hostinger' ),
'hostinger_tools_text_copied_successfully' => __( 'Text has been copied successfully', 'hostinger' ),
'hostinger_tools_free_domain_llm_unavailable' => __( 'LLM optimization features are not available for temporary subdomains. Connect a domain to unlock these features.', 'hostinger' ),
'hostinger_tools_connect_domain_cta' => __( 'Connect domain', 'hostinger' ),
),
'rest_base_url' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'wp_version' => $wp_version,
'php_version' => phpversion(),
'recommended_php_version' => $this->helper->get_recommended_php_version(),
'mcp_choice' => get_option( 'hostinger_mcp_choice', 0 ),
'ai_plugin_compatibility' => $this->check_ai_mcp_compatibility(),
)
);
}
public function check_ai_mcp_compatibility(): bool {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugin_path = 'hostinger-ai-assistant/hostinger-ai-assistant.php';
$required_version = '3.0.0';
if ( is_plugin_active( $plugin_path ) ) {
$plugin_file = WP_PLUGIN_DIR . '/' . $plugin_path;
if ( file_exists( $plugin_file ) ) {
$plugin_data = get_plugin_data( $plugin_file, false, false );
$active_version = isset( $plugin_data['Version'] ) ? $plugin_data['Version'] : '';
if ( $active_version && version_compare( $active_version, $required_version, '>=' ) ) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Hostinger\Admin;
use Hostinger\Helper;
use Hostinger\WpHelper\Utils;
defined( 'ABSPATH' ) || exit;
class Hooks {
/**
* @var Helper
*/
private Helper $helper;
/**
* @var Utils
*/
private Utils $utils;
public function __construct( $utils ) {
$this->helper = new Helper();
$this->utils = $utils ?? new Utils();
add_action( 'admin_footer', array( $this, 'rate_plugin' ) );
add_filter( 'wp_kses_allowed_html', array( $this, 'custom_kses_allowed_html' ), 10, 1 );
}
/**
* @return void
*/
public function rate_plugin(): void {
$admin_path = parse_url( admin_url(), PHP_URL_PATH );
if ( ! $this->utils->isThisPage( $admin_path . 'admin.php?page=' . Menu::MENU_SLUG ) ) {
return;
}
require_once HOSTINGER_ABSPATH . 'includes/Admin/Views/Partials/RateUs.php';
}
public function custom_kses_allowed_html( $allowed ) {
$allowed['svg'] = array(
'xmlns' => true,
'width' => true,
'height' => true,
'viewBox' => true,
'fill' => true,
'style' => true,
'class' => true,
);
$allowed['path'] = array(
'd' => true,
'fill' => true,
);
return $allowed;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Hostinger\Admin\Jobs;
use Hostinger\LlmsTxtGenerator\LlmsTxtParser;
defined( 'ABSPATH' ) || exit;
abstract class AbstractBatchedJob extends AbstractJob {
public function init(): void {
add_action( $this->get_create_batch_hook(), array( $this, 'handle_create_batch_action' ), 10, 2 );
parent::init();
}
protected function get_create_batch_hook(): string {
return "{$this->get_hook_base_name()}create_batch";
}
public function schedule( array $args = array() ): void {
$this->schedule_create_batch_action( 1, $args );
}
public function handle_create_batch_action( int $batch_number, array $args ): void {
$items = $this->get_batch( $batch_number, $args );
if ( empty( $items ) ) {
$this->handle_complete( $batch_number, $args );
} else {
$this->schedule_process_action( $items, $args );
$this->schedule_create_batch_action( $batch_number + 1, $args );
}
}
protected function get_batch_size(): int {
return apply_filters( 'hostinger_batch_item_limit', LlmsTxtParser::DEFAULT_LIMIT );
}
protected function get_query_offset( int $batch_number ): int {
return $this->get_batch_size() * ( $batch_number - 1 );
}
protected function schedule_create_batch_action( int $batch_number, array $args ): void {
if ( $this->can_schedule( array( $batch_number ) ) ) {
$this->action_scheduler->schedule_immediate(
$this->get_create_batch_hook(),
array(
$batch_number,
$args,
)
);
}
}
protected function schedule_process_action( array $items = array(), array $args = array() ): void {
$job_data = array(
'items' => $items,
'args' => $args,
);
if ( ! $this->is_processing( $job_data ) ) {
$this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), array( $job_data ) );
}
}
protected function is_processing( array $args = array() ): bool {
return $this->action_scheduler->has_scheduled_action( $this->get_process_item_hook(), array( $args ) );
}
protected function handle_complete( int $final_batch_number, array $args ): void {
return;
}
abstract protected function get_batch( int $batch_number, array $args ): array;
}

View File

@@ -0,0 +1,54 @@
<?php
declare( strict_types=1 );
namespace Hostinger\Admin\Jobs;
use Exception;
defined( 'ABSPATH' ) || exit;
abstract class AbstractJob implements JobInterface {
protected ActionScheduler $action_scheduler;
public function __construct( ActionScheduler $action_scheduler ) {
$this->action_scheduler = $action_scheduler;
}
public function init(): void {
add_action( $this->get_process_item_hook(), array( $this, 'handle_process_items_action' ) );
add_action(
$this->get_start_hook(),
function ( $args ) {
$this->schedule( $args );
}
);
}
public function can_schedule( $args = array() ): bool {
return ! $this->is_running( $args );
}
public function handle_process_items_action( array $args = array() ): void {
$this->process_items( $args );
}
public function get_process_item_hook(): string {
return "{$this->get_hook_base_name()}process_item";
}
public function get_start_hook(): string {
return $this->get_name();
}
protected function is_running( ?array $args = array() ): bool {
return $this->action_scheduler->has_scheduled_action( $this->get_process_item_hook(), array( $args ) );
}
protected function get_hook_base_name(): string {
return "{$this->action_scheduler->get_group()}/jobs/{$this->get_name()}/";
}
abstract public function get_name(): string;
abstract protected function process_items( array $args );
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Hostinger\Admin\Jobs;
defined( 'ABSPATH' ) || exit;
class ActionScheduler {
public const STATUS_PENDING = 'pending';
public const STATUS_COMPLETE = 'complete';
public const STATUS_FAILED = 'failed';
public function get_group(): string {
return defined( 'HOSTINGER_PLUGIN_SETTINGS_OPTION' ) ? HOSTINGER_PLUGIN_SETTINGS_OPTION : 'hostinger_tools';
}
public function schedule_single( int $timestamp, string $hook, $args = array() ): int {
if ( ! function_exists( 'as_schedule_single_action' ) ) {
return 0;
}
return as_schedule_single_action( $timestamp, $hook, $args, $this->get_group() );
}
public function schedule_immediate( string $hook, $args = array() ): int {
if ( ! function_exists( 'as_schedule_single_action' ) ) {
return 0;
}
return as_schedule_single_action( gmdate( 'U' ) - 1, $hook, $args, $this->get_group() );
}
public function has_scheduled_action( string $hook, $args = array() ): bool {
if ( ! function_exists( 'as_next_scheduled_action' ) ) {
return false;
}
return as_next_scheduled_action( $hook, $args, $this->get_group() ) !== false;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Hostinger\Admin\Jobs;
use Hostinger\Admin\PluginSettings;
use Hostinger\Admin\Proxy;
use Hostinger\LlmsTxtGenerator\LlmsTxtFileHelper;
use Hostinger\LlmsTxtGenerator\LlmsTxtParser;
use Hostinger\Mcp\EventHandlerFactory;
defined( 'ABSPATH' ) || exit;
class JobInitializer {
public function __construct( Proxy $proxy ) {
$jobs = array();
$jobs[] = new NotifyMcpJob( new ActionScheduler(), new EventHandlerFactory( $proxy ) );
$jobs[] = new LlmsTxtInjectContentJob( new ActionScheduler(), new LlmsTxtParser(), new LlmsTxtFileHelper(), new PluginSettings() );
foreach ( $jobs as $job ) {
$job->init();
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Hostinger\Admin\Jobs;
defined( 'ABSPATH' ) || exit;
interface JobInterface {
public function get_name(): string;
public function get_process_item_hook(): string;
public function get_start_hook(): string;
public function can_schedule( array $args = array() ): bool;
public function schedule( array $args = array() );
public function init();
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Hostinger\Admin\Jobs;
use Hostinger\Admin\PluginSettings;
use Hostinger\LlmsTxtGenerator\LlmsTxtFileHelper;
use Hostinger\LlmsTxtGenerator\LlmsTxtGenerator;
use Hostinger\LlmsTxtGenerator\LlmsTxtParser;
class LlmsTxtInjectContentJob extends AbstractBatchedJob {
public const JOB_NAME = 'generate_llmstxt';
protected LlmsTxtParser $llms_txt_parser;
protected LlmsTxtFileHelper $llms_txt_file_helper;
protected PluginSettings $plugin_settings;
public function __construct( ActionScheduler $action_scheduler, LlmsTxtParser $llms_txt_parser, LlmsTxtFileHelper $llms_txt_file_helper, PluginSettings $plugin_settings ) {
parent::__construct( $action_scheduler );
$this->llms_txt_parser = $llms_txt_parser;
$this->llms_txt_file_helper = $llms_txt_file_helper;
$this->plugin_settings = $plugin_settings;
}
protected function get_batch( int $batch_number, $args ): array {
if ( ! isset( $args['post_type'] ) ) {
return array();
}
$offset = $this->get_query_offset( $batch_number );
$limit = $this->get_batch_size();
return $this->llms_txt_parser->get_by_post_type( $args['post_type'], $limit, $offset );
}
public function get_name(): string {
return self::JOB_NAME;
}
protected function process_items( array $args = array() ): void {
if ( ! $this->is_llms_txt_enabled() ) {
return;
}
$items = $args['items'] ?? array();
$job_args = $args['args'] ?? array();
if ( ! isset( $job_args['post_type'] ) || empty( $items ) ) {
return;
}
$content = $this->llms_txt_parser->get_items( $items );
$this->inject_content( $job_args['post_type'], $content );
}
public function schedule( array $args = array() ): void {
// Initiate as 2, as the first batch will be created when the user toggles on the option.
$this->schedule_create_batch_action( 2, $args );
}
public function can_schedule( $args = array() ): bool {
return parent::can_schedule( $args ) && $this->is_llms_txt_enabled();
}
public function is_llms_txt_enabled(): bool {
$settings = $this->plugin_settings->get_plugin_settings();
return $settings->get_enable_llms_txt();
}
public function inject_content( $post_type, $new_content ): void {
$content = $this->llms_txt_file_helper->get_content();
$section = LlmsTxtGenerator::HOSTINGER_LLMSTXT_SUPPORTED_POST_TYPES[ $post_type ];
$header = "## $section\n\n";
$header_position = strpos( $content, $header );
$header_length = strlen( $header );
if ( $header_position === false ) {
return;
}
$before_injection_slot = substr( $content, 0, $header_position + $header_length );
$after_injection_slot = substr( $content, $header_position + $header_length );
$final_content = $before_injection_slot . $new_content . $after_injection_slot;
$this->llms_txt_file_helper->create( $final_content );
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Hostinger\Admin\Jobs;
use Hostinger\Mcp\EventHandlerFactory;
use Hostinger\Mcp\Handlers\EventHandler;
use PHPUnit\Exception;
class NotifyMcpJob extends AbstractJob implements JobInterface {
public const JOB_NAME = 'notify_mcp';
private EventHandlerFactory $event_handler_factory;
public function __construct( ActionScheduler $action_scheduler, EventHandlerFactory $event_handler_factory ) {
$this->event_handler_factory = $event_handler_factory;
parent::__construct( $action_scheduler );
}
public function get_name(): string {
return self::JOB_NAME;
}
public function event_handler( string $event ): EventHandler {
return $this->event_handler_factory->get_handler( $event );
}
public function process_items( array $args = array() ): void {
$handler = $this->event_handler( $args['event'] );
$handler->send( $args );
}
public function schedule( array $args = array() ): void {
if ( $this->can_schedule( $args ) ) {
$this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), array( $args ) );
}
}
public function can_schedule( $args = array() ): bool {
if ( ! parent::can_schedule( $args ) ) {
return false;
}
try {
$handler = $this->event_handler( $args['event'] );
return $handler->can_send( $args );
} catch ( Exception $e ) {
return false;
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Hostinger\Admin;
use Hostinger\Admin\PluginSettings;
use Hostinger\WpHelper\Utils;
use Hostinger\WpMenuManager\Menus;
defined( 'ABSPATH' ) || exit;
class Menu {
public const MENU_SLUG = 'hostinger-tools';
public function __construct() {
add_filter( 'hostinger_admin_menu_bar_items', array( $this, 'add_admin_bar_items' ) );
add_filter( 'hostinger_menu_subpages', array( $this, 'sub_menu' ) );
}
public function add_admin_bar_items( $menu_items ): array {
$menu_items[] = array(
'id' => 'hostinger-tools-admin-bar',
'title' => __( 'Tools', 'hostinger' ),
'href' => admin_url( 'admin.php?page=' . self::MENU_SLUG ),
);
return $menu_items;
}
public function sub_menu( $submenus ): array {
$tools_submenu = array(
'page_title' => __( 'Tools', 'hostinger' ),
'menu_title' => __( 'Tools', 'hostinger' ),
'capability' => 'manage_options',
'menu_slug' => self::MENU_SLUG,
'callback' => array( $this, 'render_tools_menu_page' ),
'menu_order' => 10,
);
$submenus[] = $tools_submenu;
return $submenus;
}
public function render_tools_menu_page(): void {
echo wp_kses( Menus::renderMenuNavigation(), 'post' );
?>
<div id="hostinger-tools-vue-app"/>
<?php
}
}

View File

@@ -0,0 +1,193 @@
<?php
namespace Hostinger\Admin\Options;
if ( ! defined( 'ABSPATH' ) ) {
die;
}
/**
* Class for handling plugin options
*/
class PluginOptions {
/**
* @var bool
*/
private bool $maintenance_mode = false;
/**
* @var string
*/
private string $bypass_code = '';
/**
* @var bool
*/
private bool $disable_xml_rpc = false;
/**
* @var bool
*/
private bool $force_https = false;
/**
* @var bool
*/
private bool $force_www = false;
/**
* @var bool
*/
private bool $disable_authentication_password = false;
/**
* @var bool
*/
private bool $enable_llms_txt = false;
/**
* @var bool
*/
private bool $optin_mcp = false;
/**
* @param array $settings plugin settings array.
*/
public function __construct( array $settings = array() ) {
$this->maintenance_mode = ! empty( $settings['maintenance_mode'] );
$this->bypass_code = ! empty( $settings['bypass_code'] ) ? $settings['bypass_code'] : '';
$this->disable_xml_rpc = ! empty( $settings['disable_xml_rpc'] );
$this->force_https = ! empty( $settings['force_https'] );
$this->force_www = ! empty( $settings['force_www'] );
$this->disable_authentication_password = ! empty( $settings['disable_authentication_password'] );
$this->enable_llms_txt = ! empty( $settings['enable_llms_txt'] );
$this->optin_mcp = ! empty( $settings['optin_mcp'] );
}
/**
* @return bool
*/
public function get_maintenance_mode(): bool {
return $this->maintenance_mode;
}
/**
* @param bool $maintenance_mode
*
* @return void
*/
public function set_maintenance_mode( bool $maintenance_mode ): void {
$this->maintenance_mode = $maintenance_mode;
}
/**
* @return string
*/
public function get_bypass_code(): string {
return $this->bypass_code;
}
/**
* @param string $bypass_code
*
* @return void
*/
public function set_bypass_code( string $bypass_code ): void {
$this->bypass_code = $bypass_code;
}
/**
* @return bool
*/
public function get_disable_xml_rpc(): bool {
return $this->disable_xml_rpc;
}
/**
* @param bool $disable_xml_rpc
*
* @return void
*/
public function set_disable_xml_rpc( bool $disable_xml_rpc ): void {
$this->disable_xml_rpc = $disable_xml_rpc;
}
/**
* @return bool
*/
public function get_force_https(): bool {
return $this->force_https;
}
/**
* @param bool $force_https
*
* @return void
*/
public function set_force_https( bool $force_https ): void {
$this->force_https = $force_https;
}
/**
* @return bool
*/
public function get_force_www(): bool {
return $this->force_www;
}
/**
* @param bool $force_www
*
* @return void
*/
public function set_force_www( bool $force_www ): void {
$this->force_www = $force_www;
}
/**
* @return bool
*/
public function get_disable_authentication_password(): bool {
return $this->disable_authentication_password;
}
/**
* @param bool $authentication_password
*
* @return void
*/
public function set_disable_authentication_password( bool $authentication_password ): void {
$this->disable_authentication_password = $authentication_password;
}
public function get_enable_llms_txt(): bool {
return $this->enable_llms_txt;
}
public function set_enable_llms_txt( bool $llmstxt_enabled ): void {
$this->enable_llms_txt = $llmstxt_enabled;
}
public function get_optin_mcp(): bool {
return $this->optin_mcp;
}
public function set_optin_mcp( bool $optin_mcp ): void {
$this->optin_mcp = $optin_mcp;
}
/**
* @return array
*/
public function to_array(): array {
return array(
'maintenance_mode' => $this->get_maintenance_mode(),
'bypass_code' => $this->get_bypass_code(),
'disable_xml_rpc' => $this->get_disable_xml_rpc(),
'force_https' => $this->get_force_https(),
'force_www' => $this->get_force_www(),
'disable_authentication_password' => $this->get_disable_authentication_password(),
'enable_llms_txt' => $this->get_enable_llms_txt(),
'optin_mcp' => $this->get_optin_mcp(),
);
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Avoid possibility to get file accessed directly
*/
namespace Hostinger\Admin;
use Hostinger\Admin\Options\PluginOptions;
if ( ! defined( 'ABSPATH' ) ) {
die;
}
/**
* Class for handling Settings
*/
class PluginSettings {
/**
* @var PluginOptions|null
*/
private ?PluginOptions $plugin_options = null;
/**
* @param PluginOptions|null $plugin_options
*/
public function __construct( ?PluginOptions $plugin_options = null ) {
if ( ! is_null( $plugin_options ) ) {
$this->plugin_options = $plugin_options;
}
}
/**
* Return plugin settings
*
* @return PluginOptions
*/
public function get_plugin_settings(): PluginOptions {
if ( ! empty( $this->plugin_options ) ) {
$settings = $this->plugin_options;
} else {
$settings = get_option(
HOSTINGER_PLUGIN_SETTINGS_OPTION,
array()
);
$settings = new PluginOptions( $settings );
}
return $settings;
}
/**
* @param PluginOptions $plugin_options plugin settings.
*
* @return PluginOptions
*/
public function save_plugin_settings( PluginOptions $plugin_options ): PluginOptions {
$existing_settings = $this->get_plugin_settings();
$update = update_option( HOSTINGER_PLUGIN_SETTINGS_OPTION, $plugin_options->to_array(), false );
return ! empty( $update ) ? $plugin_options : $existing_settings;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Hostinger\Admin;
use Hostinger\Mcp\EventHandlerFactory;
use Hostinger\WpHelper\Requests\Client;
use Hostinger\WpHelper\Utils;
use WP_Error;
defined( 'ABSPATH' ) || exit;
class Proxy {
public const HOSTINGER_FREE_SUBDOMAIN_URL = 'hostingersite.com';
public const HOSTINGER_DEV_FREE_SUBDOMAIN_URL = 'hostingersite.dev';
private Client $client;
private Utils $utils;
private string $rest_base;
public function __construct( Client $client, Utils $utils ) {
$this->client = $client;
$this->utils = $utils;
$this->rest_base = '/api/v1/events/trigger';
}
public function trigger_event( string $event, array $params = array() ): array|WP_Error {
if ( $this->is_free_subdomain() || ! $_SERVER['H_PLATFORM'] ) {
return new WP_Error( 'domain-not_allowed', 'This domain is not eligible for triggering Hostinger events' );
}
$args = array(
'domain' => $this->remove_www( $this->utils->getHostInfo() ),
'event' => array(
'name' => $event,
'params' => $params,
),
);
return $this->client->post( $this->rest_base, $args );
}
private function is_free_subdomain(): bool {
return str_contains( $this->utils->getHostInfo(), self::HOSTINGER_FREE_SUBDOMAIN_URL ) ||
str_contains( $this->utils->getHostInfo(), self::HOSTINGER_DEV_FREE_SUBDOMAIN_URL );
}
private function remove_www( string $url ): string {
if ( str_starts_with( $url, 'www.' ) ) {
return substr( $url, 4 );
}
return $url;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Hostinger\Admin;
use Hostinger\Settings;
defined( 'ABSPATH' ) || exit;
class Redirects {
private string $platform;
public const PLATFORM_HPANEL = 'hpanel';
public function __construct() {
if ( ! Settings::get_setting( 'first_login_at' ) ) {
Settings::update_setting( 'first_login_at', gmdate( 'Y-m-d H:i:s' ) );
}
/** PHPCS:disable WordPress.Security.NonceVerification.Recommended */
if ( ! isset( $_GET['platform'] ) ) {
return;
}
$this->platform = sanitize_text_field( $_GET['platform'] );
$this->login_redirect();
/** PHPCS:enable */
}
public function login_redirect(): void {
if ( $this->platform === self::PLATFORM_HPANEL ) {
add_action(
'init',
static function () {
$redirect_url = admin_url( 'admin.php?page=hostinger' );
wp_safe_redirect( $redirect_url );
exit;
}
);
}
}
}

View File

@@ -0,0 +1,52 @@
<div class="hostinger hsr-plugin-rating">
<p><?php echo esc_html__( 'Rate this plugin', 'hostinger' ); ?></p>
<div class="hsr-rate-stars">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
clip-rule="evenodd"
d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z"
fill="#673DE6"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
clip-rule="evenodd"
d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z"
fill="#673DE6"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
clip-rule="evenodd"
d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z"
fill="#673DE6"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
clip-rule="evenodd"
d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z"
fill="#673DE6"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
clip-rule="evenodd"
d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z"
fill="#673DE6"/>
</svg>
</div>
<p>
<?php
echo wp_kses(
__(
'on <a href="https://wordpress.org/support/plugin/hostinger/reviews/#new-post" target="_blank" rel="noopener noreferrer">WordPress.org</a>',
'hostinger'
),
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
),
),
);
?>
</p>
</div>

View File

@@ -0,0 +1,95 @@
<?php
namespace Hostinger;
use Hostinger\Admin\PluginSettings;
use Hostinger\Admin\Jobs\JobInitializer;
use Hostinger\Admin\Proxy;
use Hostinger\LlmsTxtGenerator\LlmsTxtFileHelper;
use Hostinger\LlmsTxtGenerator\LlmsTxtParser;
use Hostinger\Rest\Routes;
use Hostinger\Rest\SettingsRoutes;
use Hostinger\Admin\Assets as AdminAssets;
use Hostinger\Admin\Hooks as AdminHooks;
use Hostinger\Admin\Menu as AdminMenu;
use Hostinger\Admin\Redirects as AdminRedirects;
use Hostinger\WpHelper\Config;
use Hostinger\WpHelper\Requests\Client;
use Hostinger\WpHelper\Utils;
use Hostinger\LlmsTxtGenerator\LlmsTxtGenerator;
defined( 'ABSPATH' ) || exit;
class Bootstrap {
protected Loader $loader;
protected Utils $utils;
protected Config $config;
public function __construct() {
$this->loader = new Loader();
$this->utils = new Utils();
$this->config = new Config();
}
public function run(): void {
$this->load_dependencies();
$this->set_locale();
$this->loader->run();
}
private function load_dependencies(): void {
$this->load_public_dependencies();
if ( is_admin() ) {
$this->load_admin_dependencies();
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
new Cli();
}
$plugin_settings = new PluginSettings();
$plugin_options = $plugin_settings->get_plugin_settings();
if ( $plugin_options->get_maintenance_mode() ) {
require_once HOSTINGER_ABSPATH . 'includes/ComingSoon.php';
}
}
private function set_locale() {
$plugin_i18n = new I18n();
$this->loader->add_action( 'plugins_loaded', $plugin_i18n, 'load_plugin_textdomain' );
}
private function load_admin_dependencies(): void {
new AdminAssets();
new AdminHooks( $this->utils );
new AdminMenu();
new AdminRedirects();
new AdminRedirects();
}
private function load_public_dependencies(): void {
$client = new Client(
'https://wh-wordpress-proxy-api.hostinger.io',
array(
Config::TOKEN_HEADER => $this->utils->getApiToken(),
Config::DOMAIN_HEADER => $this->utils->getHostInfo(),
)
);
new JobInitializer( new Proxy( $client, $this->utils ) );
new Hooks();
$plugin_settings = new PluginSettings();
new LlmsTxtGenerator( $plugin_settings, new LlmsTxtFileHelper(), new LlmsTxtParser() );
$settings_routes = new SettingsRoutes( $plugin_settings );
$routes = new Routes( $settings_routes );
$routes->init();
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Hostinger;
use WP_CLI;
defined( 'ABSPATH' ) || exit;
class Cli {
/**
* Load required files and hooks to make the CLI work.
*/
public function __construct() {
$this->hooks();
}
/**
* Sets up and hooks WP CLI to our CLI code.
*
* @return void
*/
private function hooks(): void {
if ( ! class_exists( '\WP_CLI' ) ) {
return;
}
if ( class_exists( '\Hostinger\Cli\Commands\Maintenance' ) ) {
WP_CLI::add_hook( 'after_wp_load', array( 'Hostinger\Cli\Commands\Maintenance', 'define_command' ) );
}
if ( class_exists( '\Hostinger\Cli\Commands\AI' ) ) {
WP_CLI::add_hook( 'after_wp_load', array( 'Hostinger\Cli\Commands\AI', 'define_command' ) );
}
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Hostinger\Cli\Commands;
use Exception;
use Hostinger\Admin\PluginSettings;
use WP_CLI;
defined( 'ABSPATH' ) || exit;
class AI {
protected const WEB2AGENT_FEATURE_NAME = 'Web2Agent feature';
protected const LLMS_TXT_FEATURE_NAME = 'LLMS.txt file generation feature';
public static function define_command(): void {
if ( ! class_exists( 'WP_CLI' ) ) {
return;
}
WP_CLI::add_command(
'hostinger ai',
self::class,
array(
'shortdesc' => 'Check the status of AI Discovery Features',
'longdesc' => 'Available Hostinger AI commands:' . "\n\n" .
' wp hostinger ai llmstxt <0|1>' . "\n" .
' Manage the ' . self::LLMS_TXT_FEATURE_NAME . '. Use 1 to enable and 0 to disable it.' . "\n\n" .
' wp hostinger ai web2agent <0|1>' . "\n" .
' Manage the ' . self::WEB2AGENT_FEATURE_NAME . '. Use 1 to enable and 0 to disable it.' . "\n\n" .
' wp hostinger ai status' . "\n" .
' Display the current status for AI Discovery features.' . "\n\n" .
'## EXAMPLES' . "\n\n" .
' wp hostinger ai status' . "\n" .
' Display the current status for AI Discovery features.' . "\n\n" .
' wp hostinger ai llmstxt 0' . "\n" .
' Disables ' . self::LLMS_TXT_FEATURE_NAME . '.' . "\n\n" .
' wp hostinger ai llmstxt 1' . "\n" .
' Enables ' . self::LLMS_TXT_FEATURE_NAME . '.' . "\n",
)
);
}
/**
* Command allows enable/disable Web2Agent feature.
*
* @param array $args
*
* @return bool
* @throws Exception
*/
public function web2agent( array $args ): bool {
$plugin_settings = new PluginSettings();
if ( ! empty( $args ) ) {
$this->validate_args( $args );
$this->set_setting_status( $plugin_settings, self::WEB2AGENT_FEATURE_NAME, (bool) $args[0] );
}
return $this->get_setting_status( $plugin_settings, self::WEB2AGENT_FEATURE_NAME );
}
/**
* Command allows enable/disable LLMS.txt file generation feature.
*
* @param array $args
*
* @return bool
* @throws Exception
*/
public function llmstxt( array $args ): bool {
$plugin_settings = new PluginSettings();
if ( ! empty( $args ) ) {
$this->validate_args( $args );
$this->set_setting_status( $plugin_settings, self::LLMS_TXT_FEATURE_NAME, (bool) $args[0] );
}
return $this->get_setting_status( $plugin_settings, self::LLMS_TXT_FEATURE_NAME );
}
/**
* Get the current status of AI Discovery features.
* @return string
*/
public function status(): string {
$plugin_settings = new PluginSettings();
$plugin_options = $plugin_settings->get_plugin_settings();
$data = array(
'llmstxt' => $plugin_options->get_enable_llms_txt(),
'web2agent' => $plugin_options->get_optin_mcp(),
);
$status = wp_json_encode( $data );
WP_CLI::line( $status );
return $status;
}
private function set_setting_status( PluginSettings $plugin_settings, string $setting, bool $is_enabled ): void {
$plugin_options = $plugin_settings->get_plugin_settings();
switch ( $setting ) {
case self::WEB2AGENT_FEATURE_NAME:
$plugin_options->set_optin_mcp( $is_enabled );
break;
case self::LLMS_TXT_FEATURE_NAME:
$plugin_options->set_enable_llms_txt( $is_enabled );
break;
default:
throw new Exception( 'Invalid setting' );
}
$plugin_settings->save_plugin_settings( $plugin_options );
$this->clear_lightspeed_cache();
}
private function get_setting_status( PluginSettings $plugin_settings, string $setting ): bool {
$plugin_options = $plugin_settings->get_plugin_settings();
switch ( $setting ) {
case self::WEB2AGENT_FEATURE_NAME:
$is_enabled = $plugin_options->get_optin_mcp();
break;
case self::LLMS_TXT_FEATURE_NAME:
$is_enabled = $plugin_options->get_enable_llms_txt();
break;
default:
throw new Exception( 'Invalid setting' );
}
$enabled = $is_enabled ? 'ENABLED' : 'DISABLED';
WP_CLI::success( $setting . ' ' . $enabled );
return $is_enabled;
}
private function validate_args( mixed $args ): void {
if ( $args[0] !== '0' && $args[0] !== '1' ) {
throw new Exception( 'Invalid argument. Use 0 or 1' );
}
}
private function clear_lightspeed_cache(): void {
if ( has_action( 'litespeed_purge_all' ) ) {
do_action( 'litespeed_purge_all' );
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Hostinger\Cli\Commands;
use Hostinger\Admin\PluginSettings;
use WP_CLI;
use Hostinger\Settings;
defined( 'ABSPATH' ) || exit;
class Maintenance {
public static function define_command(): void {
if ( class_exists( '\WP_CLI' ) ) {
WP_CLI::add_command(
'hostinger',
self::class,
array(
'shortdesc' => 'List of Hostinger commands.',
'longdesc' => 'Available Hostinger commands:' . "\n\n" .
' wp hostinger maintenance mode <0|1>' . "\n" .
' Manage the maintenance mode of the site. Use 1 to enable and 0 to disable maintenance mode.' . "\n\n" .
' wp hostinger maintenance status' . "\n" .
' Display the current maintenance mode status.' . "\n\n" .
'## SUBCOMMANDS' . "\n\n" .
'* mode <0|1>' . "\n" .
': Enable (1) or disable (0) maintenance mode.' . "\n\n" .
'* status' . "\n" .
': Display the current maintenance mode status.' . "\n\n" .
'## EXAMPLES' . "\n\n" .
' wp hostinger maintenance mode 1' . "\n" .
' Enables the maintenance mode.' . "\n\n" .
' wp hostinger maintenance mode 0' . "\n" .
' Disables the maintenance mode.' . "\n\n" .
' wp hostinger maintenance status' . "\n" .
' Returns whether maintenance mode is enabled or disabled.' . "\n",
)
);
WP_CLI::add_command(
'hostinger maintenance',
self::class,
array(
'shortdesc' => 'Manage the maintenance mode of the site.',
'longdesc' => 'This command allows you to enable or disable maintenance mode for the site.' . "\n\n" .
'## OPTIONS' . "\n\n" .
'mode <0|1>' . "\n" .
': Enable (1) or disable (0) maintenance mode.' . "\n\n" .
'## EXAMPLES' . "\n\n" .
' wp hostinger maintenance mode 1' . "\n" .
' Enables the maintenance mode.' . "\n\n" .
' wp hostinger maintenance mode 0' . "\n" .
' Disables the maintenance mode.' . "\n\n" .
' wp hostinger maintenance status' . "\n" .
' Returns whether maintenance mode is enabled or disabled.' . "\n",
)
);
}
}
/**
* Command allows enable/disable maintenance mode.
*
* @param array $args
*
* @return void
* @throws \Exception
*/
public function mode( array $args ): void {
if ( empty( $args ) ) {
WP_CLI::error( 'Arguments cannot be empty. Use 0 or 1' );
}
$plugin_settings = new PluginSettings();
$plugin_options = $plugin_settings->get_plugin_settings();
switch ( $args[0] ) {
case '1':
$plugin_options->set_maintenance_mode( true );
WP_CLI::success( 'Maintenance mode ENABLED' );
break;
case '0':
$plugin_options->set_maintenance_mode( false );
WP_CLI::success( 'Maintenance mode DISABLED' );
break;
default:
throw new \Exception( 'Invalid maintenance mode value' );
}
$plugin_settings->save_plugin_settings( $plugin_options );
if ( has_action( 'litespeed_purge_all' ) ) {
do_action( 'litespeed_purge_all' );
}
}
/**
* Command return maintenance mode status.
*
* @return bool
*/
public function status(): bool {
$plugin_settings = new PluginSettings();
$plugin_options = $plugin_settings->get_plugin_settings();
if ( $plugin_options->get_maintenance_mode() ) {
WP_CLI::success( 'Maintenance mode ENABLED' );
} else {
WP_CLI::success( 'Maintenance mode DISABLED' );
}
return (bool) $plugin_options->get_maintenance_mode();
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Hostinger;
use Hostinger\Admin\Options\PluginOptions;
use Hostinger\Admin\PluginSettings;
defined( 'ABSPATH' ) || exit;
class ComingSoon {
/**
* @var PluginOptions
*/
private PluginOptions $plugin_options;
public function __construct() {
$plugin_settings = new PluginSettings();
$this->plugin_options = $plugin_settings->get_plugin_settings();
add_action( 'wp_footer', array( $this, 'register_styles' ) );
add_action( 'template_redirect', array( $this, 'coming_soon' ) );
add_filter( 'wp_headers', array( $this, 'modify_headers' ) );
// Fix deprecated warning.
if ( has_action( 'wp_footer', 'the_block_template_skip_link' ) ) {
remove_action( 'wp_footer', 'the_block_template_skip_link' );
}
}
/**
* @return void
*/
public function coming_soon(): void {
// Do not cache coming soon page.
if ( has_action( 'litespeed_purge_all' ) && ! defined( 'DONOTCACHEPAGE' ) ) {
define( 'DONOTCACHEPAGE', true );
}
if ( ! $this->can_bypass_coming_soon() ) {
include_once HOSTINGER_ABSPATH . 'includes/Views/ComingSoon.php';
die;
}
}
/**
* @return void
*/
public function register_styles(): void {
wp_enqueue_style(
'hostinger_main_styles',
HOSTINGER_ASSETS_URL . '/css/coming-soon.min.css',
array(),
HOSTINGER_VERSION
);
}
/**
* @param array $headers
*
* @return array
*/
public function modify_headers( array $headers ): array {
$headers['Cache-Control'] = 'no-cache';
return $headers;
}
/**
* @return bool
*/
/** PHPCS:disable WordPress.Security.NonceVerification.Recommended */
private function can_bypass_coming_soon(): bool {
$bypass_code = isset( $_COOKIE['hostinger_bypass_code'] ) ? sanitize_text_field( $_COOKIE['hostinger_bypass_code'] ) : '';
if ( isset( $_GET['bypass_code'] ) && $this->plugin_options->get_bypass_code() === $_GET['bypass_code'] ) {
setcookie( 'hostinger_bypass_code', $this->plugin_options->get_bypass_code() );
$bypass_code = $this->plugin_options->get_bypass_code();
}
if ( is_admin() ) {
return true;
}
if ( current_user_can( 'update_plugins' ) ) {
return true;
}
if ( ! empty( $bypass_code ) && $bypass_code === $this->plugin_options->get_bypass_code() ) {
return true;
}
return false;
}
/** PHPCS:enable */
}
new ComingSoon();

View File

@@ -0,0 +1,10 @@
<?php
namespace Hostinger;
defined( 'ABSPATH' ) || exit;
class Deactivator {
public static function deactivate(): void {
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Hostinger;
use Hostinger\Admin\Options\PluginOptions;
use Hostinger\Helper;
defined( 'ABSPATH' ) || exit;
class DefaultOptions {
/**
* @return void
*/
public function add_options(): void {
$this->configure_security_settings();
foreach ( $this->options() as $key => $option ) {
update_option( $key, $option );
}
}
public function configure_security_settings(): void {
$hostinger_plugin_settings = get_option( HOSTINGER_PLUGIN_SETTINGS_OPTION, array() );
if ( empty( $hostinger_plugin_settings['bypass_code'] ) ) {
$hostinger_plugin_settings['bypass_code'] = Helper::generate_bypass_code( 16 );
$this->update_plugin_settings( $hostinger_plugin_settings );
}
$this->configure_authentication_password();
}
public function configure_authentication_password(): void {
global $wpdb;
$hostinger_plugin_settings = get_option( HOSTINGER_PLUGIN_SETTINGS_OPTION, array() );
$existing_passwords = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key = %s", '_application_passwords' ) );
if ( $existing_passwords === 0 ) {
$hostinger_plugin_settings['disable_authentication_password'] = true;
$this->update_plugin_settings( $hostinger_plugin_settings );
}
}
private function update_plugin_settings( array $settings ): void {
$plugin_options = new PluginOptions( $settings );
update_option( HOSTINGER_PLUGIN_SETTINGS_OPTION, $plugin_options->to_array(), false );
}
/**
* @return string[]
*/
private function options(): array {
$options = array(
'optin_monster_api_activation_redirect_disabled' => 'true',
'wpforms_activation_redirect' => 'true',
'aioseo_activation_redirect' => 'false',
);
if ( Helper::is_plugin_active( 'astra-sites' ) ) {
$options = array_merge( $options, $this->get_astra_options() );
}
return $options;
}
/**
* @return string[]
*/
private function get_astra_options(): array {
return array(
'astra_sites_settings' => 'gutenberg',
);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Hostinger;
defined( 'ABSPATH' ) || exit;
class Errors {
/**
* @var array[]
*/
private array $error_messages = array();
public function __construct() {
$this->error_messages = array(
'action_failed' => array(
'default' => __( 'Action Failed. Try again or contact support. Apologies.', 'hostinger' ),
),
'unexpected_error' => array(
'default' => __( 'An unexpected error occurred. Please try again or contact support.', 'hostinger' ),
),
'server_error' => array(
'default' => __( 'We apologize for the inconvenience. The AI content generation process encountered a server error. Please try again later, and if the issue persists, kindly contact our support team for assistance.', 'hostinger' ),
),
);
}
/**
* @param string $error_code
*
* @return mixed
*/
public function get_error_message( string $error_code ) {
if ( array_key_exists( $error_code, $this->error_messages ) ) {
$message_data = $this->error_messages[ $error_code ];
return $message_data['default'];
} else {
return __( 'Unknown error code.', 'hostinger' );
}
}
}
new Errors();

View File

@@ -0,0 +1,180 @@
<?php
namespace Hostinger;
defined( 'ABSPATH' ) || exit;
class Helper {
const HOSTINGER_LOCALES = array(
'lt_LT' => 'hostinger.lt',
'uk_UA' => 'hostinger.com.ua',
'id_ID' => 'hostinger.co.id',
'en_US' => 'hostinger.com',
'es_ES' => 'hostinger.es',
'es_AR' => 'hostinger.com.ar',
'es_MX' => 'hostinger.mx',
'es_CO' => 'hostinger.co',
'pt_BR' => 'hostinger.com.br',
'ro_RO' => 'hostinger.ro',
'fr_FR' => 'hostinger.fr',
'it_IT' => 'hostinger.it',
'pl_PL' => 'hostinger.pl',
'en_PH' => 'hostinger.ph',
'ar_AE' => 'hostinger.ae',
'ms_MY' => 'hostinger.my',
'ko_KR' => 'hostinger.kr',
'vi_VN' => 'hostinger.vn',
'th_TH' => 'hostinger.in.th',
'tr_TR' => 'hostinger.web.tr',
'pt_PT' => 'hostinger.pt',
'de_DE' => 'hostinger.de',
'en_IN' => 'hostinger.in',
'ja_JP' => 'hostinger.jp',
'nl_NL' => 'hostinger.nl',
'en_GB' => 'hostinger.co.uk',
'el_GR' => 'hostinger.gr',
'cs_CZ' => 'hostinger.cz',
'hu_HU' => 'hostinger.hu',
'sv_SE' => 'hostinger.se',
'da_DK' => 'hostinger.dk',
'fi_FI' => 'hostinger.fi',
'sk_SK' => 'hostinger.sk',
'no_NO' => 'hostinger.no',
'hr_HR' => 'hostinger.hr',
'zh_HK' => 'hostinger.com.hk',
'he_IL' => 'hostinger.co.il',
'lv_LV' => 'hostinger.lv',
'et_EE' => 'hostinger.ee',
'ur_PK' => 'hostinger.pk',
);
public const HOMEPAGE_DISPLAY = 'page';
/**
*
* Check if plugin is active
*
* @since 1.0.0
* @access public
*/
public static function is_plugin_active( $plugin_slug ): bool {
$active_plugins = (array) get_option( 'active_plugins', array() );
foreach ( $active_plugins as $active_plugin ) {
if ( strpos( $active_plugin, $plugin_slug . '.php' ) !== false ) {
return true;
}
}
return false;
}
public function is_preview_domain( $headers = null ): bool {
// @codeCoverageIgnoreStart
if ( $headers === null && function_exists( 'getallheaders' ) ) {
$headers = getallheaders();
}
// @codeCoverageIgnoreEnd
if ( isset( $headers['X-Preview-Indicator'] ) && $headers['X-Preview-Indicator'] ) {
return true;
}
return false;
}
public static function woocommerce_onboarding_choice(): bool {
return (bool) get_option( 'hostinger_woo_onboarding_choice', false );
}
public static function generate_bypass_code( $length ) {
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$code = '';
$max_index = strlen( $characters ) - 1;
for ( $i = 0; $i < $length; $i++ ) {
$random_index = wp_rand( 0, $max_index );
$code .= $characters[ $random_index ];
}
return $code;
}
public function get_hostinger_plugin_url(): string {
$website_locale = get_locale() ?? 'en_US';
$reseller_locale = get_option( 'hostinger_reseller', null );
$base_domain = $reseller_locale ?? ( self::HOSTINGER_LOCALES[ $website_locale ] ?? 'hostinger.com' );
$plugin_url = rtrim( $base_domain, '/' ) . '/';
$plugin_url .= str_replace( ABSPATH, '', HOSTINGER_ABSPATH );
return $plugin_url;
}
public function get_recommended_php_version(): string {
$wp_version = get_bloginfo( 'version' );
if ( empty( $wp_version ) ) {
return '8.2';
}
// Remove any additional version info (like -RC1, -beta1, etc.).
$wp_version = preg_replace( '/[-+].*$/', '', $wp_version );
if ( version_compare( $wp_version, '6.6', '>=' ) ) {
return '8.2';
}
if ( version_compare( $wp_version, '6.3', '>=' ) ) {
return '8.1';
}
if ( version_compare( $wp_version, '5.3', '>=' ) ) {
return '7.4';
}
if ( version_compare( $wp_version, '5.0', '>=' ) ) {
return '7.3';
}
if ( version_compare( $wp_version, '4.9', '=' ) ) {
return '7.2';
}
if ( version_compare( $wp_version, '4.7', '>=' ) ) {
return '7.1';
}
if ( version_compare( $wp_version, '4.4', '>=' ) ) {
return '7.0';
}
return '5.6';
}
public function get_edit_site_url(): string {
if ( wp_is_block_theme() ) {
return add_query_arg(
array(
'canvas' => 'edit',
),
admin_url( 'site-editor.php' )
);
}
$show_on_front = get_option( 'show_on_front' );
$front_page_id = get_option( 'page_on_front' );
if ( $show_on_front === self::HOMEPAGE_DISPLAY && $front_page_id ) {
return add_query_arg(
array(
'post' => $front_page_id,
'action' => 'edit',
),
admin_url( 'post.php' )
);
}
return '';
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Hostinger;
use Hostinger\Admin\PluginSettings;
use Hostinger\Admin\Jobs\NotifyMcpJob;
use Hostinger\Mcp\EventHandlerFactory;
use Hostinger\WpHelper\Utils;
defined( 'ABSPATH' ) || exit;
class Hooks {
public function __construct() {
add_filter( 'xmlrpc_enabled', array( $this, 'check_xmlrpc_enabled' ) );
add_filter( 'wp_is_application_passwords_available', array( $this, 'check_authentication_password_enabled' ) );
add_filter( 'wp_headers', array( $this, 'check_pingback' ) );
add_filter( 'plugins_loaded', array( $this, 'plugins_loaded' ) );
add_action( 'update_option_woocommerce_coming_soon', array( $this, 'litespeed_flush_cache' ) );
add_action( 'update_option_woocommerce_store_pages_only', array( $this, 'litespeed_flush_cache' ) );
add_action( 'upgrader_process_complete', array( $this, 'disable_auth_passwords_on_update' ), 10, 2 );
add_action( 'transition_post_status', array( $this, 'handle_transition_post_status' ), 10, 3 );
add_action( 'updated_option', array( $this, 'handle_updated_option' ), 10, 3 );
}
public function handle_transition_post_status( string $new_status, string $old_status, \WP_Post $post ): void {
if ( $new_status === 'publish' || $old_status === 'publish' ) {
do_action(
NotifyMcpJob::JOB_NAME,
array(
'event' => EventHandlerFactory::MCP_EVENT_PAGE_UPDATED,
'post_id' => $post->ID,
)
);
}
}
public function handle_updated_option( string $option, mixed $old_value, mixed $value ): void {
if ( $option === 'cron' || $this->is_transient( $option ) ) {
return;
}
if ( $old_value !== $value ) {
do_action( NotifyMcpJob::JOB_NAME, array( 'event' => EventHandlerFactory::MCP_EVENT_UPDATED ) );
}
if ( $option === HOSTINGER_PLUGIN_SETTINGS_OPTION && isset( $value['optin_mcp'] ) && isset( $old_value['optin_mcp'] ) ) {
if ( $old_value['optin_mcp'] !== $value['optin_mcp'] ) {
do_action(
NotifyMcpJob::JOB_NAME,
array(
'event' => EventHandlerFactory::MCP_EVENT_OPTIN_TOGGLED,
'value' => $value['optin_mcp'],
)
);
}
}
}
public function disable_auth_passwords_on_update( \WP_Upgrader $upgrader_object, array $options ): void {
if ( $options['action'] !== 'update' || $options['type'] !== 'plugin' || empty( $options['plugins'] ) ) {
return;
}
if ( ! in_array( 'hostinger/hostinger.php', $options['plugins'], true ) ) {
return;
}
$settings = get_option( HOSTINGER_PLUGIN_SETTINGS_OPTION, array() );
if ( ! empty( $settings['disable_authentication_password'] ) ) {
return;
}
$options = new DefaultOptions();
$options->configure_authentication_password();
}
/**
* @return void
*/
public function plugins_loaded() {
$utils = new Utils();
$plugin_settings = new PluginSettings();
$settings = $plugin_settings->get_plugin_settings();
if ( defined( 'WP_CLI' ) && \WP_CLI ) {
return;
}
// Xmlrpc.
if ( $settings->get_disable_xml_rpc() && $utils->isThisPage( 'xmlrpc.php' ) ) {
exit( 'Disabled' );
}
// SSL redirect.
if ( $settings->get_force_https() && ! is_ssl() ) {
if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
$host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
if ( $settings->get_force_www() && strpos( $host, 'www.' ) === false ) {
$host = 'www.' . $host;
}
wp_safe_redirect( 'https://' . $host . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 301 );
exit;
}
}
// Force www.
if ( $settings->get_force_www() ) {
if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
$host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
if ( strpos( $host, 'www.' ) === false ) {
wp_safe_redirect( 'https://www.' . $host . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 301 );
exit;
}
}
}
}
/**
* @param mixed $headers
*
* @return mixed
*/
public function check_pingback( $headers ) {
$plugin_settings = new PluginSettings();
$settings = $plugin_settings->get_plugin_settings();
if ( $settings->get_disable_xml_rpc() ) {
unset( $headers['X-Pingback'] );
}
return $headers;
}
/**
* @return bool
*/
public function check_xmlrpc_enabled(): bool {
$plugin_settings = new PluginSettings();
$settings = $plugin_settings->get_plugin_settings();
if ( $settings->get_disable_xml_rpc() ) {
return false;
}
return true;
}
/**
* @return bool
*/
public function check_authentication_password_enabled(): bool {
$plugin_settings = new PluginSettings();
$settings = $plugin_settings->get_plugin_settings();
if ( $settings->get_disable_authentication_password() ) {
return false;
}
return true;
}
public function litespeed_flush_cache(): void {
if ( has_action( 'litespeed_purge_all' ) ) {
do_action( 'litespeed_purge_all' );
}
}
private function is_transient( $option ): bool {
return str_contains( $option, '_transient' );
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Hostinger;
use Hostinger\Bootstrap;
defined( 'ABSPATH' ) || exit;
class Hostinger {
protected string $plugin_name = 'Hostinger';
protected string $version;
/**
* @return void
*/
public function bootstrap(): void {
$this->version = $this->get_plugin_version();
$bootstrap = new Bootstrap();
$bootstrap->run();
}
/**
* @return void
*/
public function run(): void {
$this->bootstrap();
}
/** PHPCS:disable WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound */
/**
* Define constant
*
* @param string $name Constant name.
* @param string|bool $value Constant value.
*/
private function define( string $name, $value ): void {
if ( ! defined( $name ) ) {
define( $name, $value );
}
}
/** PHPCS:enable */
/**
* @return string
*/
private function get_plugin_version(): string {
if ( defined( 'HOSTINGER_VERSION' ) ) {
return HOSTINGER_VERSION;
}
return '1.0.0';
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Hostinger;
defined( 'ABSPATH' ) || exit;
class I18n {
/**
* Load the plugin text domain for translation.
*
* @since 1.2.0
*/
public function load_plugin_textdomain(): void {
load_plugin_textdomain(
'hostinger',
false,
dirname( plugin_basename( __FILE__ ), 2 ) . '/languages/'
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Hostinger\LlmsTxtGenerator;
defined( 'ABSPATH' ) || exit;
class LlmsTxtFileHelper {
public const HOSTINGER_LLMSTXT_FILENAME = 'llms.txt';
public function is_user_generated_file(): bool {
if ( ! $this->llmstxt_file_exists() ) {
return false;
}
$content = $this->get_content();
if ( $content === false ) {
return false;
}
return ! str_contains( $content, LlmsTxtGenerator::HOSTINGER_LLMSTXT_SIGNATURE );
}
public function get_content(): string {
global $wp_filesystem;
$this->init_wp_filesystem();
return $wp_filesystem->get_contents( $this->get_llmstxt_file_path() );
}
public function create( string $content ): void {
global $wp_filesystem;
$this->init_wp_filesystem();
$wp_filesystem->put_contents( $this->get_llmstxt_file_path(), $content );
}
public function delete(): void {
if ( $this->llmstxt_file_exists() && ! $this->is_user_generated_file() ) {
global $wp_filesystem;
$this->init_wp_filesystem();
$wp_filesystem->delete( $this->get_llmstxt_file_path() );
}
}
public function get_llmstxt_file_path(): string {
return ABSPATH . self::HOSTINGER_LLMSTXT_FILENAME;
}
public function get_llmstxt_file_url(): string {
return site_url( LlmsTxtFileHelper::HOSTINGER_LLMSTXT_FILENAME );
}
public function llmstxt_file_exists(): bool {
return file_exists( $this->get_llmstxt_file_path() );
}
protected function init_wp_filesystem(): void {
require_once ABSPATH . '/wp-admin/includes/file.php';
WP_Filesystem();
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Hostinger\LlmsTxtGenerator;
use Hostinger\Admin\Jobs\LlmsTxtInjectContentJob;
use Hostinger\Admin\PluginSettings;
defined( 'ABSPATH' ) || exit;
class LlmsTxtGenerator {
public const HOSTINGER_LLMSTXT_SIGNATURE = '[comment]: # (Generated by Hostinger Tools Plugin)';
protected const UTF8_BOM = "\xEF\xBB\xBF";
public const HOSTINGER_LLMSTXT_SUPPORTED_POST_TYPES = array(
'post' => 'Posts',
'page' => 'Pages',
'product' => 'Products',
);
/**
* Activation and deactivation hooks appears too early,
* so the plugin status is not available yet.
*
* That's why we use a flag this->woocommerce_status to know the plugin status.
*
* @var string|null
*/
protected ?string $woocommerce_status = null;
protected PluginSettings $plugin_settings;
protected LlmsTxtFileHelper $file_helper;
protected LlmsTxtParser $parser;
public function __construct( PluginSettings $plugin_settings, LlmsTxtFileHelper $llmstxt_file_helper, LlmsTxtParser $llms_txt_parser ) {
$this->plugin_settings = $plugin_settings;
$this->file_helper = $llmstxt_file_helper;
$this->parser = $llms_txt_parser;
add_action( 'init', array( $this, 'init' ) );
}
public function on_woocommerce_activation(): void {
$this->woocommerce_status = 'active';
$this->generate();
}
public function on_woocommerce_deactivation(): void {
$this->woocommerce_status = 'inactive';
$this->generate();
}
public function init(): void {
if ( wp_doing_ajax() || wp_doing_cron() || ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = $this->plugin_settings->get_plugin_settings();
if ( $settings->get_enable_llms_txt() && ! $this->file_helper->llmstxt_file_exists() ) {
$this->generate();
}
$this->init_hooks();
}
public function on_settings_update( bool $is_enabled ): void {
if ( $is_enabled ) {
$this->generate();
} else {
$this->delete();
}
}
public function on_post_status_change( string $new_status, string $old_status, \WP_Post $post ): void {
if ( ! $this->is_post_type_supported( $post->post_type ) ) {
return;
}
if ( $new_status === 'publish' || $old_status === 'publish' ) {
$this->generate();
}
}
public function on_blog_change( mixed $old_value, mixed $new_value ): void {
if ( $old_value !== $new_value ) {
$this->generate();
}
}
public function get_content(): string {
$content = self::UTF8_BOM;
$content .= $this->parser->inject_title();
$content .= $this->parser->inject_site_description();
$content .= $this->parser->inject_items( $this->parser->get_by_post_type( 'post' ), 'Posts' );
$content .= $this->parser->inject_items( $this->parser->get_by_post_type( 'page' ), 'Pages' );
$content .= $this->maybe_inject_woocommerce_products();
$content .= $this->maybe_inject_optional_entries();
$content .= $this->parser->inject_signature();
return $content;
}
public function init_hooks(): void {
add_action( 'transition_post_status', array( $this, 'on_post_status_change' ), 10, 3 );
add_action( 'hostinger_tools_settings_saved', array( $this, 'on_settings_saved' ) );
add_action( 'update_option_blogname', array( $this, 'on_blog_change' ), 10, 2 );
add_action( 'update_option_blogdescription', array( $this, 'on_blog_change' ), 10, 2 );
add_action( 'activate_woocommerce/woocommerce.php', array( $this, 'on_woocommerce_activation' ) );
add_action( 'deactivate_woocommerce/woocommerce.php', array( $this, 'on_woocommerce_deactivation' ) );
}
public function on_settings_saved( array $settings ): void {
if ( $settings['enable_llms_txt'] ) {
$this->generate();
} else {
$this->delete();
}
}
public function generate(): void {
$this->file_helper->create( $this->get_content() );
do_action( LlmsTxtInjectContentJob::JOB_NAME, array( 'post_type' => 'post' ) );
do_action( LlmsTxtInjectContentJob::JOB_NAME, array( 'post_type' => 'page' ) );
if ( $this->is_woocommerce_active() ) {
do_action( LlmsTxtInjectContentJob::JOB_NAME, array( 'post_type' => 'product' ) );
}
}
protected function delete(): void {
$this->file_helper->delete();
}
protected function is_post_type_supported( string $post_type ): bool {
return array_key_exists( $post_type, self::HOSTINGER_LLMSTXT_SUPPORTED_POST_TYPES );
}
protected function is_woocommerce_active(): bool {
return ( is_null( $this->woocommerce_status ) && is_plugin_active( 'woocommerce/woocommerce.php' ) ) || $this->woocommerce_status === 'active';
}
protected function maybe_inject_woocommerce_products(): string {
if ( ! $this->is_woocommerce_active() ) {
return '';
}
return $this->parser->inject_items( $this->parser->get_by_post_type( 'product' ), 'Products' );
}
protected function maybe_inject_optional_entries(): string {
$entries = array();
$mcp_entry = $this->maybe_inject_mcp_agent_entry();
if ( $mcp_entry ) {
$entries[] = $mcp_entry;
}
return $this->parser->inject_optional_entries( $entries );
}
protected function maybe_inject_mcp_agent_entry(): string {
$settings = $this->plugin_settings->get_plugin_settings();
if ( ! $settings->get_optin_mcp() ) {
return '';
}
return $this->parser->inject_mcp_agent_entry();
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Hostinger\LlmsTxtGenerator;
defined( 'ABSPATH' ) || exit;
class LlmsTxtParser {
public const DEFAULT_LIMIT = 100;
public const HOSTINGER_LLMSTXT_SIGNATURE = '[comment]: # (Generated by Hostinger Tools Plugin)';
public function get_by_post_type( string $post_type, int $limit = self::DEFAULT_LIMIT, int $offset = 0 ): array {
$limit = apply_filters( 'hostinger_batch_item_limit', $limit );
$args = array(
'post_type' => $post_type,
'post_status' => 'publish',
'fields' => 'ids',
'posts_per_page' => $limit,
'offset' => $offset,
);
return get_posts( $args );
}
public function get_items( array $items ): string {
$content = '';
foreach ( $items as $item ) {
$post = get_post( $item );
$title = $post->post_title;
$permalink = get_permalink( $post );
$excerpt = $this->prepare_excerpt( $post );
$content .= "- [$title]($permalink)";
if ( $excerpt ) {
$content .= ": $excerpt";
}
$content .= "\n";
}
return $content;
}
public function inject_site_description(): string {
$description = get_bloginfo( 'description' );
return $description ? "> $description\n\n" : '';
}
public function inject_title(): string {
$title = get_bloginfo( 'name' ) ? get_bloginfo( 'name' ) : site_url();
return "# $title\n\n";
}
public function inject_signature(): string {
return "\n\n" . self::HOSTINGER_LLMSTXT_SIGNATURE;
}
public function inject_mcp_agent_entry(): string {
$domain = parse_url( site_url(), PHP_URL_HOST );
return "- [Agent (MCP protocol)](websites-agents.hostinger.com/$domain/mcp)";
}
public function inject_items( array $items, string $title ): string {
if ( empty( $items ) ) {
return '';
}
$content = "\n## $title\n\n";
$content .= $this->get_items( $items );
return $content;
}
public function inject_optional_entries( array $entries ): string {
$output = '';
if ( ! empty( $entries ) ) {
$output = "\n## Optional\n\n";
$output .= implode( "\n", $entries );
}
return $output;
}
public function prepare_excerpt( \WP_Post $item ): string {
return str_replace( array( "\r", "\n" ), ' ', strip_tags( wp_trim_excerpt( $item->post_excerpt, $item ) ) );
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Hostinger;
defined( 'ABSPATH' ) || exit;
class Loader {
protected array $actions;
protected array $filters;
public function __construct() {
$this->actions = array();
$this->filters = array();
}
/**
* @param string $hook
* @param mixed $component
* @param string $callback
* @param int $priority
* @param int $accepted_args
*
* @return void
*/
public function add_action( string $hook, $component, string $callback, int $priority = 10, int $accepted_args = 1 ) {
$this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
}
/**
* @param string $hook
* @param mixed $component
* @param string $callback
* @param int $priority
* @param int $accepted_args
*
* @return void
*/
public function add_filter( string $hook, $component, string $callback, int $priority = 10, int $accepted_args = 1 ) {
$this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
}
/**
* @param array $hooks
* @param string $hook
* @param mixed $component
* @param string $callback
* @param int $priority
* @param int $accepted_args
*
* @return array
*/
private function add(
array $hooks,
string $hook,
$component,
string $callback,
int $priority,
int $accepted_args
): array {
$hooks[] = array(
'hook' => $hook,
'component' => $component,
'callback' => $callback,
'priority' => $priority,
'accepted_args' => $accepted_args,
);
return $hooks;
}
/**
* @return void
*/
public function run(): void {
foreach ( $this->filters as $hook ) {
add_filter(
$hook['hook'],
array( $hook['component'], $hook['callback'] ),
$hook['priority'],
$hook['accepted_args']
);
}
foreach ( $this->actions as $hook ) {
add_action(
$hook['hook'],
array( $hook['component'], $hook['callback'] ),
$hook['priority'],
$hook['accepted_args']
);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Hostinger\Mcp;
use Hostinger\Admin\Proxy;
use Hostinger\Mcp\Handlers\WebsiteMcpOptInToggled;
use Hostinger\Mcp\Handlers\WebsitePageUpdated;
use Hostinger\Mcp\Handlers\WebsiteUpdated;
defined( 'ABSPATH' ) || exit;
class EventHandlerFactory {
public const MCP_EVENT_UPDATED = 'wordpress.website.updated';
public const MCP_EVENT_PAGE_UPDATED = 'wordpress.website.page_updated';
public const MCP_EVENT_OPTIN_TOGGLED = 'wordpress.website.mcp.opt_in_toggled';
private array $handlers;
private Proxy $proxy;
public function __construct( Proxy $proxy ) {
$this->proxy = $proxy;
$this->handlers = array(
self::MCP_EVENT_UPDATED => WebsiteUpdated::class,
self::MCP_EVENT_PAGE_UPDATED => WebsitePageUpdated::class,
self::MCP_EVENT_OPTIN_TOGGLED => WebsiteMcpOptInToggled::class,
);
}
public function get_handler( string $event ) {
$handler = $this->handlers[ $event ] ?? false;
if ( ! $handler ) {
throw new \WP_Exception( 'Invalid event' );
}
return new $handler( $this->proxy );
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Hostinger\Mcp\Handlers;
use Hostinger\Admin\Proxy;
use Hostinger\Mcp\EventHandlerFactory;
defined( 'ABSPATH' ) || exit;
abstract class EventHandler {
public Proxy $proxy;
public function __construct( Proxy $proxy ) {
$this->proxy = $proxy;
}
protected function send_to_proxy( array $args = array() ): bool {
if ( ! $this->can_send( $args ) ) {
$this->debug_mcp( 'Event failed: User is not opted in' . ' -- ' . print_r( $args, true ) );
return false;
}
$event = $args['event'];
$request = $this->proxy->trigger_event( $event, $args );
if ( is_wp_error( $request ) ) {
$this->debug_mcp( 'Event failed: ' . $event . $request->get_error_message() . ' -- ' . print_r( $args, true ) );
return false;
}
$response_code = wp_remote_retrieve_response_code( $request );
if ( $response_code < 300 ) {
$this->debug_mcp( 'Event sent: ' . $event . ' -- ' . print_r( $args, true ) );
return true;
}
$this->debug_mcp( 'Event failed: ' . $event . ' --' . $response_code . ' -- ' . print_r( $args, true ) );
return false;
}
public function can_send( array $args = array() ): bool {
return isset( $args['event'] ) && ( $this->is_optin_event( $args['event'] ) || $this->is_opted_in() );
}
private function is_opted_in(): bool {
$settings = get_option( HOSTINGER_PLUGIN_SETTINGS_OPTION, array() );
return $settings['optin_mcp'] ?? false;
}
private function is_optin_event( $event ): bool {
return $event === EventHandlerFactory::MCP_EVENT_OPTIN_TOGGLED;
}
private function debug_mcp( string $msg ): void {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'Hostinger Tools MCP: ' . $msg . PHP_EOL, 3, WP_CONTENT_DIR . '/mcp.log' );
}
}
abstract public function send( array $args = array() ): void;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Hostinger\Mcp\Handlers;
defined( 'ABSPATH' ) || exit;
class WebsiteMcpOptInToggled extends EventHandler {
public function send( array $args = array() ): void {
$this->send_to_proxy( $args );
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Hostinger\Mcp\Handlers;
defined( 'ABSPATH' ) || exit;
class WebsitePageUpdated extends EventHandler {
const ALLOWED_POST_TYPES = array( 'post', 'product', 'page' );
public function send( array $args = array() ): void {
if ( ! $this->can_send( $args ) ) {
return;
}
$post = get_post( $args['post_id'] );
$args['url'] = get_permalink( $post );
$this->send_to_proxy( $args );
}
public function can_send( array $args = array() ): bool {
$post_id = $args['post_id'] ?? false;
if ( ! $post_id ) {
return false;
}
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
return parent::can_send( $args ) && in_array( $post->post_type, self::ALLOWED_POST_TYPES, true );
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Hostinger\Mcp\Handlers;
use Hostinger\Admin\Jobs\ActionScheduler;
defined( 'ABSPATH' ) || exit;
class WebsiteUpdated extends EventHandler {
const MCP_SITE_TRANSIENT = 'hostinger_mcp_site_status';
const MCP_SITE_TRANSIENT_LIFETIME = 1200; // 20 mins
public function send( array $args = array() ): void {
if ( ! $this->can_send( $args ) ) {
return;
}
$status = ActionScheduler::STATUS_FAILED;
if ( $this->send_to_proxy( $args ) ) {
$status = ActionScheduler::STATUS_COMPLETE;
}
set_transient( self::MCP_SITE_TRANSIENT, $status, self::MCP_SITE_TRANSIENT_LIFETIME );
}
public function can_send( array $args = array() ): bool {
return parent::can_send( $args ) && get_transient( self::MCP_SITE_TRANSIENT ) !== ActionScheduler::STATUS_COMPLETE;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Hostinger\Rest;
if ( ! defined( 'ABSPATH' ) ) {
die;
}
/**
* Class for handling Rest Api Routes
*/
class Routes {
/**
* @var Settings
*/
private SettingsRoutes $settings_routes;
/**
* @param SettingsRoutes $settings_routes Settings route class.
*/
public function __construct( SettingsRoutes $settings_routes ) {
$this->settings_routes = $settings_routes;
}
/**
* Init rest routes
*
* @return void
*/
public function init(): void {
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}
/**
* @return void
*/
public function register_routes() {
// Register Settings Rest API Routes.
$this->register_settings_routes();
}
/**
*
* @return void
*/
private function register_settings_routes(): void {
// Return settings.
register_rest_route(
HOSTINGER_PLUGIN_REST_API_BASE,
'get-settings',
array(
'methods' => 'GET',
'callback' => array( $this->settings_routes, 'get_settings' ),
'permission_callback' => array( $this, 'permission_check' ),
)
);
// Update settings.
register_rest_route(
HOSTINGER_PLUGIN_REST_API_BASE,
'update-settings',
array(
'methods' => 'POST',
'callback' => array( $this->settings_routes, 'update_settings' ),
'permission_callback' => array( $this, 'permission_check' ),
)
);
// Regenerate bypass link.
register_rest_route(
HOSTINGER_PLUGIN_REST_API_BASE,
'regenerate-bypass-code',
array(
'methods' => 'GET',
'callback' => array( $this->settings_routes, 'regenerate_bypass_code' ),
'permission_callback' => array( $this, 'permission_check' ),
)
);
}
/**
* @param WP_REST_Request $request WordPress rest request.
*
* @return bool
*/
/** PHPCS:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found */
public function permission_check( $request ): bool {
// Workaround if Rest Api endpoint cache is enabled.
// We don't want to cache these requests.
if ( has_action( 'litespeed_control_set_nocache' ) ) {
do_action(
'litespeed_control_set_nocache',
'Custom Rest API endpoint, not cacheable.'
);
}
if ( empty( is_user_logged_in() ) ) {
return false;
}
// Implement custom capabilities when needed.
return current_user_can( 'manage_options' );
}
}
/** PHPCS:enable */

View File

@@ -0,0 +1,315 @@
<?php
namespace Hostinger\Rest;
if ( ! defined( 'ABSPATH' ) ) {
die;
}
use Hostinger\Admin\Options\PluginOptions;
use Hostinger\Admin\PluginSettings;
use Hostinger\Helper;
/**
* Class for handling Settings Rest API
*/
class SettingsRoutes {
/**
* @var PluginSettings plugin settings.
*/
private PluginSettings $plugin_settings;
/**
* Construct class with dependencies
*
* @param PluginSettings $plugin_settings instance.
*/
public function __construct( PluginSettings $plugin_settings ) {
$this->plugin_settings = $plugin_settings;
}
/**
* Return all stored plugin settings
*
* @param WP_REST_Request $request WordPress rest request.
*
* @return \WP_REST_Response
*/
/** PHPCS:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found */
public function get_settings( \WP_REST_Request $request ): \WP_REST_Response {
global $wp_version;
$plugin_settings = $this->plugin_settings->get_plugin_settings();
$data = array(
'newest_wp_version' => $this->get_latest_wordpress_version(),
'current_wp_version' => $wp_version,
'php_version' => phpversion(),
'newest_php_version' => '8.2', // Will be refactored.
'is_eligible_www_redirect' => $this->is_eligible_www_redirect(),
'optin_mcp' => $plugin_settings->get_optin_mcp(),
);
$hostinger_plugin_settings = get_option( HOSTINGER_PLUGIN_SETTINGS_OPTION, array() );
if ( empty( $plugin_settings->get_bypass_code() ) ) {
if ( empty( $hostinger_plugin_settings['bypass_code'] ) ) {
$hostinger_plugin_settings['bypass_code'] = Helper::generate_bypass_code( 16 );
}
}
$plugin_settings = $plugin_settings->to_array();
$response = array(
'data' => array_merge( $data, $plugin_settings ),
);
$response = new \WP_REST_Response( $response );
$response->set_headers( array( 'Cache-Control' => 'no-cache' ) );
$response->set_status( \WP_Http::OK );
return $response;
}
/**
* @param \WP_REST_Request $request
*
* @return \WP_REST_Response
*/
public function regenerate_bypass_code( \WP_REST_Request $request ): \WP_REST_Response {
$settings = $this->plugin_settings->get_plugin_settings();
$settings->set_bypass_code( Helper::generate_bypass_code( 16 ) );
$new_settings = $settings->to_array();
$new_plugin_options = new PluginOptions( $new_settings );
$response = new \WP_REST_Response( array( 'data' => $this->plugin_settings->save_plugin_settings( $new_plugin_options )->to_array() ) );
$response->set_headers( array( 'Cache-Control' => 'no-cache' ) );
$response->set_status( \WP_Http::OK );
return $response;
}
/**
* @param \WP_REST_Request $request
*
* @return \WP_REST_Response
*/
public function update_settings( \WP_REST_Request $request ): \WP_REST_Response {
$settings = $this->plugin_settings->get_plugin_settings();
$new_settings = array();
$parameters = $request->get_params();
foreach ( $settings->to_array() as $field_key => $field_value ) {
if ( $field_key === 'bypass_code' ) {
$new_settings[ $field_key ] = $field_value;
continue;
}
if ( isset( $parameters[ $field_key ] ) ) {
$new_settings[ $field_key ] = ! empty( $parameters[ $field_key ] );
} else {
$new_settings[ $field_key ] = $field_value;
}
if ( $this->has_changed( $field_key, $new_settings[ $field_key ] ) ) {
do_action( "hostinger_tools_setting_{$field_key}_update", $new_settings[ $field_key ] );
}
}
$new_plugin_options = new PluginOptions( $new_settings );
$response = new \WP_REST_Response( array( 'data' => $this->plugin_settings->save_plugin_settings( $new_plugin_options )->to_array() ) );
$this->update_urls( $new_plugin_options );
$response->set_headers( array( 'Cache-Control' => 'no-cache' ) );
$response->set_status( \WP_Http::OK );
if ( has_action( 'litespeed_purge_all' ) ) {
do_action( 'litespeed_purge_all' );
}
do_action( 'hostinger_tools_settings_saved', $new_settings );
return $response;
}
/** PHPCS:enable */
/**
* @param PluginOptions $plugin_options
*
* @return bool
*/
private function update_urls( PluginOptions $plugin_options ): bool {
$siteurl = get_option( 'siteurl' );
$home = get_option( 'home' );
if ( empty( $siteurl ) || empty( $home ) ) {
return false;
}
if ( $plugin_options->get_force_https() ) {
$siteurl = str_replace( 'http://', 'https://', $siteurl );
$home = str_replace( 'http://', 'https://', $home );
}
if ( $this->is_eligible_www_redirect() ) {
if ( $plugin_options->get_force_www() ) {
$siteurl = $this->add_www_to_url( $siteurl );
$home = $this->add_www_to_url( $home );
} else {
$siteurl = str_replace( 'www.', '', $siteurl );
$home = str_replace( 'www.', '', $home );
}
}
update_option( 'siteurl', $siteurl );
update_option( 'home', $home );
return true;
}
/**
* @param string $url
*
* @return mixed
*/
private function add_www_to_url( string $url ): string {
$parsed_url = wp_parse_url( $url );
if ( isset( $parsed_url['host'] ) ) {
$host = $parsed_url['host'];
if ( strpos( $host, 'www.' ) !== 0 ) {
$host = 'www.' . $host;
}
$parsed_url['host'] = $host;
return $this->rebuild_url( $parsed_url );
}
return $url;
}
/**
* @param array $params
*
* @return string
*/
private function rebuild_url( array $params ): string {
$scheme = isset( $params['scheme'] ) ? $params['scheme'] . '://' : '';
$host = isset( $params['host'] ) ? $params['host'] : '';
$path = isset( $params['path'] ) ? $params['path'] : '';
$query = isset( $params['query'] ) ? '?' . $params['query'] : '';
$fragment = isset( $params['fragment'] ) ? '#' . $params['fragment'] : '';
return "$scheme$host$path$query$fragment";
}
/**
* @return string
*/
private function get_latest_wordpress_version(): string {
$newest_wordpress_version = get_transient( 'hostinger_newest_wordpress_version' );
if ( $newest_wordpress_version !== false ) {
return $newest_wordpress_version;
}
$newest_wordpress_version = $this->fetch_wordpress_version();
if ( ! empty( $newest_wordpress_version ) ) {
set_transient( 'hostinger_newest_wordpress_version', $newest_wordpress_version, 86400 );
return $newest_wordpress_version;
}
return '';
}
/**
* @return string
*/
private function fetch_wordpress_version(): string {
$url = 'https://api.wordpress.org/core/version-check/1.7/';
$response = wp_remote_get( $url );
if ( is_wp_error( $response ) ) {
return '';
}
$response_body = wp_remote_retrieve_body( $response );
if ( $response_body === false ) {
return '';
}
$data = json_decode( $response_body, true );
if ( json_last_error() !== JSON_ERROR_NONE || empty( $data['offers'][0]['current'] ) ) {
return '';
}
return $data['offers'][0]['current'];
}
/**
* @return bool
*/
private function is_eligible_www_redirect(): bool {
$is_eligible_www_redirect = get_transient( 'hostinger_is_eligible_www_redirect' );
if ( $is_eligible_www_redirect !== false ) {
return $is_eligible_www_redirect;
}
$domain = str_replace( 'www.', '', get_option( 'siteurl' ) );
$www_domain = $this->add_www_to_url( $domain );
$is_eligible_www_redirect = $this->check_domain_records( $domain, $www_domain );
if ( isset( $is_eligible_www_redirect ) ) {
set_transient( 'hostinger_is_eligible_www_redirect', $is_eligible_www_redirect, 120 );
return $is_eligible_www_redirect;
}
return '';
}
/**
* Check if the field has changed.
*/
private function has_changed( string $field, mixed $new_value ): bool {
$settings = $this->plugin_settings->get_plugin_settings();
$settings = $settings->to_array();
if ( ! array_key_exists( $field, $settings ) ) {
return false;
}
return $new_value !== $settings[ $field ];
}
/**
* @param string $domain_a
* @param string $domain_b
*
* @return bool
*/
public function check_domain_records( string $domain_a, string $domain_b ): bool {
return ( gethostbyname( $domain_a ) === gethostbyname( $domain_b ) );
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Hostinger;
defined( 'ABSPATH' ) || exit;
class Settings {
public const MYSELF = 'myself';
public const FREELANCER = 'freelancer';
public const DEVELOPER = 'developer';
public const OTHER = 'other';
public const BUSINESS_BEGINNER_SEGMENT = 'business_beginner';
public const LEARNER_SEGMENT = 'learner';
public const BUSINESS_OWNER_SEGMENT = 'business_owner';
public const WEBSITE_TYPE_BUSINESS = 'business';
public const WEBSITE_TYPE_PORTFOLIO = 'portfolio';
public const WEBSITE_TYPE_BLOG = 'blog';
public const SITE_TITLE_OPTION = 'blogname';
public function __construct() {
if ( ! $this->get_setting( 'user_segment' ) ) {
$this->set_user_segment();
}
}
public function set_user_segment(): void {
$created_by = self::get_setting( 'survey.website.created_by' );
$created_for = self::get_setting( 'survey.website.for' );
$need_help = self::get_setting( 'survey.website.need_help' );
$work_at = self::get_setting( 'survey.work_at' );
if ( $this->is_business_beginner( $created_by, $created_for, $need_help ) ) {
self::update_setting( 'user_segment', self::BUSINESS_BEGINNER_SEGMENT );
}
if ( $this->is_learner( $work_at, $need_help ) ) {
self::update_setting( 'user_segment', self::LEARNER_SEGMENT );
}
if ( $this->is_bussiness_owner( $created_for, $created_by ) ) {
self::update_setting( 'user_segment', self::BUSINESS_OWNER_SEGMENT );
}
}
private function is_business_beginner( string $created_by, string $created_for, bool $need_help ): bool {
return $created_by === self::MYSELF && $created_for === self::MYSELF && $need_help;
}
private function is_learner( string $work_at, bool $need_help ): bool {
return $work_at === self::FREELANCER && $need_help;
}
private function is_bussiness_owner( string $created_for, string $created_by ): bool {
return $created_for === self::MYSELF && ( $created_by === self::DEVELOPER || $created_by === self::OTHER );
}
public static function get_setting( string $setting ): string {
if ( $setting ) {
return get_option( 'hostinger_' . $setting, '' );
}
return '';
}
public static function update_setting( string $setting, $value, $autoload = null ): void {
if ( $setting ) {
update_option( 'hostinger_' . $setting, $value, $autoload );
}
}
}
new Settings();

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title><?php echo esc_html__( 'Coming Soon', 'hostinger' ); ?></title>
<style>
html *:not(body):not(.hsr-coming-soon-body > *) {
display: none;
}
.hsr-coming-soon-body {
display: flex !important;
}
</style>
</head>
<body class="hostinger">
<div class="hsr-coming-soon-body">
<img alt="logo" class="hsr-logo"
src="<?php echo esc_url( HOSTINGER_PLUGIN_URL . 'assets/images/logo-black.svg' ); ?>">
<img alt="illustration" class="hsr-coming-soon-illustration"
src="<?php echo esc_url( HOSTINGER_PLUGIN_URL . 'assets/images/illustration.png' ); ?>">
<h3>
<?php echo esc_html__( 'Coming Soon', 'hostinger' ); ?>
</h3>
<p>
<?php echo esc_html__( 'New WordPress website is being built and will be published soon', 'hostinger' ); ?>
</p>
</div>
<?php wp_footer(); ?>
</body>
</html>