Plugin Directory

Changeset 3442632


Ignore:
Timestamp:
01/19/2026 02:49:27 PM (9 days ago)
Author:
finalpos
Message:

1.3.7

  • Fixed critical error when deleting plugin
  • Improved plugin lifecycle management (activation, deactivation, uninstall)
  • Enhanced data cleanup following WordPress best practices
  • PHPCS security compliance improvements
Location:
finalpos/trunk
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • finalpos/trunk/finalpos.php

    r3420195 r3442632  
    55 * Author: FinalPOS
    66 * Author URI: https://finalpos.com
    7  * version: 1.3.6
     7 * version: 1.3.7
    88 * License: GPLv2 or later
    99 * License URI: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
     
    2323
    2424// Define plugin constants.
    25 define( 'FINALPOS_VERSION', '1.3.6' );
     25define( 'FINALPOS_VERSION', '1.3.7' );
    2626define( 'FINALPOS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2727define( 'FINALPOS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
  • finalpos/trunk/includes/admin/class-finalpos-admin-assets.php

    r3411106 r3442632  
    8989
    9090        // Load specific styles for wizard page.
    91         if ( isset( $_GET['page'] ) && $_GET['page'] === FINALPOS_PAGE_SETUP ) {
     91        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page parameter for asset loading.
     92        $current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
     93
     94        if ( $current_page === FINALPOS_PAGE_SETUP ) {
    9295            wp_enqueue_style( FINALPOS_HANDLE_WIZARD );
    9396
     
    101104
    102105        // Load specific styles for dashboard page.
    103         if ( isset( $_GET['page'] ) && $_GET['page'] === FINALPOS_PAGE_DASHBOARD ) {
     106        if ( $current_page === FINALPOS_PAGE_DASHBOARD ) {
    104107            wp_enqueue_style( FINALPOS_HANDLE_DASHBOARD );
    105108        }
  • finalpos/trunk/includes/admin/dashboard/class-finalpos-dashboard.php

    r3420195 r3442632  
    99class FinalPOS_Dashboard {
    1010    public static function init() {
    11         if ( isset( $_POST['action'] ) && $_POST['action'] === FINALPOS_ACTION_DISCONNECT ) {
     11        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in handle_disconnect().
     12        if ( isset( $_POST['action'] ) && sanitize_text_field( wp_unslash( $_POST['action'] ) ) === FINALPOS_ACTION_DISCONNECT ) {
    1213            self::handle_disconnect();
    1314        }
     
    1617
    1718    private static function handle_disconnect() {
    18         // Verify nonce
    19         if ( ! isset( $_POST['finalpos_disconnect_nonce'] ) || ! wp_verify_nonce( $_POST['finalpos_disconnect_nonce'], FINALPOS_NONCE_DISCONNECT ) ) {
     19        // Verify nonce.
     20        if ( ! isset( $_POST['finalpos_disconnect_nonce'] ) ||
     21            ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['finalpos_disconnect_nonce'] ) ), FINALPOS_NONCE_DISCONNECT ) ) {
    2022            wp_die( 'Security check failed.' );
    2123        }
  • finalpos/trunk/includes/admin/wizard/class-finalpos-wizard.php

    r3411106 r3442632  
    3535        }
    3636
    37         // Check if there's a stage parameter in the URL
    38         if ( isset( $_GET['stage'] ) && in_array( $_GET['stage'], array( 'auth_key', 'settings' ) ) ) {
    39             $requested_stage = sanitize_text_field( $_GET['stage'] );
    40 
    41             // Only allow moving to settings if we have required data
     37        // Check if there's a stage parameter in the URL.
     38        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Stage parameter controls navigation; sensitive operations use custom nonce below.
     39        if ( isset( $_GET['stage'] ) && in_array( sanitize_text_field( wp_unslash( $_GET['stage'] ) ), array( 'auth_key', 'settings' ), true ) ) {
     40            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     41            $requested_stage = sanitize_text_field( wp_unslash( $_GET['stage'] ) );
     42
     43            // Only allow moving to settings if we have required data.
    4244            if ( $requested_stage === 'settings' ) {
    43                 // If this is a redirect from WooCommerce auth with nonce, verify it
     45                // If this is a redirect from WooCommerce auth with nonce, verify it.
     46                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Custom nonce verification follows.
    4447                if ( isset( $_GET['finalpos_nonce'] ) ) {
    45                     $nonce          = sanitize_text_field( $_GET['finalpos_nonce'] );
     48                    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     49                    $nonce          = sanitize_text_field( wp_unslash( $_GET['finalpos_nonce'] ) );
    4650                    $transient_name = 'finalpos_auth_nonce_' . $nonce;
    4751
  • finalpos/trunk/includes/admin/wizard/views/activation-form.php

    r3420195 r3442632  
    9292                <?php
    9393                $error_message = get_transient( 'finalpos_setup_error' );
     94                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only page showing redirect messages.
    9495                if ( isset( $_GET['error'] ) ) :
    9596                    ?>
    96                     <p class="finalpos-error-message"><?php echo esc_html( wp_unslash( $_GET['error'] ) ); ?></p>
     97                    <p class="finalpos-error-message"><?php echo esc_html( sanitize_text_field( wp_unslash( $_GET['error'] ) ) ); ?></p>
     98                <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only page showing redirect messages. ?>
    9799                <?php elseif ( isset( $_GET['message'] ) ) : ?>
    98                     <p class="finalpos-success-message"><?php echo esc_html( wp_unslash( $_GET['message'] ) ); ?></p>
     100                    <p class="finalpos-success-message"><?php echo esc_html( sanitize_text_field( wp_unslash( $_GET['message'] ) ) ); ?></p>
    99101                <?php elseif ( $error_message ) : ?>
    100102                    <p class="finalpos-error-message"><?php echo esc_html( $error_message ); ?></p>
  • finalpos/trunk/includes/admin/wizard/views/settings-form.php

    r3420195 r3442632  
    3939    endif;
    4040
    41     // Display error message if present.
     41    // Display error message if present (passed via redirect from admin-post.php).
     42    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a display-only page showing redirect error messages.
    4243    if ( isset( $_GET['error'] ) ) {
    43         $error_message = urldecode( wp_unslash( $_GET['error'] ) );
     44        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     45        $error_message = sanitize_text_field( wp_unslash( $_GET['error'] ) );
    4446        echo '<div class="notice notice-error"><p>' . esc_html( $error_message ) . '</p></div>';
    4547    }
  • finalpos/trunk/includes/common-functions.php

    r3420195 r3442632  
    216216     * Disconnect the plugin by cleaning up all plugin data
    217217     *
     218     * Following WordPress best practices for data cleanup:
     219     * - Delete all plugin options
     220     * - Delete all plugin transients
     221     * - Clean up WooCommerce API keys and webhooks
     222     * - Clear object cache
     223     *
    218224     * @return void
    219225     */
    220226    public static function disconnect_plugin(): void {
    221         // Delete all plugin options
     227        // Delete all plugin options.
    222228        $options = array(
    223229            FINALPOS_OPTION_CURRENT_STAGE,
     
    230236            FINALPOS_OPTION_LAST_NONCE,
    231237            FINALPOS_OPTION_LAST_NONCE_TIME,
     238            FINALPOS_OPTION_REACTIVATION_ERROR,
    232239        );
    233240
     
    236243        }
    237244
     245        // Delete plugin transients.
     246        delete_transient( FINALPOS_TRANSIENT_ACTIVATION_REDIRECT );
     247
     248        // Clean up any auth nonce transients (they use a prefix pattern).
    238249        global $wpdb;
    239250
    240         // Remove API keys
     251        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    241252        $wpdb->query(
    242253            $wpdb->prepare(
    243                 'DELETE FROM `' . $wpdb->prefix . 'woocommerce_api_keys` WHERE `description` LIKE "%s"',
     254                "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
     255                '_transient_' . FINALPOS_TRANSIENT_AUTH_NONCE_PREFIX . '%',
     256                '_transient_timeout_' . FINALPOS_TRANSIENT_AUTH_NONCE_PREFIX . '%'
     257            )
     258        );
     259
     260        // Remove FinalPOS API keys from WooCommerce.
     261        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     262        $wpdb->query(
     263            $wpdb->prepare(
     264                "DELETE FROM {$wpdb->prefix}woocommerce_api_keys WHERE description LIKE %s",
    244265                FINALPOS_API_DESCRIPTION_PATTERN
    245266            )
    246267        );
    247268
    248         // Remove webhooks
     269        // Remove FinalPOS webhooks from WooCommerce.
     270        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    249271        $wpdb->query(
    250272            $wpdb->prepare(
    251                 'DELETE FROM `' . $wpdb->prefix . 'wc_webhooks` WHERE `name` LIKE "%s"',
     273                "DELETE FROM {$wpdb->prefix}wc_webhooks WHERE name LIKE %s",
    252274                FINALPOS_WEBHOOK_NAME_PATTERN
    253275            )
    254276        );
     277
     278        // Clear object cache to ensure clean state.
     279        wp_cache_flush();
    255280    }
    256281
     
    340365     * Check if REST API is available
    341366     *
    342      * Tests internal REST API availability by making a request to the WooCommerce REST API.
     367     * Tests internal REST API availability using WordPress internal REST infrastructure.
     368     * This avoids external HTTP requests which can fail in tunneled environments (ngrok, DDEV share, etc.).
    343369     *
    344370     * @return array Array with 'available' boolean and 'message' string
     
    346372    public static function is_rest_api_available(): array {
    347373
    348         // Try to make an internal REST API request to verify it's working.
    349         $rest_url = rest_url( 'wc/v3' );
    350         $response = wp_remote_get(
    351             $rest_url,
    352             array(
    353                 'timeout'   => 10,
    354                 'sslverify' => false,
    355                 'cookies'   => $_COOKIE, // Pass current cookies for authentication.
    356             )
    357         );
    358 
    359         if ( is_wp_error( $response ) ) {
     374        // First check if REST API is disabled via filters.
     375        if ( ! apply_filters( 'rest_enabled', true ) ) {
     376            return array(
     377                'available' => false,
     378                'message'   => __( 'REST API is disabled. Please check your WordPress configuration or plugins that might be disabling the REST API.', 'finalpos' ),
     379            );
     380        }
     381
     382        // Check if WooCommerce REST API namespace is registered.
     383        $rest_server = rest_get_server();
     384        $namespaces  = $rest_server->get_namespaces();
     385
     386        if ( ! in_array( 'wc/v3', $namespaces, true ) ) {
     387            return array(
     388                'available' => false,
     389                'message'   => __( 'WooCommerce REST API (wc/v3) is not available. Please ensure WooCommerce is properly installed and activated.', 'finalpos' ),
     390            );
     391        }
     392
     393        // Use internal REST request to avoid loopback/tunnel issues (ngrok, DDEV share, etc.).
     394        $request  = new WP_REST_Request( 'GET', '/wc/v3' );
     395        $response = rest_do_request( $request );
     396
     397        if ( $response->is_error() ) {
     398            $error       = $response->as_error();
     399            $status_code = $response->get_status();
     400
     401            // 401 Unauthorized is OK - it means the API is working but requires authentication.
     402            if ( 401 === $status_code ) {
     403                return array(
     404                    'available' => true,
     405                    'message'   => '',
     406                );
     407            }
     408
     409            // 403 Forbidden might indicate a Coming Soon plugin blocking access.
     410            if ( 403 === $status_code ) {
     411                return array(
     412                    'available' => false,
     413                    'message'   => __( 'REST API access is forbidden (403). This is usually caused by a Coming Soon plugin, maintenance mode, or security plugin blocking API access. Please disable any such plugins before connecting.', 'finalpos' ),
     414                );
     415            }
     416
     417            // 503 Service Unavailable usually means maintenance mode.
     418            if ( 503 === $status_code ) {
     419                return array(
     420                    'available' => false,
     421                    'message'   => __( 'REST API returned Service Unavailable (503). Your site appears to be in maintenance mode. Please disable maintenance mode before connecting.', 'finalpos' ),
     422                );
     423            }
     424
     425            // Other errors.
    360426            return array(
    361427                'available' => false,
    362428                'message'   => sprintf(
    363429                    /* translators: %s: Error message */
    364                     __( 'REST API is not accessible: %s. This may be caused by a Coming Soon plugin, security plugin, or server configuration.', 'finalpos' ),
    365                     $response->get_error_message()
    366                 ),
    367             );
    368         }
    369 
    370         $status_code = wp_remote_retrieve_response_code( $response );
    371 
    372         // 401 Unauthorized is OK - it means the API is working but requires authentication.
    373         // 200 OK is also fine.
    374         // 403 Forbidden might indicate a Coming Soon plugin blocking access.
    375         if ( 403 === $status_code ) {
    376             return array(
    377                 'available' => false,
    378                 'message'   => __( 'REST API access is forbidden (403). This is usually caused by a Coming Soon plugin, maintenance mode, or security plugin blocking API access. Please disable any such plugins before connecting.', 'finalpos' ),
    379             );
    380         }
    381 
    382         // 503 Service Unavailable usually means maintenance mode.
    383         if ( 503 === $status_code ) {
    384             return array(
    385                 'available' => false,
    386                 'message'   => __( 'REST API returned Service Unavailable (503). Your site appears to be in maintenance mode. Please disable maintenance mode before connecting.', 'finalpos' ),
    387             );
    388         }
    389 
    390         // Check for unexpected status codes.
    391         if ( $status_code >= 500 ) {
    392             return array(
    393                 'available' => false,
    394                 'message'   => sprintf(
    395                     /* translators: %d: HTTP status code */
    396                     __( 'REST API returned server error (%d). Please check your server configuration and error logs.', 'finalpos' ),
    397                     $status_code
     430                    __( 'REST API error: %s', 'finalpos' ),
     431                    $error->get_error_message()
    398432                ),
    399433            );
  • finalpos/trunk/includes/lifecycle.php

    r3411106 r3442632  
    33 * FinalPOS Plugin Lifecycle Management
    44 *
    5  * Handles plugin activation, deactivation, and uninstallation
     5 * Handles plugin activation, deactivation, and uninstallation.
     6 * Following WordPress best practices:
     7 * - Activation: Set up initial state, redirect to setup
     8 * - Deactivation: Pause integrations, clean temp data, preserve user settings
     9 * - Uninstallation: Full cleanup (handled in uninstall.php)
     10 *
     11 * @package FinalPOS
    612 */
    713
     
    1016}
    1117
     18/**
     19 * Class FinalPOS_Lifecycle
     20 *
     21 * Manages plugin lifecycle events.
     22 */
    1223class FinalPOS_Lifecycle {
    1324    /**
     
    3445     */
    3546    private function __construct() {
    36         // Hook into admin notices to display reactivation errors
     47        // Hook into admin notices to display reactivation errors.
    3748        add_action( 'admin_notices', array( $this, 'display_reactivation_error' ) );
    3849    }
    3950
    4051    /**
    41      * Handles plugin activation
     52     * Handles plugin activation.
     53     *
     54     * Sets up initial state and schedules redirect to setup wizard.
     55     *
     56     * @return void
    4257     */
    4358    public static function activate() {
     
    4661
    4762    /**
    48      * Restore the WooCommerce API key to read-write, activate all webhooks, and reinitiate sync on reactivation
     63     * Restore the WooCommerce API key to read-write, activate all webhooks, and reinitiate sync on reactivation.
     64     *
     65     * This is the reverse of deactivate() - restores full functionality.
     66     *
     67     * @return void
    4968     */
    5069    public static function reactivate() {
    51         // Only proceed if we're reactivating after a deactivation
    52         // (not first-time activation)
    53 
     70        // Only proceed if we're reactivating after a deactivation (not first-time activation).
    5471        if ( ! get_option( FINALPOS_OPTION_JWT_TOKEN ) ) {
    5572            return;
     
    5875        global $wpdb;
    5976
    60         // Restore API key to read-write
    61         $wpdb->query(
    62             $wpdb->prepare(
    63                 "UPDATE `{$wpdb->prefix}woocommerce_api_keys`
    64             SET `permissions` = %s
    65             WHERE `description` LIKE %s",
     77        // Restore API key to read-write.
     78        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     79        $wpdb->query(
     80            $wpdb->prepare(
     81                "UPDATE {$wpdb->prefix}woocommerce_api_keys SET permissions = %s WHERE description LIKE %s",
    6682                'read_write',
    6783                FINALPOS_API_DESCRIPTION_PATTERN
     
    6985        );
    7086
    71         // Restore all webhooks to active
    72         $wpdb->query(
    73             $wpdb->prepare(
    74                 "UPDATE `{$wpdb->prefix}wc_webhooks`
    75             SET `status` = %s
    76             WHERE `name` LIKE %s",
     87        // Restore all webhooks to active.
     88        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     89        $wpdb->query(
     90            $wpdb->prepare(
     91                "UPDATE {$wpdb->prefix}wc_webhooks SET status = %s WHERE name LIKE %s",
    7792                'active',
    7893                FINALPOS_WEBHOOK_NAME_PATTERN
     
    8095        );
    8196
    82         // Only attempt sync if wizard was previously completed
     97        // Only attempt sync if wizard was previously completed.
    8398        if ( get_option( FINALPOS_OPTION_CURRENT_STAGE ) === FINALPOS_STAGE_COMPLETE ) {
    8499
     
    115130                }
    116131            } catch ( Exception $e ) {
    117                 // Store the error state
     132                // Store the error state for display on next admin page load.
    118133                update_option( FINALPOS_OPTION_REACTIVATION_ERROR, true );
    119134            }
     
    123138    /**
    124139     * Display reactivation error notice if it exists
     140     *
     141     * @return void
    125142     */
    126143    public function display_reactivation_error() {
     
    131148            </div>
    132149            <?php
    133             // Clear the error state after displaying
     150            // Clear the error state after displaying.
    134151            delete_option( FINALPOS_OPTION_REACTIVATION_ERROR );
    135152        }
     
    137154
    138155    /**
    139      * Make the WooCommerce API key read-only and pause all webhooks on deactivation
     156     * Handle plugin deactivation.
     157     *
     158     * Following WordPress best practices:
     159     * - Pause external integrations (webhooks)
     160     * - Restrict API permissions (read-only)
     161     * - Clean up temporary transients
     162     * - Preserve user settings (for potential reactivation)
     163     *
     164     * This is reversible - reactivate() restores full functionality.
     165     *
     166     * @return void
    140167     */
    141168    public static function deactivate() {
    142169        global $wpdb;
    143170
    144         // Make API key read-only
    145         $wpdb->query(
    146             $wpdb->prepare(
    147                 "UPDATE `{$wpdb->prefix}woocommerce_api_keys`
    148             SET `permissions` = %s
    149             WHERE `description` LIKE %s",
     171        // Make API key read-only (prevents write operations while plugin is inactive).
     172        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     173        $wpdb->query(
     174            $wpdb->prepare(
     175                "UPDATE {$wpdb->prefix}woocommerce_api_keys SET permissions = %s WHERE description LIKE %s",
    150176                'read',
    151177                FINALPOS_API_DESCRIPTION_PATTERN
     
    153179        );
    154180
    155         // Pause all webhooks
    156         $wpdb->query(
    157             $wpdb->prepare(
    158                 "UPDATE `{$wpdb->prefix}wc_webhooks`
    159             SET `status` = %s
    160             WHERE `name` LIKE %s",
     181        // Pause all webhooks (stops webhook deliveries while plugin is inactive).
     182        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     183        $wpdb->query(
     184            $wpdb->prepare(
     185                "UPDATE {$wpdb->prefix}wc_webhooks SET status = %s WHERE name LIKE %s",
    161186                'paused',
    162187                FINALPOS_WEBHOOK_NAME_PATTERN
    163188            )
    164189        );
     190
     191        // Clean up temporary transients (not needed while plugin is inactive).
     192        delete_transient( FINALPOS_TRANSIENT_ACTIVATION_REDIRECT );
     193
     194        // Clean up any pending auth nonce transients.
     195        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     196        $wpdb->query(
     197            $wpdb->prepare(
     198                "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
     199                '_transient_' . FINALPOS_TRANSIENT_AUTH_NONCE_PREFIX . '%',
     200                '_transient_timeout_' . FINALPOS_TRANSIENT_AUTH_NONCE_PREFIX . '%'
     201            )
     202        );
     203
     204        // Note: We intentionally DO NOT delete user options/settings here.
     205        // User data is preserved for potential reactivation.
     206        // Full cleanup only happens on uninstall (see uninstall.php).
    165207    }
    166208}
  • finalpos/trunk/integrations/readonly-orders.php

    r3420195 r3442632  
    339339 */
    340340function finalpos_disable_bulk_actions_for_readonly( $actions ) {
    341     // Only disable bulk actions on the edit order screen when viewing a specific read-only order
    342     if ( isset( $_GET['post'] ) && is_numeric( $_GET['post'] ) && finalpos_is_order_readonly( $_GET['post'] ) ) {
     341    // Only disable bulk actions on the edit order screen when viewing a specific read-only order.
     342    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading URL parameter for display logic.
     343    $post_id = isset( $_GET['post'] ) && is_numeric( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
     344
     345    if ( $post_id && finalpos_is_order_readonly( $post_id ) ) {
    343346        if ( isset( $actions['trash'] ) ) {
    344347            unset( $actions['trash'] );
  • finalpos/trunk/readme.txt

    r3420195 r3442632  
    77Tested up to: 6.8.3
    88Requires PHP: 7.4
    9 Stable tag: 1.3.6
     9Stable tag: 1.3.7
    1010License: GPLv2 or later
    1111License URI: http://www.gnu.org/licenses/gpl-2.0
     
    9393
    9494== Changelog ==
     95= 1.3.7 =
     96* Fixed critical error when deleting plugin
     97* Improved plugin lifecycle management (activation, deactivation, uninstall)
     98* Enhanced data cleanup following WordPress best practices
     99* PHPCS security compliance improvements
     100
    95101= 1.3.6 =
    96102* Support for coming soon mode
  • finalpos/trunk/uninstall.php

    r3411106 r3442632  
    1313}
    1414
     15// Include constants file first (required for disconnect_plugin method).
     16require_once plugin_dir_path( __FILE__ ) . 'includes/constants.php';
     17
    1518// Include the common functions file to access disconnect_plugin method.
    1619require_once plugin_dir_path( __FILE__ ) . 'includes/common-functions.php';
Note: See TracChangeset for help on using the changeset viewer.