* @author Maximum Software Inc * @license GPL-3.0-only * @link https://agenistrator.com/ * * SPDX-FileCopyrightText: 2025-2026 Alex Ruban * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc * SPDX-License-Identifier: GPL-3.0-only */ namespace Agenistrator; // Direct access prevention if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Main class. */ class Main implements Maintenance { use Singleton; /** * Minimum requirements for the plugin. */ const MIN_PHP_VERSION = '7.4'; const MIN_WP_VERSION = '5.3'; const MIN_MYSQL_VERSION = '5.7.8'; const MIN_MARIADB_VERSION = '10.2.7'; /** * Recommended PHP memory limit in bytes (128 MB). * * The plugin is not memory-optimized at the current stage of development. * A lower limit may cause out-of-memory errors during normal operation. * * @since 1.0.4 */ const RECOMMENDED_MEMORY_LIMIT = 128 * MB_IN_BYTES; /** * Cached requirements check result. * * @var bool|null */ private $requirements_met = null; /** * Cached error messages when requirements are not met. * * @var array|null */ private $requirement_errors = null; /** * Get all plugin components * * @return Maintenance[] Array of component instances. */ private function get_components() { return array( Provider_Manager::get_instance(), LLM_Manager::get_instance(), Embeddings_Manager::get_instance(), Tool_Manager::get_instance(), Chat_Log_Manager::get_instance(), Reporter::get_instance(), Thread_Manager::get_instance(), Event_Queue::get_instance(), Admin::get_instance(), ); } /** * Invoke a method on all components * * @param string $method The method name to invoke. */ private function invoke_on_components( $method ) { foreach ( $this->get_components() as $component ) { $component->$method(); } } /** * Check if all minimum requirements are met. * * @return bool True if all requirements are met, false otherwise. * @since 1.0.0 */ public function check_requirements() { if ( null !== $this->requirements_met ) { return $this->requirements_met; } $this->requirements_met = true; $this->requirement_errors = array(); // Check PHP version if ( version_compare( PHP_VERSION, self::MIN_PHP_VERSION, '<' ) ) { $this->requirements_met = false; $this->requirement_errors[] = array( 'software' => 'PHP', 'current' => PHP_VERSION, 'required' => self::MIN_PHP_VERSION, ); } // Check WordPress version $wp_version = get_bloginfo( 'version' ); if ( version_compare( $wp_version, self::MIN_WP_VERSION, '<' ) ) { $this->requirements_met = false; $this->requirement_errors[] = array( 'software' => 'WordPress', 'current' => $wp_version, 'required' => self::MIN_WP_VERSION, ); } // Check database version $db_info = $this->get_db_type_and_version(); if ( 'mariadb' === $db_info['type'] ) { if ( version_compare( $db_info['version'], self::MIN_MARIADB_VERSION, '<' ) ) { $this->requirements_met = false; $this->requirement_errors[] = array( 'software' => 'MariaDB', 'current' => $db_info['version'], 'required' => self::MIN_MARIADB_VERSION, ); } } elseif ( version_compare( $db_info['version'], self::MIN_MYSQL_VERSION, '<' ) ) { $this->requirements_met = false; $this->requirement_errors[] = array( 'software' => 'MySQL', 'current' => $db_info['version'], 'required' => self::MIN_MYSQL_VERSION, ); } return $this->requirements_met; } /** * Get database type and version. * * Detects whether MariaDB or MySQL is being used and extracts the version. * MariaDB self-identifies in its version string. MySQL is the default * fallback, covering vanilla MySQL, Percona Server, AWS Aurora, and other * forks that do not reliably self-identify in db_server_info(). * * @return array Array with 'type' ('mysql' or 'mariadb') and 'version' keys. */ public function get_db_type_and_version() { global $wpdb; $info = $wpdb->db_server_info(); if ( stripos( $info, 'mariadb' ) !== false ) { // Handle MariaDB's "5.5.5-" version prefix in older PHP versions. // MariaDB 10.x adds a "5.5.5-" prefix to the version string in the MySQL protocol // for replication compatibility. PHP 8.0.16+ and 8.1.3+ strip this automatically. // phpcs:ignore PHPCompatibility.Constants.NewConstants.php_version_idFound -- PHP_VERSION_ID available since PHP 5.2.7. if ( ( PHP_VERSION_ID < 80016 || ( PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80103 ) ) && strpos( $info, '5.5.5-' ) === 0 ) { $info = substr( $info, 6 ); } return array( 'type' => 'mariadb', 'version' => $this->extract_version( $info ), ); } return array( 'type' => 'mysql', 'version' => $this->extract_version( $info ), ); } /** * Extract a dotted version number (e.g., "10.5.18") from a database server info string. * * @param string $info Raw server info string from $wpdb->db_server_info(). * @return string Extracted version, or '0.0.0' if no version-like pattern is found. */ private function extract_version( $info ) { return preg_match( '/(\d+\.\d+\.\d+)/', $info, $matches ) ? $matches[1] : '0.0.0'; } /** * Display admin notices (requirements errors and warnings). * @since 1.0.4 */ public function admin_notices() { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( ! $this->check_requirements() ) { $error_template = __( '{software} version {current} is installed, but version {required} or higher is required', 'agenistrator' ); $error_messages = array(); foreach ( $this->requirement_errors as $error ) { $error_messages[] = esc_html( String_Utils::replace_tags( $error_template, $error ) ); } $message = String_Utils::replace_tags( esc_html__( 'The plugin cannot run because minimum requirements are not met: {errors}', 'agenistrator' ), array( 'errors' => implode( esc_html__( ', ', 'agenistrator' ), $error_messages ) ) ); $notice = String_Utils::render_error_notice( 'agenistrator-requirements', array( 'label' => esc_html__( 'Agenistrator', 'agenistrator' ), 'message' => $message, ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All values are escaped above echo $notice; return; } // Check memory limit $memory_limit = wp_convert_hr_to_bytes( ini_get( 'memory_limit' ) ); if ( $memory_limit > 0 && $memory_limit < self::RECOMMENDED_MEMORY_LIMIT ) { $message = String_Utils::replace_tags( esc_html__( 'The PHP memory limit is set to {current}, but {recommended} or higher is recommended to avoid out-of-memory errors', 'agenistrator' ), array( 'current' => esc_html( size_format( $memory_limit ) ), 'recommended' => esc_html( size_format( self::RECOMMENDED_MEMORY_LIMIT ) ), ) ); $notice = String_Utils::render_warning_notice( 'agenistrator-memory-warning', array( 'label' => esc_html__( 'Agenistrator', 'agenistrator' ), 'message' => $message, ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All values are escaped above echo $notice; } } /** * Register hooks for the main class and all components * @since 1.0.0 */ public function register_hooks() { // Register internationalization early add_action( 'init', array( $this, 'load_plugin_textdomain' ) ); // Register admin notices add_action( 'admin_notices', array( $this, 'admin_notices' ) ); // Bail if requirements are not met if ( ! $this->check_requirements() ) { return; } // Register activation and deactivation hooks register_activation_hook( AGENISTRATOR_PLUGIN_FILE, array( $this, 'activate_plugin' ) ); register_deactivation_hook( AGENISTRATOR_PLUGIN_FILE, array( $this, 'deactivate_plugin' ) ); // Register hooks for all components $this->invoke_on_components( 'register_hooks' ); } /** * Plugin activation tasks * * Called when the plugin is activated. Initializes all components * and schedules initial embedding generation. * @since 1.0.0 */ public function activate_plugin() { // Activate each component $this->invoke_on_components( 'activate_plugin' ); } /** * Plugin deactivation tasks * * Called when the plugin is deactivated. Cleans up temporary data * but preserves user settings. * @since 1.0.0 */ public function deactivate_plugin() { // Deactivate each component $this->invoke_on_components( 'deactivate_plugin' ); } /** * Uninstall plugin * @since 1.0.0 */ public function uninstall_plugin() { // Uninstall each component $this->invoke_on_components( 'uninstall_plugin' ); } /** * Load text domain for translation. * * We explicitly load translations from the plugin's languages/ directory rather than * relying solely on WordPress.org's automatic translation system. This ensures: * * 1. Translations work when WordPress.org is blocked (hosting provider firewall, etc.) * 2. We can ship translations bundled with the plugin for immediate availability * * WordPress.org translations require the plugin to be hosted there AND for the site to * have internet access to download translation packs from translate.wordpress.org. * @since 1.0.0 */ public function load_plugin_textdomain() { // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Intentionally bundling translations for restricted network use; see PHPDoc above. load_plugin_textdomain( 'agenistrator', false, dirname( plugin_basename( AGENISTRATOR_PLUGIN_FILE ) ) . '/languages/' ); } }