HEX
Server: nginx
System: Linux pool195-106-36.bur.atomicsites.net 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17) x86_64
User: (0)
PHP: 8.3.32
Disabled: pcntl_fork
Upload Files
File: //wordpress/plugins/jetpack/16.0/extensions/plugins/block-notes/block-notes.php
<?php
/**
 * Block Editor - Block Notes plugin feature.
 *
 * @package automattic/jetpack
 */

namespace Automattic\Jetpack\Extensions\BlockNotes;

use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\My_Jetpack\Products\Jetpack_Ai;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;

if ( ! defined( 'ABSPATH' ) ) {
	exit( 0 );
}

const FEATURE_NAME            = 'block-notes';
const ASSET_BASE_PATH         = 'widgets.wp.com/agents-manager/';
const ASSET_JS_URL            = 'https://' . ASSET_BASE_PATH . 'block-notes.min.js';
const ASSET_JSON_URL          = 'https://' . ASSET_BASE_PATH . 'block-notes.asset.json';
const ASSET_JSON_PATH         = ASSET_BASE_PATH . 'block-notes.asset.json';
const ASSET_TRANSIENT         = 'jetpack_block_notes_asset';
const HEADLESS_AGENT_PROVIDER = 'block-notes/headless-agent-provider';

/**
 * Check if Block Notes is enabled.
 *
 * Enabled when the Big Sky plugin is active, or when the site has
 * a paid Jetpack AI plan and AI features are not disabled.
 *
 * @return bool
 */
function is_block_notes_enabled() {
	/**
	 * Temporarily disabled while we investigate expensive API calls
	 * triggered by has_paid_ai_plan() on every Gutenberg page load for
	 * self-hosted sites. Filter allows tests and development to re-enable.
	 *
	 * @since 15.8
	 *
	 * @param bool $enabled Whether Block Notes is force-enabled. Default false.
	 */
	if ( ! apply_filters( 'jetpack_block_notes_enabled', false ) ) {
		return false;
	}

	if ( is_big_sky_enabled() ) {
		return true;
	}

	if ( ! has_jetpack_ai_features() ) {
		return false;
	}

	if ( ! has_paid_ai_plan() ) {
		return false;
	}

	return true;
}

/**
 * Check if the site has a paid Jetpack AI plan.
 *
 * On WordPress.com, uses the lightweight wpcom_site_has_feature() lookup.
 * On self-hosted and Atomic sites, uses the My Jetpack product class.
 *
 * @return bool
 */
function has_paid_ai_plan() {
	$has_paid_plan = false;

	if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'wpcom_site_has_feature' ) ) {
		$has_paid_plan = wpcom_site_has_feature( 'ai-assistant', get_current_blog_id() );
	} elseif ( class_exists( Jetpack_Ai::class ) ) {
		$has_paid_plan = Jetpack_Ai::has_paid_plan_for_product();
	}

	/**
	 * Filter whether the site has a paid AI plan.
	 *
	 * @since 15.7
	 *
	 * @param bool $has_paid_plan Whether the site has a paid AI plan.
	 */
	return apply_filters( 'jetpack_block_notes_has_paid_ai_plan', $has_paid_plan );
}

/**
 * Check if the Big Sky plugin is active and enabled.
 *
 * Defaults to enabled ('1') when the Big_Sky class exists but the option
 * has never been set — plugin presence implies the feature should be on.
 *
 * @return bool
 */
function is_big_sky_enabled() {
	return class_exists( 'Big_Sky' ) && get_option( 'big_sky_enable', '1' );
}

/**
 * Check whether AI features are available.
 *
 * - wpcom simple: always returns true. The jetpack_ai_enabled filter
 *   does not apply here; the paid plan check in has_paid_ai_plan()
 *   gates access instead.
 * - Otherwise requires a connected owner, not in offline mode, and
 *   AI not disabled via the jetpack_ai_enabled filter.
 *
 * @return bool
 */
function has_jetpack_ai_features() {
	$host = new Host();

	if ( $host->is_wpcom_simple() ) {
		return true;
	}

	return ( new Connection_Manager( 'jetpack' ) )->has_connected_owner()
		&& ! ( new Status() )->is_offline_mode()
		&& apply_filters( 'jetpack_ai_enabled', true );
}

/**
 * Check if the current screen is the post editor for a 'post' post type.
 *
 * Block Notes is only for the post editor (not site editor, pages, or other contexts).
 *
 * @return bool
 */
function is_post_editor() {
	if ( ! function_exists( 'get_current_screen' ) ) {
		return false;
	}

	$screen = get_current_screen();
	return $screen
		&& $screen->is_block_editor()
		&& 'post' === $screen->base
		&& 'post' === $screen->post_type;
}

/**
 * Determine if Block Notes should load on the current screen.
 *
 * @return bool
 */
function should_load_on_current_screen() {
	return is_post_editor();
}

/**
 * Register the Block Notes plugin.
 *
 * Registers when Block Notes is enabled. Screen-level gating happens at
 * enqueue time since get_current_screen() is not available here.
 *
 * @return void
 */
function register_plugin() {
	if ( ! is_block_notes_enabled() ) {
		return;
	}

	\Jetpack_Gutenberg::set_extension_available( FEATURE_NAME );
}
add_action( 'jetpack_register_gutenberg_extensions', __NAMESPACE__ . '\register_plugin' );

/**
 * Fetch and cache the remote asset manifest.
 *
 * On WordPress.com, the asset file may be accessible on the local filesystem
 * (under ABSPATH). This avoids an HTTP round-trip and works on sandboxes where
 * outbound requests to widgets.wp.com may be blocked.
 *
 * @return array|false The decoded asset data, or false on failure.
 */
