prefix . 'linktender_links'; } /** * Instances (where a link appears) table name. * * @return string */ public static function instances_table() { global $wpdb; return $wpdb->prefix . 'linktender_instances'; } /** * Activation hook. * * Activation only creates the plugin's tables and stores the schema version. It prints * nothing, makes no HTTP requests, schedules no events and does not start link checking: * checks begin only after an administrator clicks "Start checking links" on the LinkTender * screen (see LinkTender_Install::start_checking()). * * On multisite, network activation sets up every site of the network, in batches of * self::SITE_BATCH. If a very large network cannot be finished within the time budget, * each remaining site creates its tables the first time its dashboard is opened * (maybe_upgrade on admin_init). Nothing is scheduled for this. * * @param bool $network_wide Network activation. * @return void */ public static function activate( $network_wide = false ) { if ( is_multisite() && $network_wide ) { delete_site_option( self::NETWORK_OPTION ); self::install_network( 0 ); return; } self::install_site(); } /** * Sets up sites of the current network, starting at the given offset. * * Sites are read with get_sites() in batches of self::SITE_BATCH, ordered by ID, until * no more are returned or the time budget is used up. There is no upper limit on the * number of sites; sites not reached set themselves up on their first admin page load. * * @param int $offset Number of sites already processed. * @return bool True when every site was processed. */ public static function install_network( $offset = 0 ) { $start = microtime( true ); /** * Filters the seconds network activation may spend before the rest continues in the background. * * @param int $seconds Seconds. */ $budget = (int) apply_filters( 'linktender_network_install_budget', 20 ); $offset = max( 0, (int) $offset ); do { $ids = self::site_ids( $offset, get_current_network_id() ); foreach ( $ids as $site_id ) { switch_to_blog( $site_id ); self::install_site(); restore_current_blog(); } $offset += count( $ids ); if ( count( $ids ) < self::SITE_BATCH ) { delete_site_option( self::NETWORK_OPTION ); return true; } } while ( microtime( true ) - $start < $budget ); // Very large network: the remaining sites are set up on their first admin page load. return false; } /** * Handles a background event left by version 1.0.3 or 1.1.0 (which continued very large * network activations in the background). Newer versions no longer schedule it. * * @return void */ public static function continue_network_install() { $offset = get_site_option( self::NETWORK_OPTION, false ); delete_site_option( self::NETWORK_OPTION ); if ( false === $offset || ! is_multisite() ) { return; } self::install_network( (int) $offset ); } /** * One batch of site IDs, oldest first. * * @param int $offset Offset. * @param int|null $network_id Network ID, or null for all networks. * @return int[] */ public static function site_ids( $offset, $network_id = null ) { $args = array( 'fields' => 'ids', 'number' => self::SITE_BATCH, 'offset' => max( 0, (int) $offset ), 'orderby' => 'id', 'order' => 'ASC', 'update_site_meta_cache' => false, ); if ( null !== $network_id ) { $args['network_id'] = (int) $network_id; } return array_map( 'intval', get_sites( $args ) ); } /** * Creates the tables on the current site. Silent and offline: no output, no HTTP requests, * no scheduled events. Link checking is not started here. * * @return void */ public static function install_site() { $installed = (int) get_option( self::DB_OPTION, 0 ); self::create_tables(); if ( $installed < LINKTENDER_DB_VERSION ) { self::migrate( $installed ); update_option( self::DB_OPTION, LINKTENDER_DB_VERSION ); } } /** * Whether the plugin's tables exist on the current site (schema version stored). * * @return bool */ public static function is_installed() { return (int) get_option( self::DB_OPTION, 0 ) >= 1; } /** * Whether an administrator has started link checking on this site. * * @return bool */ public static function checking_started() { return (bool) get_option( self::STARTED_OPTION, false ); } /** * Starts link checking on the current site after an administrator asked for it: * records the choice, queues the first content scan and schedules background batches. * * @return void */ public static function start_checking() { self::maybe_upgrade(); if ( ! self::checking_started() ) { update_option( self::STARTED_OPTION, array( 'time' => time(), 'user' => get_current_user_id(), ), false ); } // Content may have changed while checking was stopped, so start with a fresh scan. if ( ! LinkTender_Scanner::is_running() ) { LinkTender_Scanner::request_full_scan(); } LinkTender_Queue::schedule(); } /** * Deactivation hook: removes scheduled events and stops checking. Data is kept until * uninstall. After re-activation nothing runs until an administrator clicks Start again. * * @param bool $network_wide Network deactivation. * @return void */ public static function deactivate( $network_wide = false ) { if ( is_multisite() && $network_wide ) { wp_clear_scheduled_hook( self::NETWORK_HOOK ); delete_site_option( self::NETWORK_OPTION ); $offset = 0; do { $ids = self::site_ids( $offset, get_current_network_id() ); foreach ( $ids as $site_id ) { switch_to_blog( $site_id ); self::stop_site(); restore_current_blog(); } $found = count( $ids ); $offset += $found; } while ( self::SITE_BATCH === $found ); return; } self::stop_site(); } /** * Stops checking on the current site: no scheduled events, consent to check is cleared. * * @return void */ private static function stop_site() { LinkTender_Queue::unschedule(); delete_option( self::STARTED_OPTION ); delete_option( LinkTender_Scanner::PENDING_OPTION ); } /** * Adds the plugin's tables to the list core drops when a multisite site is deleted. * * @param string[] $tables Table names. * @return string[] */ public static function drop_site_tables( $tables ) { $tables[] = self::links_table(); $tables[] = self::instances_table(); return $tables; } /** * Runs table migrations when the stored version is older than the code. * * Migrations use dbDelta on the plugin's own small tables only. dbDelta never drops * columns, so an interrupted or repeated upgrade is harmless. * * @return void */ public static function maybe_upgrade() { $installed = (int) get_option( self::DB_OPTION, 0 ); if ( $installed >= LINKTENDER_DB_VERSION ) { return; } // Avoid two requests upgrading at once. if ( get_transient( 'linktender_upgrading' ) ) { return; } set_transient( 'linktender_upgrading', 1, MINUTE_IN_SECONDS ); self::create_tables(); self::migrate( $installed ); update_option( self::DB_OPTION, LINKTENDER_DB_VERSION ); delete_transient( 'linktender_upgrading' ); } /** * Data migrations between schema versions. * * @param int $installed Previously installed schema version (0 for new installs). * @return void */ private static function migrate( $installed ) { // 1.0.0–1.0.2 started checking on activation. Sites that already ran it keep running. if ( $installed >= 1 && $installed < 2 && false === get_option( self::STARTED_OPTION, false ) && false !== get_option( LinkTender_Scanner::STATE_OPTION, false ) ) { update_option( self::STARTED_OPTION, array( 'time' => time(), 'user' => 0, ), false ); } // 1.1.0 also indexes comments and shortcode attributes: re-scan sites where checking already runs. if ( $installed >= 1 && $installed < 3 && self::checking_started() ) { LinkTender_Scanner::request_full_scan(); } /** * Fires after the plugin's tables were created or upgraded. * * @param int $installed Previously installed schema version (0 for new installs). */ do_action( 'linktender_db_upgraded', $installed ); } /** * Creates or updates the tables with dbDelta. * * @return void */ private static function create_tables() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $charset = $wpdb->get_charset_collate(); $links = self::links_table(); $instances = self::instances_table(); $tables = array(); $tables[] = "CREATE TABLE {$links} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, url text NOT NULL, url_hash char(40) NOT NULL, host varchar(191) NOT NULL DEFAULT '', status varchar(20) NOT NULL DEFAULT 'pending', http_code smallint(5) unsigned NOT NULL DEFAULT 0, error_code varchar(30) NOT NULL DEFAULT '', message varchar(255) NOT NULL DEFAULT '', final_url text NULL, redirect_count tinyint(3) unsigned NOT NULL DEFAULT 0, response_ms int(10) unsigned NOT NULL DEFAULT 0, check_count int(10) unsigned NOT NULL DEFAULT 0, fail_count smallint(5) unsigned NOT NULL DEFAULT 0, first_failure datetime NULL DEFAULT NULL, last_checked datetime NULL DEFAULT NULL, next_check datetime NOT NULL DEFAULT '2000-01-01 00:00:00', dismissed tinyint(1) unsigned NOT NULL DEFAULT 0, created_at datetime NOT NULL DEFAULT '2000-01-01 00:00:00', PRIMARY KEY (id), UNIQUE KEY url_hash (url_hash), KEY status (status), KEY next_check (next_check), KEY host (host) ) {$charset};"; $tables[] = "CREATE TABLE {$instances} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, link_id bigint(20) unsigned NOT NULL, source_type varchar(20) NOT NULL, source_id bigint(20) unsigned NOT NULL DEFAULT 0, source_field varchar(191) NOT NULL DEFAULT '', element varchar(20) NOT NULL DEFAULT 'a', anchor varchar(255) NOT NULL DEFAULT '', raw_url text NOT NULL, PRIMARY KEY (id), KEY link_id (link_id), KEY source (source_type,source_id) ) {$charset};"; dbDelta( $tables ); } }