<?php
defined( 'ABSPATH' ) || exit;

/**
 * Who may read the guides, and who may change them.
 *
 * **The problem this solves.** Reading and changing used to be the same
 * capability, `edit_posts`. So a site wanting to let all its staff *read* the
 * documentation had to widen that one capability — and would, without meaning
 * to, also hand them the ability to retag every guide site-wide. Tags are shared
 * taxonomy terms and notes annotate the guide itself, not a per-user copy, so
 * "let them read" and "let them rewrite it for everybody" were one decision.
 * A site implementing exactly that workaround made exactly that mistake, and
 * caught it before it reached anyone.
 *
 *   bw_guides_read    browse, search, open a guide. Read only.
 *   bw_guides_manage  tags, private notes, triggering a sync, and the authoring
 *                     screens — anything that writes.
 *
 * **`edit_posts` remains a floor, and that widens nothing.** Anyone holding it
 * could already do all of this, so granting them both capabilities changes no
 * one's access. What it buys is that **no existing site can lose a screen on
 * upgrade** — an Editor or Author reading the documentation today keeps reading
 * it without anybody having to notice this release happened.
 *
 * (This mirrors BW Lead AI's capability split, which floors at `manage_options`
 * because that is what gated it before. The floor is always "whatever already
 * granted access", not a fixed capability — copying `manage_options` here would
 * have locked Editors and Authors out of documentation they can read today.)
 *
 * The floor is a `user_has_cap` filter rather than a stored role grant because it
 * needs no activation and no migration. Plugins update in place far more often
 * than they are activated, so anything depending on an activation hook is a thing
 * that silently does not happen on most sites.
 *
 * The role grant below is belt-and-braces for a different reason: capability
 * manager plugins list the capabilities a role actually holds. Without the grant,
 * the two names this exists to offer would not appear in the UI where somebody
 * goes to assign them.
 *
 * **Only the shipped names are floored.** A site that has FILTERED read() or
 * manage() to something of its own has made a deliberate decision about who gets
 * in, and quietly handing that to every `edit_posts` holder would undo it.
 *
 * @package BW_Guides
 */
class BW_Guides_Caps {

	const READ   = 'bw_guides_read';
	const MANAGE = 'bw_guides_manage';

	/** The capability that gated everything before this split existed. */
	const LEGACY_CAP = 'edit_posts';

	/** Bumped if the capability set changes, so the role grant re-runs. */
	const GRANT_VERSION = 1;
	const GRANT_OPTION  = 'bw_guides_caps_granted';

	public function register() {
		add_filter( 'user_has_cap', array( $this, 'legacy_floor' ), 10, 1 );
		add_action( 'admin_init', array( $this, 'maybe_grant_to_administrator' ) );
	}

	/**
	 * The capability required to READ a guide. Filterable, so a site can lower it
	 * (e.g. to `read`, letting every signed-in user read the documentation) or
	 * raise it. Lowering grants NO ability to change anything.
	 *
	 * @return string
	 */
	public static function read() {
		return (string) apply_filters( 'bw_guides_read_capability', self::READ );
	}

	/**
	 * The capability required to CHANGE guides.
	 *
	 * Note the settings screen is deliberately NOT gated by this — it exposes the
	 * site key, a bearer credential, and stays on `manage_options`.
	 *
	 * @return string
	 */
	public static function manage() {
		return (string) apply_filters( 'bw_guides_manage_capability', self::MANAGE );
	}

	public static function can_read() {
		return current_user_can( self::read() );
	}

	public static function can_manage() {
		return current_user_can( self::manage() );
	}

	/**
	 * Anyone who could `edit_posts` can do everything here, exactly as before.
	 *
	 * Not a convenience: it is what makes this release invisible to every site
	 * that upgrades into it. It grants nothing that was not already granted.
	 *
	 * @param array $allcaps All capabilities for the user.
	 * @return array
	 */
	public function legacy_floor( $allcaps ) {
		if ( ! empty( $allcaps[ self::LEGACY_CAP ] ) ) {
			$allcaps[ self::READ ]   = true;
			$allcaps[ self::MANAGE ] = true;
		}
		return $allcaps;
	}

	/**
	 * Put the capabilities on the administrator role, once.
	 *
	 * Stamped rather than checked against the role, so this is one option read on
	 * an admin request and not a role write on every one — `add_cap()` persists to
	 * the database, so it must not run per request.
	 */
	public function maybe_grant_to_administrator() {
		if ( (int) get_option( self::GRANT_OPTION ) === self::GRANT_VERSION ) {
			return;
		}
		$role = get_role( 'administrator' );
		if ( $role ) {
			$role->add_cap( self::READ );
			$role->add_cap( self::MANAGE );
		}
		update_option( self::GRANT_OPTION, self::GRANT_VERSION, false );
	}

	/** Called from uninstall.php. Leaves no capability behind on any role. */
	public static function remove_all() {
		foreach ( array_keys( wp_roles()->get_names() ) as $slug ) {
			$role = get_role( $slug );
			if ( ! $role ) {
				continue;
			}
			$role->remove_cap( self::READ );
			$role->remove_cap( self::MANAGE );
		}
		delete_option( self::GRANT_OPTION );
	}
}
