<?php
/**
 * Plugin Name: Aura
 * Plugin URI: https://aura-builder.duckdns.org
 * Description: Aura — build your site with AI. It generates sections (HTML/CSS/JS) with dynamic WordPress data, editable through a props interface. It lives alongside Gutenberg: every page belongs to one of the two editors. Generation happens on the Aura service: all you need here is the activation key.
 * Version: 0.59.12
 * Author: Michele Zulian
 * Requires at least: 5.9
 * Requires PHP: 7.2
 * Update URI: https://aura-builder.duckdns.org
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: aipb
 * Domain Path: /languages
 */

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

/**
 * The version appears in TWO places and that is not an oversight: WordPress demands a literal in the
 * header — it cannot read a constant — and the rest of the plugin cannot re-read the file on every
 * request to know which version it is running.
 *
 * THE HEADER IS IN CHARGE. It is what the dashboard shows, and what the updater compares to decide
 * whether a new version exists: if the two diverge, a zip is delivered that declares one number and
 * behaves like another, and the symptom is not an error — it is an update that is never offered, or
 * offered forever because after installing it the number in the dashboard has not changed.
 *
 * This line mirrors the header, and `bin/build-plugin.php` REFUSES to package if the two do not
 * match. The check lives there because that is the only road to the customer.
 */
define( 'AIPB_VERSION', '0.59.12' );
define( 'AIPB_PATH', plugin_dir_path( __FILE__ ) );
define( 'AIPB_URL', plugin_dir_url( __FILE__ ) );

/**
 * The Aura service's address.
 *
 * IT IS NOT A SETTING. The service is always ours: asking a customer to type an address is one more
 * field to get wrong, and every wrong character there becomes a «it does not work» that looks like a
 * rejected licence. The only thing the customer pastes is the activation key.
 *
 * THIS LINE IS REWRITTEN BY `bin/build-plugin.php` on the server, taking it from `AURA_PUBLIC_URL`:
 * the zip the customer downloads already points where it should, whatever is written here, and
 * there is no step at which anybody can forget.
 *
 * What is written here is therefore the DEVELOPMENT address, and it is written here rather than in
 * `wp-config.php` so that the plugin is self-contained: you move the directory, you symlink it into
 * another site, and it goes on talking to the same service without a second file to remember.
 */
define('AIPB_SERVER_URL', 'https://aura-builder.duckdns.org');

/**
 * The `Update URI:` header above is empty for the same reason, and the same build script fills it
 * in. It does TWO things, and the second matters as much as the first:
 *
 * 1. It tells WordPress who to ask whether a newer version exists — see `class-update.php`.
 * 2. It stops wordpress.org answering in our place. Without that line WordPress asks the public
 *    directory about EVERY plugin, using the folder name as the identifier: the day somebody
 *    publishes a plugin called `aura-client` there, WordPress offers theirs to our customers and
 *    installs it over ours. That is not a thought experiment — it has happened to others, and the
 *    symptom is a site that suddenly runs a plugin nobody chose.
 *
 * It stays empty in the source tree, so in development — where the plugin is a symlink to the
 * working directory — there is no update to offer and nothing that can overwrite it.
 */

// The meta keys used for every section/component (ai_section) and for a page's stack.
define( 'AIPB_META_TEMPLATE', '_aipb_template' );
define( 'AIPB_META_SCHEMA', '_aipb_schema' );
define( 'AIPB_META_VALUES', '_aipb_values' );
define( 'AIPB_META_BLOCKS', '_aipb_blocks' ); // JSON: an ordered array of blocks (posts/pages only).
// SUPERSEDED. The canvas is no longer a per-page preference: a page built with Aura is always full
// page, because its sections already contain a header and a footer (see AIPB_Render::use_canvas).
// The constant stays because saving cleans up the old meta wherever it was. For the exception, use
// the `aipb_canvas` filter.
define( 'AIPB_META_CANVAS', '_aipb_canvas' );

require_once AIPB_PATH . 'includes/class-cpt.php';
require_once AIPB_PATH . 'includes/class-config.php';
require_once AIPB_PATH . 'includes/class-i18n.php';
require_once AIPB_PATH . 'includes/class-secret.php';
require_once AIPB_PATH . 'includes/class-design.php';
require_once AIPB_PATH . 'includes/class-effects.php';
require_once AIPB_PATH . 'includes/class-fonts.php';
require_once AIPB_PATH . 'includes/class-icons.php';
require_once AIPB_PATH . 'includes/class-ownership.php';
require_once AIPB_PATH . 'includes/class-strings.php';
require_once AIPB_PATH . 'includes/class-wpml.php';
require_once AIPB_PATH . 'includes/class-gutenberg.php';
require_once AIPB_PATH . 'includes/class-assets.php';
require_once AIPB_PATH . 'includes/class-nav.php';
require_once AIPB_PATH . 'includes/class-site-context.php';
require_once AIPB_PATH . 'includes/class-shortcodes.php';
require_once AIPB_PATH . 'includes/class-types.php';
require_once AIPB_PATH . 'includes/class-fields.php';
require_once AIPB_PATH . 'includes/class-model-apply.php';
require_once AIPB_PATH . 'includes/class-templates.php';
require_once AIPB_PATH . 'includes/class-images.php';
require_once AIPB_PATH . 'includes/class-autofields.php';
require_once AIPB_PATH . 'includes/class-switches.php';
require_once AIPB_PATH . 'includes/class-srcset.php';
require_once AIPB_PATH . 'includes/class-webp.php';
require_once AIPB_PATH . 'includes/class-template-engine.php';
require_once AIPB_PATH . 'includes/class-css-scope.php';
require_once AIPB_PATH . 'includes/class-tidy.php';
require_once AIPB_PATH . 'includes/class-check.php';
require_once AIPB_PATH . 'includes/class-portable.php';
require_once AIPB_PATH . 'includes/class-remote.php';
require_once AIPB_PATH . 'includes/class-update.php';
require_once AIPB_PATH . 'includes/class-usage.php';
require_once AIPB_PATH . 'includes/class-site-payload.php';
require_once AIPB_PATH . 'includes/class-ai-provider.php';
require_once AIPB_PATH . 'includes/class-rest.php';
require_once AIPB_PATH . 'includes/class-filters.php';
require_once AIPB_PATH . 'includes/class-render.php';
require_once AIPB_PATH . 'includes/class-admin.php';
require_once AIPB_PATH . 'includes/class-admin-settings.php';
require_once AIPB_PATH . 'includes/class-admin-brand.php';
require_once AIPB_PATH . 'includes/class-admin-model.php';

