label) and silotree_report_bulk * (action: $action, int[] $post_ids): batch actions on the posts-nothing-links-to report. With no * actions, the report has no checkboxes at all. * - silotree_broken_bulk_actions (filter) and silotree_broken_bulk (action: $action, array[] of * from_post and url): the same for Broken links. silotree_broken_row_actions (filter: markup * strings, $row) adds a button to one row beside "Fix in editor". * - silotree_editor_assets (action: $post, $block_editor): after the editor's scripts are queued, * to add one that extends them. The publish prompt reads two wp.hooks filters, * silotree.publishPrompt.rowControl and silotree.publishPrompt.footer. * - silotree_settings_cards (action) and silotree_settings_actions (filter: null or a notice) on * Silotree, Settings. * - silotree_batch_label (filter: $label, $batch_id): what Undo history calls a batch. * - silotree_builder_written (action: int $post_id, SiloTree_Store $store): after Silotree wrote, * or undid, a page builder's own data for a post, for a cache or CDN plugin to purge that page. * - silotree_click_rate_limit (filter: int, default 600): counted link clicks a minute, site-wide. * - silotree_screen_top (action: 'reports' or 'broken'): under the heading of those screens, for * an add-on's result banner. * * Undo never depends on an add-on: every batch, whoever wrote it, is recorded and taken back by * SiloTree_Apply in this plugin. */ do_action( 'silotree_loaded' ); /** * Bring the tables and options up to date after a plugin update, rather than at the next scan. */ add_action( 'plugins_loaded', function () { if ( (int) get_option( SiloTree_Scan::SCHEMA_KEY ) !== SiloTree_Scan::SCHEMA ) { SiloTree_Scan::install(); } /* * 0.1 had no "connected" flag: it talked to the app whenever it had a key. A site that had * already mapped its posts was plainly connected, so it stays connected through the upgrade * instead of going quiet until the app's next call. */ if ( get_option( 'silotree_version' ) !== SILOTREE_VERSION ) { if ( ! SiloTree_Auth::connected() && get_option( 'silotree_last_map' ) ) { update_option( SiloTree_Auth::CONNECTED, time(), false ); } /* * 0.4.1 reads links differently (WordPress's own addresses are no longer broken links) * and measures depth on sites whose home page lists posts, so the stored graph is read * again once. The reports keep the old figures on screen until the new ones are in. * 0.5.0 reads an Elementor page's own text rather than its fallback copy: once more. */ $scan = SiloTree_Scan::state(); if ( $scan['finished'] && ! $scan['running'] && version_compare( (string) get_option( 'silotree_version', '0' ), '0.5.0', '<' ) ) { SiloTree_Scan::start(); } /* * 0.6.0 moved keyword rules, and links that follow a moved post, into Silotree Pro. * Nothing of theirs is deleted here: the saved rules stay in their option, ready for Pro * to pick up as they were, and SiloTree_Growth tells whoever had rules, once, where they * went. Batches they wrote stay in Undo history and undo from this plugin alone. */ /* * 0.8.3 keeps the two "On your pages" switches in one small autoloaded option, so a page * readers see makes no query of its own for them, and adds a daily event for upkeep. */ SiloTree_Clicks::migrate(); SiloTree_Clicks::schedule(); update_option( 'silotree_version', SILOTREE_VERSION, false ); } } ); /* * Translations: WordPress loads them from translate.wordpress.org by the text domain, as it has * since 4.6, so there is no load_plugin_textdomain() call. languages/ holds only the .pot. */ /** * The site key: the shared secret requests between this site and the app are signed with. * * Generated here. With one-click connect the app issues a fresh one during pairing and this is * replaced; the one generated here is for connecting by hand. */ function silotree_site_key() { $key = get_option( 'silotree_site_key' ); if ( ! $key ) { $key = wp_generate_password( 48, false, false ); add_option( 'silotree_site_key', $key, '', false ); } return $key; } /** * Which WordPress user requests from the app act as. * * The key authenticates a server, and a server has no capabilities. So the connection is bound to * the administrator who set it up, and requests act as them: every capability check is real, and * the revision left behind carries the name of somebody who agreed to this. */ function silotree_bound_user_id() { return (int) get_option( 'silotree_bound_user', 0 ); } function silotree_bind_user( $user_id ) { $user_id = (int) $user_id; if ( $user_id > 0 ) { update_option( 'silotree_bound_user', $user_id, false ); } return $user_id; } add_action( 'admin_init', function () { // With no binding at all (a manual connection), the first administrator to open the settings. if ( ! silotree_bound_user_id() && current_user_can( 'manage_options' ) ) { silotree_bind_user( get_current_user_id() ); } } ); /* * Settings live in Silotree's own menu, beside the reports, rather than under Settings where * nobody looks for them. The old address still works: it is sent on here. */ add_action( 'admin_menu', function () { add_submenu_page( 'silotree-overview', __( 'Silotree settings', 'silotree' ), __( 'Settings', 'silotree' ), 'manage_options', 'silotree', 'silotree_settings_page' ); }, 20 ); /* * WordPress refuses an unknown page before admin_init runs, so the old address is caught on the * way to that refusal: admin_page_access_denied fires just before it. */ add_action( 'admin_page_access_denied', function () { global $pagenow; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a redirect, nothing is changed. if ( 'options-general.php' === $pagenow && isset( $_GET['page'] ) && 'silotree' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $pair = isset( $_GET['silotree_pair'] ) ? sanitize_key( wp_unslash( $_GET['silotree_pair'] ) ) : ''; wp_safe_redirect( add_query_arg( array_filter( array( 'silotree_pair' => $pair ) ), admin_url( 'admin.php?page=silotree' ) ) ); exit; } } ); /** * The settings screen's form actions. Each one is nonce-checked. The screen itself is limited to * administrators, and each action only changes this plugin's own settings. * * @return array{0:string,1:string}|null A notice: type and sentence. */ function silotree_settings_actions() { // phpcs:disable WordPress.Security.NonceVerification.Missing -- each branch checks its own nonce. if ( isset( $_POST['silotree_regenerate'] ) && check_admin_referer( 'silotree_regenerate', '_silotree_regenerate' ) ) { SiloTree_Auth::regenerate(); return array( 'warning', __( 'New key made. Silotree cannot reach this site until you connect it again.', 'silotree' ) ); } if ( isset( $_POST['silotree_check'] ) && check_admin_referer( 'silotree_check', '_silotree_check' ) ) { $result = SiloTree_API::check(); return is_wp_error( $result ) ? array( 'error', $result->get_error_message() ) : array( 'success', __( 'Silotree answered, and recognises this site.', 'silotree' ) ); } if ( isset( $_POST['silotree_widget'] ) && check_admin_referer( 'silotree_widget', '_silotree_widget' ) ) { update_option( 'silotree_show_dashboard_widget', ! empty( $_POST['silotree_show_widget'] ), false ); return array( 'success', __( 'Saved.', 'silotree' ) ); } if ( isset( $_POST['silotree_remote_apply_save'] ) && check_admin_referer( 'silotree_remote_apply', '_silotree_remote_apply' ) ) { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You cannot change that.', 'silotree' ), '', array( 'response' => 403 ) ); } if ( ! empty( $_POST['silotree_remote_apply'] ) ) { update_option( 'silotree_remote_apply', '1', false ); } else { delete_option( 'silotree_remote_apply' ); } return array( 'success', __( 'Saved.', 'silotree' ) ); } $pages = SiloTree_Clicks::settings_action(); if ( $pages ) { return $pages; } /** * A settings form an add-on put on this screen. It checks its own nonce and capability, and * answers with a notice (type and sentence) when the form was its own, or null. * * @param array|null $notice Null, unless an earlier handler answered. */ $extra = apply_filters( 'silotree_settings_actions', null ); if ( is_array( $extra ) && 2 === count( $extra ) ) { return array_values( $extra ); } if ( isset( $_POST['silotree_uninstall'] ) && check_admin_referer( 'silotree_uninstall', '_silotree_uninstall' ) ) { $choice = isset( $_POST['silotree_uninstall_links'] ) ? sanitize_text_field( wp_unslash( $_POST['silotree_uninstall_links'] ) ) : 'keep'; if ( ! in_array( $choice, array( 'keep', 'strip', 'remove' ), true ) ) { $choice = 'keep'; } update_option( 'silotree_uninstall_links', $choice, false ); return array( 'success', __( 'Saved.', 'silotree' ) ); } // phpcs:enable // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only. $pair = isset( $_GET['silotree_pair'] ) ? sanitize_key( wp_unslash( $_GET['silotree_pair'] ) ) : ''; if ( '' !== $pair ) { if ( 'connected' === $pair && ! SiloTree_Auth::connected() ) { $pair = 'verify_failed'; } return SiloTree_Pair::message( $pair ); } return null; } function silotree_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $notice = silotree_settings_actions(); $key = silotree_site_key(); $app = SiloTree_API::app_url(); $connected = SiloTree_Auth::connected(); $since = (int) get_option( SiloTree_Auth::CONNECTED ); $manual = add_query_arg( 'site', rawurlencode( SiloTree_API::domain() ), $app . '/connect' ); ?>
%s', esc_url( admin_url( 'admin.php?page=silotree-setup' ) ), esc_html__( 'Run setup again', 'silotree' ) ) ); SiloTree_Screen::page_help( 'pair-the-plugin', __( 'Connecting, checking the connection and re-pairing', 'silotree' ) ); ?>
'success', 'error' => 'error', 'warning' => 'limit', ); SiloTree_Screen::banner( $kinds[ $notice[0] ] ?? 'info', $notice[1], '', 'error' === $notice[0] ? '

' . SiloTree_Screen::help_link( 'plugin-cant-reach-silotree', __( 'What each connection error means, and its fix', 'silotree' ), 'if-it-fails' ) . '

' : '' ); } ?>

' . esc_html( preg_replace( '#^https?://#', '', $app ) ) . '' ), SiloTree_Screen::allowed_tags() ); ?>