Plugin Directory

Changeset 3455410


Ignore:
Timestamp:
02/06/2026 01:16:01 PM (2 weeks ago)
Author:
pluxo
Message:

Release 1.2.1: fixes in detected bugs

Location:
pluxo-blueprint/trunk
Files:
21 edited

Legend:

Unmodified
Added
Removed
  • pluxo-blueprint/trunk/includes/Modules/Builders/class-pluxo-blueprint-builders-renderer.php

    r3453209 r3455410  
    5656            <div class="pluxo-blueprint-accordion-body">
    5757               
    58                 <?php $this->render_actions( $last_scan ); ?>
     58                <?php
     59                $is_print = defined( 'PLUXO_BLUEPRINT_PRINT_VIEW' ) && PLUXO_BLUEPRINT_PRINT_VIEW;
     60                if ( ! $is_print ) {
     61                    $this->render_actions( $last_scan );
     62                }
     63                ?>
    5964
    6065                <?php if ( '' !== $summary ) : ?>
  • pluxo-blueprint/trunk/includes/Modules/Builders/class-pluxo-blueprint-builders-repository.php

    r3453209 r3455410  
    139139        );
    140140
    141         $wpbakery_active  = $this->is_plugin_active_by_slug( $plugins, 'js_composer/js_composer.php' );
     141        $wpbakery_active = $this->is_any_plugin_active_by_slugs(
     142            $plugins,
     143            array(
     144                'js_composer/js_composer.php',
     145                'js_composer_theme/js_composer.php',
     146            )
     147        );
    142148        $bricks_active    = $this->is_plugin_active_by_slug( $plugins, 'bricks/bricks.php' );
    143149
     
    171177            'wpbakery' => array(
    172178                'label'         => 'WPBakery',
    173                 'detected'      => $wpbakery_active,
    174                 'notes'         => __( 'Detection uses WPBakery shortcodes in content (best-effort).', 'pluxo-blueprint' ),
     179                'detected'      => true,
     180                'notes' => $wpbakery_active
     181                    ? __( 'Plugin detected. Usage scan searches WPBakery shortcodes in content (best-effort).', 'pluxo-blueprint' )
     182                    : __( 'Usage scan searches WPBakery shortcodes in content (best-effort). Plugin not detected as active.', 'pluxo-blueprint' ),
    175183                'type'          => 'content_like',
    176184                'like'          => '[vc_',
     
    662670        return is_plugin_active( $plugin_file );
    663671    }
     672
     673    /**
     674     * Check if any plugin in a list is active.
     675     *
     676     * @param array<string, array<string,mixed>> $plugins Plugins array.
     677     * @param string[]                          $plugin_files Plugin files.
     678     * @return bool
     679     */
     680    private function is_any_plugin_active_by_slugs( $plugins, $plugin_files ) {
     681        $plugin_files = array_map( 'strval', (array) $plugin_files );
     682        $plugin_files = array_values( array_filter( array_map( 'trim', $plugin_files ) ) );
     683
     684        if ( empty( $plugin_files ) ) {
     685            return false;
     686        }
     687
     688        foreach ( $plugin_files as $plugin_file ) {
     689            if ( $this->is_plugin_active_by_slug( $plugins, $plugin_file ) ) {
     690                return true;
     691            }
     692        }
     693
     694        return false;
     695    }
    664696}
  • pluxo-blueprint/trunk/includes/Modules/Content_Types/class-pluxo-blueprint-content-types-repository.php

    r3453209 r3455410  
    9292
    9393    /**
     94     * Check if a post type starts with any blocked prefix.
     95     *
     96     * @param string   $type Post type key.
     97     * @param string[] $prefixes Prefix list.
     98     * @return bool
     99     */
     100    private function starts_with_any_prefix( $type, $prefixes ) {
     101        $type     = (string) $type;
     102        $prefixes = (array) $prefixes;
     103
     104        foreach ( $prefixes as $prefix ) {
     105            $prefix = (string) $prefix;
     106            if ( '' === $prefix ) {
     107                continue;
     108            }
     109            if ( 0 === strpos( $type, $prefix ) ) {
     110                return true;
     111            }
     112        }
     113
     114        return false;
     115    }
     116
     117    /**
     118     * Decide whether a post type should be treated as Primary content (public-facing).
     119     *
     120     * @param string   $type Post type key.
     121     * @param object   $obj Post type object.
     122     * @param bool     $is_system_type Whether it was already classified as system.
     123     * @param string[] $primary_allowlist Forced Primary list.
     124     * @param string[] $primary_blocked_prefixes Blocked prefixes list.
     125     * @return bool
     126     */
     127    private function is_primary_content_type( $type, $obj, $is_system_type, $primary_allowlist, $primary_blocked_prefixes ) {
     128        $type = (string) $type;
     129
     130        // Always primary.
     131        if ( in_array( $type, array( 'post', 'page' ), true ) ) {
     132            return true;
     133        }
     134
     135        // Forced Primary via filter.
     136        if ( in_array( $type, (array) $primary_allowlist, true ) ) {
     137            return true;
     138        }
     139
     140        // Anything already classified as system is not primary.
     141        if ( $is_system_type ) {
     142            return false;
     143        }
     144
     145        // Block known plugin/internal prefixes.
     146        if ( $this->starts_with_any_prefix( $type, $primary_blocked_prefixes ) ) {
     147            return false;
     148        }
     149
     150        $is_public             = ! empty( $obj->public );
     151        $is_publicly_queryable = ! empty( $obj->publicly_queryable );
     152        $has_archive           = ! empty( $obj->has_archive );
     153
     154        // If it's not public-facing by signals, it's not primary.
     155        if ( ! $is_public ) {
     156            return false;
     157        }
     158
     159        if ( ! $is_publicly_queryable && ! $has_archive ) {
     160            return false;
     161        }
     162
     163        // Heuristic: if it contains "_" and lacks strong public signals, treat as system/plugin.
     164        // (We keep this conservative to avoid hiding legit CPTs.)
     165        if ( false !== strpos( $type, '_' ) && ! $has_archive ) {
     166            return false;
     167        }
     168
     169        return true;
     170    }
     171
     172    /**
    94173     * Build module data: counts + diagnostics for built-in + custom post types.
    95174     *
     
    124203        $system_post_types = (array) apply_filters( 'pluxo_blueprint_system_post_types', $system_post_types );
    125204
     205        // Post type prefixes that are typically internal/plugin-driven (best-effort).
     206        $primary_blocked_prefixes = array(
     207            'wpr_',
     208            'elementskit_',
     209        );
     210
     211        // Allow developers to extend/override blocked prefixes.
     212        $primary_blocked_prefixes = (array) apply_filters( 'pluxo_blueprint_primary_post_type_blocked_prefixes', $primary_blocked_prefixes );
     213
     214        // Post types that should always be treated as Primary (besides post/page).
     215        $primary_allowlist = array();
     216
     217        // Allow developers to force some post types into Primary.
     218        $primary_allowlist = (array) apply_filters( 'pluxo_blueprint_primary_post_type_allowlist', $primary_allowlist );
     219
     220        // Post types that should always be treated as System.
     221        $system_force_list = array();
     222
     223        // Allow developers to force some post types into System.
     224        $system_force_list = (array) apply_filters( 'pluxo_blueprint_system_post_types_force', $system_force_list );
     225
    126226        $primary_builtin_types = 0;
    127227        $primary_custom_types  = 0;
     
    143243            $private   = isset( $counts->private ) ? (int) $counts->private : 0;
    144244
    145             $is_public     = ! empty( $obj->public );
    146             $is_hier       = ! empty( $obj->hierarchical );
    147             $has_archive   = ! empty( $obj->has_archive );
    148             $show_in_rest  = ! empty( $obj->show_in_rest );
    149             $is_builtin    = ! empty( $obj->_builtin );
     245            $is_public      = ! empty( $obj->public );
     246            $is_hier        = ! empty( $obj->hierarchical );
     247            $has_archive    = ! empty( $obj->has_archive );
     248            $show_in_rest   = ! empty( $obj->show_in_rest );
     249            $is_builtin     = ! empty( $obj->_builtin );
    150250            $is_system_type = false;
    151251
     
    165265            }
    166266
     267            // System signals (existing logic).
    167268            if ( 0 === strpos( (string) $type, 'acf-' ) ) {
    168269                $is_system_type = true;
     
    179280            // If it's not really public-facing, treat it as system by default.
    180281            if ( empty( $obj->publicly_queryable ) && empty( $obj->has_archive ) && 'post' !== $type && 'page' !== $type ) {
     282                $is_system_type = true;
     283            }
     284
     285            // Forced system list (wins).
     286            if ( in_array( (string) $type, (array) $system_force_list, true ) ) {
    181287                $is_system_type = true;
    182288            }
     
    197303            $item_count_for_type = (int) $total;
    198304
    199             if ( $is_system_type ) {
    200                 $system_rows[] = $row;
    201             } else {
     305            if ( $this->is_primary_content_type( $type, $obj, $is_system_type, $primary_allowlist, $primary_blocked_prefixes ) ) {
    202306                $primary_rows[] = $row;
    203307
     
    210314                    $primary_custom_types++;
    211315                }
     316            } else {
     317                $system_rows[] = $row;
    212318            }
    213319        }
  • pluxo-blueprint/trunk/includes/Modules/Content_Types/class-pluxo-blueprint-module-content-types.php

    r3453209 r3455410  
    4343    public function render() {
    4444        $data = $this->repository->get_content_types_data();
     45        $is_print = defined( 'PLUXO_BLUEPRINT_PRINT_VIEW' ) && PLUXO_BLUEPRINT_PRINT_VIEW;
    4546
    4647        $pill_class = ! empty( $data['pill']['class'] ) ? (string) $data['pill']['class'] : 'pluxo-blueprint-pill--good';
     
    144145
    145146                if ( ! empty( $data['system_rows'] ) ) {
    146                     $toggle_id = 'pluxo-bp-content-types-system-toggle';
    147                     $wrap_id   = 'pluxo-bp-content-types-system-wrap';
    148 
    149147                    $system_count = (int) count( $data['system_rows'] );
    150148
    151                     $toggle_label = sprintf(
    152                         /* translators: 1: number of system/plugin content types */
    153                         esc_html__( '%1$d System & Plugin types were also detected. Tick/untick the box to show or hide this section.', 'pluxo-blueprint' ),
    154                         $system_count
    155                     );
    156 
    157                     echo '<div class="pluxo-blueprint-toggle-row" style="margin-top: 12px;">';
    158                     echo '<label for="' . esc_attr( $toggle_id ) . '">';
    159                     echo '<input type="checkbox" id="' . esc_attr( $toggle_id ) . '" style="margin-right: 8px;" />';
    160                     echo esc_html( $toggle_label );
    161                     echo '</label>';
    162                     echo '</div>';
    163 
    164                     // Hidden by default.
    165                     echo '<div id="' . esc_attr( $wrap_id ) . '" style="display:none; margin-top: 12px;">';
    166 
    167                     echo '<p class="description" style="margin-top: 0;">' . esc_html__( 'These content types are typically used internally by plugins, themes, or WordPress features.', 'pluxo-blueprint' ) . '</p>';
    168 
    169                     echo '<table class="widefat striped pluxo-blueprint-table">';
    170                     echo '<thead><tr>';
    171                     echo '<th>' . esc_html__( 'Post type', 'pluxo-blueprint' ) . '</th>';
    172                     echo '<th>' . esc_html__( 'Label', 'pluxo-blueprint' ) . '</th>';
    173                     echo '<th>' . esc_html__( 'Total', 'pluxo-blueprint' ) . '</th>';
    174                     echo '<th>' . esc_html__( 'Published', 'pluxo-blueprint' ) . '</th>';
    175                     echo '<th>' . esc_html__( 'Drafts', 'pluxo-blueprint' ) . '</th>';
    176                     echo '<th>' . esc_html__( 'Private', 'pluxo-blueprint' ) . '</th>';
    177                     echo '<th>' . esc_html__( 'Public', 'pluxo-blueprint' ) . '</th>';
    178                     echo '<th>' . esc_html__( 'Hierarchical', 'pluxo-blueprint' ) . '</th>';
    179                     echo '<th>' . esc_html__( 'Has archive', 'pluxo-blueprint' ) . '</th>';
    180                     echo '<th>' . esc_html__( 'REST API', 'pluxo-blueprint' ) . '</th>';
    181                     echo '</tr></thead>';
    182 
    183                     echo '<tbody>';
    184 
    185                     foreach ( (array) $data['system_rows'] as $row ) {
    186                         echo '<tr>';
    187                         echo '<td><code>' . esc_html( (string) $row['post_type'] ) . '</code></td>';
    188                         echo '<td>' . esc_html( (string) $row['label'] ) . '</td>';
    189                         echo '<td>' . esc_html( (string) $row['total'] ) . '</td>';
    190                         echo '<td>' . esc_html( (string) $row['published'] ) . '</td>';
    191                         echo '<td>' . esc_html( (string) $row['drafts'] ) . '</td>';
    192                         echo '<td>' . esc_html( (string) $row['private'] ) . '</td>';
    193                         echo '<td>' . esc_html( (string) $row['public'] ) . '</td>';
    194                         echo '<td>' . esc_html( (string) $row['hierarchical'] ) . '</td>';
    195                         echo '<td>' . esc_html( (string) $row['has_archive'] ) . '</td>';
    196                         echo '<td>' . esc_html( (string) $row['show_in_rest'] ) . '</td>';
    197                         echo '</tr>';
     149                    // In print/PDF view, always show the System & Plugin section (no toggles).
     150                    if ( $is_print ) {
     151                        echo '<div style="margin-top: 12px;">';
     152                        echo '<p class="description" style="margin-top: 0;">' . esc_html__( 'These content types are typically used internally by plugins, themes, or WordPress features.', 'pluxo-blueprint' ) . '</p>';
     153
     154                        echo '<table class="widefat striped pluxo-blueprint-table">';
     155                        echo '<thead><tr>';
     156                        echo '<th>' . esc_html__( 'Post type', 'pluxo-blueprint' ) . '</th>';
     157                        echo '<th>' . esc_html__( 'Label', 'pluxo-blueprint' ) . '</th>';
     158                        echo '<th>' . esc_html__( 'Total', 'pluxo-blueprint' ) . '</th>';
     159                        echo '<th>' . esc_html__( 'Published', 'pluxo-blueprint' ) . '</th>';
     160                        echo '<th>' . esc_html__( 'Drafts', 'pluxo-blueprint' ) . '</th>';
     161                        echo '<th>' . esc_html__( 'Private', 'pluxo-blueprint' ) . '</th>';
     162                        echo '<th>' . esc_html__( 'Public', 'pluxo-blueprint' ) . '</th>';
     163                        echo '<th>' . esc_html__( 'Hierarchical', 'pluxo-blueprint' ) . '</th>';
     164                        echo '<th>' . esc_html__( 'Has archive', 'pluxo-blueprint' ) . '</th>';
     165                        echo '<th>' . esc_html__( 'REST API', 'pluxo-blueprint' ) . '</th>';
     166                        echo '</tr></thead>';
     167
     168                        echo '<tbody>';
     169
     170                        foreach ( (array) $data['system_rows'] as $row ) {
     171                            echo '<tr>';
     172                            echo '<td><code>' . esc_html( (string) $row['post_type'] ) . '</code></td>';
     173                            echo '<td>' . esc_html( (string) $row['label'] ) . '</td>';
     174                            echo '<td>' . esc_html( (string) $row['total'] ) . '</td>';
     175                            echo '<td>' . esc_html( (string) $row['published'] ) . '</td>';
     176                            echo '<td>' . esc_html( (string) $row['drafts'] ) . '</td>';
     177                            echo '<td>' . esc_html( (string) $row['private'] ) . '</td>';
     178                            echo '<td>' . esc_html( (string) $row['public'] ) . '</td>';
     179                            echo '<td>' . esc_html( (string) $row['hierarchical'] ) . '</td>';
     180                            echo '<td>' . esc_html( (string) $row['has_archive'] ) . '</td>';
     181                            echo '<td>' . esc_html( (string) $row['show_in_rest'] ) . '</td>';
     182                            echo '</tr>';
     183                        }
     184
     185                        echo '</tbody>';
     186                        echo '</table>';
     187                        echo '</div>';
     188
     189                    } else {
     190                        // Normal UI: toggleable section.
     191                        $toggle_id = 'pluxo-bp-content-types-system-toggle';
     192                        $wrap_id   = 'pluxo-bp-content-types-system-wrap';
     193
     194                        $toggle_label = sprintf(
     195                            /* translators: 1: number of system/plugin content types */
     196                            esc_html__( '%1$d System & Plugin types were also detected. Tick/untick the box to show or hide this section.', 'pluxo-blueprint' ),
     197                            $system_count
     198                        );
     199
     200                        echo '<div class="pluxo-blueprint-toggle-row" style="margin-top: 12px;">';
     201                        echo '<label for="' . esc_attr( $toggle_id ) . '">';
     202                        echo '<input type="checkbox" id="' . esc_attr( $toggle_id ) . '" style="margin-right: 8px;" />';
     203                        echo esc_html( $toggle_label );
     204                        echo '</label>';
     205                        echo '</div>';
     206
     207                        // Hidden by default.
     208                        echo '<div id="' . esc_attr( $wrap_id ) . '" style="display:none; margin-top: 12px;">';
     209
     210                        echo '<p class="description" style="margin-top: 0;">' . esc_html__( 'These content types are typically used internally by plugins, themes, or WordPress features.', 'pluxo-blueprint' ) . '</p>';
     211
     212                        echo '<table class="widefat striped pluxo-blueprint-table">';
     213                        echo '<thead><tr>';
     214                        echo '<th>' . esc_html__( 'Post type', 'pluxo-blueprint' ) . '</th>';
     215                        echo '<th>' . esc_html__( 'Label', 'pluxo-blueprint' ) . '</th>';
     216                        echo '<th>' . esc_html__( 'Total', 'pluxo-blueprint' ) . '</th>';
     217                        echo '<th>' . esc_html__( 'Published', 'pluxo-blueprint' ) . '</th>';
     218                        echo '<th>' . esc_html__( 'Drafts', 'pluxo-blueprint' ) . '</th>';
     219                        echo '<th>' . esc_html__( 'Private', 'pluxo-blueprint' ) . '</th>';
     220                        echo '<th>' . esc_html__( 'Public', 'pluxo-blueprint' ) . '</th>';
     221                        echo '<th>' . esc_html__( 'Hierarchical', 'pluxo-blueprint' ) . '</th>';
     222                        echo '<th>' . esc_html__( 'Has archive', 'pluxo-blueprint' ) . '</th>';
     223                        echo '<th>' . esc_html__( 'REST API', 'pluxo-blueprint' ) . '</th>';
     224                        echo '</tr></thead>';
     225
     226                        echo '<tbody>';
     227
     228                        foreach ( (array) $data['system_rows'] as $row ) {
     229                            echo '<tr>';
     230                            echo '<td><code>' . esc_html( (string) $row['post_type'] ) . '</code></td>';
     231                            echo '<td>' . esc_html( (string) $row['label'] ) . '</td>';
     232                            echo '<td>' . esc_html( (string) $row['total'] ) . '</td>';
     233                            echo '<td>' . esc_html( (string) $row['published'] ) . '</td>';
     234                            echo '<td>' . esc_html( (string) $row['drafts'] ) . '</td>';
     235                            echo '<td>' . esc_html( (string) $row['private'] ) . '</td>';
     236                            echo '<td>' . esc_html( (string) $row['public'] ) . '</td>';
     237                            echo '<td>' . esc_html( (string) $row['hierarchical'] ) . '</td>';
     238                            echo '<td>' . esc_html( (string) $row['has_archive'] ) . '</td>';
     239                            echo '<td>' . esc_html( (string) $row['show_in_rest'] ) . '</td>';
     240                            echo '</tr>';
     241                        }
     242
     243                        echo '</tbody>';
     244                        echo '</table>';
     245
     246                        echo '</div>'; // end wrap.
     247
     248                        echo '<script>
     249                        (function() {
     250                            var toggle = document.getElementById("' . esc_js( $toggle_id ) . '");
     251                            var wrap = document.getElementById("' . esc_js( $wrap_id ) . '");
     252                            if (!toggle || !wrap) { return; }
     253                            toggle.addEventListener("change", function() {
     254                                wrap.style.display = toggle.checked ? "block" : "none";
     255                            });
     256                        })();
     257                        </script>';
    198258                    }
    199 
    200                     echo '</tbody>';
    201                     echo '</table>';
    202 
    203                     echo '</div>'; // end wrap.
    204 
    205                     echo '<script>
    206                     (function() {
    207                         var toggle = document.getElementById("' . esc_js( $toggle_id ) . '");
    208                         var wrap = document.getElementById("' . esc_js( $wrap_id ) . '");
    209                         if (!toggle || !wrap) { return; }
    210                         toggle.addEventListener("change", function() {
    211                             wrap.style.display = toggle.checked ? "block" : "none";
    212                         });
    213                     })();
    214                     </script>';
    215259                }
    216260                ?>
  • pluxo-blueprint/trunk/includes/Modules/Cookies_Consent/class-pluxo-blueprint-cookies-consent-scan.php

    r3453209 r3455410  
    1515
    1616    /**
     17     * Cache option name for Cookies & Consent scan.
     18     *
     19     * @var string
     20     */
     21    private $option_name = 'pluxo_blueprint_cookies_consent_scan';
     22
     23    /**
     24     * Cache TTL (seconds).
     25     *
     26     * @var int
     27     */
     28    private $cache_ttl = 86400; // 24 hours.
     29
     30    /**
    1731     * Run scan and return structured raw data.
    1832     *
     33     * @param array<string,mixed> $args {
     34     *   Optional. Arguments.
     35     *   @type bool $force        Force refresh (ignore cache).
     36     *   @type bool $allow_remote Allow external provider fetches (CookieYes CDN) when possible.
     37     * }
    1938     * @return array<string, mixed>
    2039     */
    21     public function run() {
     40    public function run( $args = array() ) {
     41        $args = is_array( $args ) ? $args : array();
     42        $force        = ! empty( $args['force'] );
     43        $allow_remote = ! empty( $args['allow_remote'] );
     44
     45        // Cache (skip only when forcing).
     46        if ( ! $force ) {
     47            $cached = get_option( $this->option_name );
     48            if ( is_array( $cached ) && ! empty( $cached['metadata']['scanned_at_gmt'] ) ) {
     49                $last = strtotime( (string) $cached['metadata']['scanned_at_gmt'] );
     50                if ( $last && ( time() - $last ) < $this->cache_ttl ) {
     51                    return $cached;
     52                }
     53            }
     54        }
     55
    2256        $providers = Pluxo_Blueprint_Cookies_Consent_Registry::get_providers();
    2357
     
    86120
    87121        $cookies_all = $this->dedupe_cookie_list_by_provider_and_name( $cookies_all );
    88 
    89         return array(
     122        $home_signals = $this->detect_cookieyes_homepage_signals();
     123
     124        $result = array(
    90125            'providers' => $out_providers,
    91126            'cookies'   => $cookies_all,
    92127            'metadata'  => array(
    93                 'scanned_at_gmt' => gmdate( 'c' ),
     128                'scanned_at_gmt'           => gmdate( 'c' ),
     129                'cookieyes_web_detected'   => ! empty( $home_signals['cookieyes_web_detected'] ),
     130                'cookieyes_gcm_categories' => ! empty( $home_signals['cookieyes_gcm_categories'] ) ? (array) $home_signals['cookieyes_gcm_categories'] : array(),
    94131            ),
    95132        );
     133
     134        // Save cache.
     135        update_option( $this->option_name, $result, false );
     136
     137        return $result;
    96138    }
    97139
     
    491533        return $out;
    492534    }
     535
     536    /**
     537 * Best-effort: detect CookieYes signals in the homepage HTML.
     538 *
     539 * - CookieYes web dashboard script:
     540 *   <script id="cookieyes" src="https://cdn-cookieyes.com/client_data/.../script.js">
     541 * - CookieYes GCM config:
     542 *   <script id="cookie-law-info-gcm-var-js"> var _ckyGcm = {...}</script>
     543 *
     544 * @return array<string, mixed>
     545 */
     546private function detect_cookieyes_homepage_signals() {
     547    $out = array(
     548        'cookieyes_web_detected'   => false,
     549        'cookieyes_gcm_categories' => array(),
     550    );
     551
     552    $url = home_url( '/' );
     553
     554    $response = wp_remote_get(
     555        $url,
     556        array(
     557            'timeout'             => 6,
     558            'redirection'         => 2,
     559            'user-agent'          => 'Pluxo Blueprint; Cookies scanner',
     560            'limit_response_size' => 250000,
     561        )
     562    );
     563
     564    if ( is_wp_error( $response ) ) {
     565        return $out;
     566    }
     567
     568    $code = (int) wp_remote_retrieve_response_code( $response );
     569    if ( $code < 200 || $code >= 400 ) {
     570        return $out;
     571    }
     572
     573    $body = (string) wp_remote_retrieve_body( $response );
     574    if ( '' === trim( $body ) ) {
     575        return $out;
     576    }
     577
     578    // 1) Detect CookieYes web script (best-effort).
     579    // We look for id="cookieyes" AND the CookieYes CDN pattern.
     580    if ( false !== stripos( $body, 'id="cookieyes"' ) && false !== stripos( $body, 'cdn-cookieyes.com/client_data/' ) ) {
     581        $out['cookieyes_web_detected'] = true;
     582    }
     583
     584    // 2) Extract categories from _ckyGcm var (best-effort).
     585    $categories = $this->extract_cookieyes_gcm_categories_from_html( $body );
     586    if ( ! empty( $categories ) ) {
     587        $out['cookieyes_gcm_categories'] = $categories;
     588    }
     589
     590    return $out;
    493591}
     592
     593    /**
     594     * Best-effort: extract CookieYes categories from the _ckyGcm config script.
     595     *
     596     * Example:
     597     * var _ckyGcm = {"status":true,"default_settings":[{"analytics":"denied","advertisement":"denied",...}]...}
     598     *
     599     * @param string $html Homepage HTML.
     600     * @return array<int, string>
     601     */
     602    private function extract_cookieyes_gcm_categories_from_html( $html ) {
     603        $html = (string) $html;
     604
     605        // Capture the JSON object assigned to var _ckyGcm = {...};
     606        $matches = array();
     607        if ( ! preg_match( '/var\s+_ckyGcm\s*=\s*(\{.*?\})\s*;/s', $html, $matches ) ) {
     608            return array();
     609        }
     610
     611        $json = isset( $matches[1] ) ? (string) $matches[1] : '';
     612        $json = trim( $json );
     613
     614        if ( '' === $json ) {
     615            return array();
     616        }
     617
     618        $data = json_decode( $json, true );
     619        if ( ! is_array( $data ) ) {
     620            return array();
     621        }
     622
     623        $default_settings = isset( $data['default_settings'] ) ? $data['default_settings'] : null;
     624        if ( ! is_array( $default_settings ) || empty( $default_settings[0] ) || ! is_array( $default_settings[0] ) ) {
     625            return array();
     626        }
     627
     628        $first = (array) $default_settings[0];
     629
     630        // Known keys we care about (ignore regions and other non-category fields).
     631        $key_map = array(
     632            'necessary'          => 'Necessary',
     633            'functional'         => 'Functional',
     634            'analytics'          => 'Analytics',
     635            'advertisement'      => 'Advertisement',
     636            'ad_user_data'       => 'Ad user data',
     637            'ad_personalization' => 'Ad personalization',
     638        );
     639
     640        $out = array();
     641
     642        foreach ( $key_map as $key => $label ) {
     643            if ( array_key_exists( $key, $first ) ) {
     644                $out[] = $label;
     645            }
     646        }
     647
     648        return array_values( array_unique( $out ) );
     649    }
     650}
  • pluxo-blueprint/trunk/includes/Modules/Cookies_Consent/class-pluxo-blueprint-module-cookies-consent.php

    r3453209 r3455410  
    8585                'summary' => esc_html__( 'Cookies & consent scanner not available.', 'pluxo-blueprint' ),
    8686                'cookies' => array(),
     87                'cookieyes_any_detected' => false,
    8788            );
    8889        }
     
    9495        $cookies   = isset( $scan['cookies'] ) ? (array) $scan['cookies'] : array();
    9596
     97        $metadata = isset( $scan['metadata'] ) && is_array( $scan['metadata'] ) ? (array) $scan['metadata'] : array();
     98
     99        $cookieyes_web_detected   = ! empty( $metadata['cookieyes_web_detected'] );
     100        $cookieyes_gcm_categories = ! empty( $metadata['cookieyes_gcm_categories'] ) && is_array( $metadata['cookieyes_gcm_categories'] )
     101            ? array_values( array_filter( array_map( 'trim', (array) $metadata['cookieyes_gcm_categories'] ) ) )
     102            : array();
     103
    96104        $detected_names = array();
    97105        $notes          = array();
     106
     107        $cookieyes_plugin_detected = false;
    98108
    99109        foreach ( $providers as $p ) {
     
    105115            $active = ! empty( $p['active'] );
    106116
     117            if ( $active && 'CookieYes' === $name ) {
     118                $cookieyes_plugin_detected = true;
     119            }
     120
    107121            if ( $active && '' !== $name ) {
    108122                $detected_names[] = $name;
     
    116130        $detected_names = array_values( array_unique( array_filter( $detected_names ) ) );
    117131        $notes          = array_values( array_unique( array_filter( $notes ) ) );
     132
     133        if ( $cookieyes_web_detected && ! $cookieyes_plugin_detected ) {
     134            $detected_names[] = __( 'CookieYes (No plugin)', 'pluxo-blueprint' );
     135        }
     136
     137        $detected_names = array_values( array_unique( array_filter( $detected_names ) ) );
     138
     139        $cookieyes_any_detected = ( $cookieyes_plugin_detected || $cookieyes_web_detected );
    118140
    119141        $cookie_count = count( $cookies );
     
    123145            : esc_html__( 'No consent plugin', 'pluxo-blueprint' );
    124146
    125         $pill_label = sprintf(
    126             /* translators: 1: detected plugin names, 2: cookie count, 3: cookie/cookies label */
    127             esc_html__( '%1$s / %2$d cookies', 'pluxo-blueprint' ),
    128             $plugins_label,
    129             (int) $cookie_count
    130         );
    131 
    132         $pill_class = ( ! empty( $detected_names ) )
    133             ? ( ( $cookie_count > 0 ) ? 'pluxo-blueprint-pill--neutral' : 'pluxo-blueprint-pill--neutral' )
    134             : 'pluxo-blueprint-pill--neutral';
    135 
    136         $summary_lines = array();
    137 
    138         if ( empty( $detected_names ) ) {
    139             $summary_lines[] = esc_html__( 'No compatible cookie/consent plugin was detected.', 'pluxo-blueprint' );
     147        if ( $cookieyes_any_detected ) {
     148                    $pill_label = sprintf(
     149                        /* translators: %s: detected provider names */
     150                        esc_html__( '%s / Cookies cannot be detected', 'pluxo-blueprint' ),
     151                        $plugins_label
     152                    );
     153                } else {
     154                    $pill_label = sprintf(
     155                        /* translators: 1: detected plugin names, 2: cookie count */
     156                        esc_html__( '%1$s / %2$d cookies', 'pluxo-blueprint' ),
     157                        $plugins_label,
     158                        (int) $cookie_count
     159                    );
     160                }
     161
     162                $pill_class = ( ! empty( $detected_names ) )
     163                    ? ( ( $cookie_count > 0 ) ? 'pluxo-blueprint-pill--neutral' : 'pluxo-blueprint-pill--neutral' )
     164                    : 'pluxo-blueprint-pill--neutral';
     165
     166                $summary_lines = array();
     167
     168                if ( empty( $detected_names ) ) {
     169                    $summary_lines[] = esc_html__( 'No compatible cookie/consent plugin was detected.', 'pluxo-blueprint' );
     170                } else {
     171        if ( $cookieyes_any_detected ) {
     172            $summary_lines[] = sprintf(
     173                /* translators: %s: detected provider names */
     174                esc_html__( '%s detected on this website.', 'pluxo-blueprint' ),
     175                $plugins_label
     176            );
    140177        } else {
    141178            $summary_lines[] = sprintf(
     
    145182                (int) $cookie_count
    146183            );
     184        }
    147185
    148186            foreach ( $notes as $note ) {
    149187                $summary_lines[] = $note;
     188            }
     189        }
     190
     191        if ( $cookieyes_any_detected ) {
     192            if ( ! empty( $cookieyes_gcm_categories ) ) {
     193                $summary_lines[] = sprintf(
     194                    /* translators: %s: cookie categories */
     195                    esc_html__( 'CookieYes detected on this website, with these cookie categories configured: %s.', 'pluxo-blueprint' ),
     196                    implode( ', ', $cookieyes_gcm_categories )
     197                );
     198            } else {
     199                $summary_lines[] = esc_html__( 'CookieYes detected on this website.', 'pluxo-blueprint' );
     200            }
     201
     202            $summary_lines[] = esc_html__( 'Due to technical limitations, CookieYes cookies cannot be listed by this plugin.', 'pluxo-blueprint' );
     203
     204            if ( $cookieyes_web_detected && ! $cookieyes_plugin_detected ) {
     205                $summary_lines[] = esc_html__( 'The CookieYes WordPress plugin was not detected. This is likely the CookieYes web dashboard script connected directly to the site.', 'pluxo-blueprint' );
    150206            }
    151207        }
     
    163219            'summary' => implode( "\n", $summary_lines ),
    164220            'cookies' => $cookies,
     221            'cookieyes_any_detected' => $cookieyes_any_detected,
    165222        );
    166223    }
     
    246303
    247304        $cookies = ! empty( $data['cookies'] ) ? (array) $data['cookies'] : array();
     305        $cookieyes_any_detected = ! empty( $data['cookieyes_any_detected'] );
    248306
    249307        if ( empty( $cookies ) ) {
    250             $lines[] = 'No cookies were listed by the supported consent plugins.';
     308            if ( $cookieyes_any_detected ) {
     309                $lines[] = 'CookieYes was detected, but cookies cannot be listed due to technical limitations.';
     310            } else {
     311                $lines[] = 'No cookies were listed by the supported consent plugins.';
     312            }
     313
    251314            return Pluxo_Blueprint_Markdown_Helper::normalise( implode( "\n", $lines ) );
    252315        }
  • pluxo-blueprint/trunk/includes/Modules/Header_Footer/class-pluxo-blueprint-header-footer-repository.php

    r3453209 r3455410  
    218218        }
    219219
    220         return $line1 . "\n" . $line2 . "\n\n" . $line3;
     220        // ---- Best-effort hints block (requested) ----
     221        $hints = array();
     222
     223        $is_block_theme = ! empty( $signals['is_block_theme'] );
     224        $theme_type     = $is_block_theme ? __( 'Block theme (FSE)', 'pluxo-blueprint' ) : __( 'Classic theme', 'pluxo-blueprint' );
     225
     226        $hints[] = sprintf(
     227            /* translators: %s is the theme type. */
     228            __( 'Theme type: %s.', 'pluxo-blueprint' ),
     229            $theme_type
     230        );
     231
     232        if ( $is_block_theme ) {
     233            $hints[] = __( 'Header/footer likely controlled via Template Parts (Site Editor).', 'pluxo-blueprint' );
     234        }
     235
     236        // Classic theme + no builder templates detected => suggest likely files.
     237        $no_builders = (
     238            ( empty( $signals['elementor_headers'] ) && empty( $signals['elementor_footers'] ) )
     239            && ( empty( $signals['elementskit_headers'] ) && empty( $signals['elementskit_footers'] ) )
     240        );
     241
     242        if ( ! $is_block_theme && $no_builders ) {
     243            $files = isset( $signals['theme_files_hint'] ) && is_array( $signals['theme_files_hint'] )
     244                ? array_values( array_filter( array_map( 'strval', (array) $signals['theme_files_hint'] ) ) )
     245                : array();
     246
     247            if ( ! empty( $files ) ) {
     248                $hints[] = sprintf(
     249                    /* translators: %s is a comma-separated list of theme files. */
     250                    __( 'Possible theme files: %s.', 'pluxo-blueprint' ),
     251                    implode( ', ', $files )
     252                );
     253            }
     254        }
     255
     256        // Optional markers (1–3 paths).
     257        $markers = isset( $signals['theme_markers'] ) && is_array( $signals['theme_markers'] )
     258            ? array_values( array_filter( array_map( 'strval', (array) $signals['theme_markers'] ) ) )
     259            : array();
     260
     261        if ( ! empty( $markers ) ) {
     262            $hints[] = sprintf(
     263                /* translators: %s is a comma-separated list of theme paths. */
     264                __( 'Found markers in theme: %s.', 'pluxo-blueprint' ),
     265                implode( ', ', $markers )
     266            );
     267        }
     268
     269        $hints_block = '';
     270        if ( ! empty( $hints ) ) {
     271            $hints_block = "\n\n" . __( 'Best-effort hints', 'pluxo-blueprint' ) . "\n" . implode( "\n", $hints );
     272        }
     273
     274        return $line1 . "\n" . $line2 . "\n\n" . $line3 . $hints_block;
    221275    }
    222276}
  • pluxo-blueprint/trunk/includes/Modules/Header_Footer/class-pluxo-blueprint-header-footer-scan.php

    r3453209 r3455410  
    2020     */
    2121    public function run() {
     22        $is_block_theme = $this->is_block_theme();
     23
    2224        $results = array(
    2325            'signals'  => array(
    24                 'is_block_theme'         => $this->is_block_theme(),
     26                'is_block_theme'         => $is_block_theme,
     27                'theme_type'             => $is_block_theme ? 'block' : 'classic',
     28                'theme_files_hint'       => $this->get_theme_files_hint(),
     29                'theme_markers'          => $this->find_theme_markers(),
     30
    2531                'elementor_available'    => $this->is_elementor_available(),
    2632                'elementor_headers'      => 0,
    2733                'elementor_footers'      => 0,
     34
    2835                'elementskit_available'  => $this->is_elementskit_available(),
    2936                'elementskit_headers'    => 0,
    3037                'elementskit_footers'    => 0,
     38
    3139                'active_hf_plugins'      => array(),
    3240                'mixed_system_detected'  => false,
     
    9199
    92100    /**
     101     * Best-effort: provide "possible theme files" hint (classic themes).
     102     *
     103     * @return string[] List like ['header.php', 'footer.php'] (only the ones that exist).
     104     */
     105    private function get_theme_files_hint() {
     106        $out = array();
     107
     108        $theme = wp_get_theme();
     109        if ( ! $theme || ! is_object( $theme ) ) {
     110            return $out;
     111        }
     112
     113        $dirs = array();
     114
     115        $child = $theme->get_stylesheet_directory();
     116        if ( is_string( $child ) && '' !== $child ) {
     117            $dirs[] = $child;
     118        }
     119
     120        $parent = $theme->get_template_directory();
     121        if ( is_string( $parent ) && '' !== $parent && $parent !== $child ) {
     122            $dirs[] = $parent;
     123        }
     124
     125        $files = array( 'header.php', 'footer.php' );
     126
     127        foreach ( $files as $file ) {
     128            foreach ( $dirs as $dir ) {
     129                $path = trailingslashit( $dir ) . $file;
     130                if ( file_exists( $path ) ) {
     131                    $out[] = $file;
     132                    break;
     133                }
     134            }
     135        }
     136
     137        $out = array_values( array_unique( $out ) );
     138
     139        return $out;
     140    }
     141
     142    /**
     143     * Best-effort: find a few "markers" in the active theme that suggest where header/footer is coming from.
     144     *
     145     * This is intentionally lightweight and returns at most 3 paths.
     146     *
     147     * @return string[] Short list of relative-ish paths (filenames) where we found markers.
     148     */
     149    private function find_theme_markers() {
     150        $theme = wp_get_theme();
     151        if ( ! $theme || ! is_object( $theme ) ) {
     152            return array();
     153        }
     154
     155        $base_dirs = array();
     156
     157        $child = $theme->get_stylesheet_directory();
     158        if ( is_string( $child ) && '' !== $child ) {
     159            $base_dirs[] = $child;
     160        }
     161
     162        $parent = $theme->get_template_directory();
     163        if ( is_string( $parent ) && '' !== $parent && $parent !== $child ) {
     164            $base_dirs[] = $parent;
     165        }
     166
     167        // Candidate files only (avoid exhaustive scans).
     168        $candidates = array(
     169            'header.php',
     170            'footer.php',
     171            'functions.php',
     172            'templates/index.html',
     173            'parts/header.html',
     174            'parts/footer.html',
     175        );
     176
     177        $needles = array(
     178            'wp_head(',
     179            'wp_footer(',
     180            'wp:template-part',
     181            'template-part',
     182        );
     183
     184        $found = array();
     185
     186        foreach ( $base_dirs as $dir ) {
     187            foreach ( $candidates as $rel ) {
     188                $path = trailingslashit( $dir ) . $rel;
     189
     190                if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
     191                    continue;
     192                }
     193
     194                $contents = file_get_contents( $path ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContents
     195                if ( false === $contents || '' === $contents ) {
     196                    continue;
     197                }
     198
     199                foreach ( $needles as $needle ) {
     200                    if ( false !== strpos( $contents, $needle ) ) {
     201                        $found[] = $rel;
     202                        break;
     203                    }
     204                }
     205
     206                if ( count( $found ) >= 3 ) {
     207                    break 2;
     208                }
     209            }
     210        }
     211
     212        $found = array_values( array_unique( $found ) );
     213
     214        return $found;
     215    }
     216
     217    /**
    93218     * Determine whether Elementor is available (active).
    94219     *
     
    147272                'update_post_term_cache' => false,
    148273
    149                 // Best-effort: Elementor stores template types in post meta. Query is limited to 1 result to minimise impact.
    150274                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
    151275                'meta_query'             => array(
     
    276400                        'update_post_term_cache' => false,
    277401
    278                         // Best-effort: ElementsKit may classify templates via taxonomy. Query is limited to 1 result to minimise impact.
    279402                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
    280403                        'tax_query'              => array(
     
    318441                'update_post_term_cache' => false,
    319442
    320                 // Best-effort fallback: ElementsKit may store template type in post meta. Query is limited to 1 result to minimise impact.
    321443                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
    322444                'meta_query'             => $meta_or,
  • pluxo-blueprint/trunk/includes/Modules/Plugins/class-pluxo-blueprint-plugins-renderer.php

    r3453209 r3455410  
    250250
    251251                if ( '' === $value ) {
    252                     return esc_html( $dash );
     252                    return $dash;
    253253                }
    254254
    255255                $plugin_uri = isset( $plugin['plugin_uri'] ) ? trim( (string) $plugin['plugin_uri'] ) : '';
    256256                if ( '' !== $plugin_uri && 0 === strpos( $plugin_uri, 'http' ) ) {
    257                     return $value . ' (' . $plugin_uri . ')';
     257                    return $this->escape_markdown_table_cell( $value . ' (' . $plugin_uri . ')' );
    258258                }
    259259
    260                 return $value;
     260                return $this->escape_markdown_table_cell( $value );
    261261
    262262            case 'version':
    263263                $value = isset( $plugin['version'] ) ? trim( (string) $plugin['version'] ) : '';
    264                 return ( '' !== $value ) ? $value : esc_html( $dash );
     264                return ( '' !== $value ) ? $this->escape_markdown_table_cell( $value ) : $dash;
    265265
    266266            case 'status':
    267267                $value = isset( $plugin['status'] ) ? trim( (string) $plugin['status'] ) : '';
    268                 return ( '' !== $value ) ? ucfirst( $value ) : esc_html( esc_html( $dash ) );
     268                return ( '' !== $value ) ? $this->escape_markdown_table_cell( ucfirst( $value ) ) : $dash;
    269269
    270270            case 'type':
    271271                $value = isset( $plugin['type'] ) ? trim( (string) $plugin['type'] ) : '';
    272                 return ( '' !== $value ) ? ucfirst( str_replace( '_', ' ', $value ) ) : esc_html( $dash );
     272                return ( '' !== $value ) ? $this->escape_markdown_table_cell( ucfirst( str_replace( '_', ' ', $value ) ) ) : $dash;
    273273
    274274            case 'update_available':
     
    277277            case 'update_status':
    278278                $value = isset( $plugin['update_status'] ) ? trim( (string) $plugin['update_status'] ) : '';
    279                 return ( '' !== $value ) ? $value : esc_html( $dash );
     279                return ( '' !== $value ) ? $this->escape_markdown_table_cell( $value ) : $dash;
    280280
    281281            default:
    282                 return esc_html( $dash );
     282                return $dash;
    283283        }
    284284    }
     
    437437        <?php
    438438    }
     439
     440    /**
     441     * Escape special characters for Markdown tables.
     442     *
     443     * In Markdown tables, the pipe character "|" separates columns.
     444     * Some parsers (e.g. Confluence) do not reliably honour "\|" inside tables,
     445     * so we replace pipes with the HTML entity instead.
     446     *
     447     * @param string $text Cell text.
     448     * @return string
     449     */
     450    private function escape_markdown_table_cell( $text ) {
     451        $text = (string) $text;
     452
     453        // Replace pipes with a safe representation that still renders as "|".
     454        $text = str_replace( '|', '&#124;', $text );
     455
     456        return $text;
     457    }
    439458}
  • pluxo-blueprint/trunk/includes/Modules/Site_Info/class-pluxo-blueprint-module-site-info.php

    r3453209 r3455410  
    137137        if ( is_array( $general_info ) ) {
    138138            foreach ( $general_info as $label => $value ) {
    139                 $rows[] = array( (string) $label, (string) $value );
     139                $rows[] = array( (string) $label, trim( wp_strip_all_tags( (string) $value ) ) );
    140140            }
    141141        }
  • pluxo-blueprint/trunk/includes/Modules/Site_Info/class-pluxo-blueprint-site-info-renderer.php

    r3453209 r3455410  
    4141                    $allowed = array(
    4242                        'strong' => array(),
     43                        'a'      => array(
     44                            'href' => array(),
     45                            'rel'  => array(),
     46                        ),
    4347                    );
    4448
  • pluxo-blueprint/trunk/includes/Modules/Site_Info/class-pluxo-blueprint-site-info-repository.php

    r3453209 r3455410  
    2323        $site_url  = home_url();
    2424        $wp_ver    = get_bloginfo( 'version' );
     25        $wp_update_version = $this->get_wordpress_core_update_version();
    2526        $php_ver   = PHP_VERSION;
    2627
     
    9394        }
    9495
     96        $wp_value = (string) $wp_ver;
     97
     98        if ( '' !== $wp_update_version && version_compare( $wp_update_version, $wp_ver, '>' ) ) {
     99            $update_url = admin_url( 'update-core.php' );
     100
     101            $wp_value .= ' ' . sprintf(
     102                /* translators: 1: update url, 2: WordPress version available for update */
     103                __( '(Update <a href="%1$s">version %2$s</a> available)', 'pluxo-blueprint' ),
     104                esc_url( $update_url ),
     105                esc_html( $wp_update_version )
     106            );
     107        }
     108
    95109        return array(
    96110            __( 'Site Name', 'pluxo-blueprint' )                 => (string) $site_name,
    97111            __( 'Site URL', 'pluxo-blueprint' )                  => (string) $site_url,
    98             __( 'WordPress Version', 'pluxo-blueprint' )         => (string) $wp_ver,
     112            __( 'WordPress Version', 'pluxo-blueprint' )         => (string) $wp_value,
    99113            __( 'PHP Version', 'pluxo-blueprint' ) => (string) $php_value,
    100114            __( 'Database Version', 'pluxo-blueprint' )          => ( '' !== $db_ver ) ? $db_ver : __( 'Not available', 'pluxo-blueprint' ),
     
    103117            __( 'Generated On', 'pluxo-blueprint' )              => (string) $generated,
    104118        );
     119    }
     120
     121    /**
     122     * Best-effort: get the available WordPress core update version (if any).
     123     *
     124     * @return string Empty string when no upgrade offer is found.
     125     */
     126    private function get_wordpress_core_update_version() {
     127        if ( ! function_exists( 'get_core_updates' ) ) {
     128            require_once ABSPATH . 'wp-admin/includes/update.php';
     129        }
     130
     131        if ( ! function_exists( 'get_core_updates' ) ) {
     132            return '';
     133        }
     134
     135        $core_updates = get_core_updates( array( 'dismissed' => false ) );
     136
     137        if ( ! is_array( $core_updates ) || empty( $core_updates ) ) {
     138            return '';
     139        }
     140
     141        foreach ( $core_updates as $update ) {
     142            if ( ! is_object( $update ) ) {
     143                continue;
     144            }
     145
     146            if ( empty( $update->response ) || 'upgrade' !== $update->response ) {
     147                continue;
     148            }
     149
     150            // In most WP installs, the target version is in $update->current.
     151            if ( ! empty( $update->current ) ) {
     152                return (string) $update->current;
     153            }
     154
     155            // Fallback: some offers may use ->version.
     156            if ( ! empty( $update->version ) ) {
     157                return (string) $update->version;
     158            }
     159
     160            break;
     161        }
     162
     163        return '';
    105164    }
    106165
  • pluxo-blueprint/trunk/includes/Modules/Themes/class-pluxo-blueprint-themes-repository.php

    r3453209 r3455410  
    159159            $summary_paragraphs[] = sprintf(
    160160                /* translators: %d: total number of installed themes. */
    161                 '<strong>' . __( 'This website has %d installed themes, which is more than recommended. Consider keeping only the active theme (and its parent theme, if applicable) to reduce maintenance overhead and potential security risks.', 'pluxo-blueprint' ) . '</strong> ',
     161                '<strong>' . __( 'This website has %d installed themes, which is more than recommended. Consider keeping only the active theme (and its parent theme, if applicable) to reduce maintenance overhead and potential security risks.', 'pluxo-blueprint' ) . '</strong>',
    162162                (int) $total_themes
    163163            );
  • pluxo-blueprint/trunk/includes/Modules/Tracking/Data/class-pluxo-blueprint-tracking-registry.php

    r3453209 r3455410  
    2222                'label' => __( 'Google Tag Manager', 'pluxo-blueprint' ),
    2323                'plugin_signatures' => array(
    24                     'duracelltomi-google-tag-manager/duracelltomi-google-tag-manager.php',
     24                    // GTM4WP (Google Tag Manager for WordPress by gtm4wp.com).
     25                    'duracelltomi-google-tag-manager/duracelltomi-google-tag-manager-for-wordpress.php',
     26
     27                    // Site Kit can inject GTM/GA depending on config.
    2528                    'google-site-kit/google-site-kit.php',
    2629                ),
    2730                'html_signatures' => array(
     31                    // Classic GTM script.
    2832                    '~googletagmanager\.com/gtm\.js~i',
     33
     34                    // GTM4WP fingerprints (covers your exact example).
     35                    '~gtm4wp_datalayer_name~i',
     36                    '~Google Tag Manager for WordPress by gtm4wp\.com~i',
     37                    '~gtm4wp\.com~i',
    2938                ),
    3039                'id_patterns' => array(
     
    3948                    'google-analytics-for-wordpress/googleanalytics.php',
    4049                    'monsterinsights/monsterinsights.php',
     50
     51                    // GA Google Analytics plugin.
     52                    'ga-google-analytics/ga-google-analytics.php',
    4153                ),
    4254                'html_signatures' => array(
    4355                    '~www\.googletagmanager\.com/gtag/js~i',
    4456                    '~google-analytics\.com/analytics\.js~i',
     57
     58                    // GA4 common inline patterns.
     59                    '~\bgTag\(|\bgtag\(~i',
    4560                ),
    4661                'id_patterns' => array(
  • pluxo-blueprint/trunk/includes/Modules/Tracking/Services/class-pluxo-blueprint-tracking-scanner.php

    r3453209 r3455410  
    4646        }
    4747
    48         if ( ! $force && is_array( $cached ) && ! empty( $cached['last_scan_gmt'] ) ) {
    49             $last = strtotime( (string) $cached['last_scan_gmt'] );
    50             if ( $last && ( time() - $last ) < $this->cache_ttl ) {
    51                 return $cached;
     48        if ( ! $force && is_array( $cached ) ) {
     49            $stamp = '';
     50
     51            if ( ! empty( $cached['metadata']['scanned_at_gmt'] ) ) {
     52                $stamp = (string) $cached['metadata']['scanned_at_gmt'];
     53            } elseif ( ! empty( $cached['last_scan_gmt'] ) ) {
     54                $stamp = (string) $cached['last_scan_gmt'];
     55            }
     56
     57            if ( '' !== $stamp ) {
     58                $last = strtotime( $stamp );
     59                if ( $last && ( time() - $last ) < $this->cache_ttl ) {
     60                    return $cached;
     61                }
    5262            }
    5363        }
     
    7080    private function run_scan() {
    7181        if ( ! function_exists( 'get_plugins' ) ) {
     82            require_once ABSPATH . 'wp-admin/includes/plugin.php';
     83        }
     84
     85        if ( ! function_exists( 'is_plugin_active' ) ) {
    7286            require_once ABSPATH . 'wp-admin/includes/plugin.php';
    7387        }
     
    208222     * @return array<int, array<string, string>>
    209223     */
    210     private function match_plugins( $signatures, $active ) {
     224    private function match_plugins( $signatures, $active_unused = array() ) {
    211225        $matches = array();
    212226
    213         foreach ( $signatures as $sig ) {
     227        if ( ! function_exists( 'is_plugin_active' ) ) {
     228            require_once ABSPATH . 'wp-admin/includes/plugin.php';
     229        }
     230
     231        foreach ( (array) $signatures as $sig ) {
    214232            $sig = (string) $sig;
    215 
    216             if ( in_array( $sig, $active, true ) ) {
     233            if ( '' === $sig ) {
     234                continue;
     235            }
     236
     237            if ( is_plugin_active( $sig ) ) {
    217238                $matches[] = array(
    218239                    'plugin_file' => $sig,
  • pluxo-blueprint/trunk/includes/Modules/Translators/class-pluxo-blueprint-translators-repository.php

    r3453209 r3455410  
    1919    public function get_module_data() {
    2020        $tools     = $this->detect_translation_tools();
    21         $languages = $this->get_site_languages();
     21        $languages = $this->get_site_languages( $tools );
    2222        $pill      = $this->get_pill_data( $tools, $languages );
    2323        $summary   = $this->get_summary_text( $tools, $languages );
     
    4343            'sitepress-multilingual-cms/sitepress.php', // WPML core.
    4444            'polylang/polylang.php',
     45            'polylang-pro/polylang.php',
    4546            'translatepress-multilingual/index.php',
    4647            'weglot/weglot.php',
     
    8182     * Get best-effort site languages.
    8283     *
     84     * - If Polylang is active and functions are available, returns the configured Polylang languages.
     85     * - Otherwise falls back to WordPress locale + available languages (installed/available translations).
     86     *
     87     * @param array<string, array<int, string>> $tools Tools detection.
    8388     * @return array<string, mixed>
    8489     */
    85     private function get_site_languages() {
     90    private function get_site_languages( $tools = array() ) {
     91        $labels  = array();
     92        $locales = array();
     93        $source  = 'wordpress';
     94
     95        $has_polylang = ! empty( $tools['multilingual'] ) && is_array( $tools['multilingual'] )
     96            && ( in_array( 'Polylang', $tools['multilingual'], true ) || in_array( 'Polylang Pro', $tools['multilingual'], true ) );
     97
     98        // Prefer Polylang configured languages if available.
     99        if ( $has_polylang && function_exists( 'pll_languages_list' ) ) {
     100            // Best-effort: get locale codes (e.g. en_GB, pt_PT).
     101            $pll_locales = pll_languages_list(
     102                array(
     103                    'fields' => 'locale',
     104                )
     105            );
     106
     107            if ( is_array( $pll_locales ) ) {
     108                foreach ( $pll_locales as $loc ) {
     109                    $loc = (string) $loc;
     110                    if ( '' !== $loc ) {
     111                        $locales[] = $loc;
     112                    }
     113                }
     114            }
     115
     116            $locales = array_values( array_unique( $locales ) );
     117
     118            foreach ( $locales as $locale ) {
     119                $labels[] = $this->get_human_locale_label( $locale );
     120            }
     121
     122            $labels = array_values( array_unique( array_filter( array_map( 'strval', $labels ) ) ) );
     123            sort( $labels );
     124
     125            if ( ! empty( $labels ) ) {
     126                $source = 'polylang';
     127                return array(
     128                    'locales' => $locales,
     129                    'labels'  => $labels,
     130                    'source'  => $source,
     131                );
     132            }
     133        }
     134
     135        // Fallback: WordPress locale + available languages (installed/available translations).
    86136        $current_locale = (string) get_locale();
    87137        $available      = get_available_languages();
    88 
    89         $locales = array();
    90138
    91139        if ( '' !== $current_locale ) {
     
    102150        $locales = array_values( array_unique( $locales ) );
    103151
    104         $labels = array();
    105152        foreach ( $locales as $locale ) {
    106153            $labels[] = $this->get_human_locale_label( $locale );
    107154        }
    108155
     156        $labels = array_values( array_unique( array_filter( array_map( 'strval', $labels ) ) ) );
    109157        sort( $labels );
    110158
     
    112160            'locales' => $locales,
    113161            'labels'  => $labels,
     162            'source'  => $source,
    114163        );
    115164    }
     
    133182        }
    134183
    135         $names = array_values( array_unique( $names ) );
     184        $names = array_values( array_unique( array_filter( array_map( 'strval', $names ) ) ) );
    136185
    137186        $lang_count = ! empty( $languages['locales'] ) ? count( (array) $languages['locales'] ) : 0;
     
    141190            : __( 'No translators', 'pluxo-blueprint' );
    142191
     192        $source = ! empty( $languages['source'] ) ? (string) $languages['source'] : 'wordpress';
     193        $suffix = ( 'polylang' === $source )
     194            ? __( 'languages', 'pluxo-blueprint' )
     195            : __( 'locales', 'pluxo-blueprint' );
     196
    143197        $label = sprintf(
    144             /* translators: 1: plugin names, 2: number of languages. */
    145             __( '%1$s / %2$d languages', 'pluxo-blueprint' ),
     198            /* translators: 1: plugin names, 2: number of languages/locales, 3: suffix label */
     199            __( '%1$s / %2$d %3$s', 'pluxo-blueprint' ),
    146200            $plugin_label,
    147             (int) $lang_count
    148         );
    149 
    150         $class = 'pluxo-blueprint-pill--neutral';
    151 
    152         if ( ! empty( $tools['multilingual'] ) ) {
    153             $class = 'pluxo-blueprint-pill--neutral';
    154         } elseif ( ! empty( $tools['string_tools'] ) ) {
    155             $class = 'pluxo-blueprint-pill--neutral';
    156         }
     201            (int) $lang_count,
     202            $suffix
     203        );
    157204
    158205        return array(
    159             'class' => $class,
     206            'class' => 'pluxo-blueprint-pill--neutral',
    160207            'label' => $label,
    161208        );
     
    172219        $has_multilingual = ! empty( $tools['multilingual'] );
    173220        $has_string_tools = ! empty( $tools['string_tools'] );
     221        $source = ! empty( $languages['source'] ) ? (string) $languages['source'] : 'wordpress';
    174222
    175223        $lang_count = ! empty( $languages['labels'] ) ? count( (array) $languages['labels'] ) : 0;
     
    179227            $lang_list = implode( ', ', array_map( 'sanitize_text_field', (array) $languages['labels'] ) );
    180228        }
     229
     230        $language_label = ( 'polylang' === $source )
     231            ? __( 'language(s)', 'pluxo-blueprint' )
     232            : __( 'language pack(s) installed/available in WordPress', 'pluxo-blueprint' );
    181233
    182234        if ( $has_multilingual ) {
     
    189241            if ( $lang_count > 0 ) {
    190242                $line2 = sprintf(
    191                     /* translators: 1: number of languages, 2: languages list. */
    192                     __( 'This site currently has %1$d language(s) available: %2$s.', 'pluxo-blueprint' ),
     243                    /* translators: 1: number of languages, 2: languages list, 3: label */
     244                    __( 'This site currently has %1$d %3$s: %2$s.', 'pluxo-blueprint' ),
    193245                    (int) $lang_count,
    194                     $lang_list
     246                    $lang_list,
     247                    $language_label
    195248                );
    196249
     
    210263            if ( $lang_count > 0 ) {
    211264                $line2 = sprintf(
    212                     /* translators: 1: number of languages, 2: languages list. */
    213                     __( 'This site currently has %1$d language(s) available: %2$s.', 'pluxo-blueprint' ),
     265                    /* translators: 1: number of languages, 2: languages list, 3: label */
     266                    __( 'This site currently has %1$d %3$s: %2$s.', 'pluxo-blueprint' ),
    214267                    (int) $lang_count,
    215                     $lang_list
     268                    $lang_list,
     269                    $language_label
    216270                );
    217271
     
    222276        }
    223277
    224         if ( $lang_count > 0 ) {
     278        if ( $lang_count > 1 ) {
    225279            return sprintf(
    226                 /* translators: 1: number of languages, 2: languages list. */
    227                 esc_html__( 'No translation plugins were detected. This site currently has %1$d language(s) available: %2$s.', 'pluxo-blueprint' ),
     280                /* translators: 1: number, 2: list, 3: label */
     281                __( 'No supported translation plugins were detected. However, this site appears to have %1$d %3$s: %2$s. This may indicate an unsupported multilingual plugin, a custom setup, or leftover language packs.', 'pluxo-blueprint' ),
    228282                (int) $lang_count,
    229                 $lang_list
     283                $lang_list,
     284                $language_label
    230285            );
    231286        }
     
    248303            'gtranslate/gtranslate.php'                => 'GTranslate',
    249304            'loco-translate/loco.php'                  => 'Loco Translate',
     305            'polylang-pro/polylang.php'                => 'Polylang Pro',
    250306        );
    251307
  • pluxo-blueprint/trunk/includes/Modules/Users/class-pluxo-blueprint-module-users.php

    r3453209 r3455410  
    8080                    foreach ( $lines as $line ) :
    8181                        ?>
    82                         <p class="pluxo-blueprint-muted">
    83                             <?php echo esc_html( $line ); ?>
    84                         </p>
     82                    <p class="pluxo-blueprint-muted">
     83                        <?php
     84                        echo wp_kses(
     85                            $line,
     86                            array(
     87                                'strong' => array(),
     88                            )
     89                        );
     90                        ?>
     91                    </p>
    8592                    <?php endforeach; ?>
    8693                <?php endif; ?>
     
    140147        );
    141148
     149        $has_high_admin_ratio = false;
     150
     151        if ( $total_users >= 4 && $admin_count > 0 ) {
     152            $admin_ratio = $admin_count / $total_users;
     153            $has_high_admin_ratio = ( $admin_ratio > 0.5 );
     154        }
     155
    142156        if ( 0 === $total_users ) {
    143157            $pill_class = 'pluxo-blueprint-pill--warn';
    144158            $pill_label = __( 'No users', 'pluxo-blueprint' );
    145         } elseif ( $admin_count > 0 ) {
     159        } elseif ( $has_high_admin_ratio ) {
     160            $pill_class = 'pluxo-blueprint-pill--warning';
     161
     162            $pill_label = sprintf(
     163                /* translators: 1: total users, 2: admins count */
     164                __( '%1$d users / %2$d admins (high admin ratio)', 'pluxo-blueprint' ),
     165                (int) $total_users,
     166                (int) $admin_count
     167            );
     168        } else {
    146169            $pill_class = 'pluxo-blueprint-pill--neutral';
    147170        }
     
    158181                (int) $admin_count
    159182            );
     183
     184            if ( $has_high_admin_ratio ) {
     185                $summary_lines[] = '<strong>' . __( 'A high proportion of users have administrator access. Consider limiting admin accounts to reduce security risk and accidental changes.', 'pluxo-blueprint' ) . '</strong>';
     186            }
    160187
    161188            if ( ! empty( $role_parts ) ) {
  • pluxo-blueprint/trunk/includes/PDF/class-pluxo-blueprint-pdf-generator.php

    r3453209 r3455410  
    6363        }
    6464
     65        if ( ! defined( 'PLUXO_BLUEPRINT_PRINT_VIEW' ) ) {
     66            define( 'PLUXO_BLUEPRINT_PRINT_VIEW', true );
     67        }
     68
    6569        require $template;
    6670        exit;
  • pluxo-blueprint/trunk/includes/PDF/templates/overview-print.php

    r3453209 r3455410  
    2020    ?>
    2121    <style>
    22         /* Basic print-friendly styling */
    23         body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; margin: 24px; color: #111; }
    24         h1 { margin: 0 0 8px; font-size: 22px; }
    25         h2 { font-size: 16px; margin-top: 18px; }
    26         .pluxo-blueprint-muted { color: #555; margin: 0 0 16px; }
    27         .pluxo-blueprint-export-actions { margin: 16px 0 22px; }
    28         .pluxo-blueprint-export-actions button { padding: 10px 14px; cursor: pointer; }
     22    /* Basic print-friendly styling */
     23    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; margin: 24px; color: #111; }
     24    h1 { margin: 0 0 8px; font-size: 22px; }
     25    .pluxo-blueprint-muted { color: #555; margin: 0 0 16px; }
    2926
    30         /* Make accordions printable: open all and remove interactive hints */
    31         details.pluxo-blueprint-accordion { border: 1px solid #ddd; border-radius: 10px; padding: 10px 12px; margin: 10px 0; }
    32         summary { list-style: none; cursor: default; }
    33         summary::-webkit-details-marker { display: none; }
     27    details.pluxo-blueprint-accordion {
     28        border: 1px solid #ddd;
     29        border-radius: 12px;
     30        padding: 14px 16px;
     31        margin: 26px 0; /* 🔥 mais separação entre módulos */
     32        background: #fafafa; /* leve fundo para diferenciar */
     33    }
    3434
    35         /* Hide buttons / scan actions in export */
    36         .pluxo-blueprint-button,
    37         .pluxo-blueprint-scan-actions,
    38         button,
    39         a.button,
    40         a.page-title-action { display: none !important; }
     35    /* --- Module header (summary) typography/layout for print --- */
     36    details.pluxo-blueprint-accordion > summary {
     37        display: flex;
     38        align-items: center;
     39        justify-content: space-between;
     40        gap: 12px;
    4141
    42         /* Tables */
    43         table { width: 100%; border-collapse: collapse; margin-top: 10px; }
    44         th, td { border: 1px solid #ddd; padding: 8px; vertical-align: top; text-align: left; }
    45         th { background: #f5f5f5; }
     42        margin: -14px -16px 14px; /* puxa até às bordas do card */
     43        padding: 14px 16px;
    4644
    47         @media print {
    48             body { margin: 0; }
    49             .pluxo-blueprint-export-actions { display: none !important; }
    50             details { break-inside: avoid; }
    51         }
     45        background: #f0f0f0;
     46        border-bottom: 1px solid #ddd;
     47        border-radius: 12px 12px 0 0;
     48
     49        font-size: 18px;
     50        font-weight: 700;
     51        line-height: 1.25;
     52    }
     53
     54    .pluxo-blueprint-accordion-body {
     55        padding-top: 6px;
     56    }
     57
     58    table {
     59        margin-top: 14px;
     60        margin-bottom: 6px;
     61    }
     62
     63    /* Hide any "rescan" links in print, even if they aren't styled as buttons */
     64    a[href*="pluxo_rescan"] { display: none !important; }
     65
     66    /* If you use a title span in some modules, keep it aligned */
     67    details.pluxo-blueprint-accordion > summary .pluxo-blueprint-accordion-title {
     68        font-size: 18px;
     69        font-weight: 700;
     70    }
     71
     72    /* Ensure the pill doesn't stick to the title */
     73    details.pluxo-blueprint-accordion > summary .pluxo-blueprint-summary-actions {
     74        display: inline-flex;
     75        align-items: center;
     76        gap: 10px;
     77        margin-left: auto;
     78    }
     79
     80    /* Make pills slightly smaller in print */
     81    details.pluxo-blueprint-accordion > summary .pluxo-blueprint-pill {
     82        font-size: 12px;
     83        padding: 4px 10px;
     84        border-radius: 999px;
     85        white-space: nowrap;
     86    }
     87
     88    .pluxo-blueprint-summary-actions {
     89        display: inline-flex;
     90        align-items: center;
     91        gap: 10px;
     92        white-space: nowrap;
     93    }
     94
     95    .pluxo-blueprint-pill {
     96        display: inline-block;
     97        padding: 2px 10px;
     98        border: 1px solid #ddd;
     99        border-radius: 999px;
     100        font-size: 12px;
     101        line-height: 18px;
     102    }
     103
     104    /* Hide buttons / scan actions / interactive controls in export */
     105    .pluxo-blueprint-button,
     106    .pluxo-blueprint-scan-actions,
     107    .pluxo-blueprint-copy-md-btn,
     108    textarea,
     109    button,
     110    a.button,
     111    a.page-title-action,
     112    form,
     113    input,
     114    select,
     115    label { display: none !important; }
     116
     117    /* Tables */
     118    table { width: 100%; border-collapse: collapse; margin-top: 10px; }
     119    th, td { border: 1px solid #ddd; padding: 8px; vertical-align: top; text-align: left; }
     120    th { background: #f5f5f5; }
     121
     122    @media print {
     123        body { margin: 0; }
     124        .pluxo-blueprint-export-actions { display: none !important; }
     125        details { break-inside: avoid; }
     126    }
    52127    </style>
    53128</head>
  • pluxo-blueprint/trunk/pluxo-blueprint.php

    r3453209 r3455410  
    44 * Plugin URI:        https://pluxo.dev/#pluxo-blueprint
    55 * Description:       Generate a structured PDF snapshot of your WordPress site documentation, with a modular overview and easy Markdown copy for reuse.
    6  * Version:           1.2.0
     6 * Version:           1.2.1
    77 * Requires at least: 6.0
    88 * Requires PHP:      7.4
     
    6363
    6464if ( ! defined( 'PLUXO_BLUEPRINT_VERSION' ) ) {
    65     define( 'PLUXO_BLUEPRINT_VERSION', '1.2.0' );
     65    define( 'PLUXO_BLUEPRINT_VERSION', '1.2.1' );
    6666}
    6767
  • pluxo-blueprint/trunk/readme.txt

    r3453209 r3455410  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.2.0
     7Stable tag: 1.2.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    5858
    5959== Changelog ==
     60= 1.2.1 =
     61* Fixes in some of the modules in the overview
     62
    6063= 1.2.0 =
    6164* Add: New Overview dashboard organised into multiple documentation modules
Note: See TracChangeset for help on using the changeset viewer.