/**
 * Translations of the CONTENT, and why this one call is not inside aipb_init().
 *
 * WPML builds the list of page builders it has to support while its String Translation module
 * loads, and that happens on `plugins_loaded` as well — sometimes before our own callback, because
 * the order of two plugins on the same hook is the order they are loaded in, which is alphabetical
 * and therefore nobody's decision. A declaration arriving one moment late is not an error and
 * raises nothing: it simply produces a site where the pages have no strings to translate, and the
 * reason is invisible.
 *
 * Here, while the file is being read, we are as early as it is possible to be. Everything it
 * registers is a filter or an action: with no WPML on the site, none of them is ever applied.
 */
AIPB_WPML::init();

/**
 * Bootstrap.
 */
function aipb_init() {
	// First: it decides which language everything below speaks, and the filters it registers have to
	// be in place before anything can ask for a translated string.
	AIPB_I18n::init();

	// The brand's version bump: it has to see every write to the brand options, wherever it
	// comes from, so it hangs off the option hooks rather than off any one form.
	AIPB_Design::init();

	AIPB_CPT::init();
	AIPB_Types::init();
	AIPB_Fields::init();
	AIPB_Fonts::init();
	AIPB_Ownership::init();
	AIPB_Gutenberg::init();
	AIPB_Assets::init();
	AIPB_REST::init();
	AIPB_Filters::init();
	AIPB_Srcset::init();
	AIPB_WebP::init();
	AIPB_Render::init();
	AIPB_Admin::init();

	// New versions, offered in the dashboard like any other plugin. NOT gated on `is_admin()`:
	// WordPress checks for updates from wp-cron too, and that can arrive as a front-end request.
	AIPB_Update::init();

	// The prompt's context is cached for 5 minutes: if the menu changes, better to know at once.
	// It holds for both worlds: classic menus and the block navigation of FSE themes.
	add_action( 'wp_update_nav_menu', array( 'AIPB_Site_Context', 'flush' ) );
	add_action( 'save_post_wp_navigation', array( 'AIPB_Site_Context', 'flush' ) );

	// Updates: see aipb_maybe_upgrade().
	add_action( 'admin_init', 'aipb_maybe_upgrade' );
}
add_action( 'plugins_loaded', 'aipb_init' );

/**
 * Maintenance after a plugin update.
 *
 * It is needed because `register_activation_hook` does NOT fire when the plugin is updated: it only
 * fires on activation. A plugin that is already active and goes from 0.36 to 0.37 — through the
 * updater, through a zip, through a git pull on the server — runs neither, so everything the
 * activation used to prepare stays as it was. For the rewrite rules that means stale permalinks: the
 * CPT is registered with the new rules but WordPress goes on serving the old ones, cached in the
 * `rewrite_rules` option, until somebody saves the permalinks by hand.
 *
 * It runs on `admin_init` and not on `init` because `flush_rewrite_rules()` is an expensive write
 * and has no business in a frontend request. The comparison with the option makes the body run once
 * per version.
 */
function aipb_maybe_upgrade() {
	$stored = (string) get_option( 'aipb_version', '' );
	if ( AIPB_VERSION === $stored ) {
		return;
	}

	// No shortcut on the first round: the option is also missing for whoever is updating FROM a
	// version older than this function — which is exactly the case to fix. Repeating the activation's
	// work on a fresh install costs one write, once.
	AIPB_CPT::register();
	flush_rewrite_rules();

	// The caches derive from how the plugin reads the site, and a new version may read it
	// differently: better to rebuild them than to serve a memory of how the old code saw it.
	AIPB_Site_Context::flush();
	if ( class_exists( 'AIPB_Remote' ) ) {
		AIPB_Remote::flush_health();
	}

	// What was cached about the latest release describes, by now, the version that has just been
	// installed. Keeping it is how a plugin ends up offering to update itself to itself.
	if ( class_exists( 'AIPB_Update' ) ) {
		AIPB_Update::flush();
	}

	update_option( 'aipb_version', AIPB_VERSION, false );

	/**
	 * The update has run.
	 *
	 * @param string $to   The version now installed.
	 * @param string $from The version being upgraded from ('' on a first install).
	 */
	do_action( 'aipb_upgraded', AIPB_VERSION, $stored );
}

register_activation_hook( __FILE__, function () {
	AIPB_CPT::register(); // Makes sure the rewrite rules include the CPT.
	flush_rewrite_rules();
} );

register_deactivation_hook( __FILE__, function () {
	flush_rewrite_rules();

	// The hourly look for new releases (see AIPB_Update::watch). A cron event left behind by a
	// deactivated plugin fires forever against a hook nobody answers.
	AIPB_Update::unschedule();
} );



