Plugin Directory

Changeset 3484943


Ignore:
Timestamp:
03/17/2026 04:21:27 PM (2 weeks ago)
Author:
schqm
Message:

Release version 1.2.4

Location:
sqm-views
Files:
64 added
6 deleted
14 edited

Legend:

Unmodified
Added
Removed
  • sqm-views/trunk/CHANGELOG.md

    r3451552 r3484943  
    1313## [Unreleased]
    1414
     15## [1.2.4] - 2026-03-16
     16
     17### Added
     18  - Dashboard: Table view — new chart type showing data in a tabular format with sorting
     19  - Dashboard: URL state syncing — chart filters and chart type are persisted in the URL for shareable links
     20  - Dashboard: CSV and PNG export — export chart data as CSV or save the chart as a PNG image
     21  - Dashboard: Dynamic Y-axis rescaling — Y-axis adjusts automatically when toggling series in the legend, making smaller data visible after hiding large series
     22  - Dashboard: Clickable legend links — article IDs in the "By Content" legend are now links to the article page; clicking the title still toggles the series
     23  - Charts API: Article URLs — the by_content series response now includes a url field with the post permalink
     24  - Drop-in: Versioned archive — the build script now produces a separate versioned drop-in zip for the fast endpoint
     25  - Drop-in added to the plugin as sqm-views-pages.template, added functionality to install drop-in from the admin interface
     26
     27### Changed
     28  - Charts API: simplified post title resolution to individual cached queries instead of batch lookups
     29  - Drop-in: applied the same security measures as the main plugin (input validation, sanitization)
     30  - Ensured ABSPATH and SQMVIEWS_WP_UPLOADS are defined before utils.php is loaded via autoloader
     31
     32---
     33
     34
    1535## [1.1.9] - 2026-02-01
    1636First official release
     
    5474### Compatibility
    5575
    56   - Updated tested WordPress version to 6.9
     76  - Updated the tested WordPress version to 6.9
    5777
    5878### Build
     
    6686
    6787### Changed
    68 Minor update. Readme file improved. Screenshots removed from zip file.
     88Minor update. Readme file improved. Screenshots removed from a zip file.
    6989
    7090---
     
    88108  - Test endpoint now returns lightweight response on fast endpoint (skips diagnostics)
    89109  - WordPress filter hooks (session_timeout, ping_interval) now bypassed on fast endpoint for performance
    90   - Default log level changed from INFO to ERROR in production (DEBUG when WP_DEBUG is true)
    91   - Admin timestamp display now uses site timezone instead of UTC
     110  - The default log level changed from INFO to ERROR in production (DEBUG when WP_DEBUG is true)
     111  - Admin timestamp display now uses the site timezone instead of UTC
    92112
    93113  ### Technical Details
    94   - Test endpoint response includes 'endpoint' field ('fast' or 'api')
     114  - Test endpoint response includes the 'endpoint' field ('fast' or 'api')
    95115  - Fast endpoint identified by constant defined in sqm-views-pages.php
    96116
     
    99119
    100120## [1.0.7] - 2025-10-26
    101   - Maintenance release - version bump only
     121  - Maintenance release version bump only
    102122  - No functional changes to the plugin
    103123
  • sqm-views/trunk/README.md

    r3451552 r3484943  
    711711This plugin follows [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PATCH).
    712712
    713 **Current Version:** 1.1.9
     713**Current Version:** 1.2.4
    714714
    715715### Version Check in Code
     
    730730---
    731731
    732 **Version:** 1.1.9
     732**Version:** 1.2.4
    733733**Author:** Pavel Khloponin
    734734**License:** GPLv3 or later
  • sqm-views/trunk/composer.json

    r3451552 r3484943  
    22  "name": "sqm-views/sqm-views",
    33  "description": "Plugin to collect, store, and use pageviews.",
    4   "version": "1.1.9",
     4  "version": "1.2.4",
    55  "_comment": "SQMVIEWS_BUILD_GLOBAL_VERSION - WARNING: Do NOT edit the version field manually! Use the bump-version.sh script: ./bump-version.sh patch|minor|major",
    66  "type": "wordpress-plugin",
  • sqm-views/trunk/readme.txt

    r3451552 r3484943  
    55Tested up to: 6.9
    66Requires PHP: 8.1
    7 Stable tag: 1.1.9
     7Stable tag: 1.2.4
    88License: GPLv3
    99License URI: https://www.gnu.org/licenses/gpl-3.0.html
     
    3232* Ultra-fast tracking endpoint (bypasses a core load)
    3333* Minimal overhead (~5ms per request with the drop-in script)
    34 * File-based batch processing (no database requests during pageview event collection, only during cron processing)
     34* File-based batch processing (no database requests during the pageview event collection, only during cron processing)
    3535* Efficient data storage with automatic archiving
    3636* Configurable background processing via WP-Cron
     
    118118= Why Choose SQMViews? =
    119119
    120 **vs External Analytics:**
     120**vs. External Analytics:**
    121121* No external JavaScript libraries
    122122* No tracking cookies required
     
    166166   * Select which post types to track
    167167   * Choose taxonomy archives to monitor
    168    * Select JavaScript loading method (inline/external)
     168   * Select a JavaScript loading method (inline/external)
    169169
    1701702. **Set Processing Schedule** - Go to **SQMViews → Processing**
    171    * Choose cron interval (hourly recommended)
     171   * Choose a cron interval (hourly recommended)
    172172   * Verify endpoint status (fast endpoint preferred)
    173173
     
    182182
    1831831. **Enable Fast Endpoint** (automatic on most hosts)
    184    * Copy sqm-views-pages.php in WordPress root folder (next to wp-config.php)
     184   * Copy sqm-views-pages.php in the WordPress root folder (next to wp-config.php)
    185185   * Bypasses WordPress load
    186186   * Reduces tracking overhead from ~250ms to ~5ms
     
    382382* `sqm_views_trackable_taxonomies` - Customize which taxonomies can be tracked (1 param: $taxonomies)
    383383* `sqm_views_data_taxonomies` - Modify taxonomies included in tracking data (1 param: $taxonomies)
    384 * `sqm_views_encryption_key` - Override encryption key (1 param: $key)
     384* `sqm_views_encryption_key` - Override the encryption key (1 param: $key)
    385385* `sqm_views_tracker_endpoint` - Customize tracking endpoint URL (2 params: $endpoint, $saved_endpoint)
    386386* `sqm_views_minified_js` - Control whether to use minified tracker JS (1 param: $use_min)
    387 * `sqm_views_tracker_script_path` - Customize tracker script file path (2 params: $path, $use_min)
     387* `sqm_views_tracker_script_path` - Customize the tracker script file path (2 params: $path, $use_min)
    388388* `sqm_views_tracker_script_url` - Customize tracker script URL (2 params: $url, $use_min)
    389389* `sqm_views_inline_js` - Control whether to inline tracker JS (1 param: $use_inline)
     
    394394* `sqm_views_calculated_metrics` - Modify calculated metrics (2 params: $metrics, $records)
    395395* `sqm_views_session_timeout` - Customize session inactivity timeout (1 param: $timeout)
    396 * `sqm_views_ping_interval` - Customize ping interval for session tracking (1 param: $interval)
     396* `sqm_views_ping_interval` - Customize the ping interval for session tracking (1 param: $interval)
    397397
    398398**Dashboard:**
     
    419419* `sqm_views_activated` - Fires after plugin activation (0 params)
    420420* `sqm_views_upgraded` - Fires after plugin upgrade (2 params: $from_version, $to_version)
    421 * `sqm_views_before_upgrade` - Fires before upgrade process (2 params: $from_version, $to_version)
    422 * `sqm_views_after_upgrade` - Fires after upgrade process (2 params: $from_version, $to_version)
     421* `sqm_views_before_upgrade` - Fires before the upgrade process (2 params: $from_version, $to_version)
     422* `sqm_views_after_upgrade` - Fires after the upgrade process (2 params: $from_version, $to_version)
    423423* `sqm_views_uninstalled` - Fires during plugin uninstallation (0 params)
    424424
  • sqm-views/trunk/sqm-views.php

    r3451552 r3484943  
    1212 * Plugin URI:  https://searchquerymaster.com/sqm-views
    1313 * Description: Lightweight page view tracking and engagement analytics with interactive dashboards. No external services, privacy-focused.
    14  * Version:     1.1.9
     14 * Version:     1.2.4
    1515 * Author:      Pavel Khloponin
    1616 * Author URI:  https://github.com/pkhlop
  • sqm-views/trunk/src/SQMViewsSettings.php

    r3451552 r3484943  
    4646        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
    4747        add_action( 'wp_ajax_sqm_views_process_batch', array( $this, 'ajax_process_batch' ) );
     48        add_action( 'wp_ajax_sqm_views_install_dropin', array( $this, 'ajax_install_dropin' ) );
    4849    }
    4950
     
    377378                        <td><?php echo wp_kses_post( sqm_views_check_endpoint() ); ?></td>
    378379                    </tr>
     380                    <tr>
     381                        <th scope="row"><?php esc_html_e( 'Fast Endpoint Drop-in', 'sqm-views' ); ?></th>
     382                        <td>
     383                            <?php
     384                            $dropin_source   = plugin_dir_path( SQMVIEWS_PLUGIN_FILE ) . 'dropin/sqm-views-pages.template';
     385                            $dropin_target   = ABSPATH . 'sqm-views-pages.php';
     386                            $dropin_exists   = file_exists( $dropin_target );
     387                            $source_exists   = file_exists( $dropin_source );
     388                            $dropin_writable = wp_is_writable( ABSPATH );
     389
     390                            if ( $dropin_exists ) {
     391                                echo '<span style="color: green;">&#10004; ' . esc_html__( 'Installed', 'sqm-views' ) . '</span>';
     392                            } else {
     393                                echo '<span style="color: orange;">&#9888; ' . esc_html__( 'Not installed', 'sqm-views' ) . '</span>';
     394                            }
     395
     396                            if ( $source_exists && $dropin_writable ) {
     397                                $button_label = $dropin_exists
     398                                    ? __( 'Reinstall Drop-in', 'sqm-views' )
     399                                    : __( 'Install Drop-in', 'sqm-views' );
     400                                $button_class = $dropin_exists ? 'button' : 'button button-primary';
     401                                ?>
     402                                <button type="button" id="sqm-views-install-dropin"
     403                                        class="<?php echo esc_attr( $button_class ); ?>"
     404                                        style="margin-left: 10px;"
     405                                        data-nonce="<?php echo esc_attr( wp_create_nonce( 'sqm_views_install_dropin' ) ); ?>">
     406                                    <?php echo esc_html( $button_label ); ?>
     407                                </button>
     408                                <span id="sqm-views-dropin-status" style="margin-left: 10px;"></span>
     409                                <script>
     410                                (function() {
     411                                    var btn = document.getElementById('sqm-views-install-dropin');
     412                                    if (!btn) return;
     413                                    btn.addEventListener('click', function() {
     414                                        btn.disabled = true;
     415                                        var status = document.getElementById('sqm-views-dropin-status');
     416                                        status.textContent = '<?php echo esc_js( __( 'Installing...', 'sqm-views' ) ); ?>';
     417                                        var xhr = new XMLHttpRequest();
     418                                        xhr.open('POST', ajaxurl);
     419                                        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
     420                                        xhr.onload = function() {
     421                                            try {
     422                                                var resp = JSON.parse(xhr.responseText);
     423                                                if (resp.success) {
     424                                                    status.innerHTML = '<span style="color:green;">' + resp.data.message + '</span>';
     425                                                    setTimeout(function() { location.reload(); }, 1500);
     426                                                } else {
     427                                                    status.innerHTML = '<span style="color:red;">' + (resp.data.message || 'Failed') + '</span>';
     428                                                    btn.disabled = false;
     429                                                }
     430                                            } catch(e) {
     431                                                status.innerHTML = '<span style="color:red;">Unexpected error</span>';
     432                                                btn.disabled = false;
     433                                            }
     434                                        };
     435                                        xhr.onerror = function() {
     436                                            status.innerHTML = '<span style="color:red;">Network error</span>';
     437                                            btn.disabled = false;
     438                                        };
     439                                        xhr.send('action=sqm_views_install_dropin&nonce=' + btn.dataset.nonce);
     440                                    });
     441                                })();
     442                                </script>
     443                                <?php
     444                            } elseif ( ! $source_exists ) {
     445                                echo '<br><small>' . esc_html__( 'Drop-in template not found in plugin. Please reinstall the plugin.', 'sqm-views' ) . '</small>';
     446                            } elseif ( ! $dropin_writable ) {
     447                                echo '<br><small>' . esc_html__( 'WordPress root directory is not writable. Please install manually.', 'sqm-views' ) . '</small>';
     448                            }
     449                            ?>
     450                        </td>
     451                    </tr>
    379452                </table>
    380453            </div>
     
    708781
    709782    /**
     783     * AJAX handler for drop-in installation.
     784     *
     785     * Copies the bundled .php.dist template to the WordPress root as sqm-views-pages.php.
     786     *
     787     * @return void
     788     */
     789    public function ajax_install_dropin() {
     790        if ( ! current_user_can( 'manage_options' ) ) {
     791            wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'sqm-views' ) ) );
     792        }
     793
     794        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'sqm_views_install_dropin' ) ) {
     795            wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'sqm-views' ) ) );
     796        }
     797
     798        $source = plugin_dir_path( SQMVIEWS_PLUGIN_FILE ) . 'dropin/sqm-views-pages.template';
     799        $target = ABSPATH . 'sqm-views-pages.php';
     800
     801        if ( ! file_exists( $source ) ) {
     802            wp_send_json_error( array( 'message' => __( 'Drop-in template not found. Please reinstall the plugin.', 'sqm-views' ) ) );
     803        }
     804
     805        if ( ! wp_is_writable( ABSPATH ) ) {
     806            wp_send_json_error( array( 'message' => __( 'WordPress root directory is not writable.', 'sqm-views' ) ) );
     807        }
     808
     809        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_copy -- Copying drop-in to WordPress root requires direct file operation.
     810        if ( copy( $source, $target ) ) {
     811            // Re-check endpoint after installation.
     812            sqm_views_check_endpoint();
     813            wp_send_json_success( array( 'message' => __( 'Drop-in installed successfully.', 'sqm-views' ) ) );
     814        } else {
     815            wp_send_json_error( array( 'message' => __( 'Failed to copy drop-in file.', 'sqm-views' ) ) );
     816        }
     817    }
     818
     819    /**
    710820     * AJAX handler for batch processing.
    711821     *
  • sqm-views/trunk/src/includes/activation.php

    r3451552 r3484943  
    1313
    1414define( 'SQMVIEWS_REST_TIMEOUT', 5 );
     15
     16/**
     17 * Checks if an index exists on a table.
     18 *
     19 * Uses information_schema.STATISTICS which is compatible with both MySQL and MariaDB.
     20 *
     21 * @param string $table_name The table name (with prefix).
     22 * @param string $index_name The index name to check.
     23 *
     24 * @return bool True if index exists, false otherwise.
     25 */
     26function index_exists( string $table_name, string $index_name ): bool {
     27    global $wpdb;
     28
     29    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema check during activation.
     30    $result = $wpdb->get_var(
     31        $wpdb->prepare(
     32            'SELECT COUNT(*) FROM information_schema.STATISTICS
     33            WHERE TABLE_SCHEMA = DATABASE()
     34            AND TABLE_NAME = %s
     35            AND INDEX_NAME = %s',
     36            $table_name,
     37            $index_name
     38        )
     39    );
     40
     41    return (int) $result > 0;
     42}
     43
     44/**
     45 * Checks if a foreign key constraint exists on a table.
     46 *
     47 * Uses information_schema which is compatible with both MySQL and MariaDB.
     48 *
     49 * @param string $table_name  The table name (with prefix).
     50 * @param string $column_name The column name that has the foreign key.
     51 *
     52 * @return bool True if foreign key exists, false otherwise.
     53 */
     54function foreign_key_exists( string $table_name, string $column_name ): bool {
     55    global $wpdb;
     56
     57    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema check during activation.
     58    $result = $wpdb->get_var(
     59        $wpdb->prepare(
     60            'SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE
     61            WHERE TABLE_SCHEMA = DATABASE()
     62            AND TABLE_NAME = %s
     63            AND COLUMN_NAME = %s
     64            AND REFERENCED_TABLE_NAME IS NOT NULL',
     65            $table_name,
     66            $column_name
     67        )
     68    );
     69
     70    return (int) $result > 0;
     71}
    1572
    1673/**
     
    97154
    98155/**
    99  * Creates required database tables
     156 * Creates required database tables, indices and foreign keys
    100157 *
    101158 * @return void
     
    114171            // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    115172            /** @lang MySQL */            '
    116     CREATE TABLE IF NOT EXISTS %i (
     173    CREATE TABLE %i (
    117174    `tid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
    118175    `tgroup` VARCHAR(20) NOT NULL,
     
    120177    `wpid` BIGINT UNSIGNED NOT NULL DEFAULT 0,
    121178    `altid` VARCHAR(50),
    122     PRIMARY KEY(`tid`));',
     179    PRIMARY KEY  (`tid`));',
    123180            "{$prefix}sqm_views_trackables"
    124181        )
     
    129186            // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    130187            /** @lang MySQL */            '
    131     CREATE TABLE IF NOT EXISTS %i (
     188    CREATE TABLE %i (
    132189    `gid` VARCHAR(50) NOT NULL,
    133190    `tid` BIGINT UNSIGNED NOT NULL,
     
    142199    `exit` VARCHAR(20) NOT NULL,
    143200    `utc_moment` DATETIME NOT NULL,
    144     PRIMARY KEY(`gid`)
     201    PRIMARY KEY  (`gid`)
    145202);',
    146203            "{$prefix}sqm_views_records"
     
    151208            // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    152209            /** @lang MySQL */            '
    153     CREATE TABLE IF NOT EXISTS %i (
     210    CREATE TABLE %i (
    154211    `date` DATE NOT NULL,
    155212    `eid` BIGINT UNSIGNED NOT NULL,
     
    160217    `low_freq` BIGINT UNSIGNED NOT NULL,
    161218    `count` BIGINT UNSIGNED NOT NULL,
    162     PRIMARY KEY(`date`, `eid`, `tid`)
     219    PRIMARY KEY  (`date`, `eid`, `tid`)
    163220);',
    164221            "{$prefix}sqm_views_daily"
     
    170227            // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    171228            /** @lang MySQL */            '
    172     CREATE TABLE IF NOT EXISTS %i (
     229    CREATE TABLE %i (
    173230    `eid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
    174231    `name` VARCHAR(20) NOT NULL UNIQUE,
    175     PRIMARY KEY(`eid`)
     232    PRIMARY KEY  (`eid`)
    176233);',
    177234            "{$prefix}sqm_views_events"
     
    198255    $suppress_errors = $wpdb->suppress_errors( true );
    199256
    200     // Index creation - errors are suppressed to allow safe re-activation.
    201     // If indexes already exist, the queries will fail silently (expected behavior).
    202     $index_results[] = $wpdb->query(
    203         $wpdb->prepare(
    204             // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    205             /** @lang MySQL */
    206             'CREATE INDEX `sqm_views_trackables_index_0` ON %i (`tgroup`);',
    207             "{$prefix}sqm_views_trackables"
    208         )
    209     );
    210 
    211     $index_results[] = $wpdb->query(
    212         $wpdb->prepare(
    213             // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    214             /** @lang MySQL */
    215             'CREATE INDEX `sqm_views_trackables_index_1` ON %i (`tgroup`, `ttype`, `wpid`, `altid`);',
    216             "{$prefix}sqm_views_trackables"
    217         )
    218     );
    219 
    220     $index_results[] = $wpdb->query(
    221         $wpdb->prepare(
    222             // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    223             /** @lang MySQL */
    224             'CREATE INDEX `sqm_views_records_index_0` ON %i (`tid`);',
    225             "{$prefix}sqm_views_records"
    226         )
    227     );
    228 
    229     $index_results[] = $wpdb->query(
    230         $wpdb->prepare(
    231             // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    232             /** @lang MySQL */
    233             'CREATE INDEX `sqm_views_records_index_1` ON %i (`gid`);',
    234             "{$prefix}sqm_views_records"
    235         )
    236     );
    237 
    238     $index_results[] = $wpdb->query(
    239         $wpdb->prepare(
    240             // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    241             /** @lang MySQL */
    242             'CREATE INDEX `sqm_views_daily_index_0` ON %i (`date`, `eid`, `tid`);',
    243             "{$prefix}sqm_views_daily"
    244         )
    245     );
    246 
    247     $index_results[] = $wpdb->query(
    248         $wpdb->prepare(
    249             // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    250             /** @lang MySQL */
    251             'ALTER TABLE %i ADD FOREIGN KEY(`tid`) REFERENCES %i(`tid`) ON UPDATE NO ACTION ON DELETE NO ACTION;',
    252             "{$prefix}sqm_views_records",
    253             "{$prefix}sqm_views_trackables"
    254         )
    255     );
    256 
    257     $index_results[] = $wpdb->query(
    258         $wpdb->prepare(
    259             // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    260             /** @lang MySQL */
    261             'ALTER TABLE %i ADD FOREIGN KEY(`eid`) REFERENCES %i(`eid`) ON UPDATE NO ACTION ON DELETE NO ACTION;',
    262             "{$prefix}sqm_views_records",
    263             "{$prefix}sqm_views_events"
    264         )
    265     );
    266 
    267     $index_results[] = $wpdb->query(
    268         $wpdb->prepare(
    269             // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    270             /** @lang MySQL */
    271             'ALTER TABLE %i ADD FOREIGN KEY(`eid`) REFERENCES %i(`eid`) ON UPDATE NO ACTION ON DELETE NO ACTION;',
    272             "{$prefix}sqm_views_daily",
    273             "{$prefix}sqm_views_events"
    274         )
    275     );
    276 
    277     $index_results[] = $wpdb->query(
    278         $wpdb->prepare(
    279             // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
    280             /** @lang MySQL */
    281             'ALTER TABLE %i ADD FOREIGN KEY(`tid`) REFERENCES %i(`tid`) ON UPDATE NO ACTION ON DELETE NO ACTION;',
    282             "{$prefix}sqm_views_daily",
    283             "{$prefix}sqm_views_trackables"
    284         )
    285     );
     257    // Index creation - skip if already exists to avoid slow operations on large tables.
     258    if ( ! index_exists( "{$prefix}sqm_views_trackables", 'sqm_views_trackables_index_0' ) ) {
     259        $index_results[] = $wpdb->query(
     260            $wpdb->prepare(
     261                // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
     262                /** @lang MySQL */
     263                'CREATE INDEX `sqm_views_trackables_index_0` ON %i (`tgroup`);',
     264                "{$prefix}sqm_views_trackables"
     265            )
     266        );
     267    }
     268
     269    if ( ! index_exists( "{$prefix}sqm_views_trackables", 'sqm_views_trackables_index_1' ) ) {
     270        $index_results[] = $wpdb->query(
     271            $wpdb->prepare(
     272                // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
     273                /** @lang MySQL */
     274                'CREATE INDEX `sqm_views_trackables_index_1` ON %i (`tgroup`, `ttype`, `wpid`, `altid`);',
     275                "{$prefix}sqm_views_trackables"
     276            )
     277        );
     278    }
     279
     280    if ( ! index_exists( "{$prefix}sqm_views_records", 'sqm_views_records_index_0' ) ) {
     281        $index_results[] = $wpdb->query(
     282            $wpdb->prepare(
     283                // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
     284                /** @lang MySQL */
     285                'CREATE INDEX `sqm_views_records_index_0` ON %i (`tid`);',
     286                "{$prefix}sqm_views_records"
     287            )
     288        );
     289    }
     290
     291    if ( ! index_exists( "{$prefix}sqm_views_records", 'sqm_views_records_index_1' ) ) {
     292        $index_results[] = $wpdb->query(
     293            $wpdb->prepare(
     294                // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
     295                /** @lang MySQL */
     296                'CREATE INDEX `sqm_views_records_index_1` ON %i (`gid`);',
     297                "{$prefix}sqm_views_records"
     298            )
     299        );
     300    }
     301
     302    if ( ! index_exists( "{$prefix}sqm_views_daily", 'sqm_views_daily_index_0' ) ) {
     303        $index_results[] = $wpdb->query(
     304            $wpdb->prepare(
     305                // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
     306                /** @lang MySQL */
     307                'CREATE INDEX `sqm_views_daily_index_0` ON %i (`date`, `eid`, `tid`);',
     308                "{$prefix}sqm_views_daily"
     309            )
     310        );
     311    }
     312
     313    // Foreign key creation - skip if already exists to avoid slow ALTER TABLE on large tables.
     314    if ( ! foreign_key_exists( "{$prefix}sqm_views_records", 'tid' ) ) {
     315        $index_results[] = $wpdb->query(
     316            $wpdb->prepare(
     317                // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
     318                /** @lang MySQL */
     319                'ALTER TABLE %i ADD FOREIGN KEY(`tid`) REFERENCES %i(`tid`) ON UPDATE NO ACTION ON DELETE NO ACTION;',
     320                "{$prefix}sqm_views_records",
     321                "{$prefix}sqm_views_trackables"
     322            )
     323        );
     324    }
     325
     326    if ( ! foreign_key_exists( "{$prefix}sqm_views_records", 'eid' ) ) {
     327        $index_results[] = $wpdb->query(
     328            $wpdb->prepare(
     329                // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
     330                /** @lang MySQL */
     331                'ALTER TABLE %i ADD FOREIGN KEY(`eid`) REFERENCES %i(`eid`) ON UPDATE NO ACTION ON DELETE NO ACTION;',
     332                "{$prefix}sqm_views_records",
     333                "{$prefix}sqm_views_events"
     334            )
     335        );
     336    }
     337
     338    if ( ! foreign_key_exists( "{$prefix}sqm_views_daily", 'eid' ) ) {
     339        $index_results[] = $wpdb->query(
     340            $wpdb->prepare(
     341                // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
     342                /** @lang MySQL */
     343                'ALTER TABLE %i ADD FOREIGN KEY(`eid`) REFERENCES %i(`eid`) ON UPDATE NO ACTION ON DELETE NO ACTION;',
     344                "{$prefix}sqm_views_daily",
     345                "{$prefix}sqm_views_events"
     346            )
     347        );
     348    }
     349
     350    if ( ! foreign_key_exists( "{$prefix}sqm_views_daily", 'tid' ) ) {
     351        $index_results[] = $wpdb->query(
     352            $wpdb->prepare(
     353                // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- IDE language hint.
     354                /** @lang MySQL */
     355                'ALTER TABLE %i ADD FOREIGN KEY(`tid`) REFERENCES %i(`tid`) ON UPDATE NO ACTION ON DELETE NO ACTION;',
     356                "{$prefix}sqm_views_daily",
     357                "{$prefix}sqm_views_trackables"
     358            )
     359        );
     360    }
    286361
    287362    $index_results[] = $wpdb->query(
     
    434509 * Drops all plugin database tables
    435510 *
     511 * Tables are dropped in correct order to respect foreign key constraints:
     512 * 1. daily, records (reference trackables and events)
     513 * 2. trackables, events (referenced by daily and records)
     514 *
    436515 * @return void
    437516 */
     
    442521
    443522    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, Generic.Commenting.DocComment.MissingShort -- Schema operations during plugin uninstall.
     523
     524    // Disable foreign key checks to allow dropping tables in any order safely.
     525    $wpdb->query( 'SET FOREIGN_KEY_CHECKS = 0' );
     526
     527    // Drop tables that have foreign keys first.
    444528    $wpdb->query( $wpdb->prepare( /** @lang MySQL */ 'DROP TABLE IF EXISTS %i', "{$prefix}sqm_views_daily" ) );
     529    $wpdb->query( $wpdb->prepare( /** @lang MySQL */ 'DROP TABLE IF EXISTS %i', "{$prefix}sqm_views_records" ) );
     530    // Then drop tables that are referenced by foreign keys.
    445531    $wpdb->query( $wpdb->prepare( /** @lang MySQL */ 'DROP TABLE IF EXISTS %i', "{$prefix}sqm_views_trackables" ) );
    446     $wpdb->query( $wpdb->prepare( /** @lang MySQL */ 'DROP TABLE IF EXISTS %i', "{$prefix}sqm_views_records" ) );
    447532    $wpdb->query( $wpdb->prepare( /** @lang MySQL */ 'DROP TABLE IF EXISTS %i', "{$prefix}sqm_views_events" ) );
     533
     534    // Re-enable foreign key checks.
     535    $wpdb->query( 'SET FOREIGN_KEY_CHECKS = 1' );
     536
    448537    // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, Generic.Commenting.DocComment.MissingShort
    449538}
  • sqm-views/trunk/src/includes/charts-api.php

    r3451552 r3484943  
    299299        $top_keys = array_slice( array_keys( $totals ), 0, $limit );
    300300
     301        // Resolve post titles and URLs for the top tids via trackables → wp_posts.
     302        // Each tid maps to a wpid (WordPress post ID) in the trackables table.
     303        // The list is bounded by $limit (max 100), so individual lookups are fine.
     304        $title_map = array();
     305        $url_map   = array();
     306        foreach ( $top_keys as $k ) {
     307            $wpid_row = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- bounded by $limit, result cached with chart response
     308                $wpdb->prepare(
     309                    /* @lang MySQL */
     310                    'SELECT `wpid` FROM %i WHERE `tid` = %d',
     311                    "{$wpdb->prefix}sqm_views_trackables",
     312                    intval( $k )
     313                )
     314            );
     315            if ( $wpid_row && $wpid_row->wpid > 0 ) {
     316                $wpid       = (int) $wpid_row->wpid;
     317                $post_title = get_the_title( $wpid );
     318                if ( '' !== $post_title ) {
     319                    $title_map[ $k ] = $post_title;
     320                    $url_map[ $k ]   = get_permalink( $wpid );
     321                }
     322            }
     323        }
     324
    301325        $series = array();
    302326        foreach ( $top_keys as $k ) {
    303             $series[] = array(
    304                 'name'   => 'tid ' . $k,  // SECURITY: Integer key converted to string.
     327            $title = isset( $title_map[ $k ] ) ? $title_map[ $k ] : '';
     328            if ( '' !== $title && mb_strlen( $title ) > 40 ) {
     329                $title = mb_substr( $title, 0, 37 ) . '...';
     330            }
     331            $label = '' !== $title ? '[' . $k . '] ' . $title : 'tid ' . $k;
     332
     333            $entry = array(
     334                'name'   => $label,  // SECURITY: title from wp_posts, tid is integer.
    305335                'values' => $series_map[ $k ],
    306336            );
     337            if ( isset( $url_map[ $k ] ) ) {
     338                $entry['url'] = $url_map[ $k ];
     339            }
     340            $series[] = $entry;
    307341        }
    308342
  • sqm-views/trunk/src/includes/utils.php

    r3451552 r3484943  
    3030 * @since 1.0.0
    3131 */
    32 define( 'SQMVIEWS_BUILD_GLOBAL_VERSION', '1.1.9' );
     32define( 'SQMVIEWS_BUILD_GLOBAL_VERSION', '1.2.4' );
    3333
    3434define( 'SQMVIEWS_NAME', 'sqm-views' );
  • sqm-views/trunk/vendor/composer/installed.php

    r3451552 r3484943  
    22    'root' => array(
    33        'name' => 'sqm-views/sqm-views',
    4         'pretty_version' => '1.1.9',
    5         'version' => '1.1.9.0',
     4        'pretty_version' => '1.2.4',
     5        'version' => '1.2.4.0',
    66        'reference' => null,
    77        'type' => 'wordpress-plugin',
     
    1212    'versions' => array(
    1313        'sqm-views/sqm-views' => array(
    14             'pretty_version' => '1.1.9',
    15             'version' => '1.1.9.0',
     14            'pretty_version' => '1.2.4',
     15            'version' => '1.2.4.0',
    1616            'reference' => null,
    1717            'type' => 'wordpress-plugin',
Note: See TracChangeset for help on using the changeset viewer.