Plugin Directory

Changeset 3464901


Ignore:
Timestamp:
02/19/2026 09:22:22 AM (6 weeks ago)
Author:
osomstudio
Message:

Release 1.2.1 — CPT/taxonomy support, cascading UI, status sync, CPT registry, early URL matching

Location:
osom-multi-theme-switcher/trunk
Files:
1 added
8 edited

Legend:

Unmodified
Added
Removed
  • osom-multi-theme-switcher/trunk/README.md

    r3457914 r3464901  
    88  - Individual Pages
    99  - Individual Posts
    10   - Post Types (e.g., all Products)
     10  - Custom Post Types (all items or individual items)
    1111  - Custom URLs/Slugs
    1212  - Categories
    1313  - Tags
     14  - Custom Taxonomies
    1415
    1516- **Admin Dashboard Theme Switcher**: Switch between themes in the WordPress admin area from the top admin bar to access theme-specific settings
     
    1819- **Real-time Updates**: Add and remove rules instantly with AJAX
    1920- **Per-User Admin Theme**: Each admin user can view the dashboard with their preferred theme
     21- **Status Sync**: Automatically updates rules when post status changes (draft → published, etc.)
     22- **CPT Registry**: Remembers custom post types across themes so URL matching works even when a different theme is active
    2023- **Compatible**: Works with any WordPress theme
    2124
     
    5760- **Page**: Apply a theme to a specific page
    5861- **Post**: Apply a theme to a specific blog post
    59 - **Post Type**: Apply a theme to all posts of a certain type (e.g., all WooCommerce products)
     62- **Custom Post Type**: Apply a theme to all posts of a certain type or individual items (e.g., all WooCommerce products)
    6063- **Custom URL/Slug**: Apply a theme to a custom URL or slug (e.g., `/special-landing` or `about-us`)
    6164- **Category**: Apply a theme to all posts in a category or the category archive page
    6265- **Tag**: Apply a theme to all posts with a tag or the tag archive page
     66- **Taxonomy**: Apply a theme to terms of custom taxonomies
    6367
    6468### Deleting Rules
     
    8892- `template` filter - Changes the parent theme
    8993- `stylesheet` filter - Changes the child theme/stylesheet
     94- `setup_theme` hook - Ensures correct theme's functions.php is loaded
    9095
    9196Rules are stored in the WordPress options table and checked on every page load.
     
    106111## Changelog
    107112
     113### 1.2.1
     114- Added custom post type support (all items or individual items)
     115- Added custom taxonomy support
     116- Added category and tag rule types
     117- Added CPT registry for cross-theme URL matching
     118- Added status sync - rules auto-update when post status changes
     119- Added early matching for all rule types (before WP_Query)
     120- Added cascading selectors in admin UI
     121
    108122### 1.0.0
    109123- Initial release
  • osom-multi-theme-switcher/trunk/includes/class-omts-admin-bar.php

    r3461829 r3464901  
    130130            plugin_dir_url( dirname( __FILE__ ) ) . 'assets/admin-bar-script.js',
    131131            array( 'jquery' ),
    132             '1.0.3',
     132            '1.2.1',
    133133            true
    134134        );
  • osom-multi-theme-switcher/trunk/includes/class-omts-admin-page.php

    r3457914 r3464901  
    7373            plugin_dir_url( dirname( __FILE__ ) ) . 'assets/admin-style.css',
    7474            array(),
    75             '1.0.0'
     75            '1.2.1'
    7676        );
    7777
     
    8080            plugin_dir_url( dirname( __FILE__ ) ) . 'assets/admin-script.js',
    8181            array( 'jquery' ),
    82             '1.0.0',
     82            '1.2.1',
    8383            true
    8484        );
     
    100100     */
    101101    public function render_admin_page() {
    102         $rules          = $this->theme_switcher->get_rules();
    103         $rest_prefixes  = $this->theme_switcher->get_theme_rest_prefixes();
    104         $themes         = wp_get_themes();
    105         $current_theme  = wp_get_theme()->get_stylesheet();
     102        $rules         = $this->theme_switcher->get_rules();
     103        $rest_prefixes = $this->theme_switcher->get_theme_rest_prefixes();
     104        $themes        = wp_get_themes();
    106105        ?>
    107106        <div class="wrap omts-admin-wrap">
     
    128127                                <td>
    129128                                    <select id="omts-rule-type" name="rule_type">
     129                                        <option value=""><?php esc_html_e( '-- Select Rule Type --', 'osom-multi-theme-switcher' ); ?></option>
    130130                                        <option value="page"><?php esc_html_e( 'Page', 'osom-multi-theme-switcher' ); ?></option>
    131131                                        <option value="post"><?php esc_html_e( 'Post', 'osom-multi-theme-switcher' ); ?></option>
    132                                         <option value="post_type"><?php esc_html_e( 'Post Type', 'osom-multi-theme-switcher' ); ?></option>
    133                                         <option value="draft_page"><?php esc_html_e( 'Draft Page', 'osom-multi-theme-switcher' ); ?></option>
    134                                         <option value="draft_post"><?php esc_html_e( 'Draft Post', 'osom-multi-theme-switcher' ); ?></option>
    135                                         <option value="pending_page"><?php esc_html_e( 'Pending Page', 'osom-multi-theme-switcher' ); ?></option>
    136                                         <option value="pending_post"><?php esc_html_e( 'Pending Post', 'osom-multi-theme-switcher' ); ?></option>
    137                                         <option value="private_page"><?php esc_html_e( 'Private Page', 'osom-multi-theme-switcher' ); ?></option>
    138                                         <option value="private_post"><?php esc_html_e( 'Private Post', 'osom-multi-theme-switcher' ); ?></option>
    139                                         <option value="future_page"><?php esc_html_e( 'Scheduled Page', 'osom-multi-theme-switcher' ); ?></option>
    140                                         <option value="future_post"><?php esc_html_e( 'Scheduled Post', 'osom-multi-theme-switcher' ); ?></option>
     132                                        <option value="custom_post_type"><?php esc_html_e( 'Custom Post Type', 'osom-multi-theme-switcher' ); ?></option>
     133                                        <option value="taxonomy"><?php esc_html_e( 'Taxonomy', 'osom-multi-theme-switcher' ); ?></option>
    141134                                        <option value="url"><?php esc_html_e( 'Custom URL/Slug', 'osom-multi-theme-switcher' ); ?></option>
    142                                         <option value="category"><?php esc_html_e( 'Category', 'osom-multi-theme-switcher' ); ?></option>
    143                                         <option value="tag"><?php esc_html_e( 'Tag', 'osom-multi-theme-switcher' ); ?></option>
    144135                                    </select>
    145136                                </td>
    146137                            </tr>
    147                             <?php $this->render_page_row(); ?>
    148                             <?php $this->render_post_row(); ?>
    149                             <?php $this->render_post_type_row(); ?>
    150                             <?php $this->render_draft_page_row(); ?>
    151                             <?php $this->render_draft_post_row(); ?>
    152                             <?php $this->render_pending_page_row(); ?>
    153                             <?php $this->render_pending_post_row(); ?>
    154                             <?php $this->render_private_page_row(); ?>
    155                             <?php $this->render_private_post_row(); ?>
    156                             <?php $this->render_future_page_row(); ?>
    157                             <?php $this->render_future_post_row(); ?>
    158                             <?php $this->render_url_row(); ?>
    159                             <?php $this->render_category_row(); ?>
    160                             <?php $this->render_tag_row(); ?>
     138                            <tr id="omts-rule-object-row" class="omts-rule-row" style="display:none;">
     139                                <th scope="row">
     140                                    <label for="omts-rule-object"><?php esc_html_e( 'Rule Object', 'osom-multi-theme-switcher' ); ?></label>
     141                                </th>
     142                                <td>
     143                                    <select id="omts-rule-object" name="object_type">
     144                                        <option value=""><?php esc_html_e( '-- Select --', 'osom-multi-theme-switcher' ); ?></option>
     145                                    </select>
     146                                    <span class="spinner" id="omts-object-spinner"></span>
     147                                </td>
     148                            </tr>
     149                            <tr id="omts-rule-item-row" class="omts-rule-row" style="display:none;">
     150                                <th scope="row">
     151                                    <label for="omts-rule-item"><?php esc_html_e( 'Rule Item', 'osom-multi-theme-switcher' ); ?></label>
     152                                </th>
     153                                <td>
     154                                    <select id="omts-rule-item" name="item_id">
     155                                        <option value=""><?php esc_html_e( '-- Select --', 'osom-multi-theme-switcher' ); ?></option>
     156                                    </select>
     157                                    <span class="spinner" id="omts-item-spinner"></span>
     158                                </td>
     159                            </tr>
     160                            <tr id="omts-url-row" class="omts-rule-row" style="display:none;">
     161                                <th scope="row">
     162                                    <label for="omts-url-input"><?php esc_html_e( 'Custom URL/Slug', 'osom-multi-theme-switcher' ); ?></label>
     163                                </th>
     164                                <td>
     165                                    <input type="text" id="omts-url-input" name="custom_url" class="regular-text" placeholder="<?php esc_attr_e( 'e.g., /about-us or about-us', 'osom-multi-theme-switcher' ); ?>">
     166                                    <p class="description">
     167                                        <?php esc_html_e( 'Enter a URL path or slug (with or without leading slash)', 'osom-multi-theme-switcher' ); ?>
     168                                    </p>
     169                                </td>
     170                            </tr>
    161171                            <tr>
    162172                                <th scope="row">
    163                                     <label for="omts-theme-select"><?php esc_html_e( 'Alternative Theme', 'osom-multi-theme-switcher' ); ?></label>
     173                                    <label for="omts-theme-select"><?php esc_html_e( 'Theme', 'osom-multi-theme-switcher' ); ?></label>
    164174                                </th>
    165175                                <td>
     
    204214                                <?php foreach ( $rules as $index => $rule ) : ?>
    205215                                    <tr data-index="<?php echo esc_attr( $index ); ?>">
    206                                         <td><?php echo esc_html( ucfirst( str_replace( '_', ' ', $rule['type'] ) ) ); ?></td>
     216                                        <td><?php echo esc_html( $this->get_rule_type_display( $rule['type'] ) ); ?></td>
    207217                                        <td><?php echo esc_html( $this->get_rule_target_display( $rule ) ); ?></td>
    208218                                        <td><?php echo esc_html( $this->theme_switcher->get_theme_name( $rule['theme'] ) ); ?></td>
     
    317327
    318328    /**
    319      * Render page selection row.
    320      *
    321      * @since 1.0.0
    322      */
    323     private function render_page_row() {
    324         ?>
    325         <tr id="omts-page-row" class="omts-rule-row">
    326             <th scope="row">
    327                 <label for="omts-page-select"><?php esc_html_e( 'Select Page', 'osom-multi-theme-switcher' ); ?></label>
    328             </th>
    329             <td>
    330                 <select id="omts-page-select" name="page_id">
    331                     <option value=""><?php esc_html_e( '-- Select Page --', 'osom-multi-theme-switcher' ); ?></option>
    332                     <?php
    333                     $pages = get_pages();
    334                     foreach ( $pages as $page ) {
    335                         printf(
    336                             '<option value="%d">%s</option>',
    337                             esc_attr( $page->ID ),
    338                             esc_html( $page->post_title )
    339                         );
    340                     }
    341                     ?>
    342                 </select>
    343             </td>
    344         </tr>
    345         <?php
    346     }
    347 
    348     /**
    349      * Render post selection row.
    350      *
    351      * @since 1.0.0
    352      */
    353     private function render_post_row() {
    354         ?>
    355         <tr id="omts-post-row" class="omts-rule-row" style="display:none;">
    356             <th scope="row">
    357                 <label for="omts-post-select"><?php esc_html_e( 'Select Post', 'osom-multi-theme-switcher' ); ?></label>
    358             </th>
    359             <td>
    360                 <select id="omts-post-select" name="post_id">
    361                     <option value=""><?php esc_html_e( '-- Select Post --', 'osom-multi-theme-switcher' ); ?></option>
    362                     <?php
    363                     $posts = get_posts( array( 'numberposts' => -1 ) );
    364                     foreach ( $posts as $post ) {
    365                         printf(
    366                             '<option value="%d">%s</option>',
    367                             esc_attr( $post->ID ),
    368                             esc_html( $post->post_title )
    369                         );
    370                     }
    371                     ?>
    372                 </select>
    373             </td>
    374         </tr>
    375         <?php
    376     }
    377 
    378     /**
    379      * Render post type selection row.
    380      *
    381      * @since 1.0.0
    382      */
    383     private function render_post_type_row() {
    384         ?>
    385         <tr id="omts-post-type-row" class="omts-rule-row" style="display:none;">
    386             <th scope="row">
    387                 <label for="omts-post-type-select"><?php esc_html_e( 'Select Post Type', 'osom-multi-theme-switcher' ); ?></label>
    388             </th>
    389             <td>
    390                 <select id="omts-post-type-select" name="post_type">
    391                     <option value=""><?php esc_html_e( '-- Select Post Type --', 'osom-multi-theme-switcher' ); ?></option>
    392                     <?php
    393                     $post_types = get_post_types( array( 'public' => true ), 'objects' );
    394                     foreach ( $post_types as $post_type ) {
    395                         printf(
    396                             '<option value="%s">%s</option>',
    397                             esc_attr( $post_type->name ),
    398                             esc_html( $post_type->label )
    399                         );
    400                     }
    401                     ?>
    402                 </select>
    403             </td>
    404         </tr>
    405         <?php
    406     }
    407 
    408     /**
    409      * Render URL input row.
    410      *
    411      * @since 1.0.0
    412      */
    413     private function render_url_row() {
    414         ?>
    415         <tr id="omts-url-row" class="omts-rule-row" style="display:none;">
    416             <th scope="row">
    417                 <label for="omts-url-input"><?php esc_html_e( 'Custom URL/Slug', 'osom-multi-theme-switcher' ); ?></label>
    418             </th>
    419             <td>
    420                 <input type="text" id="omts-url-input" name="custom_url" class="regular-text" placeholder="<?php esc_attr_e( 'e.g., /about-us or about-us', 'osom-multi-theme-switcher' ); ?>">
    421                 <p class="description">
    422                     <?php esc_html_e( 'Enter a URL path or slug (with or without leading slash)', 'osom-multi-theme-switcher' ); ?>
    423                 </p>
    424             </td>
    425         </tr>
    426         <?php
    427     }
    428 
    429     /**
    430      * Render category selection row.
    431      *
    432      * @since 1.0.0
    433      */
    434     private function render_category_row() {
    435         ?>
    436         <tr id="omts-category-row" class="omts-rule-row" style="display:none;">
    437             <th scope="row">
    438                 <label for="omts-category-select"><?php esc_html_e( 'Select Category', 'osom-multi-theme-switcher' ); ?></label>
    439             </th>
    440             <td>
    441                 <select id="omts-category-select" name="category_id">
    442                     <option value=""><?php esc_html_e( '-- Select Category --', 'osom-multi-theme-switcher' ); ?></option>
    443                     <?php
    444                     $categories = get_categories( array( 'hide_empty' => false ) );
    445                     foreach ( $categories as $category ) {
    446                         printf(
    447                             '<option value="%d">%s</option>',
    448                             esc_attr( $category->term_id ),
    449                             esc_html( $category->name )
    450                         );
    451                     }
    452                     ?>
    453                 </select>
    454             </td>
    455         </tr>
    456         <?php
    457     }
    458 
    459     /**
    460      * Render tag selection row.
    461      *
    462      * @since 1.0.0
    463      */
    464     private function render_tag_row() {
    465         ?>
    466         <tr id="omts-tag-row" class="omts-rule-row" style="display:none;">
    467             <th scope="row">
    468                 <label for="omts-tag-select"><?php esc_html_e( 'Select Tag', 'osom-multi-theme-switcher' ); ?></label>
    469             </th>
    470             <td>
    471                 <select id="omts-tag-select" name="tag_id">
    472                     <option value=""><?php esc_html_e( '-- Select Tag --', 'osom-multi-theme-switcher' ); ?></option>
    473                     <?php
    474                     $tags = get_tags( array( 'hide_empty' => false ) );
    475                     foreach ( $tags as $tag ) {
    476                         printf(
    477                             '<option value="%d">%s</option>',
    478                             esc_attr( $tag->term_id ),
    479                             esc_html( $tag->name )
    480                         );
    481                     }
    482                     ?>
    483                 </select>
    484             </td>
    485         </tr>
    486         <?php
    487     }
    488 
    489     /**
    490      * Render draft page selection row.
    491      *
    492      * @since 1.0.2
    493      */
    494     private function render_draft_page_row() {
    495         ?>
    496         <tr id="omts-draft-page-row" class="omts-rule-row" style="display:none;">
    497             <th scope="row">
    498                 <label for="omts-draft-page-select"><?php esc_html_e( 'Select Draft Page', 'osom-multi-theme-switcher' ); ?></label>
    499             </th>
    500             <td>
    501                 <select id="omts-draft-page-select" name="page_id">
    502                     <option value=""><?php esc_html_e( '-- Select Draft Page --', 'osom-multi-theme-switcher' ); ?></option>
    503                     <?php
    504                     $pages = get_pages( array( 'post_status' => 'draft' ) );
    505                     foreach ( $pages as $page ) {
    506                         printf(
    507                             '<option value="%d">%s (Draft)</option>',
    508                             esc_attr( $page->ID ),
    509                             esc_html( $page->post_title )
    510                         );
    511                     }
    512                     ?>
    513                 </select>
    514             </td>
    515         </tr>
    516         <?php
    517     }
    518 
    519     /**
    520      * Render draft post selection row.
    521      *
    522      * @since 1.0.2
    523      */
    524     private function render_draft_post_row() {
    525         ?>
    526         <tr id="omts-draft-post-row" class="omts-rule-row" style="display:none;">
    527             <th scope="row">
    528                 <label for="omts-draft-post-select"><?php esc_html_e( 'Select Draft Post', 'osom-multi-theme-switcher' ); ?></label>
    529             </th>
    530             <td>
    531                 <select id="omts-draft-post-select" name="post_id">
    532                     <option value=""><?php esc_html_e( '-- Select Draft Post --', 'osom-multi-theme-switcher' ); ?></option>
    533                     <?php
    534                     $posts = get_posts( array( 'numberposts' => -1, 'post_status' => 'draft' ) );
    535                     foreach ( $posts as $post ) {
    536                         printf(
    537                             '<option value="%d">%s (Draft)</option>',
    538                             esc_attr( $post->ID ),
    539                             esc_html( $post->post_title )
    540                         );
    541                     }
    542                     ?>
    543                 </select>
    544             </td>
    545         </tr>
    546         <?php
    547     }
    548 
    549     /**
    550      * Render pending page selection row.
    551      *
    552      * @since 1.0.2
    553      */
    554     private function render_pending_page_row() {
    555         ?>
    556         <tr id="omts-pending-page-row" class="omts-rule-row" style="display:none;">
    557             <th scope="row">
    558                 <label for="omts-pending-page-select"><?php esc_html_e( 'Select Pending Page', 'osom-multi-theme-switcher' ); ?></label>
    559             </th>
    560             <td>
    561                 <select id="omts-pending-page-select" name="page_id">
    562                     <option value=""><?php esc_html_e( '-- Select Pending Page --', 'osom-multi-theme-switcher' ); ?></option>
    563                     <?php
    564                     $pages = get_pages( array( 'post_status' => 'pending' ) );
    565                     foreach ( $pages as $page ) {
    566                         printf(
    567                             '<option value="%d">%s (Pending)</option>',
    568                             esc_attr( $page->ID ),
    569                             esc_html( $page->post_title )
    570                         );
    571                     }
    572                     ?>
    573                 </select>
    574             </td>
    575         </tr>
    576         <?php
    577     }
    578 
    579     /**
    580      * Render pending post selection row.
    581      *
    582      * @since 1.0.2
    583      */
    584     private function render_pending_post_row() {
    585         ?>
    586         <tr id="omts-pending-post-row" class="omts-rule-row" style="display:none;">
    587             <th scope="row">
    588                 <label for="omts-pending-post-select"><?php esc_html_e( 'Select Pending Post', 'osom-multi-theme-switcher' ); ?></label>
    589             </th>
    590             <td>
    591                 <select id="omts-pending-post-select" name="post_id">
    592                     <option value=""><?php esc_html_e( '-- Select Pending Post --', 'osom-multi-theme-switcher' ); ?></option>
    593                     <?php
    594                     $posts = get_posts( array( 'numberposts' => -1, 'post_status' => 'pending' ) );
    595                     foreach ( $posts as $post ) {
    596                         printf(
    597                             '<option value="%d">%s (Pending)</option>',
    598                             esc_attr( $post->ID ),
    599                             esc_html( $post->post_title )
    600                         );
    601                     }
    602                     ?>
    603                 </select>
    604             </td>
    605         </tr>
    606         <?php
    607     }
    608 
    609     /**
    610      * Render private page selection row.
    611      *
    612      * @since 1.0.2
    613      */
    614     private function render_private_page_row() {
    615         ?>
    616         <tr id="omts-private-page-row" class="omts-rule-row" style="display:none;">
    617             <th scope="row">
    618                 <label for="omts-private-page-select"><?php esc_html_e( 'Select Private Page', 'osom-multi-theme-switcher' ); ?></label>
    619             </th>
    620             <td>
    621                 <select id="omts-private-page-select" name="page_id">
    622                     <option value=""><?php esc_html_e( '-- Select Private Page --', 'osom-multi-theme-switcher' ); ?></option>
    623                     <?php
    624                     $pages = get_pages( array( 'post_status' => 'private' ) );
    625                     foreach ( $pages as $page ) {
    626                         printf(
    627                             '<option value="%d">%s (Private)</option>',
    628                             esc_attr( $page->ID ),
    629                             esc_html( $page->post_title )
    630                         );
    631                     }
    632                     ?>
    633                 </select>
    634             </td>
    635         </tr>
    636         <?php
    637     }
    638 
    639     /**
    640      * Render private post selection row.
    641      *
    642      * @since 1.0.2
    643      */
    644     private function render_private_post_row() {
    645         ?>
    646         <tr id="omts-private-post-row" class="omts-rule-row" style="display:none;">
    647             <th scope="row">
    648                 <label for="omts-private-post-select"><?php esc_html_e( 'Select Private Post', 'osom-multi-theme-switcher' ); ?></label>
    649             </th>
    650             <td>
    651                 <select id="omts-private-post-select" name="post_id">
    652                     <option value=""><?php esc_html_e( '-- Select Private Post --', 'osom-multi-theme-switcher' ); ?></option>
    653                     <?php
    654                     $posts = get_posts( array( 'numberposts' => -1, 'post_status' => 'private' ) );
    655                     foreach ( $posts as $post ) {
    656                         printf(
    657                             '<option value="%d">%s (Private)</option>',
    658                             esc_attr( $post->ID ),
    659                             esc_html( $post->post_title )
    660                         );
    661                     }
    662                     ?>
    663                 </select>
    664             </td>
    665         </tr>
    666         <?php
    667     }
    668 
    669     /**
    670      * Render scheduled page selection row.
    671      *
    672      * @since 1.0.2
    673      */
    674     private function render_future_page_row() {
    675         ?>
    676         <tr id="omts-future-page-row" class="omts-rule-row" style="display:none;">
    677             <th scope="row">
    678                 <label for="omts-future-page-select"><?php esc_html_e( 'Select Scheduled Page', 'osom-multi-theme-switcher' ); ?></label>
    679             </th>
    680             <td>
    681                 <select id="omts-future-page-select" name="page_id">
    682                     <option value=""><?php esc_html_e( '-- Select Scheduled Page --', 'osom-multi-theme-switcher' ); ?></option>
    683                     <?php
    684                     $pages = get_pages( array( 'post_status' => 'future' ) );
    685                     foreach ( $pages as $page ) {
    686                         printf(
    687                             '<option value="%d">%s (Scheduled)</option>',
    688                             esc_attr( $page->ID ),
    689                             esc_html( $page->post_title )
    690                         );
    691                     }
    692                     ?>
    693                 </select>
    694             </td>
    695         </tr>
    696         <?php
    697     }
    698 
    699     /**
    700      * Render scheduled post selection row.
    701      *
    702      * @since 1.0.2
    703      */
    704     private function render_future_post_row() {
    705         ?>
    706         <tr id="omts-future-post-row" class="omts-rule-row" style="display:none;">
    707             <th scope="row">
    708                 <label for="omts-future-post-select"><?php esc_html_e( 'Select Scheduled Post', 'osom-multi-theme-switcher' ); ?></label>
    709             </th>
    710             <td>
    711                 <select id="omts-future-post-select" name="post_id">
    712                     <option value=""><?php esc_html_e( '-- Select Scheduled Post --', 'osom-multi-theme-switcher' ); ?></option>
    713                     <?php
    714                     $posts = get_posts( array( 'numberposts' => -1, 'post_status' => 'future' ) );
    715                     foreach ( $posts as $post ) {
    716                         printf(
    717                             '<option value="%d">%s (Scheduled)</option>',
    718                             esc_attr( $post->ID ),
    719                             esc_html( $post->post_title )
    720                         );
    721                     }
    722                     ?>
    723                 </select>
    724             </td>
    725         </tr>
    726         <?php
     329     * Get human-readable display name for rule type.
     330     *
     331     * @since 1.2.0
     332     *
     333     * @param string $type Rule type.
     334     * @return string Display name.
     335     */
     336    private function get_rule_type_display( $type ) {
     337        return OMTS_Theme_Switcher::get_rule_type_display( $type );
    727338    }
    728339
     
    755366            case 'post_type':
    756367                $post_type_obj = get_post_type_object( $rule['value'] );
    757                 return $post_type_obj ? $post_type_obj->label : $rule['value'];
     368                return $post_type_obj
     369                    ? sprintf(
     370                        /* translators: %s: Post type label */
     371                        __( 'All %s', 'osom-multi-theme-switcher' ),
     372                        $post_type_obj->label
     373                    )
     374                    : $rule['value'];
    758375
    759376            case 'url':
     
    768385                return $tag ? $tag->name : __( 'Unknown Tag', 'osom-multi-theme-switcher' );
    769386
     387            case 'taxonomy':
     388                $taxonomy = isset( $rule['taxonomy'] ) ? $rule['taxonomy'] : '';
     389                $term     = get_term( $rule['value'], $taxonomy );
     390                if ( $term && ! is_wp_error( $term ) ) {
     391                    $tax_obj   = get_taxonomy( $taxonomy );
     392                    $tax_label = $tax_obj ? $tax_obj->label : $taxonomy;
     393                    return $term->name . ' (' . $tax_label . ')';
     394                }
     395                return __( 'Unknown Term', 'osom-multi-theme-switcher' );
     396
     397            case 'cpt_item':
     398                $post = get_post( $rule['value'] );
     399                return $post ? $post->post_title : sprintf(
     400                    /* translators: %d: Post ID */
     401                    __( 'Unknown Item (ID: %d)', 'osom-multi-theme-switcher' ),
     402                    $rule['value']
     403                );
     404
    770405            case 'draft_page':
    771                 $page = get_post( $rule['value'] );
    772                 return $page ? $page->post_title . ' (Draft)' : sprintf(
    773                     /* translators: %d: Page ID */
    774                     __( 'Unknown Draft Page (ID: %d)', 'osom-multi-theme-switcher' ),
    775                     $rule['value']
    776                 );
    777 
    778406            case 'draft_post':
    779                 $post = get_post( $rule['value'] );
    780                 return $post ? $post->post_title . ' (Draft)' : sprintf(
    781                     /* translators: %d: Post ID */
    782                     __( 'Unknown Draft Post (ID: %d)', 'osom-multi-theme-switcher' ),
     407            case 'draft_cpt_item':
     408                $post = get_post( $rule['value'] );
     409                return $post ? '(Draft) ' . $post->post_title : sprintf(
     410                    /* translators: %d: Post ID */
     411                    __( 'Unknown Draft (ID: %d)', 'osom-multi-theme-switcher' ),
    783412                    $rule['value']
    784413                );
    785414
    786415            case 'pending_page':
    787                 $page = get_post( $rule['value'] );
    788                 return $page ? $page->post_title . ' (Pending)' : sprintf(
    789                     /* translators: %d: Page ID */
    790                     __( 'Unknown Pending Page (ID: %d)', 'osom-multi-theme-switcher' ),
    791                     $rule['value']
    792                 );
    793 
    794416            case 'pending_post':
    795                 $post = get_post( $rule['value'] );
    796                 return $post ? $post->post_title . ' (Pending)' : sprintf(
    797                     /* translators: %d: Post ID */
    798                     __( 'Unknown Pending Post (ID: %d)', 'osom-multi-theme-switcher' ),
     417            case 'pending_cpt_item':
     418                $post = get_post( $rule['value'] );
     419                return $post ? '(Pending) ' . $post->post_title : sprintf(
     420                    /* translators: %d: Post ID */
     421                    __( 'Unknown Pending (ID: %d)', 'osom-multi-theme-switcher' ),
    799422                    $rule['value']
    800423                );
    801424
    802425            case 'private_page':
    803                 $page = get_post( $rule['value'] );
    804                 return $page ? $page->post_title . ' (Private)' : sprintf(
    805                     /* translators: %d: Page ID */
    806                     __( 'Unknown Private Page (ID: %d)', 'osom-multi-theme-switcher' ),
    807                     $rule['value']
    808                 );
    809 
    810426            case 'private_post':
    811                 $post = get_post( $rule['value'] );
    812                 return $post ? $post->post_title . ' (Private)' : sprintf(
    813                     /* translators: %d: Post ID */
    814                     __( 'Unknown Private Post (ID: %d)', 'osom-multi-theme-switcher' ),
     427            case 'private_cpt_item':
     428                $post = get_post( $rule['value'] );
     429                return $post ? '(Private) ' . $post->post_title : sprintf(
     430                    /* translators: %d: Post ID */
     431                    __( 'Unknown Private (ID: %d)', 'osom-multi-theme-switcher' ),
    815432                    $rule['value']
    816433                );
    817434
    818435            case 'future_page':
    819                 $page = get_post( $rule['value'] );
    820                 return $page ? $page->post_title . ' (Scheduled)' : sprintf(
    821                     /* translators: %d: Page ID */
    822                     __( 'Unknown Scheduled Page (ID: %d)', 'osom-multi-theme-switcher' ),
    823                     $rule['value']
    824                 );
    825 
    826436            case 'future_post':
    827                 $post = get_post( $rule['value'] );
    828                 return $post ? $post->post_title . ' (Scheduled)' : sprintf(
    829                     /* translators: %d: Post ID */
    830                     __( 'Unknown Scheduled Post (ID: %d)', 'osom-multi-theme-switcher' ),
     437            case 'future_cpt_item':
     438                $post = get_post( $rule['value'] );
     439                return $post ? '(Scheduled) ' . $post->post_title : sprintf(
     440                    /* translators: %d: Post ID */
     441                    __( 'Unknown Scheduled (ID: %d)', 'osom-multi-theme-switcher' ),
    831442                    $rule['value']
    832443                );
  • osom-multi-theme-switcher/trunk/includes/class-omts-ajax-handler.php

    r3457914 r3464901  
    5252        add_action( 'wp_ajax_omts_save_rest_prefix', array( $this, 'ajax_save_rest_prefix' ) );
    5353        add_action( 'wp_ajax_omts_delete_rest_prefix', array( $this, 'ajax_delete_rest_prefix' ) );
     54        add_action( 'wp_ajax_omts_get_rule_objects', array( $this, 'ajax_get_rule_objects' ) );
     55        add_action( 'wp_ajax_omts_get_rule_items', array( $this, 'ajax_get_rule_items' ) );
    5456    }
    5557
     
    7375        }
    7476
    75         $rule = array(
    76             'type'  => $rule_type,
    77             'theme' => $theme,
    78         );
    79 
    80         // Get the value based on rule type.
    81         switch ( $rule_type ) {
    82             case 'page':
    83                 $rule['value'] = isset( $_POST['page_id'] ) ? intval( $_POST['page_id'] ) : 0;
    84                 break;
    85             case 'post':
    86                 $rule['value'] = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
    87                 break;
    88             case 'post_type':
    89                 $rule['value'] = isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : '';
    90                 break;
    91             case 'draft_page':
    92                 $rule['value'] = isset( $_POST['page_id'] ) ? intval( $_POST['page_id'] ) : 0;
    93                 break;
    94             case 'draft_post':
    95                 $rule['value'] = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
    96                 break;
    97             case 'pending_page':
    98                 $rule['value'] = isset( $_POST['page_id'] ) ? intval( $_POST['page_id'] ) : 0;
    99                 break;
    100             case 'pending_post':
    101                 $rule['value'] = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
    102                 break;
    103             case 'private_page':
    104                 $rule['value'] = isset( $_POST['page_id'] ) ? intval( $_POST['page_id'] ) : 0;
    105                 break;
    106             case 'private_post':
    107                 $rule['value'] = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
    108                 break;
    109             case 'future_page':
    110                 $rule['value'] = isset( $_POST['page_id'] ) ? intval( $_POST['page_id'] ) : 0;
    111                 break;
    112             case 'future_post':
    113                 $rule['value'] = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
    114                 break;
    115             case 'url':
    116                 $rule['value'] = isset( $_POST['custom_url'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_url'] ) ) : '';
    117                 break;
    118             case 'category':
    119                 $rule['value'] = isset( $_POST['category_id'] ) ? intval( $_POST['category_id'] ) : 0;
    120                 break;
    121             case 'tag':
    122                 $rule['value'] = isset( $_POST['tag_id'] ) ? intval( $_POST['tag_id'] ) : 0;
    123                 break;
    124         }
     77        // Build rule from the cascading selector data.
     78        $rule = $this->build_rule_from_request( $rule_type, $theme );
    12579
    12680        if ( empty( $rule['value'] ) ) {
     
    13993                'index'          => count( $rules ) - 1,
    14094                'target_display' => $this->get_rule_target_display( $rule ),
     95                'type_display'   => $this->get_rule_type_display( $rule['type'] ),
    14196                'theme_name'     => $this->theme_switcher->get_theme_name( $theme ),
    14297            )
    14398        );
     99    }
     100
     101    /**
     102     * Build rule array from the cascading selector request data.
     103     *
     104     * @since 1.2.0
     105     *
     106     * @param string $rule_type Rule type from the form.
     107     * @param string $theme     Theme slug.
     108     * @return array Rule array.
     109     */
     110    private function build_rule_from_request( $rule_type, $theme ) {
     111        $rule = array(
     112            'type'  => $rule_type,
     113            'theme' => $theme,
     114        );
     115
     116        switch ( $rule_type ) {
     117            case 'page':
     118            case 'post':
     119                $item_id = isset( $_POST['item_id'] ) ? intval( $_POST['item_id'] ) : 0;
     120                if ( $item_id ) {
     121                    $post = get_post( $item_id );
     122                    if ( $post ) {
     123                        $status = $post->post_status;
     124                        if ( 'publish' !== $status ) {
     125                            $status_map = array(
     126                                'draft'   => 'draft_',
     127                                'pending' => 'pending_',
     128                                'private' => 'private_',
     129                                'future'  => 'future_',
     130                            );
     131                            if ( isset( $status_map[ $status ] ) ) {
     132                                $rule['type'] = $status_map[ $status ] . $rule_type;
     133                            }
     134                        }
     135                    }
     136                }
     137                $rule['value'] = $item_id;
     138                break;
     139
     140            case 'custom_post_type':
     141                $object_type = isset( $_POST['object_type'] ) ? sanitize_text_field( wp_unslash( $_POST['object_type'] ) ) : '';
     142                $item_id     = isset( $_POST['item_id'] ) ? sanitize_text_field( wp_unslash( $_POST['item_id'] ) ) : '';
     143
     144                if ( empty( $object_type ) ) {
     145                    $rule['value'] = '';
     146                    break;
     147                }
     148
     149                if ( '__all__' === $item_id ) {
     150                    // "All" option — store as post_type rule.
     151                    $rule['type']  = 'post_type';
     152                    $rule['value'] = $object_type;
     153
     154                    // Store URL slugs for early matching (before CPT is registered).
     155                    $pt_obj = get_post_type_object( $object_type );
     156                    if ( $pt_obj ) {
     157                        if ( $pt_obj->has_archive ) {
     158                            $rule['archive_slug'] = true === $pt_obj->has_archive ? $object_type : $pt_obj->has_archive;
     159                        }
     160                        if ( is_array( $pt_obj->rewrite ) && isset( $pt_obj->rewrite['slug'] ) ) {
     161                            $rule['rewrite_slug'] = $pt_obj->rewrite['slug'];
     162                        }
     163                    }
     164                } else {
     165                    // Individual CPT item.
     166                    $item_id = intval( $item_id );
     167                    $post    = get_post( $item_id );
     168
     169                    if ( ! $post || $post->post_type !== $object_type ) {
     170                        $rule['value'] = '';
     171                        break;
     172                    }
     173
     174                    if ( 'publish' === $post->post_status ) {
     175                        $rule['type'] = 'cpt_item';
     176                    } else {
     177                        $status_map   = array(
     178                            'draft'   => 'draft_cpt_item',
     179                            'pending' => 'pending_cpt_item',
     180                            'private' => 'private_cpt_item',
     181                            'future'  => 'future_cpt_item',
     182                        );
     183                        $rule['type'] = isset( $status_map[ $post->post_status ] ) ? $status_map[ $post->post_status ] : 'cpt_item';
     184                    }
     185                    $rule['value']     = $item_id;
     186                    $rule['post_type'] = $object_type;
     187                }
     188                break;
     189
     190            case 'taxonomy':
     191                $object_type = isset( $_POST['object_type'] ) ? sanitize_text_field( wp_unslash( $_POST['object_type'] ) ) : '';
     192                $item_id     = isset( $_POST['item_id'] ) ? intval( $_POST['item_id'] ) : 0;
     193
     194                if ( empty( $object_type ) ) {
     195                    $rule['value'] = '';
     196                    break;
     197                }
     198
     199                // Validate the term exists in the specified taxonomy.
     200                $term = get_term( $item_id, $object_type );
     201                if ( ! $term || is_wp_error( $term ) || $term->taxonomy !== $object_type ) {
     202                    $rule['value'] = '';
     203                    break;
     204                }
     205
     206                if ( 'category' === $object_type ) {
     207                    $rule['type'] = 'category';
     208                } elseif ( 'post_tag' === $object_type ) {
     209                    $rule['type'] = 'tag';
     210                } else {
     211                    $rule['type']     = 'taxonomy';
     212                    $rule['taxonomy'] = $object_type;
     213
     214                    // Store rewrite slug for early matching (before taxonomy is registered).
     215                    $tax_obj = get_taxonomy( $object_type );
     216                    if ( $tax_obj && is_array( $tax_obj->rewrite ) && isset( $tax_obj->rewrite['slug'] ) ) {
     217                        $rule['rewrite_slug'] = $tax_obj->rewrite['slug'];
     218                    }
     219                }
     220                $rule['value'] = $item_id;
     221                break;
     222
     223            case 'url':
     224                $rule['value'] = isset( $_POST['custom_url'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_url'] ) ) : '';
     225                break;
     226
     227            default:
     228                $rule['value'] = '';
     229                break;
     230        }
     231
     232        return $rule;
     233    }
     234
     235    /**
     236     * Get human-readable display name for rule type.
     237     *
     238     * @since 1.2.0
     239     *
     240     * @param string $type Rule type.
     241     * @return string Display name.
     242     */
     243    private function get_rule_type_display( $type ) {
     244        return OMTS_Theme_Switcher::get_rule_type_display( $type );
     245    }
     246
     247    /**
     248     * AJAX handler: Get rule objects for cascading selector.
     249     *
     250     * @since 1.2.0
     251     */
     252    public function ajax_get_rule_objects() {
     253        check_ajax_referer( 'omts_nonce', 'nonce' );
     254
     255        if ( ! current_user_can( 'manage_options' ) ) {
     256            wp_send_json_error( __( 'Insufficient permissions', 'osom-multi-theme-switcher' ) );
     257        }
     258
     259        $rule_type = isset( $_POST['rule_type'] ) ? sanitize_text_field( wp_unslash( $_POST['rule_type'] ) ) : '';
     260        $objects   = array();
     261
     262        switch ( $rule_type ) {
     263            case 'custom_post_type':
     264                $post_types = get_post_types( array( 'public' => true ), 'objects' );
     265                foreach ( $post_types as $pt ) {
     266                    if ( in_array( $pt->name, array( 'page', 'post', 'attachment' ), true ) ) {
     267                        continue;
     268                    }
     269                    $objects[] = array(
     270                        'value' => $pt->name,
     271                        'label' => $pt->label,
     272                    );
     273                }
     274                break;
     275
     276            case 'taxonomy':
     277                $taxonomies = get_taxonomies( array( 'public' => true ), 'objects' );
     278                foreach ( $taxonomies as $tax ) {
     279                    $objects[] = array(
     280                        'value' => $tax->name,
     281                        'label' => $tax->label,
     282                    );
     283                }
     284                break;
     285        }
     286
     287        wp_send_json_success( array( 'objects' => $objects ) );
     288    }
     289
     290    /**
     291     * AJAX handler: Get rule items for cascading selector.
     292     *
     293     * @since 1.2.0
     294     */
     295    public function ajax_get_rule_items() {
     296        check_ajax_referer( 'omts_nonce', 'nonce' );
     297
     298        if ( ! current_user_can( 'manage_options' ) ) {
     299            wp_send_json_error( __( 'Insufficient permissions', 'osom-multi-theme-switcher' ) );
     300        }
     301
     302        $rule_type   = isset( $_POST['rule_type'] ) ? sanitize_text_field( wp_unslash( $_POST['rule_type'] ) ) : '';
     303        $object_type = isset( $_POST['object_type'] ) ? sanitize_text_field( wp_unslash( $_POST['object_type'] ) ) : '';
     304        $items       = array();
     305
     306        $all_statuses  = array( 'publish', 'draft', 'pending', 'private', 'future' );
     307        $status_labels = array(
     308            'draft'   => __( 'Draft', 'osom-multi-theme-switcher' ),
     309            'pending' => __( 'Pending', 'osom-multi-theme-switcher' ),
     310            'private' => __( 'Private', 'osom-multi-theme-switcher' ),
     311            'future'  => __( 'Scheduled', 'osom-multi-theme-switcher' ),
     312        );
     313
     314        switch ( $rule_type ) {
     315            case 'page':
     316                $pages = get_posts(
     317                    array(
     318                        'post_type'   => 'page',
     319                        'post_status' => $all_statuses,
     320                        'numberposts' => 500,
     321                        'orderby'     => 'title',
     322                        'order'       => 'ASC',
     323                    )
     324                );
     325                foreach ( $pages as $page ) {
     326                    $label = $page->post_title;
     327                    if ( 'publish' !== $page->post_status && isset( $status_labels[ $page->post_status ] ) ) {
     328                        $label = '(' . $status_labels[ $page->post_status ] . ') ' . $label;
     329                    }
     330                    $items[] = array(
     331                        'value' => $page->ID,
     332                        'label' => $label,
     333                    );
     334                }
     335                break;
     336
     337            case 'post':
     338                $posts = get_posts(
     339                    array(
     340                        'post_type'   => 'post',
     341                        'post_status' => $all_statuses,
     342                        'numberposts' => 500,
     343                        'orderby'     => 'title',
     344                        'order'       => 'ASC',
     345                    )
     346                );
     347                foreach ( $posts as $post ) {
     348                    $label = $post->post_title;
     349                    if ( 'publish' !== $post->post_status && isset( $status_labels[ $post->post_status ] ) ) {
     350                        $label = '(' . $status_labels[ $post->post_status ] . ') ' . $label;
     351                    }
     352                    $items[] = array(
     353                        'value' => $post->ID,
     354                        'label' => $label,
     355                    );
     356                }
     357                break;
     358
     359            case 'custom_post_type':
     360                if ( empty( $object_type ) ) {
     361                    break;
     362                }
     363
     364                $post_type_obj = get_post_type_object( $object_type );
     365                $all_label     = $post_type_obj
     366                    ? sprintf(
     367                        /* translators: %s: Post type label */
     368                        __( 'All %s', 'osom-multi-theme-switcher' ),
     369                        $post_type_obj->label
     370                    )
     371                    : __( 'All', 'osom-multi-theme-switcher' );
     372
     373                $items[] = array(
     374                    'value' => '__all__',
     375                    'label' => $all_label,
     376                );
     377
     378                $posts = get_posts(
     379                    array(
     380                        'post_type'   => $object_type,
     381                        'post_status' => $all_statuses,
     382                        'numberposts' => 500,
     383                        'orderby'     => 'title',
     384                        'order'       => 'ASC',
     385                    )
     386                );
     387                foreach ( $posts as $post ) {
     388                    $label = $post->post_title;
     389                    if ( 'publish' !== $post->post_status && isset( $status_labels[ $post->post_status ] ) ) {
     390                        $label = '(' . $status_labels[ $post->post_status ] . ') ' . $label;
     391                    }
     392                    $items[] = array(
     393                        'value' => $post->ID,
     394                        'label' => $label,
     395                    );
     396                }
     397                break;
     398
     399            case 'taxonomy':
     400                if ( empty( $object_type ) ) {
     401                    break;
     402                }
     403
     404                $terms = get_terms(
     405                    array(
     406                        'taxonomy'   => $object_type,
     407                        'hide_empty' => false,
     408                        'orderby'    => 'name',
     409                        'order'      => 'ASC',
     410                        'number'     => 500,
     411                    )
     412                );
     413
     414                if ( ! is_wp_error( $terms ) ) {
     415                    foreach ( $terms as $term ) {
     416                        $items[] = array(
     417                            'value' => $term->term_id,
     418                            'label' => $term->name,
     419                        );
     420                    }
     421                }
     422                break;
     423        }
     424
     425        wp_send_json_success( array( 'items' => $items ) );
    144426    }
    145427
     
    228510            case 'post_type':
    229511                $post_type_obj = get_post_type_object( $rule['value'] );
    230                 return $post_type_obj ? $post_type_obj->label : $rule['value'];
     512                return $post_type_obj
     513                    ? sprintf(
     514                        /* translators: %s: Post type label */
     515                        __( 'All %s', 'osom-multi-theme-switcher' ),
     516                        $post_type_obj->label
     517                    )
     518                    : $rule['value'];
    231519
    232520            case 'url':
     
    241529                return $tag ? $tag->name : __( 'Unknown Tag', 'osom-multi-theme-switcher' );
    242530
     531            case 'taxonomy':
     532                $taxonomy = isset( $rule['taxonomy'] ) ? $rule['taxonomy'] : '';
     533                $term     = get_term( $rule['value'], $taxonomy );
     534                if ( $term && ! is_wp_error( $term ) ) {
     535                    $tax_obj   = get_taxonomy( $taxonomy );
     536                    $tax_label = $tax_obj ? $tax_obj->label : $taxonomy;
     537                    return $term->name . ' (' . $tax_label . ')';
     538                }
     539                return __( 'Unknown Term', 'osom-multi-theme-switcher' );
     540
     541            case 'cpt_item':
     542                $post = get_post( $rule['value'] );
     543                return $post ? $post->post_title : sprintf(
     544                    /* translators: %d: Post ID */
     545                    __( 'Unknown Item (ID: %d)', 'osom-multi-theme-switcher' ),
     546                    $rule['value']
     547                );
     548
    243549            case 'draft_page':
    244                 $page = get_post( $rule['value'] );
    245                 return $page ? $page->post_title . ' (Draft)' : sprintf(
    246                     /* translators: %d: Page ID */
    247                     __( 'Unknown Draft Page (ID: %d)', 'osom-multi-theme-switcher' ),
     550            case 'draft_post':
     551            case 'draft_cpt_item':
     552                $post = get_post( $rule['value'] );
     553                return $post ? '(Draft) ' . $post->post_title : sprintf(
     554                    /* translators: %d: Post ID */
     555                    __( 'Unknown Draft (ID: %d)', 'osom-multi-theme-switcher' ),
    248556                    $rule['value']
    249557                );
    250558
    251             case 'draft_post':
     559            case 'pending_page':
     560            case 'pending_post':
     561            case 'pending_cpt_item':
    252562                $post = get_post( $rule['value'] );
    253                 return $post ? $post->post_title . ' (Draft)' : sprintf(
     563                return $post ? '(Pending) ' . $post->post_title : sprintf(
    254564                    /* translators: %d: Post ID */
    255                     __( 'Unknown Draft Post (ID: %d)', 'osom-multi-theme-switcher' ),
     565                    __( 'Unknown Pending (ID: %d)', 'osom-multi-theme-switcher' ),
    256566                    $rule['value']
    257567                );
    258568
    259             case 'pending_page':
    260                 $page = get_post( $rule['value'] );
    261                 return $page ? $page->post_title . ' (Pending)' : sprintf(
    262                     /* translators: %d: Page ID */
    263                     __( 'Unknown Pending Page (ID: %d)', 'osom-multi-theme-switcher' ),
     569            case 'private_page':
     570            case 'private_post':
     571            case 'private_cpt_item':
     572                $post = get_post( $rule['value'] );
     573                return $post ? '(Private) ' . $post->post_title : sprintf(
     574                    /* translators: %d: Post ID */
     575                    __( 'Unknown Private (ID: %d)', 'osom-multi-theme-switcher' ),
    264576                    $rule['value']
    265577                );
    266578
    267             case 'pending_post':
     579            case 'future_page':
     580            case 'future_post':
     581            case 'future_cpt_item':
    268582                $post = get_post( $rule['value'] );
    269                 return $post ? $post->post_title . ' (Pending)' : sprintf(
     583                return $post ? '(Scheduled) ' . $post->post_title : sprintf(
    270584                    /* translators: %d: Post ID */
    271                     __( 'Unknown Pending Post (ID: %d)', 'osom-multi-theme-switcher' ),
    272                     $rule['value']
    273                 );
    274 
    275             case 'private_page':
    276                 $page = get_post( $rule['value'] );
    277                 return $page ? $page->post_title . ' (Private)' : sprintf(
    278                     /* translators: %d: Page ID */
    279                     __( 'Unknown Private Page (ID: %d)', 'osom-multi-theme-switcher' ),
    280                     $rule['value']
    281                 );
    282 
    283             case 'private_post':
    284                 $post = get_post( $rule['value'] );
    285                 return $post ? $post->post_title . ' (Private)' : sprintf(
    286                     /* translators: %d: Post ID */
    287                     __( 'Unknown Private Post (ID: %d)', 'osom-multi-theme-switcher' ),
    288                     $rule['value']
    289                 );
    290 
    291             case 'future_page':
    292                 $page = get_post( $rule['value'] );
    293                 return $page ? $page->post_title . ' (Scheduled)' : sprintf(
    294                     /* translators: %d: Page ID */
    295                     __( 'Unknown Scheduled Page (ID: %d)', 'osom-multi-theme-switcher' ),
    296                     $rule['value']
    297                 );
    298 
    299             case 'future_post':
    300                 $post = get_post( $rule['value'] );
    301                 return $post ? $post->post_title . ' (Scheduled)' : sprintf(
    302                     /* translators: %d: Post ID */
    303                     __( 'Unknown Scheduled Post (ID: %d)', 'osom-multi-theme-switcher' ),
     585                    __( 'Unknown Scheduled (ID: %d)', 'osom-multi-theme-switcher' ),
    304586                    $rule['value']
    305587                );
  • osom-multi-theme-switcher/trunk/includes/class-omts-theme-switcher.php

    r3457914 r3464901  
    3636
    3737    /**
     38     * Option name for storing per-theme CPT/taxonomy registry.
     39     *
     40     * @var string
     41     */
     42    private $theme_registry_option = 'omts_theme_object_registry';
     43
     44    /**
    3845     * Flag to prevent recursion in filter_rest_url_prefix.
    3946     *
     
    5764        add_filter( 'rest_url_prefix', array( $this, 'filter_rest_url_prefix' ) );
    5865        add_action( 'init', array( $this, 'add_custom_rest_rewrite_rules' ), 1 );
     66        add_action( 'init', array( $this, 'reregister_missing_cpts' ), 998 );
     67        add_action( 'init', array( $this, 'capture_theme_objects' ), 999 );
    5968        add_filter( 'rest_pre_dispatch', array( $this, 'set_rest_theme_early' ), 1, 3 );
    6069    }
     
    7584        }
    7685
    77         add_filter( 'option_template', function() use ( $theme ) {
    78             return $theme;
    79         }, 1 );
    80 
    81         add_filter( 'option_stylesheet', function() use ( $theme ) {
    82             return $theme;
    83         }, 1 );
     86        $theme_obj = wp_get_theme( $theme );
     87        if ( ! $theme_obj->exists() ) {
     88            return;
     89        }
     90
     91        add_filter(
     92            'option_template',
     93            function() use ( $theme ) {
     94                return $theme;
     95            },
     96            1
     97        );
     98
     99        add_filter(
     100            'option_stylesheet',
     101            function() use ( $theme ) {
     102                return $theme;
     103            },
     104            1
     105        );
    84106    }
    85107
     
    94116    public function switch_theme_template( $template ) {
    95117        $theme = $this->get_theme_for_current_request();
     118
     119        if ( $theme ) {
     120            $theme_obj = wp_get_theme( $theme );
     121            if ( ! $theme_obj->exists() ) {
     122                return $template;
     123            }
     124        }
     125
    96126        return $theme ? $theme : $template;
    97127    }
     
    107137    public function switch_theme_stylesheet( $stylesheet ) {
    108138        $theme = $this->get_theme_for_current_request();
     139
     140        if ( $theme ) {
     141            $theme_obj = wp_get_theme( $theme );
     142            if ( ! $theme_obj->exists() ) {
     143                return $stylesheet;
     144            }
     145        }
     146
    109147        return $theme ? $theme : $stylesheet;
    110148    }
     
    228266
    229267    /**
     268     * Check if the WordPress query has run and conditional tags are available.
     269     *
     270     * Conditional query tags like is_page(), is_single(), etc. only work
     271     * after the main query has been parsed.
     272     *
     273     * @since 1.0.4
     274     *
     275     * @return bool Whether conditional query tags can be used.
     276     */
     277    private function is_query_ready() {
     278        global $wp_query;
     279
     280        if ( did_action( 'wp' ) ) {
     281            return true;
     282        }
     283
     284        if ( isset( $wp_query ) && $wp_query instanceof WP_Query && ! empty( $wp_query->query ) ) {
     285            return true;
     286        }
     287
     288        return false;
     289    }
     290
     291    /**
    230292     * Check if a rule matches the current request.
    231293     *
     
    237299     */
    238300    private function rule_matches( $rule, $early = false ) {
     301        if ( ! $early && ! $this->is_query_ready() ) {
     302            $early = true;
     303        }
     304
    239305        // If called early, we can only check URL-based rules
    240306        if ( $early ) {
     
    276342                return $this->match_future_post_early( $rule['value'] );
    277343            }
     344            if ( 'cpt_item' === $rule['type'] ) {
     345                return $this->match_cpt_item_early( $rule['value'], 'publish' );
     346            }
     347            if ( 'draft_cpt_item' === $rule['type'] ) {
     348                return $this->match_cpt_item_early( $rule['value'], 'draft' );
     349            }
     350            if ( 'pending_cpt_item' === $rule['type'] ) {
     351                return $this->match_cpt_item_early( $rule['value'], 'pending' );
     352            }
     353            if ( 'private_cpt_item' === $rule['type'] ) {
     354                return $this->match_cpt_item_early( $rule['value'], 'private' );
     355            }
     356            if ( 'future_cpt_item' === $rule['type'] ) {
     357                return $this->match_cpt_item_early( $rule['value'], 'future' );
     358            }
     359            if ( 'category' === $rule['type'] ) {
     360                return $this->match_category_early( $rule['value'] );
     361            }
     362            if ( 'tag' === $rule['type'] ) {
     363                return $this->match_tag_early( $rule['value'] );
     364            }
     365            if ( 'taxonomy' === $rule['type'] ) {
     366                $taxonomy = isset( $rule['taxonomy'] ) ? $rule['taxonomy'] : '';
     367                return $taxonomy ? $this->match_taxonomy_early( $rule['value'], $taxonomy, $rule ) : false;
     368            }
     369            if ( 'post_type' === $rule['type'] ) {
     370                return $this->match_post_type_early( $rule );
     371            }
    278372            return false;
    279373        }
     
    300394            case 'tag':
    301395                return is_tag( $rule['value'] ) || ( is_single() && has_tag( $rule['value'] ) );
     396
     397            case 'taxonomy':
     398                $taxonomy = isset( $rule['taxonomy'] ) ? $rule['taxonomy'] : '';
     399                if ( ! $taxonomy ) {
     400                    return false;
     401                }
     402                return is_tax( $taxonomy, $rule['value'] ) || ( is_singular() && has_term( $rule['value'], $taxonomy ) );
    302403
    303404            case 'draft_page':
     
    317418                return $post && is_single( $rule['value'] );
    318419
     420            case 'cpt_item':
     421                return is_singular() && get_queried_object_id() === absint( $rule['value'] );
     422
     423            case 'draft_cpt_item':
     424            case 'pending_cpt_item':
     425            case 'private_cpt_item':
     426            case 'future_cpt_item':
     427                $post = get_post( $rule['value'] );
     428                return $post && is_singular() && get_queried_object_id() === absint( $rule['value'] );
     429
    319430            default:
    320431                return false;
     
    382493        $theme = wp_get_theme( $stylesheet );
    383494        return $theme->exists() ? $theme->get( 'Name' ) : $stylesheet;
     495    }
     496
     497    /**
     498     * Get human-readable display name for rule type.
     499     *
     500     * Shared by OMTS_Admin_Page and OMTS_Ajax_Handler to avoid duplication.
     501     *
     502     * @since 1.2.0
     503     *
     504     * @param string $type Rule type.
     505     * @return string Display name.
     506     */
     507    public static function get_rule_type_display( $type ) {
     508        $type_map = array(
     509            'page'             => __( 'Page', 'osom-multi-theme-switcher' ),
     510            'post'             => __( 'Post', 'osom-multi-theme-switcher' ),
     511            'post_type'        => __( 'Custom Post Type', 'osom-multi-theme-switcher' ),
     512            'url'              => __( 'Custom URL', 'osom-multi-theme-switcher' ),
     513            'category'         => __( 'Category', 'osom-multi-theme-switcher' ),
     514            'tag'              => __( 'Tag', 'osom-multi-theme-switcher' ),
     515            'taxonomy'         => __( 'Taxonomy', 'osom-multi-theme-switcher' ),
     516            'cpt_item'         => __( 'CPT Item', 'osom-multi-theme-switcher' ),
     517            'draft_page'       => __( 'Page', 'osom-multi-theme-switcher' ),
     518            'draft_post'       => __( 'Post', 'osom-multi-theme-switcher' ),
     519            'pending_page'     => __( 'Page', 'osom-multi-theme-switcher' ),
     520            'pending_post'     => __( 'Post', 'osom-multi-theme-switcher' ),
     521            'private_page'     => __( 'Page', 'osom-multi-theme-switcher' ),
     522            'private_post'     => __( 'Post', 'osom-multi-theme-switcher' ),
     523            'future_page'      => __( 'Page', 'osom-multi-theme-switcher' ),
     524            'future_post'      => __( 'Post', 'osom-multi-theme-switcher' ),
     525            'draft_cpt_item'   => __( 'CPT Item', 'osom-multi-theme-switcher' ),
     526            'pending_cpt_item' => __( 'CPT Item', 'osom-multi-theme-switcher' ),
     527            'private_cpt_item' => __( 'CPT Item', 'osom-multi-theme-switcher' ),
     528            'future_cpt_item'  => __( 'CPT Item', 'osom-multi-theme-switcher' ),
     529        );
     530
     531        return isset( $type_map[ $type ] ) ? $type_map[ $type ] : ucfirst( str_replace( '_', ' ', $type ) );
    384532    }
    385533
     
    11551303
    11561304    /**
    1157      * Match preview post against URL rules.
     1305     * Match preview post against rules.
     1306     *
     1307     * Checks for page/post ID rules, status-based rules (draft_page, etc.),
     1308     * and URL rules.
    11581309     *
    11591310     * @since 1.0.3
     
    11721323        $post = $wpdb->get_row(
    11731324            $wpdb->prepare(
    1174                 "SELECT post_name, post_parent, post_type FROM {$wpdb->posts} WHERE ID = %d",
     1325                "SELECT post_name, post_parent, post_type, post_status FROM {$wpdb->posts} WHERE ID = %d",
    11751326                $post_id
    11761327            )
     
    11811332        }
    11821333
     1334        // First, check for direct page/post ID rules and status-based rules.
     1335        foreach ( $rules as $rule ) {
     1336            // Check for direct page rule (page type with matching ID).
     1337            if ( 'page' === $rule['type'] && 'page' === $post->post_type && absint( $rule['value'] ) === $post_id ) {
     1338                return $rule['theme'];
     1339            }
     1340
     1341            // Check for direct post rule (post type with matching ID).
     1342            if ( 'post' === $rule['type'] && 'post' === $post->post_type && absint( $rule['value'] ) === $post_id ) {
     1343                return $rule['theme'];
     1344            }
     1345
     1346            // Check for status-based rules (draft_page, pending_page, private_page, future_page, etc.).
     1347            $status_type = $post->post_status . '_' . $post->post_type;
     1348            if ( $rule['type'] === $status_type && absint( $rule['value'] ) === $post_id ) {
     1349                return $rule['theme'];
     1350            }
     1351
     1352            // Check for CPT item rules.
     1353            if ( in_array( $rule['type'], array( 'cpt_item', 'draft_cpt_item', 'pending_cpt_item', 'private_cpt_item', 'future_cpt_item' ), true ) && absint( $rule['value'] ) === $post_id ) {
     1354                return $rule['theme'];
     1355            }
     1356        }
     1357
     1358        // Then, check URL rules.
    11831359        if ( 'page' === $post->post_type ) {
    11841360            return $this->match_page_preview_against_rules( $post, $rules, $wpdb );
     
    12381414     */
    12391415    private function match_post_preview_against_rules( $post, $rules ) {
    1240         $post_path = $post->post_name;
     1416        $post_path            = $post->post_name;
    12411417        $post_path_normalized = trim( $post_path, '/' );
    12421418
    12431419        foreach ( $rules as $rule ) {
    12441420            if ( 'url' === $rule['type'] ) {
    1245                 $rule_url = trim( $rule['value'], '/' );
     1421                $rule_url          = trim( $rule['value'], '/' );
    12461422                $rule_url_segments = explode( '/', $rule_url );
    12471423
     
    12601436        return false;
    12611437    }
     1438
     1439    /**
     1440     * Match category rule early by checking URL against category base.
     1441     *
     1442     * @since 1.2.0
     1443     *
     1444     * @param int $term_id Category term ID.
     1445     * @return bool Whether current URL matches the category.
     1446     */
     1447    private function match_category_early( $term_id ) {
     1448        global $wpdb;
     1449
     1450        if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
     1451            return false;
     1452        }
     1453
     1454        $term_id = absint( $term_id );
     1455        $term    = $wpdb->get_row(
     1456            $wpdb->prepare(
     1457                "SELECT t.slug FROM {$wpdb->terms} t
     1458                INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
     1459                WHERE t.term_id = %d AND tt.taxonomy = 'category'",
     1460                $term_id
     1461            )
     1462        );
     1463
     1464        if ( ! $term ) {
     1465            return false;
     1466        }
     1467
     1468        $sanitized_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
     1469        $request_uri   = parse_url( $sanitized_uri, PHP_URL_PATH );
     1470        $path          = trim( $request_uri, '/' );
     1471
     1472        $category_base = get_option( 'category_base' );
     1473        if ( empty( $category_base ) ) {
     1474            $category_base = 'category';
     1475        }
     1476
     1477        // Check if URL matches pattern: {category_base}/{slug}
     1478        $expected_path = trim( $category_base, '/' ) . '/' . $term->slug;
     1479
     1480        return $path === $expected_path || 0 === strpos( $path, $expected_path . '/' );
     1481    }
     1482
     1483    /**
     1484     * Match tag rule early by checking URL against tag base.
     1485     *
     1486     * @since 1.2.0
     1487     *
     1488     * @param int $term_id Tag term ID.
     1489     * @return bool Whether current URL matches the tag.
     1490     */
     1491    private function match_tag_early( $term_id ) {
     1492        global $wpdb;
     1493
     1494        if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
     1495            return false;
     1496        }
     1497
     1498        $term_id = absint( $term_id );
     1499        $term    = $wpdb->get_row(
     1500            $wpdb->prepare(
     1501                "SELECT t.slug FROM {$wpdb->terms} t
     1502                INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
     1503                WHERE t.term_id = %d AND tt.taxonomy = 'post_tag'",
     1504                $term_id
     1505            )
     1506        );
     1507
     1508        if ( ! $term ) {
     1509            return false;
     1510        }
     1511
     1512        $sanitized_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
     1513        $request_uri   = parse_url( $sanitized_uri, PHP_URL_PATH );
     1514        $path          = trim( $request_uri, '/' );
     1515
     1516        $tag_base = get_option( 'tag_base' );
     1517        if ( empty( $tag_base ) ) {
     1518            $tag_base = 'tag';
     1519        }
     1520
     1521        // Check if URL matches pattern: {tag_base}/{slug}
     1522        $expected_path = trim( $tag_base, '/' ) . '/' . $term->slug;
     1523
     1524        return $path === $expected_path || 0 === strpos( $path, $expected_path . '/' );
     1525    }
     1526
     1527    /**
     1528     * Match custom taxonomy rule early by checking URL against taxonomy rewrite slug.
     1529     *
     1530     * Falls back to a stored rewrite_slug on the rule when get_taxonomy() is not
     1531     * available yet (e.g., during setup_theme before taxonomies are registered).
     1532     *
     1533     * @since 1.2.0
     1534     *
     1535     * @param int    $term_id  Term ID.
     1536     * @param string $taxonomy Taxonomy name.
     1537     * @param array  $rule     Rule array with optional rewrite_slug.
     1538     * @return bool Whether current URL matches the taxonomy term.
     1539     */
     1540    private function match_taxonomy_early( $term_id, $taxonomy, $rule = array() ) {
     1541        global $wpdb;
     1542
     1543        if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
     1544            return false;
     1545        }
     1546
     1547        $term_id = absint( $term_id );
     1548        $term    = $wpdb->get_row(
     1549            $wpdb->prepare(
     1550                "SELECT t.slug FROM {$wpdb->terms} t
     1551                INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
     1552                WHERE t.term_id = %d AND tt.taxonomy = %s",
     1553                $term_id,
     1554                $taxonomy
     1555            )
     1556        );
     1557
     1558        if ( ! $term ) {
     1559            return false;
     1560        }
     1561
     1562        // Try get_taxonomy() first (available after init).
     1563        $rewrite_slug = '';
     1564        $tax_obj      = get_taxonomy( $taxonomy );
     1565        if ( $tax_obj && is_array( $tax_obj->rewrite ) && isset( $tax_obj->rewrite['slug'] ) ) {
     1566            $rewrite_slug = $tax_obj->rewrite['slug'];
     1567        } elseif ( ! empty( $rule['rewrite_slug'] ) ) {
     1568            // Fallback to stored slug (works during setup_theme before taxonomies are registered).
     1569            $rewrite_slug = $rule['rewrite_slug'];
     1570        } else {
     1571            return false;
     1572        }
     1573
     1574        $sanitized_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
     1575        $request_uri   = parse_url( $sanitized_uri, PHP_URL_PATH );
     1576        $path          = trim( $request_uri, '/' );
     1577
     1578        $rewrite_slug  = trim( $rewrite_slug, '/' );
     1579        $expected_path = $rewrite_slug . '/' . $term->slug;
     1580
     1581        return $path === $expected_path || 0 === strpos( $path, $expected_path . '/' );
     1582    }
     1583
     1584    /**
     1585     * Match post type rule early by checking URL against stored slugs.
     1586     *
     1587     * Uses slugs stored in the rule at save time, because CPTs may not be
     1588     * registered yet during setup_theme hook.
     1589     *
     1590     * @since 1.2.0
     1591     *
     1592     * @param array $rule Rule array with optional archive_slug and rewrite_slug.
     1593     * @return bool Whether current URL matches the post type.
     1594     */
     1595    private function match_post_type_early( $rule ) {
     1596        if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
     1597            return false;
     1598        }
     1599
     1600        $sanitized_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
     1601        $request_uri   = parse_url( $sanitized_uri, PHP_URL_PATH );
     1602        $path          = trim( $request_uri, '/' );
     1603
     1604        // First try stored slugs (works even before CPT is registered).
     1605        if ( ! empty( $rule['archive_slug'] ) ) {
     1606            $archive_slug = trim( $rule['archive_slug'], '/' );
     1607            if ( $path === $archive_slug || 0 === strpos( $path, $archive_slug . '/' ) ) {
     1608                return true;
     1609            }
     1610        }
     1611
     1612        if ( ! empty( $rule['rewrite_slug'] ) ) {
     1613            $rewrite_slug = trim( $rule['rewrite_slug'], '/' );
     1614            if ( 0 === strpos( $path, $rewrite_slug . '/' ) ) {
     1615                return true;
     1616            }
     1617        }
     1618
     1619        // Fallback: try get_post_type_object if available (e.g., late calls).
     1620        $post_type     = $rule['value'];
     1621        $post_type_obj = get_post_type_object( $post_type );
     1622        if ( $post_type_obj ) {
     1623            if ( $post_type_obj->has_archive ) {
     1624                $archive_slug = true === $post_type_obj->has_archive ? $post_type : $post_type_obj->has_archive;
     1625                $archive_slug = trim( $archive_slug, '/' );
     1626                if ( $path === $archive_slug || 0 === strpos( $path, $archive_slug . '/' ) ) {
     1627                    return true;
     1628                }
     1629            }
     1630            if ( is_array( $post_type_obj->rewrite ) && isset( $post_type_obj->rewrite['slug'] ) ) {
     1631                $rewrite_slug = trim( $post_type_obj->rewrite['slug'], '/' );
     1632                if ( 0 === strpos( $path, $rewrite_slug . '/' ) ) {
     1633                    return true;
     1634                }
     1635            }
     1636        }
     1637
     1638        return false;
     1639    }
     1640
     1641    /**
     1642     * Match CPT item rule early by querying database directly.
     1643     *
     1644     * @since 1.2.0
     1645     *
     1646     * @param int    $post_id     Post ID.
     1647     * @param string $post_status Expected post status.
     1648     * @return bool Whether current URL matches the CPT item.
     1649     */
     1650    private function match_cpt_item_early( $post_id, $post_status = 'publish' ) {
     1651        global $wpdb;
     1652
     1653        if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
     1654            return false;
     1655        }
     1656
     1657        $post_id = absint( $post_id );
     1658        $post    = $wpdb->get_row(
     1659            $wpdb->prepare(
     1660                "SELECT post_name, post_type, post_status FROM {$wpdb->posts} WHERE ID = %d AND post_status = %s",
     1661                $post_id,
     1662                $post_status
     1663            )
     1664        );
     1665
     1666        if ( ! $post ) {
     1667            return false;
     1668        }
     1669
     1670        // Skip built-in page/post types — they have their own matchers.
     1671        if ( in_array( $post->post_type, array( 'page', 'post' ), true ) ) {
     1672            return false;
     1673        }
     1674
     1675        $slug          = $post->post_name;
     1676        $sanitized_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
     1677        $request_uri   = parse_url( $sanitized_uri, PHP_URL_PATH );
     1678        $path          = trim( $request_uri, '/' );
     1679        $path_segments = explode( '/', $path );
     1680
     1681        // Determine the rewrite base for this CPT.
     1682        $rewrite_base  = $post->post_type;
     1683        $post_type_obj = get_post_type_object( $post->post_type );
     1684        if ( $post_type_obj && is_array( $post_type_obj->rewrite ) && isset( $post_type_obj->rewrite['slug'] ) ) {
     1685            $rewrite_base = trim( $post_type_obj->rewrite['slug'], '/' );
     1686        }
     1687
     1688        // Match only when the slug appears directly after the rewrite base.
     1689        $base_segments = explode( '/', $rewrite_base );
     1690        $base_len      = count( $base_segments );
     1691
     1692        for ( $i = 0, $total = count( $path_segments ); $i < $total; $i++ ) {
     1693            // Check if rewrite base starts at this position.
     1694            if ( array_slice( $path_segments, $i, $base_len ) === $base_segments ) {
     1695                $slug_index = $i + $base_len;
     1696                if ( isset( $path_segments[ $slug_index ] ) && $path_segments[ $slug_index ] === $slug ) {
     1697                    return true;
     1698                }
     1699            }
     1700        }
     1701
     1702        return false;
     1703    }
     1704
     1705    /**
     1706     * Capture registered CPTs and taxonomies for the currently active theme.
     1707     *
     1708     * Runs on `init` at priority 999 (after all CPTs/taxonomies are registered).
     1709     * Stores full registration args so CPTs can be re-registered when a different
     1710     * theme is active.
     1711     *
     1712     * @since 1.2.0
     1713     */
     1714    public function capture_theme_objects() {
     1715        // Get the currently active theme (may be the switched theme due to our rules).
     1716        // We capture CPTs/taxonomies under this theme key so they can be
     1717        // re-registered when a different theme is active via switching rules.
     1718        $default_theme    = get_option( 'stylesheet' );
     1719        $registry         = get_option( $this->theme_registry_option, array() );
     1720        $installed_themes = wp_get_themes();
     1721
     1722        // Prune registry entries for themes that are no longer installed.
     1723        $registry = array_intersect_key( $registry, $installed_themes );
     1724
     1725        // Collect public CPTs (excluding built-in page/post/attachment).
     1726        $post_types = get_post_types( array( 'public' => true ), 'objects' );
     1727        $cpt_data   = array();
     1728        foreach ( $post_types as $pt ) {
     1729            if ( in_array( $pt->name, array( 'page', 'post', 'attachment' ), true ) ) {
     1730                continue;
     1731            }
     1732            // Store args needed to re-register this CPT.
     1733            $cpt_data[ $pt->name ] = array(
     1734                'label'        => $pt->label,
     1735                'labels'       => (array) $pt->labels,
     1736                'public'       => $pt->public,
     1737                'has_archive'  => $pt->has_archive,
     1738                'rewrite'      => $pt->rewrite,
     1739                'supports'     => get_all_post_type_supports( $pt->name ),
     1740                'menu_icon'    => $pt->menu_icon,
     1741                'show_in_rest' => $pt->show_in_rest,
     1742                'rest_base'    => $pt->rest_base,
     1743                'taxonomies'   => get_object_taxonomies( $pt->name ),
     1744            );
     1745        }
     1746
     1747        // Collect public taxonomies with registration args.
     1748        $taxonomies = get_taxonomies( array( 'public' => true ), 'objects' );
     1749        $tax_data   = array();
     1750        foreach ( $taxonomies as $tax ) {
     1751            if ( in_array( $tax->name, array( 'category', 'post_tag', 'post_format' ), true ) ) {
     1752                continue;
     1753            }
     1754            $tax_data[ $tax->name ] = array(
     1755                'label'        => $tax->label,
     1756                'labels'       => (array) $tax->labels,
     1757                'public'       => $tax->public,
     1758                'hierarchical' => $tax->hierarchical,
     1759                'rewrite'      => $tax->rewrite,
     1760                'show_in_rest' => $tax->show_in_rest,
     1761                'rest_base'    => $tax->rest_base,
     1762                'object_type'  => $tax->object_type,
     1763            );
     1764        }
     1765
     1766        $new_entry = array(
     1767            'post_types' => $cpt_data,
     1768            'taxonomies' => $tax_data,
     1769        );
     1770
     1771        // Only update if data changed to avoid unnecessary DB writes.
     1772        if ( ! isset( $registry[ $default_theme ] ) || $registry[ $default_theme ] !== $new_entry ) {
     1773            $registry[ $default_theme ] = $new_entry;
     1774            update_option( $this->theme_registry_option, $registry, false );
     1775        }
     1776    }
     1777
     1778    /**
     1779     * Re-register CPTs and taxonomies that are missing in the current theme
     1780     * but are referenced by switching rules.
     1781     *
     1782     * Runs on `init` at priority 998, just before capture (999).
     1783     * This ensures that when an alternative theme is active (via our rules),
     1784     * CPTs from the original theme are still available so WordPress can
     1785     * parse URLs and serve content correctly.
     1786     *
     1787     * @since 1.2.0
     1788     */
     1789    public function reregister_missing_cpts() {
     1790        $registry = get_option( $this->theme_registry_option, array() );
     1791
     1792        if ( empty( $registry ) ) {
     1793            return;
     1794        }
     1795
     1796        // Collect all CPT slugs and taxonomy slugs referenced in rules.
     1797        $rules        = $this->get_rules();
     1798        $needed_cpts  = array();
     1799        $needed_taxes = array();
     1800
     1801        foreach ( $rules as $rule ) {
     1802            if ( 'post_type' === $rule['type'] ) {
     1803                $needed_cpts[ $rule['value'] ] = true;
     1804            }
     1805            if ( in_array( $rule['type'], array( 'cpt_item', 'draft_cpt_item', 'pending_cpt_item', 'private_cpt_item', 'future_cpt_item' ), true ) ) {
     1806                if ( ! empty( $rule['post_type'] ) ) {
     1807                    $needed_cpts[ $rule['post_type'] ] = true;
     1808                }
     1809            }
     1810            if ( 'taxonomy' === $rule['type'] && ! empty( $rule['taxonomy'] ) ) {
     1811                $needed_taxes[ $rule['taxonomy'] ] = true;
     1812            }
     1813        }
     1814
     1815        if ( empty( $needed_cpts ) && empty( $needed_taxes ) ) {
     1816            return;
     1817        }
     1818
     1819        // Find CPT/taxonomy args from any theme in the registry.
     1820        foreach ( $needed_cpts as $cpt_slug => $_ ) {
     1821            if ( post_type_exists( $cpt_slug ) ) {
     1822                continue;
     1823            }
     1824
     1825            // Search all themes in registry for this CPT's args.
     1826            $args = $this->find_cpt_args_in_registry( $cpt_slug, $registry );
     1827            if ( $args ) {
     1828                register_post_type( $cpt_slug, $args );
     1829            }
     1830        }
     1831
     1832        foreach ( $needed_taxes as $tax_slug => $_ ) {
     1833            if ( taxonomy_exists( $tax_slug ) ) {
     1834                continue;
     1835            }
     1836
     1837            $tax_info = $this->find_taxonomy_args_in_registry( $tax_slug, $registry );
     1838            if ( $tax_info ) {
     1839                register_taxonomy( $tax_slug, $tax_info['object_type'], $tax_info['args'] );
     1840            }
     1841        }
     1842    }
     1843
     1844    /**
     1845     * Find CPT registration args from any theme in the registry.
     1846     *
     1847     * @since 1.2.0
     1848     *
     1849     * @param string $cpt_slug CPT slug to find.
     1850     * @param array  $registry Full theme object registry.
     1851     * @return array|false Registration args or false if not found.
     1852     */
     1853    private function find_cpt_args_in_registry( $cpt_slug, $registry ) {
     1854        foreach ( $registry as $theme_data ) {
     1855            if ( isset( $theme_data['post_types'][ $cpt_slug ] ) ) {
     1856                $stored = $theme_data['post_types'][ $cpt_slug ];
     1857                return array(
     1858                    'label'        => $stored['label'],
     1859                    'labels'       => $stored['labels'],
     1860                    'public'       => $stored['public'],
     1861                    'has_archive'  => $stored['has_archive'],
     1862                    'rewrite'      => $stored['rewrite'],
     1863                    'supports'     => array_keys( array_filter( $stored['supports'] ) ),
     1864                    'menu_icon'    => $stored['menu_icon'],
     1865                    'show_in_rest' => $stored['show_in_rest'],
     1866                    'rest_base'    => $stored['rest_base'],
     1867                    'taxonomies'   => $stored['taxonomies'],
     1868                );
     1869            }
     1870        }
     1871        return false;
     1872    }
     1873
     1874    /**
     1875     * Find taxonomy registration args from any theme in the registry.
     1876     *
     1877     * @since 1.2.0
     1878     *
     1879     * @param string $tax_slug Taxonomy slug to find.
     1880     * @param array  $registry Full theme object registry.
     1881     * @return array|false Array with 'object_type' and 'args' keys, or false if not found.
     1882     */
     1883    private function find_taxonomy_args_in_registry( $tax_slug, $registry ) {
     1884        foreach ( $registry as $theme_data ) {
     1885            if ( isset( $theme_data['taxonomies'][ $tax_slug ] ) ) {
     1886                $stored = $theme_data['taxonomies'][ $tax_slug ];
     1887                return array(
     1888                    'object_type' => $stored['object_type'],
     1889                    'args'        => array(
     1890                        'label'        => $stored['label'],
     1891                        'labels'       => $stored['labels'],
     1892                        'public'       => $stored['public'],
     1893                        'hierarchical' => $stored['hierarchical'],
     1894                        'rewrite'      => $stored['rewrite'],
     1895                        'show_in_rest' => $stored['show_in_rest'],
     1896                        'rest_base'    => $stored['rest_base'],
     1897                    ),
     1898                );
     1899            }
     1900        }
     1901        return false;
     1902    }
    12621903}
  • osom-multi-theme-switcher/trunk/includes/class-osom-multi-theme-switcher.php

    r3461829 r3464901  
    2626     * @var string
    2727     */
    28     const VERSION = '1.0.3';
     28    const VERSION = '1.2.1';
    2929
    3030    /**
     
    7171
    7272    /**
     73     * Status sync instance.
     74     *
     75     * @var OMTS_Status_Sync
     76     */
     77    public $status_sync;
     78
     79    /**
    7380     * Get single instance.
    7481     *
     
    105112        require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-omts-ajax-handler.php';
    106113        require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-omts-acf-loader.php';
     114        require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-omts-status-sync.php';
    107115    }
    108116
     
    118126        // Initialize ACF loader (loads ACF JSON from all themes).
    119127        $this->acf_loader = new OMTS_ACF_Loader();
     128
     129        // Initialize status sync (automatically updates rules on post status changes).
     130        $this->status_sync = new OMTS_Status_Sync( $this->theme_switcher );
    120131
    121132        // Initialize admin components.
  • osom-multi-theme-switcher/trunk/osom-multi-theme-switcher.php

    r3461829 r3464901  
    44 * Plugin URI:        https://github.com/osomstudio/osom-multi-theme-switcher
    55 * Description:       Allows you to use different themes for specific pages, posts, or URLs while keeping a main theme active.
    6  * Version:           1.0.3
     6 * Version:           1.2.1
    77 * Requires at least: 5.0
    88 * Requires PHP:      7.0
     
    2323// Define plugin constants.
    2424if ( ! defined( 'OMTS_VERSION' ) ) {
    25     define( 'OMTS_VERSION', '1.0.3' );
     25    define( 'OMTS_VERSION', '1.2.1' );
    2626}
    2727
  • osom-multi-theme-switcher/trunk/readme.txt

    r3461835 r3464901  
    9393== Screenshots ==
    9494
    95 1. Theme Switcher rules panel — manage all your theme rules from one place
    96 2. Adding a new rule — select content type, target, and alternative theme
    97 3. Admin bar theme switcher — quickly access settings for any installed theme
    98 4. Different themes on different pages — same site, different designs
     951. Admin settings page - Add new theme rules and manage existing ones
     962. Rule type selection - Choose from pages, posts, post types, URLs, categories, or tags
     973. Admin bar theme switcher - Quickly switch themes in the WordPress dashboard
     984. Side-by-side comparison of themes before/after theme switch
    9999
    100100= Built by Osom Studio =
     
    105105
    106106== Changelog ==
     107
     108= 1.2.1 =
     109* Added cascading selector UI for adding theme rules (Custom Post Type, Taxonomy support)
     110* Added automatic status synchronization (OMTS_Status_Sync) — rules update when post status changes
     111* Added CPT/taxonomy registry for re-registering missing CPTs across themes (OMTS_Theme_Switcher)
     112* Added early URL matching for categories, tags, custom taxonomies, post types, and CPT items
     113* Added is_query_ready() guard to prevent rule matching before WP_Query is available
     114* Added theme existence validation before switching template/stylesheet
     115* Added get_rule_type_display() shared static method for consistent rule type labels
     116* Added new AJAX handlers: omts_get_rule_objects, omts_get_rule_items
    107117
    108118= 1.0.3 =
Note: See TracChangeset for help on using the changeset viewer.