function get_asset_data() {
	$cached = get_transient( ASSET_TRANSIENT );
	if ( false !== $cached ) {
		return $cached;
	}

	$data = get_asset_data_from_file();
	if ( false === $data ) {
		$data = get_asset_data_from_remote();
	}

	if ( false === $data ) {
		return false;
	}

	if ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ) {
		set_transient( ASSET_TRANSIENT, $data, HOUR_IN_SECONDS );
	}
	return $data;
}

/**
 * Try to read the asset manifest from the local filesystem.
 *
 * On WordPress.com, widgets.wp.com assets are available at ABSPATH.
 *
 * @return array|false The decoded asset data, or false if not available locally.
 */
function get_asset_data_from_file() {
	$local_path = ABSPATH . ASSET_JSON_PATH;
	if ( ! file_exists( $local_path ) ) {
		return false;
	}

	// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a local file, not a remote URL.
	$contents = file_get_contents( $local_path );
	if ( false === $contents ) {
		return false;
	}

	$data = json_decode( $contents, true );
	if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
		return false;
	}

	return $data;
}

/**
 * Fetch the asset manifest via HTTP.
 *
 * Used as a fallback when the file is not available locally (e.g. self-hosted sites).
 *
 * @return array|false The decoded asset data, or false on failure.
 */
function get_asset_data_from_remote() {
	$response = wp_safe_remote_get( ASSET_JSON_URL );
	if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
		return false;
	}

	$content_type = wp_remote_retrieve_header( $response, 'content-type' );
	if ( is_string( $content_type ) && false === stripos( $content_type, 'json' ) ) {
		return false;
	}

	$body = wp_remote_retrieve_body( $response );
	$data = json_decode( $body, true );
	if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
		return false;
	}

	return $data;
}

/**
 * Enqueue Block Notes script asset.
 *
 * @return void
 */
function do_enqueue_assets() {
	if ( ! is_block_notes_enabled() ) {
		return;
	}

	$asset_data = get_asset_data();
	if ( ! $asset_data ) {
		return;
	}

	$version      = $asset_data['version'] ?? false;
	$dependencies = $asset_data['dependencies'] ?? array();

	wp_enqueue_script(
		FEATURE_NAME,
		ASSET_JS_URL,
		$dependencies,
		$version,
		true
	);

	wp_add_inline_script(
		FEATURE_NAME,
		'if ( typeof window.blockNotesData === "undefined" ) { window.blockNotesData = ' . wp_json_encode( array( 'enabled' => true ), JSON_HEX_TAG | JSON_HEX_AMP ) . '; }',
		'before'
	);
}

/**
 * Enqueue Block Notes assets in the post editor.
 *
 * Only loads when should_load_on_current_screen() returns true.
 *
 * @return void
 */
function enqueue_block_notes() {
	if ( ! should_load_on_current_screen() ) {
		return;
	}

	do_enqueue_assets();
}
add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\enqueue_block_notes' );

/**
 * Enable the agents manager unified experience on self-hosted sites
 * when Block Notes is enabled.
 *
 * This ensures the agents manager loads and can host the headless agent
 * even when the unified chat experience is not otherwise enabled.
 *
 * @param bool $use_unified_experience Current value of the filter.
 * @return bool
 */
function enable_agents_manager_for_block_notes( $use_unified_experience ) {
	if ( $use_unified_experience ) {
		return true;
	}

	return is_block_notes_enabled();
}
add_filter( 'agents_manager_use_unified_experience', __NAMESPACE__ . '\enable_agents_manager_for_block_notes' );

/**
 * Register the Block Notes headless agent provider with the agents manager.
 *
 * When Block Notes is enabled, adds the Block Notes headless
 * agent provider module so the agents manager can load it.
 *
 * @param array $providers Existing agent provider module IDs.
 * @return array Modified array of provider module IDs.
 */
function register_headless_agent_provider( $providers ) {
	if ( ! is_block_notes_enabled() ) {
		return $providers;
	}

	$providers[] = HEADLESS_AGENT_PROVIDER;
	return $providers;
}
add_filter( 'agents_manager_agent_providers', __NAMESPACE__ . '\register_headless_agent_provider' );

/**
 * Register Block Notes meta fields and filters.
 *
 * Registers the comment meta field used to track AI processing, and hooks
 * the avatar filter for AI-authored notes.
 *
 * @return void
 */
function register_meta_fields() {
	if ( ! is_block_notes_enabled() ) {
		return;
	}

	register_meta(
		'comment',
		'bigsky_ai_processed_date',
		array(
			'type'              => 'string',
			'description'       => 'ISO date when this note was processed by AI (empty if not processed)',
			'single'            => true,
			'show_in_rest'      => true,
			'auth_callback'     => __NAMESPACE__ . '\meta_auth_callback',
			'sanitize_callback' => 'sanitize_text_field',
		)
	);

	add_filter( 'get_avatar_data', __NAMESPACE__ . '\customize_ai_avatar', 10, 2 );
}
add_action( 'init', __NAMESPACE__ . '\register_meta_fields' );

/**
 * Authorization callback for the bigsky_ai_processed_date comment meta.
 *
 * @return bool
 */
function meta_auth_callback() {
	return current_user_can( 'edit_posts' );
}

/**
 * Customize the avatar for AI-authored block notes.
 *
 * @param array $args         Avatar data arguments.
 * @param mixed $id_or_email  The Gravatar to retrieve.
 * @return array Modified avatar arguments.
 */
function customize_ai_avatar( $args, $id_or_email ) {
	if ( is_object( $id_or_email ) && isset( $id_or_email->comment_author ) ) {
		if ( 'AI [experimental]' === $id_or_email->comment_author ) {
			$args['url'] = plugins_url( 'images/big-sky.svg', JETPACK__PLUGIN_FILE );
		}
	}
	return $args;
}