Plugin Directory

Changeset 2864100


Ignore:
Timestamp:
02/13/2023 02:01:35 AM (3 years ago)
Author:
manfcarlo
Message:

v1.2.0

Location:
wp-super-network
Files:
18 added
12 edited
1 copied

Legend:

Unmodified
Added
Removed
  • wp-super-network/tags/1.2.0/init.php

    r2671848 r2864100  
    2424require_once SUPER_NETWORK_DIR . 'vendor/autoload.php';
    2525
     26// Load superclasses.
     27require_once SUPER_NETWORK_DIR . 'src/node.php';
     28
    2629// Load classes.
    2730require_once SUPER_NETWORK_DIR . 'src/blog.php';
     31require_once SUPER_NETWORK_DIR . 'src/expression.php';
    2832require_once SUPER_NETWORK_DIR . 'src/field.php';
     33require_once SUPER_NETWORK_DIR . 'src/insert.php';
    2934require_once SUPER_NETWORK_DIR . 'src/network.php';
    3035require_once SUPER_NETWORK_DIR . 'src/page.php';
    3136require_once SUPER_NETWORK_DIR . 'src/plugin.php';
     37require_once SUPER_NETWORK_DIR . 'src/query.php';
    3238require_once SUPER_NETWORK_DIR . 'src/section.php';
     39require_once SUPER_NETWORK_DIR . 'src/subquery.php';
     40require_once SUPER_NETWORK_DIR . 'src/table.php';
    3341
    3442// Initialize the plugin.
  • wp-super-network/tags/1.2.0/readme.txt

    r2671848 r2864100  
    22Contributors: manfcarlo
    33Tags: network, multisite, share, sharing, move, migrate, migration, duplicate, syndication, content, management
    4 Tested up to: 5.9
    5 Stable tag: 1.1.0
     4Tested up to: 6.1
     5Stable tag: 1.2.0
    66License: GPLv2 or later
    77License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    4040= How do I republish a page to the whole network? =
    4141
    42 As of version 1.0.5, you could flag any post or page on a main site for republication by finding it on the Posts or Pages screen and clicking Republish, but it would not take any effect. Version 1.1.0 now implements the behaviour of this feature!
     42Flag any post or page on a main site for republication by finding it on the Posts or Pages screen and clicking Republish.
    4343
    4444After you have republished the post or page, it will start showing up across every site on your network. You can also access the network admin and click on Super Network. You will see a list of all posts and pages across all sites in the network that have been flagged for republication.
     
    5252= How do I turn an existing site into the main site for a new network? =
    5353
    54 This feature is still to come.
     54This feature is scheduled for the next major release.
    5555
    5656= How do I manage all of my sites in a single admin area? =
     
    6262If two or more sites on a network have a post with the same numeric ID, none of these posts is able to be accessed on consolidated mode. You can eliminate post ID collisions by deleting posts you don't want via the network admin screen > Super Network.
    6363
    64 = Why are republished posts and pages not editable in the admin area? =
     64= Is this the same as Global Terms? =
    6565
    66 This feature is still to come.
     66No!
     67
     68WP Super Network does not enable any "global" data relationships. The data you can create with WP Super Network is the same as the data you can create without it. WP Super Network merely brings the management and the viewing of the data together into the one place.
     69
     70= Is this the same as WP Multi Network? =
     71
     72No!
     73
     74WP Super Network is compatible with multi-network environments, but it does not include the management tools that WP Multi Network includes. Therefore, the two plugins have different purposes and can work well together.
     75
     76= Is this the same as Distributor? =
     77
     78No!
     79
     80WP Super Network does not duplicate content between sites, and all posts will always continue to have one and only one permalink. Posts from across the network may preview in archives, but their permalinks will always direct back to their original respective sites.
     81
     82= Is this the same as MainWP? =
     83
     84Only a little bit.
     85
     86WP Super Network and MainWP share a few similar features. However, MainWP is only a good fit for users who don't use the block editor and don't use multisite, since MainWP is yet to announce any support for the block editor and MainWP's own documentation states that it does not test or support multisite. This is despite the block editor and multisite both being core WordPress features for a significant number of years.
     87
     88Also, MainWP's features do not integrate with the core WordPress interface and can only be accessed through its own specially-built interface.
     89
     90Although WP Super Network requires multisite, it does not require you to learn a new interface. Instead, you can continue working with the core WordPress interfaces you are already familiar with, including both the block editor and classic editor.
     91
     92= Which plugins are incompatible with WP Super Network? =
     93
     94WP Super Network is incompatible with [Link Manager](https://wordpress.org/plugins/link-manager/). Users of Link Manager will not be supported and are advised to not install WP Super Network.
     95
     96WP Super Network is currently incompatible with [SQLite Database Integration](https://wordpress.org/plugins/sqlite-database-integration/). Users of SQLite Database Integration are advised to not install WP Super Network for the time being. Compatibility is planned for a later release, so check back later.
     97
     98If you use a plugin that adds a db.php drop-in, it may or may not be incompatible with WP Super Network. To know if you have a db.php drop-in, go to the Plugins screen, click on Drop-in and check if db.php is listed. If the db.php drop-in was added by a plugin that follows the WordPress conventions well and has high user ratings, you are unlikely to see any incompatibility issues with WP Super Network, but you are welcome to ask on the support forum.
     99
     100= Is WP Super Network safe to install? =
     101
     102See above question about incompatible plugins.
     103
     104For best results, the free version of WP Super Network should be activated on a fresh network. While results will vary, you may find that some features (e.g. consolidated mode) are limited in utility for existing networks and/or cause a slow-down in performance for large networks.
     105
     106None the less, the utmost care has been taken to avoid permanent data loss or corruption. Keeping regular database back-ups is always recommended, but deactivating the plugin should return your network back to its previous state.
     107
     108A premium version is planned for the future, focusing on enhanced performance for medium-to-large networks.
    67109
    68110= How do I suggest a new feature or submit a bug report? =
     
    71113
    72114== Changelog ==
     115
     116= 1.2.0 =
     117* Republished posts and pages can be inserted, updated and deleted
     118* Availability of taxonomies and comments for republished posts
     119* Changed behaviour of select queries that target a specific post
     120* Cache implemented for duplicate queries
     121* Fixed a few bugs with network-based options
     122* Minimum PHP version lifted to 7.2
    73123
    74124= 1.1.0 =
  • wp-super-network/tags/1.2.0/src/blog.php

    r2671848 r2864100  
    3838            return $this->wp_site->site_id;
    3939        }
     40
     41        if ( $key === 'name' )
     42        {
     43            return $this->wp_site->blogname;
     44        }
    4045    }
    4146   
  • wp-super-network/tags/1.2.0/src/network.php

    r2671848 r2864100  
    77class Network
    88{
    9     const ID_COLS = array(
    10         'comments' => 'comment_post_ID',
    11         'postmeta' => 'post_id',
    12         'posts' => 'ID',
    13         'term_relationships' => 'object_id'
    14     );
    15 
    169    /**
    1710     * Supernetwork
     
    5447    private $page;
    5548
    56     private $parser;
    57     private $creator;
    58 
    59     private $collisions = array();
     49    /**
     50     * Keeps track of ID collisions.
     51     *
     52     * @since 1.2.0
     53     * @var array
     54     */
     55    private $collisions = WP_Super_Network::ENTITIES_TO_REPLACE;
     56
     57    /**
     58     * Cache that maps ID's to blogs.
     59     *
     60     * @since 1.2.0
     61     * @var array
     62     */
     63    private $blog_cache = WP_Super_Network::ENTITIES_TO_REPLACE;
     64
    6065    private $republished = array();
    6166    private $post_types = array();
    6267
     68    private $meta_ids = array();
     69
    6370    public function shared_auto_increment( $post_ID, $post, $update )
    6471    {
     
    6976    }
    7077
    71     private function set_auto_increment( $new )
     78    public function auto_increment_comments( $id, $comment )
     79    {
     80        if ( $this->consolidated || in_array( (string) $comment->comment_post_ID, $this->republished, true ) )
     81        {
     82            $this->set_auto_increment( $id + 1, 'comments' );
     83        }
     84    }
     85
     86    public function auto_increment_terms( $term_id, $tt_id, $taxonomy, $args )
     87    {
     88        if ( $this->consolidated )
     89        {
     90            $this->set_auto_increment( $tt_id + 1, 'term_taxonomy' );
     91            $this->set_auto_increment( $term_id + 1, 'terms' );
     92        }
     93    }
     94
     95    public function auto_increment_term_relationships( $object_id, $tt_id, $taxonomy )
     96    {
     97        if ( !$this->consolidated && in_array( (string) $object_id, $this->republished, true ) )
     98        {
     99            $this->set_auto_increment( $tt_id + 1, 'term_taxonomy' );
     100            $term = get_term_by( 'term_taxonomy_id', $tt_id ) and $this->set_auto_increment( $term->term_id + 1, 'terms' );
     101        }
     102    }
     103
     104    private function set_auto_increment( $new, $entity = 'posts' )
    72105    {
    73106        foreach ( $this->blogs as $blog )
    74107        {
    75             if ( $new > (int) $GLOBALS['wpdb']->get_var( 'select auto_increment from information_schema.tables where table_schema = \'' . DB_NAME . '\' and table_name = \'' . $blog->table( 'posts' ) . '\'' ) )
    76             {
    77                 $GLOBALS['wpdb']->query( 'alter table ' . $blog->table( 'posts' ) . ' auto_increment = ' . (string) $new );
     108            if ( $new > (int) $GLOBALS['wpdb']->get_var( 'SELECT `auto_increment` FROM `information_schema`.`tables` WHERE `table_schema` = \'' . DB_NAME . '\' AND `table_name` = \'' . $blog->table( $entity ) . '\'' ) )
     109            {
     110                $GLOBALS['wpdb']->query( 'ALTER TABLE `' . $blog->table( $entity ) . '` AUTO_INCREMENT = ' . (string) $new );
    78111            }
    79112        }
     
    96129            return $this->republished;
    97130        }
    98     }
    99 
    100     public function __set( $key, $value )
    101     {
    102         if ( $key === 'consolidated' )
    103         {
    104             if ( $value === true )
    105             {
    106                 $this->consolidated = true;
    107             }
    108            
    109             if ( $value === false )
    110             {
    111                 $this->consolidated = false;
    112 
    113                 if ( !empty( $this->republished ) )
    114                 {
    115                     $this->set_auto_increment( $this->republished[0] + 1 );
    116                 }
    117             }
    118         }
    119     }
    120 
    121     private function union( $table )
    122     {
    123         if ( !$this->consolidated && empty( $this->republished ) )
     131
     132        if ( $key === 'post_types' )
     133        {
     134            return $this->post_types;
     135        }
     136    }
     137
     138    public function union( $table )
     139    {
     140        // No need to do anything if not consolidated and no republished posts.
     141        if ( !$this->consolidated && ( empty( $this->republished ) || empty( $post_cols = array_keys( WP_Super_Network::TABLES_TO_REPLACE[ $table ], 'posts', true ) ) ) )
    124142        {
    125143            return $GLOBALS['wpdb']->__get( $table );
     
    131149        {
    132150            $where = array();
    133 
    134             if ( $this->consolidated )
    135             {
    136                 if ( !empty( $this->collisions ) )
    137                 {
    138                     $where[] = self::ID_COLS[ $table ] . ' not in (' . implode( ', ', $this->collisions ) . ')';
    139                 }
    140 
    141                 if ( $table === 'posts' && !empty( $this->post_types ) && !$blog->is_network() )
    142                 {
    143                     $where[] = 'post_type not in (\'' . implode( '\', \'', $this->post_types ) . '\')';
    144                 }
    145             }
    146             else
    147             {
     151            $this->exclude( $where, $table, $blog );
     152
     153            if ( !$this->consolidated )
     154            {
     155                // Add republished posts.
    148156                if ( $blog->table( $table ) !== $GLOBALS['wpdb']->__get( $table ) )
    149157                {
    150                     $where[] = self::ID_COLS[ $table ] . ' in (' . implode( ', ', $this->republished ) . ')';
     158                    foreach ( $post_cols as $col )
     159                    {
     160                        // Post parent is currently not being handled.
     161                        $col === 'post_parent' or $where[] = '`' . $col . '` IN (' . implode( ', ', $this->republished ) . ')';
     162                    }
    151163                }
    152164            }
    153165
    154             $tables[] = 'select * from ' . $blog->table( $table ) . ( empty( $where ) ? '' : ( ' where ' . implode( ' and ', $where ) ) );
    155         }
    156 
    157         return implode( ' union ', $tables );
     166            $tables[] = 'SELECT * FROM `' . $blog->table( $table ) . ( empty( $where ) ? '`' : ( '` WHERE ' . implode( ' AND ', $where ) ) );
     167        }
     168
     169        return implode( ' UNION ALL ', $tables );
     170    }
     171
     172    public function exclude( &$where, $table, $blog, $alias = '' )
     173    {
     174        if ( $this->consolidated )
     175        {
     176            empty( $alias ) or $alias .= '.';
     177
     178            // Exclude any entities involved in collisions.
     179            foreach ( array_keys( $this->collisions ) as $entity )
     180            {
     181                if ( !empty( $this->collisions[ $entity ] ) )
     182                {
     183                    foreach ( array_keys( WP_Super_Network::TABLES_TO_REPLACE[ $table ], $entity, true ) as $col )
     184                    {
     185                        $where[] = $alias . '`' . $col . '` NOT IN (' . implode( ', ', $this->collisions[ $entity ] ) . ')';
     186                    }
     187                }
     188            }
     189
     190            // Exclude network-based post types.
     191            if ( $table === 'posts' && !empty( $this->post_types ) && !$blog->is_network() )
     192            {
     193                $where[] = $alias . '`post_type` NOT IN (\'' . implode( '\', \'', $this->post_types ) . '\')';
     194            }
     195        }
    158196    }
    159197
    160198    public function report_collisions()
    161199    {
    162         if ( $this->consolidated && !empty( $this->collisions ) && current_user_can( 'manage_network_options' ) )
    163         {
    164             echo '<div class="notice notice-error"><p><strong>WP Super Network detected ' . count( $this->collisions ) . ' post ID collisions</strong> across your network! These posts have been temporarily hidden. To access them again, please turn off consolidated mode.</p></div>';
     200        $term_collisions = count( $this->collisions['term_taxonomy'] ) + count( $this->collisions['terms'] );
     201
     202        if ( $this->consolidated && ( !empty( $this->collisions['posts'] ) || $term_collisions > 0 || !empty( $this->collisions['comments'] ) ) && current_user_can( 'manage_network_options' ) )
     203        {
     204            echo '<div class="notice notice-error"><p><strong>WP Super Network detected ';
     205
     206            echo empty( $this->collisions['posts'] ) ? '' : count( $this->collisions['posts'] ) . ' post ID collisions';
     207
     208            if ( !empty( $this->collisions['posts'] ) && $term_collisions > 0 )
     209            {
     210                echo empty( $this->collisions['comments'] ) ? ' and ' : ', ';
     211            }
     212
     213            echo $term_collisions > 0 ? $term_collisions . ' taxonomy term ID collisions' : '';
     214
     215            if ( !empty( $this->collisions['comments'] ) )
     216            {
     217                echo ( !empty( $this->collisions['posts'] ) || $term_collisions > 0 ) ? ' and ' : '';
     218                echo count( $this->collisions['comments'] ) . ' comment ID collisions';
     219            }
     220
     221            echo '</strong> across your network! These entities have been temporarily hidden. To access them again, please turn off consolidated mode.</p></div>';
    165222        }
    166223    }
     
    169226     * Constructor.
    170227     *
    171      * Constructs the network.
     228     * Queries sent during this method do not get modified, as the filter is applied later.
    172229     *
    173230     * @since 1.0.4
     
    179236        foreach ( get_sites( 'network_id=' . $network->id ) as $site )
    180237        {
    181             array_push( $this->blogs, new Blog( $site ) );
    182         }
    183 
    184         add_filter( 'admin_footer', array( $this, 'report_collisions' ) );
    185 
    186         $this->parser = new \PHPSQLParser\PHPSQLParser();
    187         $this->creator = new \PHPSQLParser\PHPSQLCreator();
    188 
    189         $this->collisions = $GLOBALS['wpdb']->get_col( 'select ID from (' . $this->union( 'posts' ) . ') posts group by ID having count(*) > 1 order by ID asc' );
    190         $this->republished = $GLOBALS['wpdb']->get_col( 'select post_id from (' . $this->union( 'postmeta' ) . ') postmeta where meta_key = \'_supernetwork_share\' order by post_id desc' );
    191         $this->post_types = array_keys( (array) get_option( 'supernetwork_post_types' ) );
     238            $this->blogs[ (int) $site->blog_id ] = new Blog( $site );
     239        }
    192240
    193241        $this->page = new Settings_Page(
     
    209257
    210258        global $wpdb;
    211         $results = $wpdb->get_results( "select distinct option_name from $wpdb->options where option_name not like '\_%' and option_name not like 'supernetwork\_%' order by option_name" );
     259        $results = $wpdb->get_results( "SELECT DISTINCT `option_name` FROM $wpdb->options WHERE `option_name` NOT LIKE '\_%' AND `option_name` NOT LIKE 'supernetwork\_%' ORDER BY `option_name`" );
    212260        $labels = array();
    213261
     
    252300            'Consolidated Mode',
    253301            'Turn on consolidated mode?',
    254             '<strong>Warning:</strong> Consolidated mode is highly unstable. It is strongly suggested that you have a REGULAR backup regime in place for your database, before activating consolidated mode.'
     302            '<strong>Warning:</strong> Consolidated mode is recommended for fresh networks. If you have pre-existing data (e.g. posts and pages) on your network, some of it may not be compatible with consolidated mode.'
    255303        );
    256304
     
    263311    }
    264312
     313    public function add_new_post( $hook_suffix )
     314    {
     315        $post_type = empty( $_GET['post_type'] ) ? 'post' : $_GET['post_type'];
     316
     317        if ( $hook_suffix === 'edit.php' && $this->consolidated && !in_array( $post_type, $this->post_types, true ) )
     318        {
     319            wp_enqueue_script(
     320                'supernetwork-post-new',
     321                SUPER_NETWORK_URL . 'assets/js/post-new.js',
     322                array(),
     323                null,
     324                true
     325            );
     326
     327            $blogs = array();
     328            $type = get_post_type_object( $post_type );
     329            $edit = isset( $post_type ) ? $type->cap->edit_posts : 'do_not_allow';
     330            $create = isset( $post_type ) ? $type->cap->create_posts : 'do_not_allow';
     331
     332            foreach ( $this->blogs as $blog )
     333            {
     334                $id = $blog->id;
     335
     336                if ( current_user_can_for_blog( $id, $edit ) && current_user_can_for_blog( $id, $create ) )
     337                {
     338                    $blogs[ $id ] = $blog->name;
     339                }
     340            }
     341
     342            wp_add_inline_script(
     343                'supernetwork-post-new',
     344                'var blogs = ' . json_encode( $blogs ) . '; var currentId = "' . get_current_blog_id() . '";',
     345                'before'
     346            );
     347        }
     348    }
     349
    265350    public function post_types( $args, $field )
    266351    {
     
    276361    }
    277362
    278     public function register_pages()
     363    private function push_meta_ids( $meta_ids, $object_id )
     364    {
     365        $this->meta_ids[] = array( $meta_ids, $object_id );
     366    }
     367
     368    public function meta_ids()
     369    {
     370        return empty( $this->meta_ids ) ? array() : $this->meta_ids[ count( $this->meta_ids ) - 1 ][0];
     371    }
     372
     373    public function meta_object_id()
     374    {
     375        return empty( $this->meta_ids ) ? 0 : $this->meta_ids[ count( $this->meta_ids ) - 1 ][1];
     376    }
     377
     378    public function pop_meta_ids()
     379    {
     380        array_pop( $this->meta_ids );
     381    }
     382
     383    public function delete_meta( $meta_ids, $object_id )
     384    {
     385        $this->push_meta_ids( $meta_ids, $object_id );
     386        return $meta_ids;
     387    }
     388
     389    public function register()
    279390    {
    280391        $this->page->register();
     392
     393        add_filter( 'post_type_link', array( $this, 'intercept_permalink_for_post' ), 10, 2 );
     394        add_filter( 'post_link', array( $this, 'intercept_permalink_for_post' ), 10, 2 );
     395        add_filter( 'page_link', array( $this, 'intercept_permalink' ), 10, 2 );
     396        add_filter( 'preview_post_link', array( $this, 'intercept_preview_link' ), 10, 2 );
     397        add_filter( 'supernetwork_preview_link', array( $this, 'replace_preview_link' ), 10, 2 );
     398        add_filter( 'user_has_cap', array( $this, 'intercept_capability' ), 10, 4 );
     399        add_filter( 'pre_handle_404', array( $this, 'singular_access' ), 10, 2 );
     400        add_action( 'wp', array( $this, 'preview_access' ) );
     401        add_filter( 'query', array( $this, 'intercept_query' ), 10, 2 );
     402        add_filter( 'wp_insert_post', array( $this, 'shared_auto_increment' ), 10, 3 );
     403        add_filter( 'wp_insert_comment', array( $this, 'auto_increment_comments' ), 10, 2 );
     404        add_filter( 'created_term', array( $this, 'auto_increment_terms' ), 10, 4 );
     405        add_filter( 'added_term_relationship', array( $this, 'auto_increment_term_relationships' ), 10, 3 );
     406        add_filter( 'admin_enqueue_scripts', array( $this, 'add_new_post' ) );
     407        add_filter( 'admin_footer', array( $this, 'report_collisions' ) );
     408        add_filter( 'delete_comment_meta', array( $this, 'delete_meta' ), 10, 2 );
     409        add_filter( 'delete_post_meta', array( $this, 'delete_meta' ), 10, 2 );
     410        add_filter( 'delete_term_meta', array( $this, 'delete_meta' ), 10, 2 );
     411
     412        // Comments must be queried before posts so as not to mask any comment ID collisions.
     413        // Taxonomy terms must be queried before terms so as not to mask any taxonomy term ID collisions.
     414        foreach ( array_keys( $this->collisions ) as $entity )
     415        {
     416            // `ID` comes before `post_parent` in the `posts` sub-array, so `ID` will be correctly returned.
     417            $id = array_search( $entity, WP_Super_Network::TABLES_TO_REPLACE[ $entity ], true );
     418
     419            $this->collisions[ $entity ] = $GLOBALS['wpdb']->get_col( 'SELECT `' . $id . '` FROM `' . $GLOBALS['wpdb']->__get( $entity ) . '` GROUP BY `' . $id . '` HAVING COUNT(*) > 1 ORDER BY `' . $id . '` ASC' );
     420        }
     421
     422        $consolidated = !empty( get_option( 'supernetwork_consolidated', array() )['consolidated'] );
     423
     424        if ( $consolidated )
     425        {
     426            $this->post_types = array_keys( get_option( 'supernetwork_post_types', array() ) );
     427        }
     428        else
     429        {
     430            $extra_where = '';
     431            $extra_where_2 = '';
     432            $relationships = $GLOBALS['wpdb']->term_relationships;
     433            $taxonomy = $GLOBALS['wpdb']->term_taxonomy;
     434
     435            // The `AND 1=1` is to avoid a bug in the SQL parser caused by consecutive brackets.
     436            empty( $this->collisions['comments'] ) or $extra_where .= ' AND `post_id` NOT IN (SELECT `comment_post_ID` FROM `' . $GLOBALS['wpdb']->comments . '` WHERE `comment_ID` IN (' . implode( ', ', $this->collisions[ 'comments' ] ) . ') AND 1=1)';
     437
     438            if ( !empty( $this->collisions['term_taxonomy'] ) )
     439            {
     440                $extra_where .= ' AND `post_id` NOT IN (SELECT `object_id` FROM `' . $relationships . '` WHERE `term_taxonomy_id` IN (' . implode( ', ', $this->collisions[ 'term_taxonomy' ] ) . ') AND 1=1)';
     441                $extra_where_2 = ' AND `' . $relationships . '`.`term_taxonomy_id` NOT IN (' . implode( ', ', $this->collisions[ 'term_taxonomy' ] ) . ')';
     442            }
     443
     444            empty( $this->collisions['terms'] ) or $extra_where .= ' AND `post_id` NOT IN (SELECT `object_id` FROM `' . $relationships . '` LEFT JOIN `' . $taxonomy . '` ON `' . $relationships . '`.`term_taxonomy_id` = `' . $taxonomy . '`.`term_taxonomy_id` WHERE `term_id` IN (' . implode( ', ', $this->collisions[ 'terms' ] ) . ')' . $extra_where_2 . ' AND 1=1)';
     445
     446            // This query requires post collisions, but not the other collisions, to be recorded.
     447            $old_comment_collisions = $this->collisions['comments'];
     448            $old_taxonomy_term_collisions = $this->collisions['term_taxonomy'];
     449            $old_term_collisions = $this->collisions['terms'];
     450
     451            foreach ( array( 'comments', 'term_taxonomy', 'terms' ) as $entity ) $this->collisions[ $entity ] = array();
     452
     453            $this->republished = $GLOBALS['wpdb']->get_col( 'SELECT DISTINCT `post_id` FROM `' . $GLOBALS['wpdb']->postmeta . '` WHERE `meta_key` = \'_supernetwork_share\'' . $extra_where . ' ORDER BY `post_id` DESC' );
     454
     455            $this->collisions['comments'] = $old_comment_collisions;
     456            $this->collisions['term_taxonomy'] = $old_taxonomy_term_collisions;
     457            $this->collisions['terms'] = $old_term_collisions;
     458        }
     459
     460        $this->consolidated = $consolidated;
     461
     462        if ( !$this->consolidated && !empty( $this->republished ) )
     463        {
     464            $this->set_auto_increment( $this->republished[0] + 1 );
     465        }
     466    }
     467
     468    /**
     469     * Selects an entity type to be first in line for collision elimination, based on a calculated score.
     470     *
     471     * The score for each entity type is the median ID of its collisions, divided by the median ID overall.
     472     * A score of 1 means the collisions are evenly distributed.
     473     * A lower score means the collisions are concentrated on the lower ID's, which are more likely to be older entities with more references and importance.
     474     *
     475     * The entity type with the lowest score is selected for collision elimination.
     476     *
     477     * @since 1.2.0
     478     */
     479    private function get_entity_by_median_score()
     480    {
     481        // Temporarily empty collisions.
     482        $old = $this->collisions;
     483        $this->collisions = WP_Super_Network::ENTITIES_TO_REPLACE;
     484        $scores = WP_Super_Network::ENTITIES_TO_REPLACE;
     485
     486        foreach ( $scores as $entity => &$score )
     487        {
     488            // `ID` comes before `post_parent` in the `posts` sub-array, so `ID` will be correctly returned.
     489            $id = array_search( $entity, WP_Super_Network::TABLES_TO_REPLACE[ $entity ], true );
     490
     491            $collisions = $GLOBALS['wpdb']->get_col( 'SELECT `' . $id . '` FROM `' . $GLOBALS['wpdb']->__get( $entity ) . '` WHERE `' . $id . '` IN (SELECT `' . $id . '` FROM `' . $GLOBALS['wpdb']->__get( $entity ) . '` GROUP BY `' . $id . '` HAVING COUNT(*) > 1) ORDER BY `' . $id . '` ASC' );
     492            $all = $GLOBALS['wpdb']->get_col( 'SELECT `' . $id . '` FROM `' . $GLOBALS['wpdb']->__get( $entity ) . '` ORDER BY `' . $id . '` ASC' );
     493
     494            $score = empty( $collisions ) ? PHP_FLOAT_MAX : $collisions[ floor( count( $collisions ) / 2 ) ] / $all[ floor( count( $all ) / 2 ) ];
     495        }
     496
     497        $this->collisions = $old;
     498        return array_search( min( $scores ), $scores );
     499    }
     500
     501    /**
     502     * Force deletes a term from a taxonomy.
     503     *
     504     * @since 1.2.0
     505     */
     506    private function force_delete_taxonomy_term( $term_id, $taxonomy )
     507    {
     508        // Temporarily register taxonomy to force deletion.
     509        if ( !( $exists = taxonomy_exists( $taxonomy ) ) ) register_taxonomy( $taxonomy, array() );
     510
     511        // 0 is returned if the default term was not deleted.
     512        $success = 0 !== wp_delete_term( $term_id, $taxonomy );
     513
     514        if ( !$exists ) unregister_taxonomy( $taxonomy );
     515        return $success;
    281516    }
    282517
     
    288523        if ( $this->consolidated )
    289524        {
    290             echo '<h2>Post ID Collisions</h2>';
    291 
    292             if ( !empty( $this->collisions ) && isset( $_POST['supernetwork_post_collision_' . $this->collisions[0] ] ) )
    293             {
    294                 $this->consolidated = false;
    295 
    296                 foreach ( $this->blogs as $blog )
     525        $entity = $this->get_entity_by_median_score();
     526
     527        $this->consolidated = false;
     528
     529            echo '<h2>ID Collisions</h2>';
     530
     531        if ( !empty( $this->collisions[ $entity ] ) && isset( $_POST[ 'supernetwork_' . $entity . '_collision_' . $this->collisions[ $entity ][0] ] ) )
     532        {
     533            $success = true;
     534
     535            foreach ( $this->blogs as $blog )
     536            {
     537                if ( $blog->id !== (int) explode( '_', $_POST[ 'supernetwork_' . $entity . '_collision_' . $this->collisions[ $entity ][0] ] )[0] )
    297538                {
    298539                    switch_to_blog( $blog->id );
    299                    
    300                     if ( empty( $GLOBALS['wpdb']->get_col( 'select ID from ' . $blog->table( 'posts' ) . ' where guid = \'' . $_POST['supernetwork_post_collision_' . $this->collisions[0] ] . '\' limit 1' ) ) )
     540
     541                    if ( $entity === 'comments' ) wp_delete_comment( (int) $this->collisions['comments'][0], true );
     542                    if ( $entity === 'posts' ) wp_delete_post( (int) $this->collisions['posts'][0], true );
     543
     544                    if ( $entity === 'term_taxonomy' )
    301545                    {
    302                         wp_delete_post( (int) $this->collisions[0], true );
     546                        $term = $GLOBALS['wpdb']->get_row( 'SELECT `taxonomy`, `term_id` FROM `' . $GLOBALS['wpdb']->term_taxonomy . '` WHERE `term_taxonomy_id` = ' . $this->collisions['term_taxonomy'][0], ARRAY_A );
     547
     548                        if ( isset( $term['term_id'] ) && isset( $term['taxonomy'] ) )
     549                        {
     550                            $success = $this->force_delete_taxonomy_term( (int) $term['term_id'], $term['taxonomy'] ) && $success;
     551                        }
     552                    }
     553
     554                    if ( $entity === 'terms' )
     555                    {
     556                        $results = $GLOBALS['wpdb']->get_results( 'SELECT `taxonomy`, `term_id` FROM `' . $GLOBALS['wpdb']->term_taxonomy . '` WHERE `term_id` = ' . $this->collisions['terms'][0] );
     557
     558                        foreach ( $results as $result )
     559                        {
     560                            $success = $this->force_delete_taxonomy_term( (int) $result['term_id'], $result['taxonomy'] ) && $success;
     561                        }
    303562                    }
    304563
    305564                    restore_current_blog();
    306565                }
    307 
     566            }
     567
     568            // Start again if collision was eliminated.
     569            if ( $success )
     570            {
     571                array_shift( $this->collisions[ $entity ] );
    308572                $this->consolidated = true;
    309                 array_shift( $this->collisions );
    310             }
    311 
    312             if ( empty( $this->collisions ) )
    313             {
    314                 echo '<p>No collisions on this network!</p>';
    315             }
    316             else
    317             {
    318                 $old_collisions = $this->collisions;
    319                 $old_post_types = $this->post_types;
    320 
    321                 $this->collisions = array();
    322                 $this->post_types = array();
    323 
    324                 echo '<p>Consolidated mode is designed for fresh networks. When activated on an existing network, a large number of ID collisions are inevitable. However, you may be able to eliminate some collisions when the ID refers to a post of low importance, such as a revision or autosave.</p>';
    325                 echo '<p>The below table allows you to eliminate post ID collisions, one ID at a time. For each ID, you must select just ONE post to keep. All others with the same ID will be immediately and irretrievably deleted.</p>';
    326 
    327                 echo '<form method="post" action="">';
    328                 echo '<table class="widefat">';
    329                 echo '<thead><tr><th scope="col">Keep?</th><th scope="col">GUID</th><th scope="col">Post Title</th><th scope="col">Post Preview</th><th scope="col">Post Type</th><th scope="col">Post Status</th></tr></thead>';
    330                 echo '<tbody>';
    331 
    332                 foreach ( $GLOBALS['wpdb']->get_results( 'select guid, post_title, substring(post_content, 1, 500) as post_preview, post_type, post_status from ' . $GLOBALS['wpdb']->posts . ' where ID = ' . $old_collisions[0], ARRAY_A ) as $site )
     573                $entity = $this->get_entity_by_median_score();
     574                $this->consolidated = false;
     575            }
     576        }
     577
     578        if ( empty( $this->collisions[ $entity ] ) )
     579        {
     580            echo '<p>No collisions on this network!</p>';
     581        }
     582        else
     583        {
     584            // `ID` comes before `post_parent` in the `posts` sub-array, so `ID` will be correctly returned.
     585            $id = array_search( $entity, WP_Super_Network::TABLES_TO_REPLACE[ $entity ], true );
     586            $id = 'terms' === $entity ? 't.' . $id : $id;
     587
     588            $fields = array(
     589                'comments' => array(
     590                    'post_title',
     591                    'comment_content',
     592                    'comment_author',
     593                    'comment_author_email'
     594                ),
     595                'posts' => array(
     596                    'post_title',
     597                    'post_content',
     598                    'post_type',
     599                    'post_status'
     600                ),
     601                'term_taxonomy' => array(
     602                    'name',
     603                    'description',
     604                    'taxonomy',
     605                    'count'
     606                ),
     607                'terms' => array(
     608                    'name',
     609                    'description',
     610                    'taxonomy',
     611                    'count'
     612                )
     613            );
     614
     615            $labels = array(
     616                'comments' => array(
     617                    'Site Containing Post/Comment',
     618                    'Post Containing Comment',
     619                    'Comment Preview',
     620                    'Comment Author',
     621                    'Comment Author Email'
     622                ),
     623                'posts' => array(
     624                    'Site Containing Post',
     625                    'Post Title',
     626                    'Post Preview',
     627                    'Post Type',
     628                    'Post Status'
     629                ),
     630                'term_taxonomy' => array(
     631                    'Site Containing Taxonomy Term',
     632                    'Taxonomy Term Name',
     633                    'Taxonomy Term Description',
     634                    'Taxonomy',
     635                    'Post Count'
     636                ),
     637                'terms' => array(
     638                    'Site Containing Taxonomy Term',
     639                    'Taxonomy Term Name',
     640                    'Taxonomy Term Description',
     641                    'Taxonomy',
     642                    'Post Count'
     643                )
     644            );
     645
     646            echo '<p>Consolidated mode is designed for fresh networks. When activated on an existing network, a large number of ID collisions are inevitable. However, you may be able to eliminate some collisions when the ID refers to a post of low importance, such as a revision or autosave.</p>';
     647            echo '<p>The below tables allow you to eliminate post, taxonomy term and comment ID collisions, one ID at a time. For each ID, you must select just ONE entity to keep. All others with the same ID will be immediately and irretrievably deleted.</p>';
     648
     649            if ( in_array( $entity, array( 'term_taxonomy', 'terms' ), true ) ) echo '<div class="notice notice-warning inline"><p><strong>Note:</strong> Default taxonomy terms can not be deleted! If a default taxonomy term is in a collision, you must go to Writing Settings and select a new default term for the taxonomy before you can eliminate the collision.</p></div>';
     650
     651            echo '<form method="post" action="">';
     652            echo '<table class="widefat">';
     653            echo '<thead><tr><th scope="col">Keep?</th><th scope="col">' . $labels[ $entity ][0] . '</th><th scope="col">' . $labels[ $entity ][1] . '</th><th scope="col">' . $labels[ $entity ][2] . '</th><th scope="col">' . $labels[ $entity ][3] . '</th><th scope="col">' . $labels[ $entity ][4] . '</th></tr></thead>';
     654            echo '<tbody>';
     655
     656            foreach ( $this->blogs as $blog )
     657            {
     658                $tables = array(
     659                    'comments' => $blog->table( 'comments' ) . ' LEFT JOIN ' . $blog->table( 'posts' ) . ' ON comment_post_ID = ID',
     660                    'posts' => $blog->table( 'posts' ),
     661                    'term_taxonomy' => $blog->table( 'term_taxonomy' ) . ' tt LEFT JOIN ' . $blog->table( 'terms' ) . ' t ON tt.term_id = t.term_id',
     662                    'terms' => $blog->table( 'term_taxonomy' ) . ' tt LEFT JOIN ' . $blog->table( 'terms' ) . ' t ON tt.term_id = t.term_id'
     663                );
     664
     665                $rows = $GLOBALS['wpdb']->get_results( 'SELECT `' . $fields[ $entity ][0] . '`, SUBSTRING(`' . $fields[ $entity ][1] . '`, 1, 500) AS `' . $fields[ $entity ][1] . '_preview`, `' . $fields[ $entity ][2] . '`, `' . $fields[ $entity ][3] . '` FROM ' . $tables[ $entity ] . ' WHERE `' . $id . '` = ' . $this->collisions[ $entity ][0], ARRAY_A );
     666
     667                // There may be more than one row if terms are not split.
     668                foreach ( $rows as $key => $row )
    333669                {
    334670                    echo '<tr>';
    335                     echo '<td><input type="radio" id="supernetwork__' . esc_textarea( $site['guid'] ) . '" value="' . esc_textarea( $site['guid'] ) . '" name="supernetwork_post_collision_' . $old_collisions[0] . '"></td>';
    336                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['guid'] ) . '</label></td>';
    337                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['post_title'] ) . '</label></td>';
    338                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['post_preview'] ) . '</label></td>';
    339                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['post_type'] ) . '</label></td>';
    340                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['post_status'] ) . '</label></td>';
     671                    echo '<td><input type="radio" id="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '" value="' . esc_attr( $blog->id ) . '" name="supernetwork_' . $entity . '_collision_' . $this->collisions[ $entity ][0] . '"></td>';
     672                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $blog->name ) . '</label></td>';
     673                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $row[ $fields[ $entity ][0] ] ) . '</label></td>';
     674                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $row[ $fields[ $entity ][1] . '_preview' ] ) . '</label></td>';
     675                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $row[ $fields[ $entity ][2] ] ) . '</label></td>';
     676                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $row[ $fields[ $entity ][3] ] ) . '</label></td>';
    341677                    echo '</tr>';
    342678                }
    343 
    344                 $this->collisions = $old_collisions;
    345                 $this->post_types = $old_post_types;
    346 
    347                 echo '</tbody></table>';
    348                 submit_button( 'Keep Selected and Delete All Others' );
    349                 echo '</form>';
    350             }
    351         }
     679            }
     680
     681            echo '</tbody></table>';
     682            submit_button( 'Keep Selected and Delete All Others' );
     683            echo '</form>';
     684        }
     685
     686        $this->consolidated = true;
     687    }
    352688        else
    353689        {
     
    419755    public function intercept_query( $query )
    420756    {
    421         try
    422         {
    423             $parsed = $this->parser->parse( $query );
    424 
    425             // Insert, update and delete queries are not currently modified.
    426             return $this->modify_query( $parsed ) ? $this->creator->create( $parsed ) : $query;
    427         }
    428         catch ( \PHPSQLParser\exceptions\UnsupportedFeatureException $uf )
    429         {
    430             return $query;
    431         }
    432         catch ( \PHPSQLParser\exceptions\UnableToCreateSQLException $utcsql )
    433         {
    434             return $query;
    435         }
    436     }
    437 
    438     private function modify_query( &$parsed )
    439     {
    440         if ( in_array( array_keys( $parsed )[0], array( 'UNION', 'SELECT' ), true ) )
    441         {
    442             if ( isset( $parsed['UNION'] ) )
    443             {
    444                 foreach ( $parsed['UNION'] as &$query )
    445                 {
    446                     $this->modify_query( $query );
    447                 }
    448             }
    449             else
    450             {
    451                 if ( isset( $parsed['SELECT'] ) && isset( $parsed['FROM'] ) )
    452                 {
    453                     foreach ( $parsed['FROM'] as &$from )
    454                     {
    455                         if ( !empty( $from['sub_tree'] ) )
    456                         {
    457                             $this->modify_query( $from['sub_tree'] );
    458                         }
    459                         else
    460                         {
    461                             foreach ( array_keys( self::ID_COLS ) as $table )
    462                             {
    463                                 $local_table = $GLOBALS['wpdb']->__get( $table );
    464 
    465                                 if ( isset( $from['table'] ) && $from['table'] === $local_table )
    466                                 {
    467                                     if ( ( $union = $this->union( $table ) ) === $from['table'] )
    468                                     {
    469                                         continue;
    470                                     }
    471 
    472                                     $from['expr_type'] = 'subquery';
    473                                     $from['sub_tree'] = $this->parser->parse( $union );
    474 
    475                                     if ( false === $from['alias'] )
    476                                     {
    477                                         $from['alias'] = array(
    478                                             'as' => false,
    479                                             'name' => $local_table,
    480                                             'no_quotes' => array(
    481                                                 'delim' => false,
    482                                                 'parts' => array( $local_table )
    483                                             ),
    484                                             'base_expr' => $local_table
    485                                         );
    486                                     }
    487 
    488                                     unset( $from['table'] );
    489                                     break;
    490                                 }
    491                             }
    492                         }
    493                     }
    494 
    495                     foreach ( array( 'WHERE', 'HAVING' ) as $clause )
    496                     {
    497                         if ( isset( $parsed[ $clause ] ) )
    498                         {
    499                             foreach ( $parsed[ $clause ] as &$subclause )
    500                             {
    501                                 if ( !empty( $subclause['sub_tree'] ) )
    502                                 {
    503                                     $this->modify_query( $subclause['sub_tree'] );
    504                                 }
    505                             }
    506                         }
    507                     }
    508                 }
    509             }
    510 
    511             return true;
    512         }
    513 
    514         return false;
     757        $cache = wp_cache_get( 'supernetwork_queries', $query );
     758
     759        if ( $cache )
     760        {
     761            return $cache->transformed;
     762        }
     763        else
     764        {
     765            $transformed = new Query( $query, $this );
     766            wp_cache_set( 'supernetwork_queries', $transformed, $query );
     767            return $transformed->transformed;
     768        }
    515769    }
    516770
    517771    public function intercept_permalink( $permalink, $post_ID )
    518772    {
    519         // Prevent infinite loop
    520         if ( empty( $GLOBALS['_wp_switched_stack'] ) )
    521         {
    522             if ( !is_null( $blog = $this->get_blog( $post_ID ) ) )
    523             {
    524                 switch_to_blog( $blog->id );
    525                 $permalink = get_permalink( $post_ID );
    526                 restore_current_blog();
    527             }
     773        if ( !doing_filter( 'supernetwork_preview_link' ) && !is_null( $blog = $this->get_blog( $post_ID ) ) )
     774        {
     775            switch_to_blog( $blog->id );
     776            $permalink = get_permalink( $post_ID );
     777            restore_current_blog();
    528778        }
    529779
     
    536786    }
    537787
    538     public function intercept_capability_write( $caps, $cap, $user_id, $args )
    539     {
    540         if ( in_array( $cap, array( 'delete_post', 'edit_post', 'publish_post' ), true ) )
    541         {
    542             if ( !is_null( $blog = $this->get_blog( get_post( $args[0] )->ID ) ) && get_current_blog_id() !== $blog->id )
    543             {
    544                 return array( 'do_not_allow' );
    545             }
    546         }
    547 
    548         return $caps;
    549     }
    550 
    551     public function intercept_capability_read( $allcaps, $caps, $args, $user )
    552     {
    553         if ( $args[0] === 'read_post' )
     788    public function intercept_preview_link( $preview_link, $post )
     789    {
     790        return doing_filter( 'supernetwork_preview_link' ) ? $preview_link : apply_filters( 'supernetwork_preview_link', $preview_link, $post );
     791    }
     792
     793    public function replace_preview_link( $preview_link, $post )
     794    {
     795        $query_args = array();
     796        parse_str( parse_url( $preview_link, PHP_URL_QUERY ), $query_args );
     797        return get_preview_post_link( $post, array_intersect_key( $query_args, array( 'preview_nonce' => null, 'preview_id' => null ) ) );
     798    }
     799
     800    public function intercept_capability( $allcaps, $caps, $args, $user )
     801    {
     802        if ( in_array( $args[0], array( 'delete_post', 'edit_post', 'publish_post', 'read_post' ), true ) )
    554803        {
    555804            if ( !is_null( $blog = $this->get_blog( get_post( $args[2] )->ID ) ) )
     
    564813    public function singular_access( $false, $wp_query )
    565814    {
    566         if ( $wp_query->is_singular() && !is_admin() && ( !defined( 'REST_REQUEST' ) || !REST_REQUEST ) && isset( $wp_query->post ) && !is_null( $blog = $this->get_blog( $wp_query->post->ID ) ) && get_current_blog_id() !== $blog->id )
     815        if ( $wp_query->is_singular() && !is_admin() && ( !defined( 'REST_REQUEST' ) || !REST_REQUEST ) && !is_preview() && isset( $wp_query->post ) && !is_null( $this->get_blog( $wp_query->post->ID ) ) )
    567816        {
    568817            $wp_query->set_404();
     
    575824    }
    576825
    577     private function get_blog( $id )
    578     {
    579         if ( !in_array( (string) $id, $this->collisions, true ) )
    580         {
     826    public function preview_access()
     827    {
     828        if ( is_preview() && !is_null( $blog = $this->get_blog( get_the_ID() ) ) )
     829        {
     830            switch_to_blog( $blog->id );
     831        }
     832    }
     833
     834    public function get_blog( $id, $entity = 'posts' )
     835    {
     836        if ( in_array( $entity, array_keys( WP_Super_Network::ENTITIES_TO_REPLACE ), true ) && !in_array( (string) $id, $this->collisions[ $entity ], true ) && ( $this->consolidated || !empty( $this->republished ) && $entity !== 'posts' || in_array( (string) $id, $this->republished, true ) ) )
     837        {
     838            if ( isset( $this->blog_cache[ $entity ][ $id ] ) ) return get_current_blog_id() === $this->blog_cache[ $entity ][ $id ]->id ? null : $this->blog_cache[ $entity ][ $id ];
     839
     840            $republished = null;
     841            $extra_where = '';
     842
     843            // `ID` comes before `post_parent` in the `posts` sub-array, so `ID` will be correctly returned.
     844            $col = array_search( $entity, WP_Super_Network::TABLES_TO_REPLACE[ $entity ], true );
     845            $select = $entity === 'posts' ? 'post_type' : $col;
     846
    581847            $old_consolidated = $this->consolidated;
    582848            $old_republished = $this->republished;
     
    587853            foreach ( $this->blogs as $blog )
    588854            {
    589                 if ( !empty( $GLOBALS['wpdb']->get_col( 'select ID from ' . $blog->table( 'posts' ) . ' where ID = ' . $id . ' limit 1' ) ) )
     855                if ( !$old_consolidated )
     856                {
     857                    isset( $republished ) or $republished = '(' . implode( ', ', $old_republished ) . ')';
     858
     859                    // The `AND 1=1` is to avoid a bug in the SQL parser caused by consecutive brackets.
     860                    $entity !== 'comments' or $extra_where = ' AND `comment_post_ID` IN ' . $republished . ' AND 1=1';
     861                    $entity !== 'term_taxonomy' or $extra_where = ' AND EXISTS (SELECT * FROM `' . $blog->table( 'term_relationships' ) . '` WHERE `term_taxonomy_id` = ' . $id . ' AND `object_id` IN ' . $republished . ' AND 1=1)';
     862                    $entity !== 'terms' or $extra_where = ' AND EXISTS (SELECT * FROM `' . $blog->table( 'term_relationships' ) . '` WHERE `term_taxonomy_id` IN (SELECT `term_taxonomy_id` FROM ' . $blog->table( 'term_taxonomy' ) . ' WHERE `term_id` = ' . $id . ') AND `object_id` IN ' . $republished . ' AND 1=1)';
     863                }
     864
     865                $result = $GLOBALS['wpdb']->get_var( 'SELECT `' . $select . '` FROM `' . $blog->table( $entity ) . '` WHERE `' . $col . '` = ' . $id . $extra_where . ' LIMIT 1' );
     866
     867                if ( !empty( $result ) )
    590868                {
    591869                    $this->consolidated = $old_consolidated;
    592870                    $this->republished = $old_republished;
    593871
    594                     return $blog;
     872                    $this->blog_cache[ $entity ][ $id ] = $this->consolidated && in_array( $result, $this->post_types, true ) && !$blog->is_network() ? null : $blog;
     873                    return get_current_blog_id() === $blog->id ? null : $this->blog_cache[ $entity ][ $id ];
    595874                }
    596875            }
     
    600879        }
    601880
    602         return null;
     881        return $this->blog_cache[ $entity ][ $id ] = null;
     882    }
     883
     884    public function get_blog_by_id( $id )
     885    {
     886        return isset( $this->blogs[ $id ] ) ? $this->blogs[ $id ] : null;
    603887    }
    604888}
  • wp-super-network/tags/1.2.0/src/plugin.php

    r2671848 r2864100  
    77class WP_Super_Network
    88{
     9    const ENTITIES_TO_REPLACE = array(
     10        'comments' => array(),
     11        'posts' => array(),
     12        'term_taxonomy' => array(),
     13        'terms' => array()
     14    );
     15
     16    const TABLES_TO_REPLACE = array(
     17        'commentmeta' => array(
     18            'comment_id' => 'comments'
     19        ),
     20        'comments' => array(
     21            'comment_ID' => 'comments',
     22            'comment_post_ID' => 'posts'
     23        ),
     24        'postmeta' => array(
     25            'post_id' => 'posts'
     26        ),
     27        'posts' => array(
     28            'ID' => 'posts',
     29            'post_parent' => 'posts'
     30        ),
     31        'term_relationships' => array(
     32            'object_id' => 'posts',
     33            'term_taxonomy_id' => 'term_taxonomy'
     34        ),
     35        'term_taxonomy' => array(
     36            'term_id' => 'terms',
     37            'term_taxonomy_id' => 'term_taxonomy'
     38        ),
     39        'termmeta' => array(
     40            'term_id' => 'terms'
     41        ),
     42        'terms' => array(
     43            'term_id' => 'terms'
     44        )
     45    );
     46
    947    /**
    1048     * Static instance of the plugin.
     
    4583    }
    4684
    47     function options( $value, $option )
     85    public static function options( $value, $option, $default )
    4886    {
    4987        if ( !is_main_site() )
    5088        {
    51             switch_to_blog( get_main_site_id() );
    52             $value = get_option( $option );
    53             restore_current_blog();
     89            $main = get_main_site_id();
     90
     91            if ( $main > 0 )
     92            {
     93                switch_to_blog( $main );
     94                $value = get_option( $option, $default );
     95                restore_current_blog();
     96            }
     97            else
     98            {
     99                if ( in_array( $option, array( 'supernetwork_consolidated', 'supernetwork_post_types', 'supernetwork_options' ), true ) )
     100                {
     101                    $value = $default;
     102                }
     103            }
     104
     105            if ( $value === false )
     106            {
     107                add_filter( 'option_' . $option, '__return_false' );
     108            }
    54109        }
    55110
     
    57112    }
    58113
     114    public static function add_option( $option, $value )
     115    {
     116        if ( !is_main_site() && has_filter( 'pre_option_' . $option, array( __CLASS__, 'options' ) ) )
     117        {
     118            $main = get_main_site_id();
     119
     120            if ( $main > 0 && current_user_can_for_blog( $main, 'manage_options' ) )
     121            {
     122                switch_to_blog( $main );
     123                add_option( $option, $value );
     124                restore_current_blog();
     125            }
     126        }
     127    }
     128
     129    public static function update_option( $option, $old_value, $value )
     130    {
     131        if ( !is_main_site() && has_filter( 'pre_option_' . $option, array( __CLASS__, 'options' ) ) )
     132        {
     133            $main = get_main_site_id();
     134
     135            if ( $main > 0 && current_user_can_for_blog( $main, 'manage_options' ) )
     136            {
     137                switch_to_blog( $main );
     138                update_option( $option, $value );
     139                restore_current_blog();
     140            }
     141        }
     142    }
     143
    59144    /**
    60145     * Launch the initialization process.
     
    64149    public function run()
    65150    {
     151        // Complete this before accessing the option on next line
     152        add_filter( 'pre_option_supernetwork_options', array( __CLASS__, 'options' ), 10, 3 );
     153        add_filter( 'pre_option_supernetwork_post_types', array( __CLASS__, 'options' ), 10, 3 );
     154        add_filter( 'pre_option_supernetwork_consolidated', array( __CLASS__, 'options' ), 10, 3 );
     155
     156        foreach ( get_option( 'supernetwork_options', array() ) as $option => $val )
     157        {
     158            if ( $val && strpos( $option, '_' ) !== 0 && strpos( $option, 'supernetwork_' ) !== 0 )
     159            {
     160                add_filter( 'pre_option_' . $option, array( __CLASS__, 'options' ), 10, 3 );
     161            }
     162        }
     163
     164        add_filter( 'add_option', array( __CLASS__, 'add_option' ), 10, 2 );
     165        add_filter( 'update_option', array( __CLASS__, 'update_option' ), 10, 3 );
     166
     167        // Disable querying of meta ID. See issue #10
     168        add_filter( 'update_comment_metadata_by_mid', '__return_false' );
     169        add_filter( 'update_post_metadata_by_mid', '__return_false' );
     170        add_filter( 'update_term_metadata_by_mid', '__return_false' );
     171        add_filter( 'delete_comment_metadata_by_mid', '__return_false' );
     172        add_filter( 'delete_post_metadata_by_mid', '__return_false' );
     173        add_filter( 'delete_term_metadata_by_mid', '__return_false' );
     174
     175        $this->network->register();
     176
    66177        // Load functions
    67         add_filter( 'post_type_link', array( $this->network, 'intercept_permalink_for_post' ), 10, 2 );
    68         add_filter( 'post_link', array( $this->network, 'intercept_permalink_for_post' ), 10, 2 );
    69         add_filter( 'page_link', array( $this->network, 'intercept_permalink' ), 10, 2 );
    70         add_filter( 'map_meta_cap', array( $this->network, 'intercept_capability_write' ), 10, 4 );
    71         add_filter( 'user_has_cap', array( $this->network, 'intercept_capability_read' ), 10, 4 );
    72         add_filter( 'pre_handle_404', array( $this->network, 'singular_access' ), 10, 2 );
    73         add_filter( 'query', array( $this->network, 'intercept_query' ), 10, 2 );
    74         add_filter( 'wp_insert_post', array( $this->network, 'shared_auto_increment' ), 10, 3 );
    75178        add_filter( 'network_admin_menu', array( $this, 'summary' ) );
    76 
    77         if ( is_main_site() ) $this->network->register_pages();
    78 
    79         // Complete this before accessing the option on next line
    80         add_filter( 'pre_option_supernetwork_options', array( $this, 'options' ), 10, 2 );
    81         add_filter( 'pre_option_supernetwork_post_types', array( $this, 'options' ), 10, 2 );
    82         add_filter( 'pre_option_supernetwork_consolidated', array( $this, 'options' ), 10, 2 );
    83        
    84         foreach ( (array) get_option( 'supernetwork_options' ) as $option => $val )
    85         {
    86             if ( $val && strpos( $option, '_' ) !== 0 && strpos( $option, 'supernetwork_' ) !== 0 )
    87             {
    88                 add_filter( 'pre_option_' . $option, array( $this, 'options' ), 10, 2 );
    89             }
    90         }
    91 
    92         $this->network->consolidated = !empty( get_option( 'supernetwork_consolidated' )['consolidated'] );
    93179
    94180        if ( !$this->network->consolidated )
     
    154240            if ( current_user_can( 'edit_post', $post->ID ) )
    155241            {
    156                 if ( in_array( (string) $post->ID, $this->network->collisions, true ) )
     242                $collisions = $this->network->collisions;
     243
     244                if ( array_intersect( get_comments( 'fields=ids&post_id=' . $post->ID ), $collisions['comments'] ) !== array() || in_array( (string) $post->ID, $collisions['posts'], true ) || array_intersect( wp_get_object_terms( $post->ID, array_keys( $GLOBALS['wp_taxonomies'] ), 'fields=tt_ids' ), $collisions['term_taxonomy'] ) !== array() || array_intersect( wp_get_object_terms( $post->ID, array_keys( $GLOBALS['wp_taxonomies'] ), 'fields=ids' ), $collisions['terms'] ) !== array() )
    157245                {
    158246                    $actions['republish'] = '<i style="color: #888;">' . __( 'Can&apos;t Republish', 'supernetwork' ) . '</i>';
  • wp-super-network/tags/1.2.0/wp-super-network.php

    r2671848 r2864100  
    44 * Plugin URI:
    55 * Description: Share content between sites and create offspring networks.
    6  * Version: 1.1.0
     6 * Version: 1.2.0
    77 * Requires at least: 5.0
    8  * Requires PHP: 5.6
     8 * Requires PHP: 7.2
    99 * Author: Ask Carlo
    1010 * Author URI: https://askcarlo.com
  • wp-super-network/trunk/init.php

    r2671848 r2864100  
    2424require_once SUPER_NETWORK_DIR . 'vendor/autoload.php';
    2525
     26// Load superclasses.
     27require_once SUPER_NETWORK_DIR . 'src/node.php';
     28
    2629// Load classes.
    2730require_once SUPER_NETWORK_DIR . 'src/blog.php';
     31require_once SUPER_NETWORK_DIR . 'src/expression.php';
    2832require_once SUPER_NETWORK_DIR . 'src/field.php';
     33require_once SUPER_NETWORK_DIR . 'src/insert.php';
    2934require_once SUPER_NETWORK_DIR . 'src/network.php';
    3035require_once SUPER_NETWORK_DIR . 'src/page.php';
    3136require_once SUPER_NETWORK_DIR . 'src/plugin.php';
     37require_once SUPER_NETWORK_DIR . 'src/query.php';
    3238require_once SUPER_NETWORK_DIR . 'src/section.php';
     39require_once SUPER_NETWORK_DIR . 'src/subquery.php';
     40require_once SUPER_NETWORK_DIR . 'src/table.php';
    3341
    3442// Initialize the plugin.
  • wp-super-network/trunk/readme.txt

    r2671848 r2864100  
    22Contributors: manfcarlo
    33Tags: network, multisite, share, sharing, move, migrate, migration, duplicate, syndication, content, management
    4 Tested up to: 5.9
    5 Stable tag: 1.1.0
     4Tested up to: 6.1
     5Stable tag: 1.2.0
    66License: GPLv2 or later
    77License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    4040= How do I republish a page to the whole network? =
    4141
    42 As of version 1.0.5, you could flag any post or page on a main site for republication by finding it on the Posts or Pages screen and clicking Republish, but it would not take any effect. Version 1.1.0 now implements the behaviour of this feature!
     42Flag any post or page on a main site for republication by finding it on the Posts or Pages screen and clicking Republish.
    4343
    4444After you have republished the post or page, it will start showing up across every site on your network. You can also access the network admin and click on Super Network. You will see a list of all posts and pages across all sites in the network that have been flagged for republication.
     
    5252= How do I turn an existing site into the main site for a new network? =
    5353
    54 This feature is still to come.
     54This feature is scheduled for the next major release.
    5555
    5656= How do I manage all of my sites in a single admin area? =
     
    6262If two or more sites on a network have a post with the same numeric ID, none of these posts is able to be accessed on consolidated mode. You can eliminate post ID collisions by deleting posts you don't want via the network admin screen > Super Network.
    6363
    64 = Why are republished posts and pages not editable in the admin area? =
     64= Is this the same as Global Terms? =
    6565
    66 This feature is still to come.
     66No!
     67
     68WP Super Network does not enable any "global" data relationships. The data you can create with WP Super Network is the same as the data you can create without it. WP Super Network merely brings the management and the viewing of the data together into the one place.
     69
     70= Is this the same as WP Multi Network? =
     71
     72No!
     73
     74WP Super Network is compatible with multi-network environments, but it does not include the management tools that WP Multi Network includes. Therefore, the two plugins have different purposes and can work well together.
     75
     76= Is this the same as Distributor? =
     77
     78No!
     79
     80WP Super Network does not duplicate content between sites, and all posts will always continue to have one and only one permalink. Posts from across the network may preview in archives, but their permalinks will always direct back to their original respective sites.
     81
     82= Is this the same as MainWP? =
     83
     84Only a little bit.
     85
     86WP Super Network and MainWP share a few similar features. However, MainWP is only a good fit for users who don't use the block editor and don't use multisite, since MainWP is yet to announce any support for the block editor and MainWP's own documentation states that it does not test or support multisite. This is despite the block editor and multisite both being core WordPress features for a significant number of years.
     87
     88Also, MainWP's features do not integrate with the core WordPress interface and can only be accessed through its own specially-built interface.
     89
     90Although WP Super Network requires multisite, it does not require you to learn a new interface. Instead, you can continue working with the core WordPress interfaces you are already familiar with, including both the block editor and classic editor.
     91
     92= Which plugins are incompatible with WP Super Network? =
     93
     94WP Super Network is incompatible with [Link Manager](https://wordpress.org/plugins/link-manager/). Users of Link Manager will not be supported and are advised to not install WP Super Network.
     95
     96WP Super Network is currently incompatible with [SQLite Database Integration](https://wordpress.org/plugins/sqlite-database-integration/). Users of SQLite Database Integration are advised to not install WP Super Network for the time being. Compatibility is planned for a later release, so check back later.
     97
     98If you use a plugin that adds a db.php drop-in, it may or may not be incompatible with WP Super Network. To know if you have a db.php drop-in, go to the Plugins screen, click on Drop-in and check if db.php is listed. If the db.php drop-in was added by a plugin that follows the WordPress conventions well and has high user ratings, you are unlikely to see any incompatibility issues with WP Super Network, but you are welcome to ask on the support forum.
     99
     100= Is WP Super Network safe to install? =
     101
     102See above question about incompatible plugins.
     103
     104For best results, the free version of WP Super Network should be activated on a fresh network. While results will vary, you may find that some features (e.g. consolidated mode) are limited in utility for existing networks and/or cause a slow-down in performance for large networks.
     105
     106None the less, the utmost care has been taken to avoid permanent data loss or corruption. Keeping regular database back-ups is always recommended, but deactivating the plugin should return your network back to its previous state.
     107
     108A premium version is planned for the future, focusing on enhanced performance for medium-to-large networks.
    67109
    68110= How do I suggest a new feature or submit a bug report? =
     
    71113
    72114== Changelog ==
     115
     116= 1.2.0 =
     117* Republished posts and pages can be inserted, updated and deleted
     118* Availability of taxonomies and comments for republished posts
     119* Changed behaviour of select queries that target a specific post
     120* Cache implemented for duplicate queries
     121* Fixed a few bugs with network-based options
     122* Minimum PHP version lifted to 7.2
    73123
    74124= 1.1.0 =
  • wp-super-network/trunk/src/blog.php

    r2671848 r2864100  
    3838            return $this->wp_site->site_id;
    3939        }
     40
     41        if ( $key === 'name' )
     42        {
     43            return $this->wp_site->blogname;
     44        }
    4045    }
    4146   
  • wp-super-network/trunk/src/network.php

    r2671848 r2864100  
    77class Network
    88{
    9     const ID_COLS = array(
    10         'comments' => 'comment_post_ID',
    11         'postmeta' => 'post_id',
    12         'posts' => 'ID',
    13         'term_relationships' => 'object_id'
    14     );
    15 
    169    /**
    1710     * Supernetwork
     
    5447    private $page;
    5548
    56     private $parser;
    57     private $creator;
    58 
    59     private $collisions = array();
     49    /**
     50     * Keeps track of ID collisions.
     51     *
     52     * @since 1.2.0
     53     * @var array
     54     */
     55    private $collisions = WP_Super_Network::ENTITIES_TO_REPLACE;
     56
     57    /**
     58     * Cache that maps ID's to blogs.
     59     *
     60     * @since 1.2.0
     61     * @var array
     62     */
     63    private $blog_cache = WP_Super_Network::ENTITIES_TO_REPLACE;
     64
    6065    private $republished = array();
    6166    private $post_types = array();
    6267
     68    private $meta_ids = array();
     69
    6370    public function shared_auto_increment( $post_ID, $post, $update )
    6471    {
     
    6976    }
    7077
    71     private function set_auto_increment( $new )
     78    public function auto_increment_comments( $id, $comment )
     79    {
     80        if ( $this->consolidated || in_array( (string) $comment->comment_post_ID, $this->republished, true ) )
     81        {
     82            $this->set_auto_increment( $id + 1, 'comments' );
     83        }
     84    }
     85
     86    public function auto_increment_terms( $term_id, $tt_id, $taxonomy, $args )
     87    {
     88        if ( $this->consolidated )
     89        {
     90            $this->set_auto_increment( $tt_id + 1, 'term_taxonomy' );
     91            $this->set_auto_increment( $term_id + 1, 'terms' );
     92        }
     93    }
     94
     95    public function auto_increment_term_relationships( $object_id, $tt_id, $taxonomy )
     96    {
     97        if ( !$this->consolidated && in_array( (string) $object_id, $this->republished, true ) )
     98        {
     99            $this->set_auto_increment( $tt_id + 1, 'term_taxonomy' );
     100            $term = get_term_by( 'term_taxonomy_id', $tt_id ) and $this->set_auto_increment( $term->term_id + 1, 'terms' );
     101        }
     102    }
     103
     104    private function set_auto_increment( $new, $entity = 'posts' )
    72105    {
    73106        foreach ( $this->blogs as $blog )
    74107        {
    75             if ( $new > (int) $GLOBALS['wpdb']->get_var( 'select auto_increment from information_schema.tables where table_schema = \'' . DB_NAME . '\' and table_name = \'' . $blog->table( 'posts' ) . '\'' ) )
    76             {
    77                 $GLOBALS['wpdb']->query( 'alter table ' . $blog->table( 'posts' ) . ' auto_increment = ' . (string) $new );
     108            if ( $new > (int) $GLOBALS['wpdb']->get_var( 'SELECT `auto_increment` FROM `information_schema`.`tables` WHERE `table_schema` = \'' . DB_NAME . '\' AND `table_name` = \'' . $blog->table( $entity ) . '\'' ) )
     109            {
     110                $GLOBALS['wpdb']->query( 'ALTER TABLE `' . $blog->table( $entity ) . '` AUTO_INCREMENT = ' . (string) $new );
    78111            }
    79112        }
     
    96129            return $this->republished;
    97130        }
    98     }
    99 
    100     public function __set( $key, $value )
    101     {
    102         if ( $key === 'consolidated' )
    103         {
    104             if ( $value === true )
    105             {
    106                 $this->consolidated = true;
    107             }
    108            
    109             if ( $value === false )
    110             {
    111                 $this->consolidated = false;
    112 
    113                 if ( !empty( $this->republished ) )
    114                 {
    115                     $this->set_auto_increment( $this->republished[0] + 1 );
    116                 }
    117             }
    118         }
    119     }
    120 
    121     private function union( $table )
    122     {
    123         if ( !$this->consolidated && empty( $this->republished ) )
     131
     132        if ( $key === 'post_types' )
     133        {
     134            return $this->post_types;
     135        }
     136    }
     137
     138    public function union( $table )
     139    {
     140        // No need to do anything if not consolidated and no republished posts.
     141        if ( !$this->consolidated && ( empty( $this->republished ) || empty( $post_cols = array_keys( WP_Super_Network::TABLES_TO_REPLACE[ $table ], 'posts', true ) ) ) )
    124142        {
    125143            return $GLOBALS['wpdb']->__get( $table );
     
    131149        {
    132150            $where = array();
    133 
    134             if ( $this->consolidated )
    135             {
    136                 if ( !empty( $this->collisions ) )
    137                 {
    138                     $where[] = self::ID_COLS[ $table ] . ' not in (' . implode( ', ', $this->collisions ) . ')';
    139                 }
    140 
    141                 if ( $table === 'posts' && !empty( $this->post_types ) && !$blog->is_network() )
    142                 {
    143                     $where[] = 'post_type not in (\'' . implode( '\', \'', $this->post_types ) . '\')';
    144                 }
    145             }
    146             else
    147             {
     151            $this->exclude( $where, $table, $blog );
     152
     153            if ( !$this->consolidated )
     154            {
     155                // Add republished posts.
    148156                if ( $blog->table( $table ) !== $GLOBALS['wpdb']->__get( $table ) )
    149157                {
    150                     $where[] = self::ID_COLS[ $table ] . ' in (' . implode( ', ', $this->republished ) . ')';
     158                    foreach ( $post_cols as $col )
     159                    {
     160                        // Post parent is currently not being handled.
     161                        $col === 'post_parent' or $where[] = '`' . $col . '` IN (' . implode( ', ', $this->republished ) . ')';
     162                    }
    151163                }
    152164            }
    153165
    154             $tables[] = 'select * from ' . $blog->table( $table ) . ( empty( $where ) ? '' : ( ' where ' . implode( ' and ', $where ) ) );
    155         }
    156 
    157         return implode( ' union ', $tables );
     166            $tables[] = 'SELECT * FROM `' . $blog->table( $table ) . ( empty( $where ) ? '`' : ( '` WHERE ' . implode( ' AND ', $where ) ) );
     167        }
     168
     169        return implode( ' UNION ALL ', $tables );
     170    }
     171
     172    public function exclude( &$where, $table, $blog, $alias = '' )
     173    {
     174        if ( $this->consolidated )
     175        {
     176            empty( $alias ) or $alias .= '.';
     177
     178            // Exclude any entities involved in collisions.
     179            foreach ( array_keys( $this->collisions ) as $entity )
     180            {
     181                if ( !empty( $this->collisions[ $entity ] ) )
     182                {
     183                    foreach ( array_keys( WP_Super_Network::TABLES_TO_REPLACE[ $table ], $entity, true ) as $col )
     184                    {
     185                        $where[] = $alias . '`' . $col . '` NOT IN (' . implode( ', ', $this->collisions[ $entity ] ) . ')';
     186                    }
     187                }
     188            }
     189
     190            // Exclude network-based post types.
     191            if ( $table === 'posts' && !empty( $this->post_types ) && !$blog->is_network() )
     192            {
     193                $where[] = $alias . '`post_type` NOT IN (\'' . implode( '\', \'', $this->post_types ) . '\')';
     194            }
     195        }
    158196    }
    159197
    160198    public function report_collisions()
    161199    {
    162         if ( $this->consolidated && !empty( $this->collisions ) && current_user_can( 'manage_network_options' ) )
    163         {
    164             echo '<div class="notice notice-error"><p><strong>WP Super Network detected ' . count( $this->collisions ) . ' post ID collisions</strong> across your network! These posts have been temporarily hidden. To access them again, please turn off consolidated mode.</p></div>';
     200        $term_collisions = count( $this->collisions['term_taxonomy'] ) + count( $this->collisions['terms'] );
     201
     202        if ( $this->consolidated && ( !empty( $this->collisions['posts'] ) || $term_collisions > 0 || !empty( $this->collisions['comments'] ) ) && current_user_can( 'manage_network_options' ) )
     203        {
     204            echo '<div class="notice notice-error"><p><strong>WP Super Network detected ';
     205
     206            echo empty( $this->collisions['posts'] ) ? '' : count( $this->collisions['posts'] ) . ' post ID collisions';
     207
     208            if ( !empty( $this->collisions['posts'] ) && $term_collisions > 0 )
     209            {
     210                echo empty( $this->collisions['comments'] ) ? ' and ' : ', ';
     211            }
     212
     213            echo $term_collisions > 0 ? $term_collisions . ' taxonomy term ID collisions' : '';
     214
     215            if ( !empty( $this->collisions['comments'] ) )
     216            {
     217                echo ( !empty( $this->collisions['posts'] ) || $term_collisions > 0 ) ? ' and ' : '';
     218                echo count( $this->collisions['comments'] ) . ' comment ID collisions';
     219            }
     220
     221            echo '</strong> across your network! These entities have been temporarily hidden. To access them again, please turn off consolidated mode.</p></div>';
    165222        }
    166223    }
     
    169226     * Constructor.
    170227     *
    171      * Constructs the network.
     228     * Queries sent during this method do not get modified, as the filter is applied later.
    172229     *
    173230     * @since 1.0.4
     
    179236        foreach ( get_sites( 'network_id=' . $network->id ) as $site )
    180237        {
    181             array_push( $this->blogs, new Blog( $site ) );
    182         }
    183 
    184         add_filter( 'admin_footer', array( $this, 'report_collisions' ) );
    185 
    186         $this->parser = new \PHPSQLParser\PHPSQLParser();
    187         $this->creator = new \PHPSQLParser\PHPSQLCreator();
    188 
    189         $this->collisions = $GLOBALS['wpdb']->get_col( 'select ID from (' . $this->union( 'posts' ) . ') posts group by ID having count(*) > 1 order by ID asc' );
    190         $this->republished = $GLOBALS['wpdb']->get_col( 'select post_id from (' . $this->union( 'postmeta' ) . ') postmeta where meta_key = \'_supernetwork_share\' order by post_id desc' );
    191         $this->post_types = array_keys( (array) get_option( 'supernetwork_post_types' ) );
     238            $this->blogs[ (int) $site->blog_id ] = new Blog( $site );
     239        }
    192240
    193241        $this->page = new Settings_Page(
     
    209257
    210258        global $wpdb;
    211         $results = $wpdb->get_results( "select distinct option_name from $wpdb->options where option_name not like '\_%' and option_name not like 'supernetwork\_%' order by option_name" );
     259        $results = $wpdb->get_results( "SELECT DISTINCT `option_name` FROM $wpdb->options WHERE `option_name` NOT LIKE '\_%' AND `option_name` NOT LIKE 'supernetwork\_%' ORDER BY `option_name`" );
    212260        $labels = array();
    213261
     
    252300            'Consolidated Mode',
    253301            'Turn on consolidated mode?',
    254             '<strong>Warning:</strong> Consolidated mode is highly unstable. It is strongly suggested that you have a REGULAR backup regime in place for your database, before activating consolidated mode.'
     302            '<strong>Warning:</strong> Consolidated mode is recommended for fresh networks. If you have pre-existing data (e.g. posts and pages) on your network, some of it may not be compatible with consolidated mode.'
    255303        );
    256304
     
    263311    }
    264312
     313    public function add_new_post( $hook_suffix )
     314    {
     315        $post_type = empty( $_GET['post_type'] ) ? 'post' : $_GET['post_type'];
     316
     317        if ( $hook_suffix === 'edit.php' && $this->consolidated && !in_array( $post_type, $this->post_types, true ) )
     318        {
     319            wp_enqueue_script(
     320                'supernetwork-post-new',
     321                SUPER_NETWORK_URL . 'assets/js/post-new.js',
     322                array(),
     323                null,
     324                true
     325            );
     326
     327            $blogs = array();
     328            $type = get_post_type_object( $post_type );
     329            $edit = isset( $post_type ) ? $type->cap->edit_posts : 'do_not_allow';
     330            $create = isset( $post_type ) ? $type->cap->create_posts : 'do_not_allow';
     331
     332            foreach ( $this->blogs as $blog )
     333            {
     334                $id = $blog->id;
     335
     336                if ( current_user_can_for_blog( $id, $edit ) && current_user_can_for_blog( $id, $create ) )
     337                {
     338                    $blogs[ $id ] = $blog->name;
     339                }
     340            }
     341
     342            wp_add_inline_script(
     343                'supernetwork-post-new',
     344                'var blogs = ' . json_encode( $blogs ) . '; var currentId = "' . get_current_blog_id() . '";',
     345                'before'
     346            );
     347        }
     348    }
     349
    265350    public function post_types( $args, $field )
    266351    {
     
    276361    }
    277362
    278     public function register_pages()
     363    private function push_meta_ids( $meta_ids, $object_id )
     364    {
     365        $this->meta_ids[] = array( $meta_ids, $object_id );
     366    }
     367
     368    public function meta_ids()
     369    {
     370        return empty( $this->meta_ids ) ? array() : $this->meta_ids[ count( $this->meta_ids ) - 1 ][0];
     371    }
     372
     373    public function meta_object_id()
     374    {
     375        return empty( $this->meta_ids ) ? 0 : $this->meta_ids[ count( $this->meta_ids ) - 1 ][1];
     376    }
     377
     378    public function pop_meta_ids()
     379    {
     380        array_pop( $this->meta_ids );
     381    }
     382
     383    public function delete_meta( $meta_ids, $object_id )
     384    {
     385        $this->push_meta_ids( $meta_ids, $object_id );
     386        return $meta_ids;
     387    }
     388
     389    public function register()
    279390    {
    280391        $this->page->register();
     392
     393        add_filter( 'post_type_link', array( $this, 'intercept_permalink_for_post' ), 10, 2 );
     394        add_filter( 'post_link', array( $this, 'intercept_permalink_for_post' ), 10, 2 );
     395        add_filter( 'page_link', array( $this, 'intercept_permalink' ), 10, 2 );
     396        add_filter( 'preview_post_link', array( $this, 'intercept_preview_link' ), 10, 2 );
     397        add_filter( 'supernetwork_preview_link', array( $this, 'replace_preview_link' ), 10, 2 );
     398        add_filter( 'user_has_cap', array( $this, 'intercept_capability' ), 10, 4 );
     399        add_filter( 'pre_handle_404', array( $this, 'singular_access' ), 10, 2 );
     400        add_action( 'wp', array( $this, 'preview_access' ) );
     401        add_filter( 'query', array( $this, 'intercept_query' ), 10, 2 );
     402        add_filter( 'wp_insert_post', array( $this, 'shared_auto_increment' ), 10, 3 );
     403        add_filter( 'wp_insert_comment', array( $this, 'auto_increment_comments' ), 10, 2 );
     404        add_filter( 'created_term', array( $this, 'auto_increment_terms' ), 10, 4 );
     405        add_filter( 'added_term_relationship', array( $this, 'auto_increment_term_relationships' ), 10, 3 );
     406        add_filter( 'admin_enqueue_scripts', array( $this, 'add_new_post' ) );
     407        add_filter( 'admin_footer', array( $this, 'report_collisions' ) );
     408        add_filter( 'delete_comment_meta', array( $this, 'delete_meta' ), 10, 2 );
     409        add_filter( 'delete_post_meta', array( $this, 'delete_meta' ), 10, 2 );
     410        add_filter( 'delete_term_meta', array( $this, 'delete_meta' ), 10, 2 );
     411
     412        // Comments must be queried before posts so as not to mask any comment ID collisions.
     413        // Taxonomy terms must be queried before terms so as not to mask any taxonomy term ID collisions.
     414        foreach ( array_keys( $this->collisions ) as $entity )
     415        {
     416            // `ID` comes before `post_parent` in the `posts` sub-array, so `ID` will be correctly returned.
     417            $id = array_search( $entity, WP_Super_Network::TABLES_TO_REPLACE[ $entity ], true );
     418
     419            $this->collisions[ $entity ] = $GLOBALS['wpdb']->get_col( 'SELECT `' . $id . '` FROM `' . $GLOBALS['wpdb']->__get( $entity ) . '` GROUP BY `' . $id . '` HAVING COUNT(*) > 1 ORDER BY `' . $id . '` ASC' );
     420        }
     421
     422        $consolidated = !empty( get_option( 'supernetwork_consolidated', array() )['consolidated'] );
     423
     424        if ( $consolidated )
     425        {
     426            $this->post_types = array_keys( get_option( 'supernetwork_post_types', array() ) );
     427        }
     428        else
     429        {
     430            $extra_where = '';
     431            $extra_where_2 = '';
     432            $relationships = $GLOBALS['wpdb']->term_relationships;
     433            $taxonomy = $GLOBALS['wpdb']->term_taxonomy;
     434
     435            // The `AND 1=1` is to avoid a bug in the SQL parser caused by consecutive brackets.
     436            empty( $this->collisions['comments'] ) or $extra_where .= ' AND `post_id` NOT IN (SELECT `comment_post_ID` FROM `' . $GLOBALS['wpdb']->comments . '` WHERE `comment_ID` IN (' . implode( ', ', $this->collisions[ 'comments' ] ) . ') AND 1=1)';
     437
     438            if ( !empty( $this->collisions['term_taxonomy'] ) )
     439            {
     440                $extra_where .= ' AND `post_id` NOT IN (SELECT `object_id` FROM `' . $relationships . '` WHERE `term_taxonomy_id` IN (' . implode( ', ', $this->collisions[ 'term_taxonomy' ] ) . ') AND 1=1)';
     441                $extra_where_2 = ' AND `' . $relationships . '`.`term_taxonomy_id` NOT IN (' . implode( ', ', $this->collisions[ 'term_taxonomy' ] ) . ')';
     442            }
     443
     444            empty( $this->collisions['terms'] ) or $extra_where .= ' AND `post_id` NOT IN (SELECT `object_id` FROM `' . $relationships . '` LEFT JOIN `' . $taxonomy . '` ON `' . $relationships . '`.`term_taxonomy_id` = `' . $taxonomy . '`.`term_taxonomy_id` WHERE `term_id` IN (' . implode( ', ', $this->collisions[ 'terms' ] ) . ')' . $extra_where_2 . ' AND 1=1)';
     445
     446            // This query requires post collisions, but not the other collisions, to be recorded.
     447            $old_comment_collisions = $this->collisions['comments'];
     448            $old_taxonomy_term_collisions = $this->collisions['term_taxonomy'];
     449            $old_term_collisions = $this->collisions['terms'];
     450
     451            foreach ( array( 'comments', 'term_taxonomy', 'terms' ) as $entity ) $this->collisions[ $entity ] = array();
     452
     453            $this->republished = $GLOBALS['wpdb']->get_col( 'SELECT DISTINCT `post_id` FROM `' . $GLOBALS['wpdb']->postmeta . '` WHERE `meta_key` = \'_supernetwork_share\'' . $extra_where . ' ORDER BY `post_id` DESC' );
     454
     455            $this->collisions['comments'] = $old_comment_collisions;
     456            $this->collisions['term_taxonomy'] = $old_taxonomy_term_collisions;
     457            $this->collisions['terms'] = $old_term_collisions;
     458        }
     459
     460        $this->consolidated = $consolidated;
     461
     462        if ( !$this->consolidated && !empty( $this->republished ) )
     463        {
     464            $this->set_auto_increment( $this->republished[0] + 1 );
     465        }
     466    }
     467
     468    /**
     469     * Selects an entity type to be first in line for collision elimination, based on a calculated score.
     470     *
     471     * The score for each entity type is the median ID of its collisions, divided by the median ID overall.
     472     * A score of 1 means the collisions are evenly distributed.
     473     * A lower score means the collisions are concentrated on the lower ID's, which are more likely to be older entities with more references and importance.
     474     *
     475     * The entity type with the lowest score is selected for collision elimination.
     476     *
     477     * @since 1.2.0
     478     */
     479    private function get_entity_by_median_score()
     480    {
     481        // Temporarily empty collisions.
     482        $old = $this->collisions;
     483        $this->collisions = WP_Super_Network::ENTITIES_TO_REPLACE;
     484        $scores = WP_Super_Network::ENTITIES_TO_REPLACE;
     485
     486        foreach ( $scores as $entity => &$score )
     487        {
     488            // `ID` comes before `post_parent` in the `posts` sub-array, so `ID` will be correctly returned.
     489            $id = array_search( $entity, WP_Super_Network::TABLES_TO_REPLACE[ $entity ], true );
     490
     491            $collisions = $GLOBALS['wpdb']->get_col( 'SELECT `' . $id . '` FROM `' . $GLOBALS['wpdb']->__get( $entity ) . '` WHERE `' . $id . '` IN (SELECT `' . $id . '` FROM `' . $GLOBALS['wpdb']->__get( $entity ) . '` GROUP BY `' . $id . '` HAVING COUNT(*) > 1) ORDER BY `' . $id . '` ASC' );
     492            $all = $GLOBALS['wpdb']->get_col( 'SELECT `' . $id . '` FROM `' . $GLOBALS['wpdb']->__get( $entity ) . '` ORDER BY `' . $id . '` ASC' );
     493
     494            $score = empty( $collisions ) ? PHP_FLOAT_MAX : $collisions[ floor( count( $collisions ) / 2 ) ] / $all[ floor( count( $all ) / 2 ) ];
     495        }
     496
     497        $this->collisions = $old;
     498        return array_search( min( $scores ), $scores );
     499    }
     500
     501    /**
     502     * Force deletes a term from a taxonomy.
     503     *
     504     * @since 1.2.0
     505     */
     506    private function force_delete_taxonomy_term( $term_id, $taxonomy )
     507    {
     508        // Temporarily register taxonomy to force deletion.
     509        if ( !( $exists = taxonomy_exists( $taxonomy ) ) ) register_taxonomy( $taxonomy, array() );
     510
     511        // 0 is returned if the default term was not deleted.
     512        $success = 0 !== wp_delete_term( $term_id, $taxonomy );
     513
     514        if ( !$exists ) unregister_taxonomy( $taxonomy );
     515        return $success;
    281516    }
    282517
     
    288523        if ( $this->consolidated )
    289524        {
    290             echo '<h2>Post ID Collisions</h2>';
    291 
    292             if ( !empty( $this->collisions ) && isset( $_POST['supernetwork_post_collision_' . $this->collisions[0] ] ) )
    293             {
    294                 $this->consolidated = false;
    295 
    296                 foreach ( $this->blogs as $blog )
     525        $entity = $this->get_entity_by_median_score();
     526
     527        $this->consolidated = false;
     528
     529            echo '<h2>ID Collisions</h2>';
     530
     531        if ( !empty( $this->collisions[ $entity ] ) && isset( $_POST[ 'supernetwork_' . $entity . '_collision_' . $this->collisions[ $entity ][0] ] ) )
     532        {
     533            $success = true;
     534
     535            foreach ( $this->blogs as $blog )
     536            {
     537                if ( $blog->id !== (int) explode( '_', $_POST[ 'supernetwork_' . $entity . '_collision_' . $this->collisions[ $entity ][0] ] )[0] )
    297538                {
    298539                    switch_to_blog( $blog->id );
    299                    
    300                     if ( empty( $GLOBALS['wpdb']->get_col( 'select ID from ' . $blog->table( 'posts' ) . ' where guid = \'' . $_POST['supernetwork_post_collision_' . $this->collisions[0] ] . '\' limit 1' ) ) )
     540
     541                    if ( $entity === 'comments' ) wp_delete_comment( (int) $this->collisions['comments'][0], true );
     542                    if ( $entity === 'posts' ) wp_delete_post( (int) $this->collisions['posts'][0], true );
     543
     544                    if ( $entity === 'term_taxonomy' )
    301545                    {
    302                         wp_delete_post( (int) $this->collisions[0], true );
     546                        $term = $GLOBALS['wpdb']->get_row( 'SELECT `taxonomy`, `term_id` FROM `' . $GLOBALS['wpdb']->term_taxonomy . '` WHERE `term_taxonomy_id` = ' . $this->collisions['term_taxonomy'][0], ARRAY_A );
     547
     548                        if ( isset( $term['term_id'] ) && isset( $term['taxonomy'] ) )
     549                        {
     550                            $success = $this->force_delete_taxonomy_term( (int) $term['term_id'], $term['taxonomy'] ) && $success;
     551                        }
     552                    }
     553
     554                    if ( $entity === 'terms' )
     555                    {
     556                        $results = $GLOBALS['wpdb']->get_results( 'SELECT `taxonomy`, `term_id` FROM `' . $GLOBALS['wpdb']->term_taxonomy . '` WHERE `term_id` = ' . $this->collisions['terms'][0] );
     557
     558                        foreach ( $results as $result )
     559                        {
     560                            $success = $this->force_delete_taxonomy_term( (int) $result['term_id'], $result['taxonomy'] ) && $success;
     561                        }
    303562                    }
    304563
    305564                    restore_current_blog();
    306565                }
    307 
     566            }
     567
     568            // Start again if collision was eliminated.
     569            if ( $success )
     570            {
     571                array_shift( $this->collisions[ $entity ] );
    308572                $this->consolidated = true;
    309                 array_shift( $this->collisions );
    310             }
    311 
    312             if ( empty( $this->collisions ) )
    313             {
    314                 echo '<p>No collisions on this network!</p>';
    315             }
    316             else
    317             {
    318                 $old_collisions = $this->collisions;
    319                 $old_post_types = $this->post_types;
    320 
    321                 $this->collisions = array();
    322                 $this->post_types = array();
    323 
    324                 echo '<p>Consolidated mode is designed for fresh networks. When activated on an existing network, a large number of ID collisions are inevitable. However, you may be able to eliminate some collisions when the ID refers to a post of low importance, such as a revision or autosave.</p>';
    325                 echo '<p>The below table allows you to eliminate post ID collisions, one ID at a time. For each ID, you must select just ONE post to keep. All others with the same ID will be immediately and irretrievably deleted.</p>';
    326 
    327                 echo '<form method="post" action="">';
    328                 echo '<table class="widefat">';
    329                 echo '<thead><tr><th scope="col">Keep?</th><th scope="col">GUID</th><th scope="col">Post Title</th><th scope="col">Post Preview</th><th scope="col">Post Type</th><th scope="col">Post Status</th></tr></thead>';
    330                 echo '<tbody>';
    331 
    332                 foreach ( $GLOBALS['wpdb']->get_results( 'select guid, post_title, substring(post_content, 1, 500) as post_preview, post_type, post_status from ' . $GLOBALS['wpdb']->posts . ' where ID = ' . $old_collisions[0], ARRAY_A ) as $site )
     573                $entity = $this->get_entity_by_median_score();
     574                $this->consolidated = false;
     575            }
     576        }
     577
     578        if ( empty( $this->collisions[ $entity ] ) )
     579        {
     580            echo '<p>No collisions on this network!</p>';
     581        }
     582        else
     583        {
     584            // `ID` comes before `post_parent` in the `posts` sub-array, so `ID` will be correctly returned.
     585            $id = array_search( $entity, WP_Super_Network::TABLES_TO_REPLACE[ $entity ], true );
     586            $id = 'terms' === $entity ? 't.' . $id : $id;
     587
     588            $fields = array(
     589                'comments' => array(
     590                    'post_title',
     591                    'comment_content',
     592                    'comment_author',
     593                    'comment_author_email'
     594                ),
     595                'posts' => array(
     596                    'post_title',
     597                    'post_content',
     598                    'post_type',
     599                    'post_status'
     600                ),
     601                'term_taxonomy' => array(
     602                    'name',
     603                    'description',
     604                    'taxonomy',
     605                    'count'
     606                ),
     607                'terms' => array(
     608                    'name',
     609                    'description',
     610                    'taxonomy',
     611                    'count'
     612                )
     613            );
     614
     615            $labels = array(
     616                'comments' => array(
     617                    'Site Containing Post/Comment',
     618                    'Post Containing Comment',
     619                    'Comment Preview',
     620                    'Comment Author',
     621                    'Comment Author Email'
     622                ),
     623                'posts' => array(
     624                    'Site Containing Post',
     625                    'Post Title',
     626                    'Post Preview',
     627                    'Post Type',
     628                    'Post Status'
     629                ),
     630                'term_taxonomy' => array(
     631                    'Site Containing Taxonomy Term',
     632                    'Taxonomy Term Name',
     633                    'Taxonomy Term Description',
     634                    'Taxonomy',
     635                    'Post Count'
     636                ),
     637                'terms' => array(
     638                    'Site Containing Taxonomy Term',
     639                    'Taxonomy Term Name',
     640                    'Taxonomy Term Description',
     641                    'Taxonomy',
     642                    'Post Count'
     643                )
     644            );
     645
     646            echo '<p>Consolidated mode is designed for fresh networks. When activated on an existing network, a large number of ID collisions are inevitable. However, you may be able to eliminate some collisions when the ID refers to a post of low importance, such as a revision or autosave.</p>';
     647            echo '<p>The below tables allow you to eliminate post, taxonomy term and comment ID collisions, one ID at a time. For each ID, you must select just ONE entity to keep. All others with the same ID will be immediately and irretrievably deleted.</p>';
     648
     649            if ( in_array( $entity, array( 'term_taxonomy', 'terms' ), true ) ) echo '<div class="notice notice-warning inline"><p><strong>Note:</strong> Default taxonomy terms can not be deleted! If a default taxonomy term is in a collision, you must go to Writing Settings and select a new default term for the taxonomy before you can eliminate the collision.</p></div>';
     650
     651            echo '<form method="post" action="">';
     652            echo '<table class="widefat">';
     653            echo '<thead><tr><th scope="col">Keep?</th><th scope="col">' . $labels[ $entity ][0] . '</th><th scope="col">' . $labels[ $entity ][1] . '</th><th scope="col">' . $labels[ $entity ][2] . '</th><th scope="col">' . $labels[ $entity ][3] . '</th><th scope="col">' . $labels[ $entity ][4] . '</th></tr></thead>';
     654            echo '<tbody>';
     655
     656            foreach ( $this->blogs as $blog )
     657            {
     658                $tables = array(
     659                    'comments' => $blog->table( 'comments' ) . ' LEFT JOIN ' . $blog->table( 'posts' ) . ' ON comment_post_ID = ID',
     660                    'posts' => $blog->table( 'posts' ),
     661                    'term_taxonomy' => $blog->table( 'term_taxonomy' ) . ' tt LEFT JOIN ' . $blog->table( 'terms' ) . ' t ON tt.term_id = t.term_id',
     662                    'terms' => $blog->table( 'term_taxonomy' ) . ' tt LEFT JOIN ' . $blog->table( 'terms' ) . ' t ON tt.term_id = t.term_id'
     663                );
     664
     665                $rows = $GLOBALS['wpdb']->get_results( 'SELECT `' . $fields[ $entity ][0] . '`, SUBSTRING(`' . $fields[ $entity ][1] . '`, 1, 500) AS `' . $fields[ $entity ][1] . '_preview`, `' . $fields[ $entity ][2] . '`, `' . $fields[ $entity ][3] . '` FROM ' . $tables[ $entity ] . ' WHERE `' . $id . '` = ' . $this->collisions[ $entity ][0], ARRAY_A );
     666
     667                // There may be more than one row if terms are not split.
     668                foreach ( $rows as $key => $row )
    333669                {
    334670                    echo '<tr>';
    335                     echo '<td><input type="radio" id="supernetwork__' . esc_textarea( $site['guid'] ) . '" value="' . esc_textarea( $site['guid'] ) . '" name="supernetwork_post_collision_' . $old_collisions[0] . '"></td>';
    336                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['guid'] ) . '</label></td>';
    337                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['post_title'] ) . '</label></td>';
    338                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['post_preview'] ) . '</label></td>';
    339                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['post_type'] ) . '</label></td>';
    340                     echo '<td><label for="supernetwork__' . esc_textarea( $site['guid'] ) . '">' . esc_textarea( $site['post_status'] ) . '</label></td>';
     671                    echo '<td><input type="radio" id="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '" value="' . esc_attr( $blog->id ) . '" name="supernetwork_' . $entity . '_collision_' . $this->collisions[ $entity ][0] . '"></td>';
     672                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $blog->name ) . '</label></td>';
     673                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $row[ $fields[ $entity ][0] ] ) . '</label></td>';
     674                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $row[ $fields[ $entity ][1] . '_preview' ] ) . '</label></td>';
     675                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $row[ $fields[ $entity ][2] ] ) . '</label></td>';
     676                    echo '<td><label for="supernetwork__' . esc_attr( $blog->id ) . '_' . $key . '">' . esc_html( $row[ $fields[ $entity ][3] ] ) . '</label></td>';
    341677                    echo '</tr>';
    342678                }
    343 
    344                 $this->collisions = $old_collisions;
    345                 $this->post_types = $old_post_types;
    346 
    347                 echo '</tbody></table>';
    348                 submit_button( 'Keep Selected and Delete All Others' );
    349                 echo '</form>';
    350             }
    351         }
     679            }
     680
     681            echo '</tbody></table>';
     682            submit_button( 'Keep Selected and Delete All Others' );
     683            echo '</form>';
     684        }
     685
     686        $this->consolidated = true;
     687    }
    352688        else
    353689        {
     
    419755    public function intercept_query( $query )
    420756    {
    421         try
    422         {
    423             $parsed = $this->parser->parse( $query );
    424 
    425             // Insert, update and delete queries are not currently modified.
    426             return $this->modify_query( $parsed ) ? $this->creator->create( $parsed ) : $query;
    427         }
    428         catch ( \PHPSQLParser\exceptions\UnsupportedFeatureException $uf )
    429         {
    430             return $query;
    431         }
    432         catch ( \PHPSQLParser\exceptions\UnableToCreateSQLException $utcsql )
    433         {
    434             return $query;
    435         }
    436     }
    437 
    438     private function modify_query( &$parsed )
    439     {
    440         if ( in_array( array_keys( $parsed )[0], array( 'UNION', 'SELECT' ), true ) )
    441         {
    442             if ( isset( $parsed['UNION'] ) )
    443             {
    444                 foreach ( $parsed['UNION'] as &$query )
    445                 {
    446                     $this->modify_query( $query );
    447                 }
    448             }
    449             else
    450             {
    451                 if ( isset( $parsed['SELECT'] ) && isset( $parsed['FROM'] ) )
    452                 {
    453                     foreach ( $parsed['FROM'] as &$from )
    454                     {
    455                         if ( !empty( $from['sub_tree'] ) )
    456                         {
    457                             $this->modify_query( $from['sub_tree'] );
    458                         }
    459                         else
    460                         {
    461                             foreach ( array_keys( self::ID_COLS ) as $table )
    462                             {
    463                                 $local_table = $GLOBALS['wpdb']->__get( $table );
    464 
    465                                 if ( isset( $from['table'] ) && $from['table'] === $local_table )
    466                                 {
    467                                     if ( ( $union = $this->union( $table ) ) === $from['table'] )
    468                                     {
    469                                         continue;
    470                                     }
    471 
    472                                     $from['expr_type'] = 'subquery';
    473                                     $from['sub_tree'] = $this->parser->parse( $union );
    474 
    475                                     if ( false === $from['alias'] )
    476                                     {
    477                                         $from['alias'] = array(
    478                                             'as' => false,
    479                                             'name' => $local_table,
    480                                             'no_quotes' => array(
    481                                                 'delim' => false,
    482                                                 'parts' => array( $local_table )
    483                                             ),
    484                                             'base_expr' => $local_table
    485                                         );
    486                                     }
    487 
    488                                     unset( $from['table'] );
    489                                     break;
    490                                 }
    491                             }
    492                         }
    493                     }
    494 
    495                     foreach ( array( 'WHERE', 'HAVING' ) as $clause )
    496                     {
    497                         if ( isset( $parsed[ $clause ] ) )
    498                         {
    499                             foreach ( $parsed[ $clause ] as &$subclause )
    500                             {
    501                                 if ( !empty( $subclause['sub_tree'] ) )
    502                                 {
    503                                     $this->modify_query( $subclause['sub_tree'] );
    504                                 }
    505                             }
    506                         }
    507                     }
    508                 }
    509             }
    510 
    511             return true;
    512         }
    513 
    514         return false;
     757        $cache = wp_cache_get( 'supernetwork_queries', $query );
     758
     759        if ( $cache )
     760        {
     761            return $cache->transformed;
     762        }
     763        else
     764        {
     765            $transformed = new Query( $query, $this );
     766            wp_cache_set( 'supernetwork_queries', $transformed, $query );
     767            return $transformed->transformed;
     768        }
    515769    }
    516770
    517771    public function intercept_permalink( $permalink, $post_ID )
    518772    {
    519         // Prevent infinite loop
    520         if ( empty( $GLOBALS['_wp_switched_stack'] ) )
    521         {
    522             if ( !is_null( $blog = $this->get_blog( $post_ID ) ) )
    523             {
    524                 switch_to_blog( $blog->id );
    525                 $permalink = get_permalink( $post_ID );
    526                 restore_current_blog();
    527             }
     773        if ( !doing_filter( 'supernetwork_preview_link' ) && !is_null( $blog = $this->get_blog( $post_ID ) ) )
     774        {
     775            switch_to_blog( $blog->id );
     776            $permalink = get_permalink( $post_ID );
     777            restore_current_blog();
    528778        }
    529779
     
    536786    }
    537787
    538     public function intercept_capability_write( $caps, $cap, $user_id, $args )
    539     {
    540         if ( in_array( $cap, array( 'delete_post', 'edit_post', 'publish_post' ), true ) )
    541         {
    542             if ( !is_null( $blog = $this->get_blog( get_post( $args[0] )->ID ) ) && get_current_blog_id() !== $blog->id )
    543             {
    544                 return array( 'do_not_allow' );
    545             }
    546         }
    547 
    548         return $caps;
    549     }
    550 
    551     public function intercept_capability_read( $allcaps, $caps, $args, $user )
    552     {
    553         if ( $args[0] === 'read_post' )
     788    public function intercept_preview_link( $preview_link, $post )
     789    {
     790        return doing_filter( 'supernetwork_preview_link' ) ? $preview_link : apply_filters( 'supernetwork_preview_link', $preview_link, $post );
     791    }
     792
     793    public function replace_preview_link( $preview_link, $post )
     794    {
     795        $query_args = array();
     796        parse_str( parse_url( $preview_link, PHP_URL_QUERY ), $query_args );
     797        return get_preview_post_link( $post, array_intersect_key( $query_args, array( 'preview_nonce' => null, 'preview_id' => null ) ) );
     798    }
     799
     800    public function intercept_capability( $allcaps, $caps, $args, $user )
     801    {
     802        if ( in_array( $args[0], array( 'delete_post', 'edit_post', 'publish_post', 'read_post' ), true ) )
    554803        {
    555804            if ( !is_null( $blog = $this->get_blog( get_post( $args[2] )->ID ) ) )
     
    564813    public function singular_access( $false, $wp_query )
    565814    {
    566         if ( $wp_query->is_singular() && !is_admin() && ( !defined( 'REST_REQUEST' ) || !REST_REQUEST ) && isset( $wp_query->post ) && !is_null( $blog = $this->get_blog( $wp_query->post->ID ) ) && get_current_blog_id() !== $blog->id )
     815        if ( $wp_query->is_singular() && !is_admin() && ( !defined( 'REST_REQUEST' ) || !REST_REQUEST ) && !is_preview() && isset( $wp_query->post ) && !is_null( $this->get_blog( $wp_query->post->ID ) ) )
    567816        {
    568817            $wp_query->set_404();
     
    575824    }
    576825
    577     private function get_blog( $id )
    578     {
    579         if ( !in_array( (string) $id, $this->collisions, true ) )
    580         {
     826    public function preview_access()
     827    {
     828        if ( is_preview() && !is_null( $blog = $this->get_blog( get_the_ID() ) ) )
     829        {
     830            switch_to_blog( $blog->id );
     831        }
     832    }
     833
     834    public function get_blog( $id, $entity = 'posts' )
     835    {
     836        if ( in_array( $entity, array_keys( WP_Super_Network::ENTITIES_TO_REPLACE ), true ) && !in_array( (string) $id, $this->collisions[ $entity ], true ) && ( $this->consolidated || !empty( $this->republished ) && $entity !== 'posts' || in_array( (string) $id, $this->republished, true ) ) )
     837        {
     838            if ( isset( $this->blog_cache[ $entity ][ $id ] ) ) return get_current_blog_id() === $this->blog_cache[ $entity ][ $id ]->id ? null : $this->blog_cache[ $entity ][ $id ];
     839
     840            $republished = null;
     841            $extra_where = '';
     842
     843            // `ID` comes before `post_parent` in the `posts` sub-array, so `ID` will be correctly returned.
     844            $col = array_search( $entity, WP_Super_Network::TABLES_TO_REPLACE[ $entity ], true );
     845            $select = $entity === 'posts' ? 'post_type' : $col;
     846
    581847            $old_consolidated = $this->consolidated;
    582848            $old_republished = $this->republished;
     
    587853            foreach ( $this->blogs as $blog )
    588854            {
    589                 if ( !empty( $GLOBALS['wpdb']->get_col( 'select ID from ' . $blog->table( 'posts' ) . ' where ID = ' . $id . ' limit 1' ) ) )
     855                if ( !$old_consolidated )
     856                {
     857                    isset( $republished ) or $republished = '(' . implode( ', ', $old_republished ) . ')';
     858
     859                    // The `AND 1=1` is to avoid a bug in the SQL parser caused by consecutive brackets.
     860                    $entity !== 'comments' or $extra_where = ' AND `comment_post_ID` IN ' . $republished . ' AND 1=1';
     861                    $entity !== 'term_taxonomy' or $extra_where = ' AND EXISTS (SELECT * FROM `' . $blog->table( 'term_relationships' ) . '` WHERE `term_taxonomy_id` = ' . $id . ' AND `object_id` IN ' . $republished . ' AND 1=1)';
     862                    $entity !== 'terms' or $extra_where = ' AND EXISTS (SELECT * FROM `' . $blog->table( 'term_relationships' ) . '` WHERE `term_taxonomy_id` IN (SELECT `term_taxonomy_id` FROM ' . $blog->table( 'term_taxonomy' ) . ' WHERE `term_id` = ' . $id . ') AND `object_id` IN ' . $republished . ' AND 1=1)';
     863                }
     864
     865                $result = $GLOBALS['wpdb']->get_var( 'SELECT `' . $select . '` FROM `' . $blog->table( $entity ) . '` WHERE `' . $col . '` = ' . $id . $extra_where . ' LIMIT 1' );
     866
     867                if ( !empty( $result ) )
    590868                {
    591869                    $this->consolidated = $old_consolidated;
    592870                    $this->republished = $old_republished;
    593871
    594                     return $blog;
     872                    $this->blog_cache[ $entity ][ $id ] = $this->consolidated && in_array( $result, $this->post_types, true ) && !$blog->is_network() ? null : $blog;
     873                    return get_current_blog_id() === $blog->id ? null : $this->blog_cache[ $entity ][ $id ];
    595874                }
    596875            }
     
    600879        }
    601880
    602         return null;
     881        return $this->blog_cache[ $entity ][ $id ] = null;
     882    }
     883
     884    public function get_blog_by_id( $id )
     885    {
     886        return isset( $this->blogs[ $id ] ) ? $this->blogs[ $id ] : null;
    603887    }
    604888}
  • wp-super-network/trunk/src/plugin.php

    r2671848 r2864100  
    77class WP_Super_Network
    88{
     9    const ENTITIES_TO_REPLACE = array(
     10        'comments' => array(),
     11        'posts' => array(),
     12        'term_taxonomy' => array(),
     13        'terms' => array()
     14    );
     15
     16    const TABLES_TO_REPLACE = array(
     17        'commentmeta' => array(
     18            'comment_id' => 'comments'
     19        ),
     20        'comments' => array(
     21            'comment_ID' => 'comments',
     22            'comment_post_ID' => 'posts'
     23        ),
     24        'postmeta' => array(
     25            'post_id' => 'posts'
     26        ),
     27        'posts' => array(
     28            'ID' => 'posts',
     29            'post_parent' => 'posts'
     30        ),
     31        'term_relationships' => array(
     32            'object_id' => 'posts',
     33            'term_taxonomy_id' => 'term_taxonomy'
     34        ),
     35        'term_taxonomy' => array(
     36            'term_id' => 'terms',
     37            'term_taxonomy_id' => 'term_taxonomy'
     38        ),
     39        'termmeta' => array(
     40            'term_id' => 'terms'
     41        ),
     42        'terms' => array(
     43            'term_id' => 'terms'
     44        )
     45    );
     46
    947    /**
    1048     * Static instance of the plugin.
     
    4583    }
    4684
    47     function options( $value, $option )
     85    public static function options( $value, $option, $default )
    4886    {
    4987        if ( !is_main_site() )
    5088        {
    51             switch_to_blog( get_main_site_id() );
    52             $value = get_option( $option );
    53             restore_current_blog();
     89            $main = get_main_site_id();
     90
     91            if ( $main > 0 )
     92            {
     93                switch_to_blog( $main );
     94                $value = get_option( $option, $default );
     95                restore_current_blog();
     96            }
     97            else
     98            {
     99                if ( in_array( $option, array( 'supernetwork_consolidated', 'supernetwork_post_types', 'supernetwork_options' ), true ) )
     100                {
     101                    $value = $default;
     102                }
     103            }
     104
     105            if ( $value === false )
     106            {
     107                add_filter( 'option_' . $option, '__return_false' );
     108            }
    54109        }
    55110
     
    57112    }
    58113
     114    public static function add_option( $option, $value )
     115    {
     116        if ( !is_main_site() && has_filter( 'pre_option_' . $option, array( __CLASS__, 'options' ) ) )
     117        {
     118            $main = get_main_site_id();
     119
     120            if ( $main > 0 && current_user_can_for_blog( $main, 'manage_options' ) )
     121            {
     122                switch_to_blog( $main );
     123                add_option( $option, $value );
     124                restore_current_blog();
     125            }
     126        }
     127    }
     128
     129    public static function update_option( $option, $old_value, $value )
     130    {
     131        if ( !is_main_site() && has_filter( 'pre_option_' . $option, array( __CLASS__, 'options' ) ) )
     132        {
     133            $main = get_main_site_id();
     134
     135            if ( $main > 0 && current_user_can_for_blog( $main, 'manage_options' ) )
     136            {
     137                switch_to_blog( $main );
     138                update_option( $option, $value );
     139                restore_current_blog();
     140            }
     141        }
     142    }
     143
    59144    /**
    60145     * Launch the initialization process.
     
    64149    public function run()
    65150    {
     151        // Complete this before accessing the option on next line
     152        add_filter( 'pre_option_supernetwork_options', array( __CLASS__, 'options' ), 10, 3 );
     153        add_filter( 'pre_option_supernetwork_post_types', array( __CLASS__, 'options' ), 10, 3 );
     154        add_filter( 'pre_option_supernetwork_consolidated', array( __CLASS__, 'options' ), 10, 3 );
     155
     156        foreach ( get_option( 'supernetwork_options', array() ) as $option => $val )
     157        {
     158            if ( $val && strpos( $option, '_' ) !== 0 && strpos( $option, 'supernetwork_' ) !== 0 )
     159            {
     160                add_filter( 'pre_option_' . $option, array( __CLASS__, 'options' ), 10, 3 );
     161            }
     162        }
     163
     164        add_filter( 'add_option', array( __CLASS__, 'add_option' ), 10, 2 );
     165        add_filter( 'update_option', array( __CLASS__, 'update_option' ), 10, 3 );
     166
     167        // Disable querying of meta ID. See issue #10
     168        add_filter( 'update_comment_metadata_by_mid', '__return_false' );
     169        add_filter( 'update_post_metadata_by_mid', '__return_false' );
     170        add_filter( 'update_term_metadata_by_mid', '__return_false' );
     171        add_filter( 'delete_comment_metadata_by_mid', '__return_false' );
     172        add_filter( 'delete_post_metadata_by_mid', '__return_false' );
     173        add_filter( 'delete_term_metadata_by_mid', '__return_false' );
     174
     175        $this->network->register();
     176
    66177        // Load functions
    67         add_filter( 'post_type_link', array( $this->network, 'intercept_permalink_for_post' ), 10, 2 );
    68         add_filter( 'post_link', array( $this->network, 'intercept_permalink_for_post' ), 10, 2 );
    69         add_filter( 'page_link', array( $this->network, 'intercept_permalink' ), 10, 2 );
    70         add_filter( 'map_meta_cap', array( $this->network, 'intercept_capability_write' ), 10, 4 );
    71         add_filter( 'user_has_cap', array( $this->network, 'intercept_capability_read' ), 10, 4 );
    72         add_filter( 'pre_handle_404', array( $this->network, 'singular_access' ), 10, 2 );
    73         add_filter( 'query', array( $this->network, 'intercept_query' ), 10, 2 );
    74         add_filter( 'wp_insert_post', array( $this->network, 'shared_auto_increment' ), 10, 3 );
    75178        add_filter( 'network_admin_menu', array( $this, 'summary' ) );
    76 
    77         if ( is_main_site() ) $this->network->register_pages();
    78 
    79         // Complete this before accessing the option on next line
    80         add_filter( 'pre_option_supernetwork_options', array( $this, 'options' ), 10, 2 );
    81         add_filter( 'pre_option_supernetwork_post_types', array( $this, 'options' ), 10, 2 );
    82         add_filter( 'pre_option_supernetwork_consolidated', array( $this, 'options' ), 10, 2 );
    83        
    84         foreach ( (array) get_option( 'supernetwork_options' ) as $option => $val )
    85         {
    86             if ( $val && strpos( $option, '_' ) !== 0 && strpos( $option, 'supernetwork_' ) !== 0 )
    87             {
    88                 add_filter( 'pre_option_' . $option, array( $this, 'options' ), 10, 2 );
    89             }
    90         }
    91 
    92         $this->network->consolidated = !empty( get_option( 'supernetwork_consolidated' )['consolidated'] );
    93179
    94180        if ( !$this->network->consolidated )
     
    154240            if ( current_user_can( 'edit_post', $post->ID ) )
    155241            {
    156                 if ( in_array( (string) $post->ID, $this->network->collisions, true ) )
     242                $collisions = $this->network->collisions;
     243
     244                if ( array_intersect( get_comments( 'fields=ids&post_id=' . $post->ID ), $collisions['comments'] ) !== array() || in_array( (string) $post->ID, $collisions['posts'], true ) || array_intersect( wp_get_object_terms( $post->ID, array_keys( $GLOBALS['wp_taxonomies'] ), 'fields=tt_ids' ), $collisions['term_taxonomy'] ) !== array() || array_intersect( wp_get_object_terms( $post->ID, array_keys( $GLOBALS['wp_taxonomies'] ), 'fields=ids' ), $collisions['terms'] ) !== array() )
    157245                {
    158246                    $actions['republish'] = '<i style="color: #888;">' . __( 'Can&apos;t Republish', 'supernetwork' ) . '</i>';
  • wp-super-network/trunk/wp-super-network.php

    r2671848 r2864100  
    44 * Plugin URI:
    55 * Description: Share content between sites and create offspring networks.
    6  * Version: 1.1.0
     6 * Version: 1.2.0
    77 * Requires at least: 5.0
    8  * Requires PHP: 5.6
     8 * Requires PHP: 7.2
    99 * Author: Ask Carlo
    1010 * Author URI: https://askcarlo.com
Note: See TracChangeset for help on using the changeset viewer.