/* * School Management Pro - API Controller * This file is autoloaded by the plugin. Do not access directly. */ /** * School Management System Pro - Capability Manager. * * Manages plugin-specific user roles and their associated capabilities, * utilizing WordPress transients for caching to enhance performance. * * @package SchoolManagementSystemPro * @subpackage Includes/Capabilities * @since 2.3.0 */ // Prevent direct file access for security. if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Final class SMS_Pro_Capability_Manager. * * This class is responsible for defining, generating, caching, and retrieving * capabilities for custom roles within the School Management System Pro plugin. * It is designed as a final class to prevent extension, ensuring a stable API. * * @since 2.3.0 */ final class SMS_Pro_Capability_Manager { /** * Prefix used for options, transients, capabilities, etc. * This should match the SMS_PRO_PREFIX constant defined in the main plugin file. * * @since 2.3.0 * @var string */ private static string $prefix = 'sms_pro_'; // Default, ensure SMS_PRO_PREFIX is available globally if this changes /** * Stores the role definitions (slug => display name) provided by the main plugin. * * @since 2.3.0 * @var array */ private static array $role_definitions = []; /** * Sets the plugin-defined roles and their display names. * * This method is intended to be called by the main plugin instance * during its initialization phase, providing the roles this manager will handle. * * @since 2.3.0 * @param array $roles An associative array where keys are role slugs * (e.g., 'student', 'teacher') and values are their * display names (e.g., 'Student', 'Teacher'). */ public static function set_role_definitions( array $roles ): void { self::$role_definitions = $roles; // If SMS_PRO_PREFIX is dynamically set and needs to be fetched, do it here. // For now, assuming 'sms_pro_' is the fixed prefix used consistently. if ( defined( 'SMS_PRO_PREFIX' ) ) { self::$prefix = SMS_PRO_PREFIX; } } /** * Retrieves the role definitions currently set in the manager. * * @since 2.3.0 * @return array The array of role slugs to display names. */ public static function get_role_definitions(): array { return self::$role_definitions; } /** * Retrieves the capabilities for a given role slug. * * Uses WordPress transients to cache capabilities for performance. If the cache * is empty or expired, capabilities are regenerated and re-cached. * * @since 2.3.0 * @param string $role_slug The unique slug of the role (e.g., 'student'). * @return array An associative array of capabilities (capability_name => true). * Returns a default array with 'read' => true if generation fails or * the role is unknown in a critical way, ensuring basic WP compatibility. */ public static function get_role_capabilities( string $role_slug ): array { if ( empty( $role_slug ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'SMS_Pro_Capability_Manager::get_role_capabilities() called with an empty role slug.' ); } return array( 'read' => true ); // Basic fallback } $cache_key = self::$prefix . 'role_caps_' . sanitize_key( $role_slug ); $caps = get_transient( $cache_key ); if ( false === $caps || ! is_array( $caps ) ) { // Check if it's not a valid array too $caps = self::generate_capabilities_for_role( $role_slug ); /** * Filters the Time-To-Live (TTL) for capability cache. * * @since 2.3.0 * @param int $ttl The cache duration in seconds. Default is 12 hours. * @param string $role_slug The slug of the role for which capabilities are being cached. */ $cache_ttl = apply_filters( self::$prefix . 'capability_cache_ttl', 12 * HOUR_IN_SECONDS, $role_slug ); set_transient( $cache_key, $caps, (int) $cache_ttl ); } // Ensure it's always an array, with 'read' as a fundamental fallback. return is_array( $caps ) ? $caps : array( 'read' => true ); } /** * Generates the definitive array of capabilities for a specific role slug. * * This method contains the core logic for assigning capabilities to roles. * All roles receive the 'read' capability by default. Specific capabilities * are added based on the role_slug. The list can be extended via a filter. * * @since 2.3.0 * @access private * @param string $role_slug The slug of the role. * @return array An associative array of capabilities. */ private static function generate_capabilities_for_role( string $role_slug ): array { // Ensure prefix is up-to-date, especially if SMS_PRO_PREFIX might be defined after class load. if ( defined( 'SMS_PRO_PREFIX' ) ) { self::$prefix = SMS_PRO_PREFIX; } $p = self::$prefix; $caps = array( 'read' => true ); // Fundamental WordPress capability for all roles. $role_specific_caps = []; // Initialize to ensure it's an array. switch ( $role_slug ) { case 'student': $role_specific_caps = array( $p . 'view_own_dashboard' => true, $p . 'view_own_profile' => true, $p . 'edit_own_profile' => true, $p . 'view_own_grades' => true, $p . 'view_own_attendance' => true, $p . 'access_lms_content' => true, $p . 'submit_assignments' => true, $p . 'view_own_timetable' => true, ); break; case 'parent': $role_specific_caps = array( $p . 'view_child_dashboard' => true, $p . 'view_child_profile' => true, $p . 'view_child_grades' => true, $p . 'view_child_attendance' => true, $p . 'view_child_timetable' => true, $p . 'communicate_with_teachers' => true, $p . 'view_school_events' => true, $p . 'view_fee_details' => true, ); break; case 'teacher': $role_specific_caps = array( $p . 'view_teacher_dashboard' => true, $p . 'manage_own_students' => true, $p . 'record_student_attendance' => true, $p . 'manage_student_grades' => true, $p . 'create_lms_content' => true, $p . 'grade_assignments' => true, $p . 'communicate_with_parents' => true, $p . 'manage_class_timetable' => true, ); break; case 'administrator_school': $role_specific_caps = array( $p . 'view_admin_dashboard' => true, $p . 'manage_plugin_settings' => true, // The line for 'manage_settings' has been removed to avoid redundancy. $p . 'manage_all_students' => true, $p . 'manage_all_staff' => true, $p . 'manage_all_attendance' => true, $p . 'manage_all_grades' => true, $p . 'manage_lms' => true, $p . 'manage_finances' => true, $p . 'manage_library_resources' => true, $p . 'manage_transport_system' => true, $p . 'manage_school_inventory' => true, $p . 'manage_health_module' => true, $p . 'manage_student_behavior' => true, $p . 'manage_school_events' => true, $p . 'send_broadcast_messages' => true, $p . 'generate_all_reports' => true, $p . 'access_plugin_api' => true, $p . 'manage_academic_year' => true, $p . 'manage_user_roles' => true, ); break; case 'librarian': $role_specific_caps = array( $p . 'manage_library_dashboard' => true, $p . 'manage_library_books' => true, $p . 'manage_book_circulation' => true, $p . 'generate_library_reports' => true, ); break; case 'accountant': $role_specific_caps = array( $p . 'manage_fees_structure' => true, $p . 'record_fee_payments' => true, $p . 'generate_financial_reports' => true, $p . 'manage_expense_tracking' => true, ); break; case 'nurse': $role_specific_caps = array( $p . 'view_health_dashboard' => true, $p . 'manage_student_health_records' => true, $p . 'record_medical_incidents' => true, $p . 'manage_medication_log' => true, ); break; case 'alumni': $role_specific_caps = array( $p . 'view_alumni_portal' => true, $p . 'update_own_alumni_profile' => true, $p . 'view_alumni_directory' => true, $p . 'view_alumni_events' => true, ); break; default: // If the role slug doesn't match any predefined cases, // it will only have the 'read' capability from $caps initialization. // This allows for dynamically added roles to get basic access, // with their specific caps potentially added via the filter below. break; } /** * Filters the role-specific capabilities before they are merged with default capabilities. * * @since 2.3.0 * @param array $role_specific_caps An array of capabilities specific to the role. * @param string $role_slug The slug of the role being processed. */ $role_specific_caps = apply_filters( self::$prefix . 'role_capabilities_' . $role_slug, $role_specific_caps, $role_slug ); return array_merge( $caps, (array) $role_specific_caps ); // Ensure $role_specific_caps is an array. } /** * Clears the capability cache (transients) for one or more roles. * * This is typically called during plugin activation, deactivation, or when * role definitions or capabilities are updated programmatically. * * @since 2.3.0 * @param array|string|null $role_slugs Specific role slug(s) to clear cache for. * If null, clears cache for all roles defined * in self::$role_definitions. */ public static function clear_capability_cache( $role_slugs = null ): void { if ( null === $role_slugs ) { if ( empty( self::$role_definitions ) ) { // Attempt to fetch from main plugin if roles haven't been set here yet. // This might occur if called very early, though set_role_definitions should be prioritized. if ( function_exists('sms_pro_get_plugin_instance') ) { $plugin_instance = sms_pro_get_plugin_instance(); self::set_role_definitions( $plugin_instance->get_role_definitions() ); // Ensure local copy is populated } if ( empty( self::$role_definitions ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'SMS_Pro_Capability_Manager::clear_capability_cache() called with null $role_slugs but internal role definitions are empty.' ); return; } } $role_slugs = array_keys( self::$role_definitions ); } elseif ( is_string( $role_slugs ) ) { $role_slugs = array( $role_slugs ); } if ( ! is_array( $role_slugs ) || empty( $role_slugs ) ) { return; } foreach ( $role_slugs as $slug ) { if ( ! empty( $slug ) && is_string( $slug ) ) { delete_transient( self::$prefix . 'role_caps_' . sanitize_key( $slug ) ); } } /** * Action hook fired after capability caches have been cleared. * * @since 2.3.0 * @param array $cleared_role_slugs The slugs of the roles whose caches were cleared. */ do_action( self::$prefix . 'capability_cache_cleared', $role_slugs ); } }/* * School Management Pro - Core Include * This file is autoloaded by the plugin. Do not access directly. */ /** * School Management System Pro - Installer. * * Handles activation, deactivation, database table creation, role registration, * and potentially version upgrade tasks for the plugin. * * @package SchoolManagementSystemPro * @subpackage Includes/Lifecycle * @since 2.3.0 */ // Prevent direct file access for security. if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class SMS_Pro_Install. * * Contains static methods for plugin lifecycle events. This class ensures * that the plugin is set up correctly on activation and cleaned up (where appropriate) * on deactivation. * * @since 2.3.0 */ class SMS_Pro_Install { /** * Plugin activation routines. * * This static method is hooked to `register_activation_hook`. It orchestrates * the creation of database tables, registration of custom user roles, and * other setup tasks necessary for the plugin to function correctly. * * @since 2.3.0 * @throws Exception If critical components like the main plugin instance or * Capability Manager are unavailable. */ public static function activate(): void { if ( ! function_exists( 'sms_pro_get_plugin_instance' ) ) { // This is a critical failure. The main plugin isn't loaded correctly. // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'SMS Pro Install Error: sms_pro_get_plugin_instance() function not found during activation. Plugin may not be correctly structured or loaded.' ); // Optionally, could throw an Exception or `wp_die()` but that might be too aggressive for activation. // Allowing it to proceed might lead to partial setup; logging is crucial here. return; // Exit early to prevent further errors. } $plugin_instance = sms_pro_get_plugin_instance(); // Ensure Role Definitions are passed to the Capability Manager. // The main plugin's instance() method should call define_internal_roles(), // which in turn calls SMS_Pro_Capability_Manager::set_role_definitions(). // We explicitly fetch them here to ensure they are available for table/role creation. $role_definitions = $plugin_instance->get_role_definitions(); $table_schemas = $plugin_instance->get_table_schemas(); if ( empty( $role_definitions ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'SMS Pro Install Warning: Role definitions are empty during activation.' ); } if ( empty( $table_schemas ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'SMS Pro Install Warning: Table schemas are empty during activation.' ); } self::create_plugin_tables( $table_schemas ); self::register_plugin_roles( $role_definitions ); // Clear capability cache for all plugin-defined roles. if ( class_exists( 'SMS_Pro_Capability_Manager' ) ) { SMS_Pro_Capability_Manager::clear_capability_cache( array_keys( $role_definitions ) ); } else { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'SMS Pro Install Error: SMS_Pro_Capability_Manager class not found during activation. Capabilities cache may not be cleared.' ); // This would be a significant issue if SMS_Pro_Capability_Manager.php isn't included correctly. } flush_rewrite_rules(); // Set a transient for a welcome notice or post-activation setup wizard. set_transient( SMS_PRO_PREFIX . 'activation_redirect', true, 30 ); // 30 seconds TTL /** * Action hook fired after the plugin has been successfully activated and set up. * * @since 2.3.0 * @param School_Management_System_Pro $plugin_instance The main plugin instance. */ do_action( SMS_PRO_PREFIX . 'plugin_activated', $plugin_instance ); } /** * Plugin deactivation routines. * * This static method is hooked to `register_deactivation_hook`. It performs * cleanup tasks such as clearing scheduled cron jobs and flushing rewrite rules. * It does NOT typically remove data (like tables or options) to preserve user data. * * @since 2.3.0 */ public static function deactivate(): void { // Clear capability cache. // Attempt to get roles if needed; main instance might not be fully available but should provide role keys. $role_slugs_to_clear = []; if ( function_exists('sms_pro_get_plugin_instance') ) { $plugin_instance = sms_pro_get_plugin_instance(); $role_definitions = $plugin_instance->get_role_definitions(); if( !empty( $role_definitions ) ) { $role_slugs_to_clear = array_keys( $role_definitions ); } } if ( class_exists( 'SMS_Pro_Capability_Manager' ) ) { // If we couldn't get role slugs, clear_capability_cache(null) will use its internal list if populated. SMS_Pro_Capability_Manager::clear_capability_cache( empty($role_slugs_to_clear) ? null : $role_slugs_to_clear ); } else { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'SMS Pro Install Warning: SMS_Pro_Capability_Manager class not found during deactivation. Capabilities cache may not be cleared.' ); } } /** * Filters the list of plugin-specific cron hook names to be cleared on deactivation. * * @since 2.3.0 * @param array $cron_hooks An array of cron hook names. */ $cron_hooks = apply_filters( SMS_PRO_PREFIX . 'cron_hooks_to_clear', array() ); if ( ! empty( $cron_hooks ) && is_array( $cron_hooks ) ) { foreach ( $cron_hooks as $hook_name ) { if ( is_string( $hook_name ) && ! empty( $hook_name ) ) { wp_clear_scheduled_hook( sanitize_key( $hook_name ) ); } } } flush_rewrite_rules(); $plugin_instance = function_exists('sms_pro_get_plugin_instance') ? sms_pro_get_plugin_instance() : null; /** * Action hook fired after the plugin has been deactivated. * * @since 2.3.0 * @param School_Management_System_Pro|null $plugin_instance The main plugin instance, if available. */ do_action( SMS_PRO_PREFIX . 'plugin_deactivated', $plugin_instance ); } /** * Creates or updates custom database tables using WordPress dbDelta function. * * @since 2.3.0 * @access private * @param array $table_schemas An associative array of table names (keys, without prefix) * and their SQL CREATE TABLE statements (values). */ private static function create_plugin_tables( array $table_schemas ): void { if ( empty( $table_schemas ) ) { return; } global $wpdb; if ( ! function_exists( 'dbDelta' ) ) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; } $charset_collate = $wpdb->get_charset_collate(); $db_prefix = $wpdb->prefix; // Standard WordPress table prefix. $tables_log = []; // Check for potential table conflicts before creating tables $conflict_check = self::check_table_conflicts( $table_schemas, $db_prefix ); if ( ! empty( $conflict_check['conflicts'] ) ) { // Log conflicts and potentially stop activation foreach ( $conflict_check['conflicts'] as $table_name => $conflict_info ) { $tables_log[] = "CONFLICT DETECTED: Table '{$table_name}' - {$conflict_info}"; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( "SMS Pro Install: Table conflict detected for {$table_name} - {$conflict_info}" ); } } // In production, we might want to stop activation if conflicts are detected if ( ! defined( 'SMS_PRO_ALLOW_TABLE_CONFLICTS' ) || ! SMS_PRO_ALLOW_TABLE_CONFLICTS ) { $tables_log[] = "STOPPING ACTIVATION due to table conflicts. Set SMS_PRO_ALLOW_TABLE_CONFLICTS=true to override."; return; } } foreach ( $table_schemas as $table_key => $schema_sql ) { // Construct the full table name. Schema SQL should use '%prefix%sms_TABLEKEY'. // $table_key is like 'students', 'staff', etc. // So, $db_prefix . 'sms_' . $table_key (e.g., 'wp_sms_students') should match placeholder in schema. $table_name_for_log = $db_prefix . 'sms_' . sanitize_key( $table_key ); // Replace placeholders in the SQL schema. // The SQL should look like: CREATE TABLE %prefix%sms_students (...) %charset_collate%; $sql = str_replace( array( '%prefix%sms_', '%charset_collate%' ), array( $db_prefix . 'sms_', $charset_collate ), // Results in 'wp_sms_students' $schema_sql ); // dbDelta is particular about formatting. Ensure two spaces after PRIMARY KEY. // And ensure table names in SQL match expected WordPress format (including prefix). $sql = preg_replace( '/PRIMARY KEY\s?\(/', 'PRIMARY KEY (', $sql ); // Example: Make sure keys like `KEY idx_user_id (user_id)` are well-formed. $results = dbDelta( $sql ); // dbDelta expects full SQL statements for each table. if ( ! empty( $wpdb->last_error ) ) { $tables_log[] = "Table '{$table_name_for_log}': Error during dbDelta - " . $wpdb->last_error; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( "SMS Pro Install Error creating table {$table_name_for_log}: {$wpdb->last_error}. SQL: {$sql}" ); } } elseif ( ! empty( $results ) ) { // $results array contains messages about table creation/alteration. // We can log these for detailed diagnostics if needed. $tables_log[] = "Table '{$table_name_for_log}': Processed by dbDelta. Results: " . wp_json_encode( $results ); } else { $tables_log[] = "Table '{$table_name_for_log}': No changes detected by dbDelta."; } } if ( ! empty( $tables_log ) && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { $log_message = "School Management System Pro - Database Table Setup Log:\n" . implode( "\n", $tables_log ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( $log_message ); } /** * Action hook fired after database tables have been processed by dbDelta. * * @since 2.3.0 * @param array $processed_tables_log Log of dbDelta operations. */ do_action( SMS_PRO_PREFIX . 'database_tables_created', $tables_log ); } /** * Check for potential table conflicts with existing plugins * * @since 2.3.0 * @access private * @param array $table_schemas Array of table schemas to check * @param string $db_prefix Database prefix * @return array Array with conflicts if any */ private static function check_table_conflicts( array $table_schemas, string $db_prefix ): array { global $wpdb; $conflicts = array(); // Common conflicting table prefixes from other school management plugins $conflicting_prefixes = array( 'sms_', 'school_', 'student_', 'education_', 'academy_', 'wpsms_', 'school_management_', 'student_management_' ); foreach ( $table_schemas as $table_key => $schema_sql ) { $table_name = $db_prefix . 'sms_' . sanitize_key( $table_key ); // Check if table already exists if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) ) ) { $conflicts[ $table_name ] = 'Table already exists - possible conflict with another plugin'; continue; } // Check for similar table names with different prefixes foreach ( $conflicting_prefixes as $conflict_prefix ) { $conflict_table = $db_prefix . $conflict_prefix . sanitize_key( $table_key ); if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $conflict_table ) ) ) { $conflicts[ $table_name ] = "Similar table exists: {$conflict_table} - potential conflict"; break; } } } return array( 'conflicts' => $conflicts ); } /** * Registers or updates custom user roles and their capabilities. * * Uses SMS_Pro_Capability_Manager to get the capabilities for each role. * * @since 2.3.0 * @access private * @param array $role_definitions An associative array of role slugs (keys) * and their display names (values). */ private static function register_plugin_roles( array $role_definitions ): void { if ( empty( $role_definitions ) ) { return; } if ( ! class_exists( 'SMS_Pro_Capability_Manager' ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'SMS Pro Install Error: SMS_Pro_Capability_Manager class not found during role registration. Roles cannot be registered.' ); } return; } foreach ( $role_definitions as $slug => $name ) { $slug = sanitize_key( $slug ); $name = sanitize_text_field( $name ); if ( empty( $slug ) || empty( $name ) ) { continue; } // Get capabilities from the (already loaded and populated) Capability Manager. $capabilities = SMS_Pro_Capability_Manager::get_role_capabilities( $slug ); $role_object = get_role( $slug ); if ( ! $role_object ) { // Role does not exist, add it. add_role( $slug, $name, $capabilities ); } else { // Role exists, update its capabilities to ensure they are current. // First, remove all plugin-specific capabilities. foreach ( $role_object->capabilities as $cap_key => $grant ) { if ( strpos( $cap_key, SMS_PRO_PREFIX ) === 0 ) { // Only manage our own capabilities. $role_object->remove_cap( $cap_key ); } } // Then, add the current set of capabilities for this role. foreach ( $capabilities as $cap_key => $grant ) { // We only add our own or 'read'. 'read' is handled by get_role_capabilities. if ( strpos( $cap_key, SMS_PRO_PREFIX ) === 0 || $cap_key === 'read' ) { $role_object->add_cap( $cap_key, $grant ); } } } } /** * Action hook fired after plugin roles have been registered or updated. * * @since 2.3.0 * @param array $role_definitions The definitions of roles that were processed. */ do_action( SMS_PRO_PREFIX . 'plugin_roles_registered', $role_definitions ); } /** * Checks for and applies plugin version upgrades. * * This method can be hooked to 'plugins_loaded' or 'admin_init'. It compares * the current plugin version with a stored version and runs upgrade routines * if necessary (e.g., schema changes, data migration, capability updates). * * @since 2.3.0 */ public static function check_for_upgrades(): void { $current_plugin_version = SMS_PRO_VERSION; // Defined in main plugin file. $stored_version = get_option( SMS_PRO_PREFIX . 'version', '0.0.0' ); if ( version_compare( $stored_version, $current_plugin_version, '<' ) ) { // --- Example Upgrade Path --- // if ( version_compare( $stored_version, '2.3.0', '<' ) ) { // self::upgrade_to_2_3_0(); // } // if ( version_compare( $stored_version, '2.3.0', '<' ) ) { // self::upgrade_to_2_3_0(); // } // After all specific upgrade routines have run: // 1. Potentially re-register roles if capabilities changed between versions. if ( function_exists('sms_pro_get_plugin_instance') ) { $plugin_instance = sms_pro_get_plugin_instance(); $role_definitions = $plugin_instance->get_role_definitions(); self::register_plugin_roles( $role_definitions ); // This will update capabilities. } // 2. Clear capability cache. if ( class_exists( 'SMS_Pro_Capability_Manager' ) ) { SMS_Pro_Capability_Manager::clear_capability_cache(); // Clear all } // 3. Flush rewrite rules if CPTs, taxonomies, or REST routes might have changed. flush_rewrite_rules(); // 4. Update the stored version to the current plugin version. update_option( SMS_PRO_PREFIX . 'version', $current_plugin_version ); /** * Action hook fired after plugin upgrade routines have been completed. * * @since 2.3.0 * @param string $new_version The version upgraded to. * @param string $old_version The version upgraded from. */ do_action( SMS_PRO_PREFIX . 'plugin_upgraded', $current_plugin_version, $stored_version ); } } // --- Example Upgrade Step Methods --- // /** // * Upgrade routines for version 2.3.0. // * @since 2.3.0 (Placeholder method) // */ // private static function upgrade_to_2_3_0(): void { // // Example: Add a new column to a table, update an option, migrate data. // // Always backup database before running schema changes. // // error_log( 'SMS Pro: Upgrading to version 2.1.0...' ); // } /** * Define table schemas for the plugin * * @since 2.3.0 * @return array */ public static function get_table_schemas(): array { $table_schemas = array( 'students' => "CREATE TABLE %prefix%sms_students ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned DEFAULT NULL, student_id varchar(20) NOT NULL, first_name varchar(50) NOT NULL, last_name varchar(50) NOT NULL, email varchar(100) DEFAULT NULL, phone varchar(20) DEFAULT NULL, date_of_birth date DEFAULT NULL, gender enum('male','female','other') DEFAULT NULL, address text, city varchar(50) DEFAULT NULL, state varchar(50) DEFAULT NULL, postal_code varchar(20) DEFAULT NULL, country varchar(50) DEFAULT 'US', class_id bigint(20) unsigned DEFAULT NULL, parent_id bigint(20) unsigned DEFAULT NULL, enrollment_date date DEFAULT NULL, status enum('active','inactive','graduated','transferred') DEFAULT 'active', created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY student_id (student_id), KEY user_id (user_id), KEY class_id (class_id), KEY parent_id (parent_id), KEY status (status) ) %charset_collate%;", 'staff' => "CREATE TABLE %prefix%sms_staff ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned DEFAULT NULL, employee_id varchar(20) NOT NULL, first_name varchar(50) NOT NULL, last_name varchar(50) NOT NULL, email varchar(100) DEFAULT NULL, phone varchar(20) DEFAULT NULL, position varchar(100) DEFAULT NULL, department varchar(100) DEFAULT NULL, hire_date date DEFAULT NULL, status enum('active','inactive','terminated') DEFAULT 'active', created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY employee_id (employee_id), KEY user_id (user_id), KEY status (status) ) %charset_collate%;", 'classes' => "CREATE TABLE %prefix%sms_classes ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, description text, teacher_id bigint(20) unsigned DEFAULT NULL, capacity int(11) DEFAULT NULL, schedule text, room_number varchar(20) DEFAULT NULL, academic_year varchar(20) DEFAULT NULL, semester varchar(20) DEFAULT NULL, status enum('active','inactive') DEFAULT 'active', created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY teacher_id (teacher_id), KEY status (status) ) %charset_collate%;", 'attendance' => "CREATE TABLE %prefix%sms_attendance ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, student_id bigint(20) unsigned NOT NULL, class_id bigint(20) unsigned DEFAULT NULL, date date NOT NULL, status enum('present','absent','late','excused') NOT NULL DEFAULT 'present', time_in time DEFAULT NULL, time_out time DEFAULT NULL, notes text, recorded_by bigint(20) unsigned DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY student_date (student_id, date), KEY class_id (class_id), KEY date (date), KEY status (status), KEY recorded_by (recorded_by) ) %charset_collate%;", 'grades' => "CREATE TABLE %prefix%sms_grades ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, student_id bigint(20) unsigned NOT NULL, class_id bigint(20) unsigned NOT NULL, assignment_name varchar(100) NOT NULL, assignment_type enum('homework','quiz','test','project','exam','participation') DEFAULT 'homework', grade decimal(5,2) DEFAULT NULL, max_grade decimal(5,2) DEFAULT 100.00, weight decimal(5,2) DEFAULT 1.00, due_date date DEFAULT NULL, submitted_date date DEFAULT NULL, comments text, graded_by bigint(20) unsigned DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY student_id (student_id), KEY class_id (class_id), KEY assignment_type (assignment_type), KEY due_date (due_date), KEY graded_by (graded_by) ) %charset_collate%;", 'logs' => "CREATE TABLE %prefix%sms_logs ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, level enum('error','warning','info','debug') NOT NULL DEFAULT 'info', message text NOT NULL, context longtext, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY level (level), KEY created_at (created_at) ) %charset_collate%;" ); return apply_filters( SMS_PRO_PREFIX . 'table_schemas', $table_schemas ); } }/* * School Management Pro - Core Module * This file is autoloaded by the plugin. Do not access directly. */ /** * Core Module for School Management System Pro. * * This file handles the registration of foundational post types and taxonomies * that are shared across various modules within the plugin. * * @package School_Management_System_Pro * @subpackage School_Management_System_Pro/modules/core * @author Yakobo Abeld Chusi * @since 2.3.0 */ // Prevent direct file access for security. if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class sms_pro_Module_Core * * Implements the core functionalities like registering shared CPTs and Taxonomies. */ class sms_pro_Module_Core implements SMS_Module_Interface { /** * Constructor for the Core Module. * * Initializes the module and registers its hooks with WordPress. * @since 2.3.0 */ public function __construct() { $this->register_hooks(); } /** * Registers all the hooks for this module. * * This method is the central place for all WordPress action and filter hooks. * @since 2.3.0 */ public function register_hooks() { add_action( 'init', array( $this, 'register_core_taxonomies' ), 5 ); add_filter( SMS_PRO_PREFIX . 'admin_submenus', array( $this, 'add_core_submenus' ) ); } /** * Registers core, shared taxonomies for the plugin. * * Taxonomies such as 'Subjects' and 'Academic Years' are foundational * and used by multiple other modules (e.g., Classes, Grading, Reports). * * @since 2.3.0 */ public function register_core_taxonomies() { // Register Subject Taxonomy $subject_labels = array( 'name' => _x( 'Subjects', 'taxonomy general name', SMS_PRO_TEXT_DOMAIN ), 'singular_name' => _x( 'Subject', 'taxonomy singular name', SMS_PRO_TEXT_DOMAIN ), 'search_items' => __( 'Search Subjects', SMS_PRO_TEXT_DOMAIN ), 'all_items' => __( 'All Subjects', SMS_PRO_TEXT_DOMAIN ), 'parent_item' => __( 'Parent Subject', SMS_PRO_TEXT_DOMAIN ), 'parent_item_colon' => __( 'Parent Subject:', SMS_PRO_TEXT_DOMAIN ), 'edit_item' => __( 'Edit Subject', SMS_PRO_TEXT_DOMAIN ), 'update_item' => __( 'Update Subject', SMS_PRO_TEXT_DOMAIN ), 'add_new_item' => __( 'Add New Subject', SMS_PRO_TEXT_DOMAIN ), 'new_item_name' => __( 'New Subject Name', SMS_PRO_TEXT_DOMAIN ), 'menu_name' => __( 'Subjects', SMS_PRO_TEXT_DOMAIN ), ); $subject_args = array( 'hierarchical' => true, // e.g., 'Science' -> 'Physics' 'labels' => $subject_labels, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => array( 'slug' => 'subject' ), 'show_in_rest' => true, // We will associate this taxonomy with specific CPTs (like 'sms_pro_class') in their respective modules. ); register_taxonomy( SMS_PRO_PREFIX . 'subject', array(), $subject_args ); // Register Academic Year Taxonomy $academic_year_labels = array( 'name' => _x( 'Academic Years', 'taxonomy general name', SMS_PRO_TEXT_DOMAIN ), 'singular_name' => _x( 'Academic Year', 'taxonomy singular name', SMS_PRO_TEXT_DOMAIN ), 'search_items' => __( 'Search Academic Years', SMS_PRO_TEXT_DOMAIN ), 'all_items' => __( 'All Academic Years', SMS_PRO_TEXT_DOMAIN ), 'edit_item' => __( 'Edit Academic Year', SMS_PRO_TEXT_DOMAIN ), 'update_item' => __( 'Update Academic Year', SMS_PRO_TEXT_DOMAIN ), 'add_new_item' => __( 'Add New Academic Year', SMS_PRO_TEXT_DOMAIN ), 'new_item_name' => __( 'New Academic Year Name', SMS_PRO_TEXT_DOMAIN ), 'menu_name' => __( 'Academic Years', SMS_PRO_TEXT_DOMAIN ), ); $academic_year_args = array( 'hierarchical' => false, // e.g., '2023-2024' 'labels' => $academic_year_labels, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => array( 'slug' => 'academic-year' ), 'show_in_rest' => true, ); register_taxonomy( SMS_PRO_PREFIX . 'academic_year', array(), $academic_year_args ); } /** * Adds admin submenus for the core module functionalities. * * This method hooks into the main plugin's submenu filter to add * management pages for core taxonomies. * * @param array $submenus The existing array of submenus. * @return array The modified array of submenus. * @since 2.3.0 */ public function add_core_submenus( array $submenus ): array { $submenus[ SMS_PRO_PREFIX . 'subjects' ] = array( 'parent_slug' => SMS_PRO_PREFIX . 'dashboard', 'page_title' => esc_html__( 'Manage Subjects', SMS_PRO_TEXT_DOMAIN ), 'menu_title' => esc_html__( 'Subjects', SMS_PRO_TEXT_DOMAIN ), 'capability' => SMS_PRO_PREFIX . 'manage_core_settings', // Example capability 'callback' => function() { // Use wp_redirect() for better security and user experience $url = admin_url( 'edit-tags.php?taxonomy=' . SMS_PRO_PREFIX . 'subject' ); wp_redirect( esc_url_raw( $url ) ); exit; }, 'module_slug' => 'core', 'position' => 30, ); $submenus[ SMS_PRO_PREFIX . 'academic_years' ] = array( 'parent_slug' => SMS_PRO_PREFIX . 'dashboard', 'page_title' => esc_html__( 'Manage Academic Years', SMS_PRO_TEXT_DOMAIN ), 'menu_title' => esc_html__( 'Academic Years', SMS_PRO_TEXT_DOMAIN ), 'capability' => SMS_PRO_PREFIX . 'manage_core_settings', // Example capability 'callback' => function() { // Use wp_redirect() for better security and user experience $url = admin_url( 'edit-tags.php?taxonomy=' . SMS_PRO_PREFIX . 'academic_year' ); wp_redirect( esc_url_raw( $url ) ); exit; }, 'module_slug' => 'core', 'position' => 40, ); return $submenus; } }/* * School Management Pro - Students Module * This file is autoloaded by the plugin. Do not access directly. */ /** * Main Module file for Student Management. * * This file acts as the entry point for the 'students' module. It loads all necessary * sub-components (Admin, AJAX, Data handlers) and initializes them. * * @package School_Management_System_Pro * @subpackage School_Management_System_Pro/modules/students * @author Yakobo Abeld Chusi * @since 2.3.0 */ // Prevent direct file access for security. if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class sms_pro_Module_Students * * Main orchestrator for the Students module. */ class sms_pro_Module_Students implements SMS_Module_Interface { /** * Instance of the Admin handler class. * @var sms_pro_Students_Admin|null */ public ?sms_pro_Students_Admin $admin = null; /** * Constructor for the Students Module. * * Loads dependencies and registers initial hooks. * @since 2.3.0 */ public function __construct() { $this->load_dependencies(); $this->register_hooks(); } /** * Loads all the necessary files for this module. * @since 2.3.0 */ private function load_dependencies() { // Data Handler (Originally CPT, now for custom table interaction) require_once SMS_PRO_PATH . 'modules/students/class-students-data.php'; // Admin area handler require_once SMS_PRO_PATH . 'modules/students/class-students-admin.php'; // AJAX handler require_once SMS_PRO_PATH . 'modules/students/class-students-ajax.php'; } /** * Registers all hooks for this module. * * Instantiates the Admin and AJAX classes, allowing them to register their own hooks. * @since 2.3.0 */ public function register_hooks() { $this->admin = new sms_pro_Students_Admin(); $ajax_handler = new sms_pro_Students_AJAX(); $ajax_handler->register_hooks(); // AJAX hooks are registered within its class. } }/* * School Management Pro - Staff Module * This file is autoloaded by the plugin. Do not access directly. */ /* * School Management Pro - Staff Module * This file is autoloaded by the plugin. Do not access directly. */ // FILE: modules/staff/class-staff-admin.php if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'sms_pro_Staff_Admin' ) ) { /** * Admin class for the Staff Module. * Handles admin-specific functionalities, settings pages, and interactions. */ class sms_pro_Staff_Admin { private static $module_slug = 'staff'; public function __construct() { // Add hooks specific to the admin area for this module. // These hooks are typically added by the main module class (sms_pro_Module_Staff) // Example hook registration, actual calls would be in sms_pro_Module_Staff::register_hooks() // if ( is_admin() ) { // add_filter( SMS_PRO_PREFIX . 'admin_submenus', array( $this, 'add_submenu_pages' ), 10, 1 ); // add_action( 'admin_init', array( $this, 'register_staff_settings' ) ); // } // error_log( get_class( $this ) . ' constructed.' ); } /** * Registers admin pages or submenus for this module via the main plugin's filter. * Hooked to `SMS_PRO_PREFIX . 'admin_submenus'`. * * @param array $submenus Existing submenus. * @return array Modified submenus. */ // public function add_submenu_pages( array $submenus ): array { // $parent_slug = SMS_PRO_PREFIX . 'dashboard'; // Main plugin dashboard slug // // $submenus[ SMS_PRO_PREFIX . self::$module_slug . '_overview' ] = array( // 'parent_slug' => $parent_slug, // 'page_title' => esc_html__( 'Staff Management', SMS_PRO_TEXT_DOMAIN ), // 'menu_title' => esc_html__( 'Staff', SMS_PRO_TEXT_DOMAIN ), // 'capability' => SMS_PRO_PREFIX . 'manage_staff_overview', // Define this capability // 'callback' => array( School_Management_System_Pro::instance(), 'render_admin_module_page' ), // 'module_slug' => self::$module_slug, // Crucial for main plugin to know which module content to render // // 'position' => 10, // Optional: order of submenu item // ); // // $submenus[ SMS_PRO_PREFIX . self::$module_slug . '_settings' ] = array( // 'parent_slug' => $parent_slug, // Could also be under a staff-specific top-level menu if desired // 'page_title' => esc_html__( 'Staff Settings', SMS_PRO_TEXT_DOMAIN ), // 'menu_title' => esc_html__( 'Settings', SMS_PRO_TEXT_DOMAIN ), // 'capability' => SMS_PRO_PREFIX . 'manage_staff_settings', // Define this capability // 'callback' => array( School_Management_System_Pro::instance(), 'render_admin_module_page' ), // 'module_slug' => self::$module_slug, // Points to a 'settings' view for 'staff' // // 'position' => 20, // ); // // return $submenus; // } /** * Registers settings for the Staff module. * This method would typically be hooked to 'admin_init'. */ // public function register_staff_settings() { // // Register a settings group // register_setting( // SMS_PRO_PREFIX . 'staff_settings_group', // Option group (unique identifier) // SMS_PRO_PREFIX . 'staff_options', // Option name (stored in wp_options) // array( $this, 'sanitize_staff_settings' ) // Sanitization callback // ); // // // Add a settings section // add_settings_section( // SMS_PRO_PREFIX . 'staff_general_section', // ID for the section // esc_html__( 'General Staff Settings', SMS_PRO_TEXT_DOMAIN ), // Title of the section // array( $this, 'render_staff_general_section_info' ),// Callback for section description // SMS_PRO_PREFIX . 'staff_settings_page' // Page slug where this section is shown (must match add_submenu_page slug or custom page) // ); // // // Add settings fields to the section // add_settings_field( // SMS_PRO_PREFIX . 'staff_field_example', // ID for the field // esc_html__( 'Example Staff Setting', SMS_PRO_TEXT_DOMAIN ), // Title of the field // array( $this, 'render_staff_field_example_html' ),// Callback to render the field's HTML // SMS_PRO_PREFIX . 'staff_settings_page', // Page slug // SMS_PRO_PREFIX . 'staff_general_section', // Section ID // array( 'label_for' => SMS_PRO_PREFIX . 'staff_field_example' ) // For