Plugin Directory

Changeset 3419682


Ignore:
Timestamp:
12/15/2025 04:27:39 AM (3 months ago)
Author:
cnvrse
Message:

Performance improvements

Location:
cnvrse
Files:
173 added
4 edited

Legend:

Unmodified
Added
Removed
  • cnvrse/trunk/cnvrse-lite.php

    r3405526 r3419682  
    44 * Plugin URI: https://cnvrse.com/cnvrse-lite
    55 * Description: Cnvrse Lite is a simple, privacy-friendly live chat widget for WordPress. Reply to your visitors directly from your WordPress Admin or from Telegram. No external accounts required.
    6  * Version: 025.11.28.01
     6 * Version: 025.12.14.02
    77 * Author: cnvrse
    88 * Author URI: https://profiles.wordpress.org/cnvrse/
     
    2222
    2323
    24 define( 'CNVRSE_VERSION', '025.11.28.01' );
     24define( 'CNVRSE_VERSION', '025.12.14.02' );
    2525define( 'CNVRSE_FILE', __FILE__ );
    2626define( 'CNVRSE_DIR', plugin_dir_path( __FILE__ ) );
  • cnvrse/trunk/readme.txt

    r3405542 r3419682  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 025.11.28.01
     7Stable tag: 025.12.14.02
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
  • cnvrse/trunk/src/Utilities/helper-functions.php

    r3405526 r3419682  
    11<?php
    22/**
    3  * Helper functions for Cnvrse.
    4  *
    5  * @package Cnvrse
    6  */
    7 
    8 declare(strict_types=1);
     3 * This file is part of Cnvrse Lite .
     4 *
     5 * Copyright (C) 2010-2025, Renzo Johnson (email: renzo at cnvrse.com)
     6 *
     7 * For the full copyright and license information, please view the LICENSE
     8 * file that was distributed with this source code, or visit:
     9 * https://www.gnu.org/licenses/gpl-2.0.html
     10*/
     11
    912
    1013if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
    12 }
    13 
    14 /*
    15 |--------------------------------------------------------------------------
    16 | REST API Helpers
    17 |--------------------------------------------------------------------------
    18 */
    19 
    20 /**
    21  * Create a successful API response.
    22  */
    23 function cnvrse_api_success( array $data = [] ): WP_REST_Response {
    24     return new WP_REST_Response(
    25         array_merge( [ 'success' => true ], $data ),
    26         200
    27     );
    28 }
    29 
    30 /**
    31  * Create an error API response.
    32  */
    33 function cnvrse_api_error( string $code, string $message, int $status = 200 ): WP_REST_Response {
    34     return new WP_REST_Response(
    35         [
    36             'success' => false,
    37             'code'    => $code,
    38             'message' => $message,
    39         ],
    40         $status
    41     );
    42 }
    43 
    44 /*
    45 |--------------------------------------------------------------------------
    46 | Rate Limiting (extracted from TanuSecurity)
    47 |--------------------------------------------------------------------------
    48 */
    49 
    50 /**
    51  * Check rate limit for an action.
    52  *
    53  * @param string $key    Unique key for the action.
    54  * @param int    $max    Maximum attempts allowed.
    55  * @param int    $window Time window in seconds.
    56  * @return bool True if within limit, false if exceeded.
    57  */
    58 function cnvrse_check_rate_limit( string $key, int $max = 30, int $window = 60 ): bool {
    59     $transient_key = 'cnvrse_rate_' . md5( $key );
    60     $attempts      = get_transient( $transient_key );
    61 
    62     if ( false === $attempts ) {
    63         set_transient( $transient_key, 1, $window );
    64         return true;
    65     }
    66 
    67     if ( $attempts >= $max ) {
    68         return false;
    69     }
    70 
    71     set_transient( $transient_key, $attempts + 1, $window );
    72     return true;
    73 }
    74 
    75 /*
    76 |--------------------------------------------------------------------------
    77 | Validation Helpers (extracted from TanuSecurity)
    78 |--------------------------------------------------------------------------
    79 */
    80 
    81 /**
    82  * Validate visitor ID format.
    83  *
    84  * Format: cnvrse_lite_1234567890_abc123xyz or cnvrse_pro_...
    85  */
    86 function cnvrse_validate_visitor_id( string $visitor_id ): string|false {
    87     $pattern = '/^cnvrse_(lite|pro)_\d{10,13}_[a-z0-9]{9}$/';
    88 
    89     return preg_match( $pattern, $visitor_id ) === 1
    90         ? sanitize_text_field( $visitor_id )
    91         : false;
    92 }
    93 
    94 /**
    95  * Validate Telegram bot token format.
    96  *
    97  * Format: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz
    98  */
    99 function cnvrse_validate_bot_token( string $token ): string|false {
    100     $pattern = '/^\d{8,10}:[A-Za-z0-9_-]{35}$/';
    101 
    102     return preg_match( $pattern, $token ) === 1
    103         ? sanitize_text_field( $token )
    104         : false;
    105 }
    106 
    107 /**
    108  * Generate a secure random string.
    109  */
    110 function cnvrse_generate_random_string( int $length = 32 ): string {
    111     $chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
    112     $str   = '';
    113 
    114     for ( $i = 0; $i < $length; $i++ ) {
    115         $str .= $chars[ random_int( 0, strlen( $chars ) - 1 ) ];
    116     }
    117 
    118     return $str;
    119 }
    120 
    121 /*
    122 |--------------------------------------------------------------------------
    123 | Conversation Helpers
    124 |--------------------------------------------------------------------------
    125 */
    126 
    127 /**
    128  * Build conversation query arguments.
    129  *
    130  * @param string $status Status filter ('all', 'open', 'pending', 'closed').
    131  * @param array  $extra  Additional query arguments.
    132  * @return array Query arguments for get_posts() or ConversationManager.
    133  */
    134 function cnvrse_build_conversation_query( string $status = 'all', array $extra = [] ): array {
    135     $args = array_merge([
    136         'posts_per_page' => -1,
    137         'orderby'        => 'modified',
    138         'order'          => 'DESC',
    139     ], $extra );
    140 
    141     // phpcs:disable WordPress.DB.SlowDBQuery
    142     if ( 'all' !== $status && ! empty( $status ) ) {
    143         $args['tax_query'] = [[
    144             'taxonomy' => 'cnvrse_status',
    145             'field'    => 'slug',
    146             'terms'    => $status,
    147         ]];
    148     }
    149     // phpcs:enable WordPress.DB.SlowDBQuery
    150 
    151     return $args;
    152 }
    153 
    154 /**
    155  * Get formatted conversation number.
    156  *
    157  * Format: #YYMMDD.POSTID.COUNT (e.g., #251028.69.016)
    158  */
    159 function cnvrse_get_conversation_number( int $conversation_id, ?string $created_date = null ): string {
    160     if ( ! $created_date ) {
    161         $post         = get_post( $conversation_id );
    162         $created_date = $post?->post_date ?? current_time( 'mysql' );
    163     }
    164 
    165     $date_part = gmdate( 'ymd', strtotime( $created_date ) );
    166 
    167     // Count conversations on the same day.
    168     $conversations = get_posts([
    169         'post_type'      => 'cnvrse_conversation',
    170         'posts_per_page' => -1,
    171         'post_status'    => 'publish',
    172         'date_query'     => [[
    173             'year'  => gmdate( 'Y', strtotime( $created_date ) ),
    174             'month' => gmdate( 'n', strtotime( $created_date ) ),
    175             'day'   => gmdate( 'j', strtotime( $created_date ) ),
    176         ]],
    177         'orderby'        => 'date',
    178         'order'          => 'ASC',
    179         'fields'         => 'ids',
    180     ]);
    181 
    182     $position   = array_search( $conversation_id, $conversations, true );
    183     $count      = false !== $position ? $position + 1 : count( $conversations ) + 1;
    184     $count_part = str_pad( (string) $count, 3, '0', STR_PAD_LEFT );
    185 
    186     return "#{$date_part}.{$conversation_id}.{$count_part}";
    187 }
    188 
    189 /*
    190 |--------------------------------------------------------------------------
    191 | Pro Features
    192 |--------------------------------------------------------------------------
    193 */
    194 
    195 /**
    196  * Get Pro upgrade URL.
    197  */
    198 function cnvrse_get_pro_url(): string {
    199     return 'https://cnvrse.com/pro/';
    200 }
    201 
    202 /**
    203  * Check if Pro version is active.
    204  */
    205 function cnvrse_is_pro(): bool {
    206     return defined( 'CNVRSE_PRO_VERSION' );
    207 }
    208 
    209 /**
    210  * Display Pro features notice.
    211  */
    212 function cnvrse_show_pro_features(): void {
    213     if ( cnvrse_is_pro() ) {
    214         return;
    215     }
    216 
    217     ?>
    218     <div class="cnvrse-pro-notice">
    219         <h3><?php esc_html_e( 'Upgrade to Cnvrse Pro', 'cnvrse' ); ?></h3>
    220         <p><?php esc_html_e( 'Get advanced features including multiple agents, canned responses, analytics, and more.', 'cnvrse' ); ?></p>
    221         <a href="<?php echo esc_url( cnvrse_get_pro_url() ); ?>" class="button button-primary" target="_blank">
    222             <?php esc_html_e( 'Learn More', 'cnvrse' ); ?>
    223         </a>
    224     </div>
    225     <?php
    226 }
     14    exit;
     15}
     16
     17/**
     18 * Get the storage provider instance
     19 *
     20 * @return Cnvrse_Storage_Provider
     21 */
     22function cnvrse_get_storage_provider() {
     23    static $provider = null;
     24
     25    if ( null === $provider ) {
     26        // Allow Pro to override the storage provider
     27        if ( has_filter( 'cnvrse_storage_provider' ) ) {
     28            $provider = apply_filters( 'cnvrse_storage_provider', null );
     29        } else {
     30            // Default to container resolution (TransientStorage for Lite)
     31            $provider = cnvrse_make( 'Cnvrse_Storage_Provider' );
     32        }
     33    }
     34
     35    return $provider;
     36}
     37
     38/**
     39 * Get Pro upgrade URL
     40 *
     41 * @return string
     42 */
     43function cnvrse_get_pro_url() {
     44    return apply_filters(
     45        'cnvrse_pro_upgrade_url',
     46        'https://cnvrse.com/pro/?utm_source=lite&utm_medium=admin&utm_campaign=upgrade'
     47    );
     48}
     49
     50/**
     51 * Get formatted conversation number (e.g., #251027.012)
     52 *
     53 * PERFORMANCE FIX: Now reads from _cnvrse_chat_number meta field (stored on creation)
     54 * Falls back to generation if meta doesn't exist (for backward compatibility)
     55 *
     56 * Format: #YYMMDD.XXX where XXX is the conversation count for that day
     57 *
     58 * @param int    $conversation_id Conversation post ID
     59 * @param string $created_date    Conversation creation date (optional, for generation fallback)
     60 * @return string Formatted conversation number
     61 */
     62function cnvrse_get_conversation_number( $conversation_id, $created_date = null ) {
     63    // Try to get from meta first (fast path)
     64    $chat_number = get_post_meta( $conversation_id, '_cnvrse_chat_number', true );
     65
     66    if ( ! empty( $chat_number ) ) {
     67        return $chat_number;
     68    }
     69
     70    // Fallback: Generate and store (for backward compatibility with old conversations)
     71    $chat_number = cnvrse_generate_chat_number( $conversation_id, $created_date );
     72
     73    // Store for future requests
     74    update_post_meta( $conversation_id, '_cnvrse_chat_number', $chat_number );
     75
     76    return $chat_number;
     77}
     78
     79/**
     80 * Generate chat number (internal function - called once per conversation)
     81 *
     82 * Format: #YYMMDD.POSTID.COUNT
     83 *
     84 * @param int    $conversation_id Conversation post ID
     85 * @param string $created_date    Conversation creation date (optional)
     86 * @return string Formatted conversation number
     87 */
     88function cnvrse_generate_chat_number( $conversation_id, $created_date = null ) {
     89    // Get creation date
     90    if ( ! $created_date ) {
     91        $post         = get_post( $conversation_id );
     92        $created_date = $post ? $post->post_date : current_time( 'mysql' );
     93    }
     94
     95    // Format: YYMMDD
     96    $date_part = gmdate( 'ymd', strtotime( $created_date ) );
     97
     98    // Count conversations created on the same day (before this one)
     99    $args = array(
     100        'post_type'      => 'cnvrse_conversation',
     101        'posts_per_page' => -1,
     102        'post_status'    => 'publish',
     103        'date_query'     => array(
     104            array(
     105                'year'  => gmdate( 'Y', strtotime( $created_date ) ),
     106                'month' => gmdate( 'n', strtotime( $created_date ) ),
     107                'day'   => gmdate( 'j', strtotime( $created_date ) ),
     108            ),
     109        ),
     110        'orderby'        => 'date',
     111        'order'          => 'ASC',
     112        'fields'         => 'ids',
     113    );
     114
     115    $conversations = get_posts( $args );
     116    $position      = array_search( $conversation_id, $conversations );
     117    $count         = false !== $position ? $position + 1 : count( $conversations ) + 1;
     118
     119    // Format: XXX (3 digits, zero-padded)
     120    $count_part = str_pad( $count, 3, '0', STR_PAD_LEFT );
     121
     122    // Format: #YYMMDD.POSTID.COUNT (e.g., #251028.69.016)
     123    return '#' . $date_part . '.' . $conversation_id . '.' . $count_part;
     124}
     125
     126/**
     127 * Detect visitor tier based on message engagement and contact identification
     128 *
     129 * Tier hierarchy (Intercom industry standard):
     130 * - Visitor: Browsing only, no messages sent (message_count = 0)
     131 * - Conversation: Has sent messages (message_count > 0) but no email/name captured
     132 * - User: Has email OR name captured (identified contact)
     133 *
     134 * @param int $conversation_id Conversation post ID.
     135 * @return string|false 'user', 'conversation', 'visitor', or false on error.
     136 */
     137function cnvrse_detect_visitor_tier( $conversation_id ) {
     138    // Remove capability check - it causes issues during hydration if context is slightly off
     139    // Callers (REST API, Admin Page) already handle permission checks
     140
     141    // Validate conversation ID
     142    if ( ! is_numeric( $conversation_id ) ) {
     143        return false;
     144    }
     145
     146    $conversation_id = (int) $conversation_id;
     147
     148    // Verify it's actually a conversation post
     149    if ( 'cnvrse_conversation' !== get_post_type( $conversation_id ) ) {
     150        return false;
     151    }
     152
     153    // Get REAL message count to determine engagement level
     154    // Excludes system messages like "Visitor returned" which don't indicate engagement
     155    $db            = Cnvrse_Database::instance();
     156    $message_count = $db->get_real_message_count( $conversation_id );
     157
     158    // Get contact identification (email or name)
     159    $email = get_post_meta( $conversation_id, '_cnvrse_visitor_email', true ) ?? '';
     160    $name  = get_post_meta( $conversation_id, '_cnvrse_visitor_name', true ) ?? '';
     161
     162    // Tier 1: Visitor (browsing only, no messages sent)
     163    if ( 0 === $message_count ) {
     164        return 'visitor';
     165    }
     166
     167    // Tier 2: Conversation (has engaged but not identified)
     168    // Message count > 0 but no email/name captured
     169    if ( empty( $email ) && empty( $name ) ) {
     170        return 'conversation';
     171    }
     172
     173    // Tier 3: User (identified contact with email or name)
     174    return 'user';
     175}
     176
     177/**
     178 * Get unread message count for conversation
     179 *
     180 * @param int $conversation_id Conversation post ID.
     181 * @return int Unread count.
     182 */
     183function cnvrse_get_unread_count( $conversation_id ) {
     184    // Add capability check
     185    if ( ! current_user_can( 'manage_options' ) ) {
     186        return 0;
     187    }
     188
     189    // Use existing Database class method
     190    $db = Cnvrse_Database::instance();
     191    return (int) $db->get_unread_count( $conversation_id );
     192}
     193
     194/**
     195 * Update last activity timestamp for conversation
     196 *
     197 * Universal function that works for both local and satellite conversations.
     198 * Updates _cnvrse_last_activity meta field with current Unix timestamp.
     199 *
     200 * This function is called automatically on:
     201 * - Message sent (visitor or admin)
     202 * - Page view tracked
     203 * - Chat started
     204 * - Session ended
     205 *
     206 * @since 0.9.13
     207 * @param int $conversation_id Conversation post ID.
     208 * @return bool True on success, false on failure.
     209 */
     210function cnvrse_update_last_activity( $conversation_id ) {
     211    // Validate conversation ID
     212    if ( empty( $conversation_id ) || ! is_numeric( $conversation_id ) ) {
     213        return false;
     214    }
     215
     216    $conversation_id = (int) $conversation_id;
     217
     218    // Verify it's a conversation post
     219    if ( 'cnvrse_conversation' !== get_post_type( $conversation_id ) ) {
     220        return false;
     221    }
     222
     223    // Use pure UTC Unix timestamp (matches messages table and allows direct comparison)
     224    // Note: current_time('timestamp') applies timezone offset, causing 5-hour discrepancy
     225    $timestamp = time();
     226
     227    // Update meta field
     228    $result = update_post_meta( $conversation_id, '_cnvrse_last_activity', $timestamp );
     229
     230    return $result;
     231}
  • cnvrse/trunk/src/bootstrap.php

    r3405526 r3419682  
    11<?php
    22/**
    3  * Bootstrap file for Cnvrse.
    4  *
    5  * @package Cnvrse
    6  */
    7 
    8 declare(strict_types=1);
     3 * Cnvrse Bootstrap
     4 *
     5 * Single entry point for the Cnvrse plugin.
     6 * Contains: Autoloader, Main Class, and initialization.
     7 *
     8 * This file is part of Cnvrse Lite.
     9 *
     10 * Copyright (C) 2010-2025, Renzo Johnson (email: renzo at cnvrse.com)
     11 *
     12 * For the full copyright and license information, please view the LICENSE
     13 * file that was distributed with this source code, or visit:
     14 * https://www.gnu.org/licenses/gpl-2.0.html
     15 *
     16 * @package Cnvrse_Lite
     17 * @since 2.5.0
     18 */
    919
    1020if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
    12 }
    13 
    14 /**
    15  * Main Cnvrse plugin class.
     21    exit;
     22}
     23
     24/*
     25|--------------------------------------------------------------------------
     26| Autoloader
     27|--------------------------------------------------------------------------
     28*/
     29
     30/**
     31 * Class Cnvrse_Autoloader
     32 *
     33 * Convention-based autoloader for the Cnvrse plugin.
     34 * No class map needed - derives file paths from class names.
     35 *
     36 * Conventions:
     37 * - Classes: class-{name}.php (e.g., Cnvrse_Database → class-cnvrse-database.php)
     38 * - Interfaces: interface-{name}.php (e.g., Cnvrse_Storage_Provider → interface-cnvrse-storage-provider.php)
     39 * - Traits: trait-{name}.php (e.g., Cnvrse_Singleton_Trait → trait-cnvrse-singleton.php)
     40 */
     41final class Cnvrse_Autoloader {
     42
     43    /**
     44     * Base path for class files.
     45     *
     46     * @var string
     47     */
     48    private static $base_path = '';
     49
     50    /**
     51     * Directories to search (in order).
     52     *
     53     * @var array
     54     */
     55    private static $search_dirs = array(
     56        'Actions/',
     57        'Core/',
     58        'Domain/',
     59        'Infrastructure/AdminBar/',
     60        'Infrastructure/Geolocation/',
     61        'Infrastructure/Geolocation/Providers/',
     62        'Infrastructure/Http/',
     63        'Infrastructure/Repository/',
     64        'Infrastructure/Storage/',
     65        'Infrastructure/Telegram/',
     66        'Infrastructure/Utilities/',
     67        'Infrastructure/WordPress/',
     68        'Navigation/',
     69        'Presentation/',
     70        'REST/Controllers/',
     71        'Services/',
     72        'Shared/',
     73        'Utilities/',
     74    );
     75
     76    /**
     77     * Register the autoloader.
     78     *
     79     * @param string $base_path Base path for src/ directory.
     80     * @return void
     81     */
     82    public static function register( string $base_path ): void {
     83        self::$base_path = trailingslashit( $base_path );
     84        spl_autoload_register( array( __CLASS__, 'autoload' ) );
     85    }
     86
     87    /**
     88     * Autoload callback.
     89     *
     90     * @param string $class_name Class name to load.
     91     * @return void
     92     */
     93    public static function autoload( string $class_name ): void {
     94        // Handle namespaced classes (e.g., Cnvrse\Shared\Tanu_Container).
     95        if ( strpos( $class_name, 'Cnvrse\\' ) === 0 ) {
     96            $parts         = explode( '\\', $class_name );
     97            $short_class   = end( $parts );
     98            $namespace_dir = implode( '/', array_slice( $parts, 1, -1 ) );
     99            $base_name     = strtolower( str_replace( '_', '-', $short_class ) ) . '.php';
     100
     101            // Try class-, interface-, trait- prefixes for namespaced types.
     102            foreach ( array( 'class-', 'interface-', 'trait-' ) as $prefix ) {
     103                $file = self::$base_path . $namespace_dir . '/' . $prefix . $base_name;
     104                if ( file_exists( $file ) ) {
     105                    require_once $file;
     106                    return;
     107                }
     108            }
     109        }
     110
     111        // Skip non-Cnvrse/Tanu classes.
     112        if ( strpos( $class_name, 'Cnvrse' ) !== 0 && strpos( $class_name, 'Tanu' ) !== 0 ) {
     113            return;
     114        }
     115
     116        // Determine file prefix based on naming convention.
     117        $prefix = 'class-';
     118        if ( strpos( $class_name, '_Trait' ) !== false ) {
     119            $prefix = 'trait-';
     120        } elseif ( strpos( $class_name, '_Interface' ) !== false ) {
     121            $prefix = 'interface-';
     122        }
     123
     124        // Special case: Cnvrse_Storage_Provider is an interface.
     125        if ( 'Cnvrse_Storage_Provider' === $class_name ) {
     126            $prefix = 'interface-';
     127        }
     128
     129        // Convert class name to file name: Cnvrse_Admin_Hooks → cnvrse-admin-hooks.
     130        $file_name = $prefix . strtolower( str_replace( '_', '-', $class_name ) ) . '.php';
     131
     132        // Search in all directories.
     133        foreach ( self::$search_dirs as $dir ) {
     134            $file = self::$base_path . $dir . $file_name;
     135            if ( file_exists( $file ) ) {
     136                require_once $file;
     137                return;
     138            }
     139        }
     140    }
     141}
     142
     143// Register autoloader.
     144Cnvrse_Autoloader::register( __DIR__ . '/' );
     145
     146/*
     147|--------------------------------------------------------------------------
     148| DI Container (global functions)
     149|--------------------------------------------------------------------------
     150*/
     151
     152/**
     153 * Get the DI container instance
     154 *
     155 * @return \Cnvrse\Shared\Tanu_Container
     156 */
     157function cnvrse_container() {
     158    static $container = null;
     159
     160    if ( null === $container ) {
     161        $container = new \Cnvrse\Shared\Tanu_Container();
     162
     163        // Bind storage provider - Lite uses transient storage.
     164        $storage_class = apply_filters( 'cnvrse_storage_provider_class', \Cnvrse\Infrastructure\Storage\Cnvrse_Transient_Storage::class );
     165        $container->bind( 'Cnvrse_Storage_Provider', $storage_class );
     166        $container->singleton( $storage_class );
     167
     168        // Allow Pro to modify bindings.
     169        do_action( 'cnvrse_register_bindings', $container );
     170    }
     171
     172    return $container;
     173}
     174
     175/**
     176 * Resolve from container
     177 *
     178 * @param string $abstract Class or interface to resolve.
     179 * @return mixed
     180 */
     181function cnvrse_make( string $abstract ) {
     182    return cnvrse_container()->make( $abstract );
     183}
     184
     185/*
     186|--------------------------------------------------------------------------
     187| Helper Functions & Templates
     188|--------------------------------------------------------------------------
     189*/
     190
     191// Load helper functions (not autoloadable - procedural code).
     192require_once __DIR__ . '/Utilities/helper-functions.php';
     193require_once __DIR__ . '/Utilities/rest-param-helpers.php';
     194
     195// Load templates (not autoloadable - pure rendering).
     196require_once __DIR__ . '/Presentation/conversation-columns.php';
     197require_once __DIR__ . '/Presentation/chat-widget-view.php';
     198
     199/*
     200|--------------------------------------------------------------------------
     201| Main Plugin Class
     202|--------------------------------------------------------------------------
     203*/
     204
     205/**
     206 * Main Cnvrse Plugin Class
     207 *
     208 * @since 1.0.0
    16209 */
    17210final class Cnvrse {
    18211
    19     private static ?self $instance = null;
    20 
    21     public static function instance(): self {
    22         return self::$instance ??= new self();
    23     }
    24 
    25     private function __construct() {
    26         $this->load_dependencies();
    27         $this->init_hooks();
    28     }
    29 
    30     /**
    31      * Load required files.
    32      */
    33     private function load_dependencies(): void {
    34         // Utilities.
    35         require_once CNVRSE_DIR . 'src/Utilities/helper-functions.php';
    36         require_once CNVRSE_DIR . 'src/Core/visitor-auth-helpers.php';
    37 
    38         // Core classes.
    39         require_once CNVRSE_DIR . 'src/Core/Database.php';
    40         require_once CNVRSE_DIR . 'src/Core/CacheBuster.php';
    41         require_once CNVRSE_DIR . 'src/Core/TelegramClient.php';
    42         require_once CNVRSE_DIR . 'src/Core/ConversationManager.php';
    43         require_once CNVRSE_DIR . 'src/Core/RestApiManager.php';
    44 
    45         // API layer.
    46         require_once CNVRSE_DIR . 'src/Api/routes.php';
    47         require_once CNVRSE_DIR . 'src/Api/VisitorEndpoints.php';
    48         require_once CNVRSE_DIR . 'src/Api/AdminEndpoints.php';
    49         require_once CNVRSE_DIR . 'src/Api/TelegramEndpoints.php';
    50 
    51         // Hooks.
    52         require_once CNVRSE_DIR . 'src/Actions/conversation-hooks.php';
    53         require_once CNVRSE_DIR . 'src/Actions/frontend-hooks.php';
    54 
    55         if ( is_admin() ) {
    56             require_once CNVRSE_DIR . 'src/Actions/admin-hooks.php';
    57             require_once CNVRSE_DIR . 'src/Navigation/admin-menu.php';
    58         }
    59 
    60         // Templates.
    61         require_once CNVRSE_DIR . 'src/Templates/conversation-columns.php';
    62         require_once CNVRSE_DIR . 'src/Templates/chat-widget-view.php';
    63 
    64         if ( is_admin() ) {
    65             require_once CNVRSE_DIR . 'src/Templates/admin-dashboard-view.php';
    66             require_once CNVRSE_DIR . 'src/Templates/admin-settings-view.php';
    67         }
    68     }
    69 
    70     /**
    71      * Initialize hooks.
    72      */
    73     private function init_hooks(): void {
    74         register_activation_hook( CNVRSE_FILE, [ $this, 'activate' ] );
    75         register_deactivation_hook( CNVRSE_FILE, [ $this, 'deactivate' ] );
    76 
    77         add_action( 'init', [ $this, 'init' ] );
    78         add_action( 'plugins_loaded', [ $this, 'loaded' ] );
    79     }
    80 
    81     /**
    82      * Plugin activation.
    83      */
    84     public function activate(): void {
    85         Cnvrse_Database::instance()->create_tables();
    86 
    87         // Generate Telegram webhook secret.
    88         if ( ! get_option( 'cnvrse_telegram_webhook_secret' ) ) {
    89             update_option( 'cnvrse_telegram_webhook_secret', bin2hex( random_bytes( 32 ) ) );
    90         }
    91 
    92         flush_rewrite_rules();
    93         set_transient( 'cnvrse_activation_redirect', true, 30 );
    94     }
    95 
    96     /**
    97      * Plugin deactivation.
    98      */
    99     public function deactivate(): void {
    100         flush_rewrite_rules();
    101     }
    102 
    103     /**
    104      * Init hook.
    105      */
    106     public function init(): void {
    107         $this->register_post_type();
    108         $this->register_taxonomy();
    109 
    110         load_plugin_textdomain( 'cnvrse', false, dirname( plugin_basename( CNVRSE_FILE ) ) . '/languages' );
    111     }
    112 
    113     /**
    114      * Plugins loaded hook.
    115      */
    116     public function loaded(): void {
    117         // Schedule cleanup.
    118         if ( ! wp_next_scheduled( 'cnvrse_cleanup_transients' ) ) {
    119             wp_schedule_event( time(), 'hourly', 'cnvrse_cleanup_transients' );
    120         }
    121 
    122         add_action( 'cnvrse_cleanup_transients', [ $this, 'cleanup_expired_data' ] );
    123     }
    124 
    125     /**
    126      * Register conversation post type.
    127      */
    128     private function register_post_type(): void {
    129         register_post_type( 'cnvrse_conversation', [
    130             'labels'              => [
    131                 'name'          => __( 'Conversations', 'cnvrse' ),
    132                 'singular_name' => __( 'Conversation', 'cnvrse' ),
    133             ],
    134             'public'              => false,
    135             'show_ui'             => false,
    136             'show_in_rest'        => false,
    137             'supports'            => [ 'title' ],
    138             'capability_type'     => 'post',
    139             'map_meta_cap'        => true,
    140             'exclude_from_search' => true,
    141         ]);
    142     }
    143 
    144     /**
    145      * Register status taxonomy.
    146      */
    147     private function register_taxonomy(): void {
    148         register_taxonomy( 'cnvrse_status', 'cnvrse_conversation', [
    149             'labels'       => [
    150                 'name'          => __( 'Status', 'cnvrse' ),
    151                 'singular_name' => __( 'Status', 'cnvrse' ),
    152             ],
    153             'public'       => false,
    154             'hierarchical' => false,
    155             'show_ui'      => false,
    156             'show_in_rest' => false,
    157         ]);
    158 
    159         // Create default terms.
    160         foreach ( [ 'open', 'pending', 'closed' ] as $status ) {
    161             if ( ! term_exists( $status, 'cnvrse_status' ) ) {
    162                 wp_insert_term( ucfirst( $status ), 'cnvrse_status', [ 'slug' => $status ] );
    163             }
    164         }
    165     }
    166 
    167     /**
    168      * Cleanup expired data.
    169      */
    170     public function cleanup_expired_data(): void {
    171         ConversationManager::instance()->auto_close_inactive( 24 );
    172     }
    173 }
    174 
    175 // Initialize.
    176 Cnvrse::instance();
     212    /**
     213     * Singleton instance
     214     *
     215     * @var Cnvrse
     216     */
     217    private static $instance = null;
     218
     219    /**
     220     * Get singleton instance
     221     *
     222     * @return Cnvrse
     223     */
     224    public static function instance() {
     225        if ( null === self::$instance ) {
     226            self::$instance = new self();
     227        }
     228        return self::$instance;
     229    }
     230
     231    /**
     232     * Constructor
     233     */
     234    private function __construct() {
     235        $this->init_components();
     236        $this->init_hooks();
     237    }
     238
     239    /**
     240     * Initialize plugin components.
     241     *
     242     * @return void
     243     */
     244    private function init_components(): void {
     245        // Initialize hook handlers (singletons register their hooks).
     246        Cnvrse_Conversation_Hooks::instance();
     247        Cnvrse_REST_API_Hooks::instance();
     248        Cnvrse_Frontend_Hooks::instance();
     249
     250        // Initialize notification center (works on frontend and admin).
     251        Cnvrse_Notification_Center::get();
     252
     253        // Admin bar menu (shows on both frontend and admin when logged in).
     254        Cnvrse_Admin_Bar_Menu::instance();
     255
     256        if ( is_admin() ) {
     257            Cnvrse_Admin_Hooks::instance();
     258            Cnvrse_Admin_Menu::instance();
     259
     260            // Load admin-only templates.
     261            require_once __DIR__ . '/Presentation/admin-dashboard-view.php';
     262            require_once __DIR__ . '/Presentation/admin-settings-view.php';
     263            require_once __DIR__ . '/Presentation/system-status-view.php';
     264        }
     265    }
     266
     267    /**
     268     * Initialize hooks
     269     *
     270     * @return void
     271     */
     272    private function init_hooks(): void {
     273        register_activation_hook( CNVRSE_FILE, array( $this, 'activate' ) );
     274        register_deactivation_hook( CNVRSE_FILE, array( $this, 'deactivate' ) );
     275
     276        add_action( 'init', array( $this, 'init' ) );
     277        add_action( 'plugins_loaded', array( $this, 'loaded' ) );
     278
     279        // CORS Security: Allow X-Cnvrse-Site-Key header.
     280        add_filter( 'rest_allowed_cors_headers', array( $this, 'allow_site_key_header' ), 10, 2 );
     281
     282        // CORS Security: Restrict origins to registered satellite sites.
     283        add_filter( 'allowed_http_origins', array( $this, 'allow_registered_origins' ) );
     284
     285        // Schedule cleanup.
     286        add_action( 'cnvrse_cleanup_transients', array( $this, 'cleanup_expired_data' ) );
     287    }
     288
     289    /**
     290     * Allow X-Cnvrse-Site-Key header
     291     *
     292     * @param array           $allow_headers Headers to allow.
     293     * @param WP_REST_Request $request       Request object.
     294     * @return array
     295     */
     296    public function allow_site_key_header( $allow_headers, $request ) {
     297        $allow_headers[] = 'X-Cnvrse-Site-Key';
     298        return $allow_headers;
     299    }
     300
     301    /**
     302     * Allow registered satellite origins for CORS
     303     *
     304     * @param array $origins Allowed origins.
     305     * @return array
     306     */
     307    public function allow_registered_origins( $origins ) {
     308        $satellites = get_option( 'cnvrse_hub_registered_sites', array() );
     309
     310        if ( ! is_array( $satellites ) ) {
     311            return $origins;
     312        }
     313
     314        foreach ( $satellites as $satellite ) {
     315            if ( ! empty( $satellite['origin'] ) ) {
     316                $origins[] = $satellite['origin'];
     317            }
     318        }
     319
     320        return $origins;
     321    }
     322
     323    /**
     324     * Plugin activation
     325     *
     326     * @return void
     327     */
     328    public function activate(): void {
     329        Cnvrse_Database::instance()->create_tables();
     330        flush_rewrite_rules();
     331
     332        // Schedule cleanup.
     333        if ( ! wp_next_scheduled( 'cnvrse_cleanup_transients' ) ) {
     334            wp_schedule_event( time(), 'hourly', 'cnvrse_cleanup_transients' );
     335        }
     336    }
     337
     338    /**
     339     * Plugin deactivation
     340     *
     341     * @return void
     342     */
     343    public function deactivate(): void {
     344        wp_clear_scheduled_hook( 'cnvrse_auto_close_conversations' );
     345        wp_clear_scheduled_hook( 'cnvrse_check_inactive_visitors' );
     346        wp_clear_scheduled_hook( 'cnvrse_cleanup_transients' );
     347        flush_rewrite_rules();
     348    }
     349
     350    /**
     351     * Init hook callback
     352     *
     353     * Note: Translations are loaded automatically by WordPress since version 4.6
     354     * for plugins hosted on WordPress.org. No manual load_plugin_textdomain() needed.
     355     *
     356     * @return void
     357     */
     358    public function init(): void {
     359        // Placeholder for future init tasks. Translations auto-loaded by WordPress 4.6+.
     360    }
     361
     362    /**
     363     * Plugins loaded callback
     364     *
     365     * @return void
     366     */
     367    public function loaded(): void {
     368        do_action( 'cnvrse_loaded' );
     369    }
     370
     371    /**
     372     * Cleanup expired transient data
     373     *
     374     * @return void
     375     */
     376    public function cleanup_expired_data(): void {
     377        $storage = cnvrse_make( 'Cnvrse_Storage_Provider' );
     378
     379        if ( method_exists( $storage, 'cleanup' ) ) {
     380            $storage->cleanup();
     381        }
     382    }
     383}
     384
     385/**
     386 * Get main Cnvrse instance
     387 *
     388 * @return Cnvrse
     389 */
     390function cnvrse() {
     391    return Cnvrse::instance();
     392}
     393
     394// Initialize plugin.
     395cnvrse();
Note: See TracChangeset for help on using the changeset viewer.