<?php
/**
 * Custom editorial roles for Brentwood.
 *
 * The SOW collapses the old CMS's 100+ roles down to a small practical set.
 * WordPress core covers most of it:
 *
 *   Administrator  → Erin + IT/dev (full control)
 *   Editor         → Tracy (edit AND publish any page/post)
 *   Author         → blog contributors (create + publish their OWN posts, no pages)
 *   Subscriber     → logged-in, read-only (for viewing any login-gated content)
 *   bw_staff       → teachers: read-only + the /my-profile self-service editor
 *                    (registered in inc/bw-google-login.php)
 *
 * The one role core doesn't provide is bw_site_editor — "Site Editor": create
 * and publish ANY content type, but only content they authored. See
 * bw_roles_site_editor_caps() for the model and why it is not a draft/approval
 * role (core has no native "submit a revision for approval"; that needs a
 * plugin such as Edit Flow / PublishPress).
 *
 * @package Kadence-Child
 */

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

/**
 * Bump this when the capability list below changes, so existing sites pick the
 * change up (add_role() is a no-op once the role exists in the DB).
 */
const BW_ROLES_VERSION = 8;

/**
 * Gravity Forms capabilities granted to Editors.
 *
 * Deliberately form/entry management only — the GLOBAL settings caps
 * (`gravityforms_edit_settings`, `view_addons`, `uninstall`, `system_status`,
 * `api_settings`, `logging`) stay Administrator-only, so an Editor can build
 * and manage forms and read submissions without being able to reconfigure the
 * plugin, install add-ons or reach the licence/API keys.
 */
function bw_roles_gravityforms_caps() {
	return array(
		'gravityforms_create_form',
		'gravityforms_edit_forms',
		'gravityforms_delete_forms',
		'gravityforms_preview_forms',
		'gravityforms_view_entries',
		'gravityforms_edit_entries',
		'gravityforms_delete_entries',
		'gravityforms_export_entries',
		'gravityforms_view_entry_notes',
		'gravityforms_edit_entry_notes',
	);
}

/**
 * Let Editors reach the Redirection plugin.
 *
 * Redirection gates itself on a capability that defaults to `manage_options`
 * (Redirection_Capabilities::CAP_DEFAULT), exposed through the `redirection_role`
 * filter. Rather than grant Editors `manage_options` — which would hand them the
 * entire Settings menu — we simply lower Redirection's own requirement to a
 * capability Editors already hold.
 */
add_filter( 'redirection_role', function () {
	return 'edit_others_pages';
} );

/**
 * Surface Kadence "Elements" for Editors.
 *
 * Kadence Pro registers Elements with `add_theme_page()` (kadence-pro/dist/
 * elements/elements-init.php), i.e. under **Appearance** — which requires
 * `edit_theme_options`. We deliberately don't grant that (it would also open
 * Widgets and the whole Customizer), so an Editor holds the
 * `edit_kadence_elements` capability but has no menu to reach it through.
 *
 * Fix: add the Elements list under the Kadence menu they already see. Admins
 * are left alone — they keep the stock Appearance → Kadence → Elements path.
 */
add_action(
	'admin_menu',
	function () {
		if ( ! current_user_can( 'edit_kadence_elements' ) || current_user_can( 'edit_theme_options' ) ) {
			return;
		}
		global $admin_page_hooks;
		$url    = 'edit.php?post_type=kadence_element';
		$parent = null;
		foreach ( array( 'kadence-blocks', 'kadence-blocks-home' ) as $slug ) {
			if ( isset( $admin_page_hooks[ $slug ] ) ) {
				$parent = $slug;
				break;
			}
		}
		if ( $parent ) {
			add_submenu_page( $parent, __( 'Elements', 'kadence-child' ), __( 'Elements', 'kadence-child' ), 'edit_kadence_elements', $url );
		} else {
			// Fallback: stand-alone top-level entry if the Kadence menu isn't there.
			add_menu_page( __( 'Elements', 'kadence-child' ), __( 'Elements', 'kadence-child' ), 'edit_kadence_elements', $url, '', 'dashicons-layout', 59 );
		}
	},
	100
);

/**
 * Capabilities for Kadence's unused Forms / Queries / Query Cards features.
 *
 * Read from the registered post types rather than hard-coded — the real cap
 * names are not guessable: both `kadence_query` AND `kadence_query_card` map to
 * `edit_kadence_queries`. Matching on `kadence_(form|quer)` covers form/forms and
 * query/queries/query_card, and never matches `kadence_element`, which Editors
 * are meant to keep.
 */
function bw_roles_revoked_kadence_caps() {
	static $caps = null;
	if ( null !== $caps ) {
		return $caps;
	}
	// Too early — post types aren't registered yet; don't cache an empty list.
	if ( ! did_action( 'init' ) ) {
		return array();
	}
	$found    = array();
	$complete = true;
	foreach ( array( 'kadence_form', 'kadence_query', 'kadence_query_card' ) as $type ) {
		$obj = get_post_type_object( $type );
		if ( ! $obj ) {
			// Registered later than this call — don't cache a partial list, or
			// the types that register after us keep their capabilities forever.
			$complete = false;
			continue;
		}
		foreach ( (array) $obj->cap as $cap ) {
			if ( is_string( $cap ) && preg_match( '/kadence_(form|quer)/', $cap ) ) {
				$found[ $cap ] = true;
			}
		}
	}
	if ( ! $complete ) {
		return $found; // usable now, re-computed next time.
	}
	$caps = $found;
	return $caps;
}

/**
 * Revoke those capabilities for everyone below Administrator.
 *
 * Kadence grants them through a `user_has_cap` filter rather than on the role
 * itself (the role reports `has_cap = false` while `user_can` returns true), so
 * `remove_cap()` is a no-op — the revoke has to happen at the same layer, and
 * later in the chain. The site runs Gravity Forms, so these screens are pure
 * menu noise.
 */
add_filter(
	'user_has_cap',
	function ( $allcaps ) {
		if ( ! empty( $allcaps['manage_options'] ) ) {
			return $allcaps; // Administrators keep everything.
		}
		foreach ( bw_roles_revoked_kadence_caps() as $cap => $unused ) {
			unset( $allcaps[ $cap ] );
		}
		return $allcaps;
	},
	999
);

/**
 * Keep Kadence Blocks' own settings screens Administrator-only.
 *
 * Kadence gates its admin menu on `edit_pages` (filterable), which every Editor
 * has — so Editors were seeing Kadence Home / Settings / Forms / Entries /
 * Queries. Those are plugin configuration, not content. Raising the bar to
 * `manage_options` hides them without affecting Elements above, which is gated
 * on its own `edit_kadence_elements` capability.
 */
add_filter( 'kadence_blocks_admin_settings_capability', function () {
	return 'manage_options';
} );

/**
 * Remove Yoast's top-level SEO menu for anyone who can't manage its settings.
 *
 * Two reasons a targeted `remove_menu_page()` doesn't work here:
 *
 *  1. The parent menu keeps appearing even without `wpseo_manage_options`,
 *     because WordPress shows a parent whenever ANY one of its submenus is
 *     permitted — and Yoast grants Editors `wpseo_edit_advanced_metadata` /
 *     `wpseo_bulk_edit`. WordPress then re-points the parent at the first
 *     permitted submenu (here `wpseo_page_academy`).
 *  2. The slug is version-dependent (`wpseo_dashboard` in older releases,
 *     `wpseo_page_*` in current ones), so hard-coding it silently misses.
 *
 * So: scan the built menu late and drop any top-level entry belonging to Yoast.
 * For Editors this removes only the settings/upsell menu — the per-post SEO
 * metabox (keyphrase, SEO title, meta description) is untouched and handled
 * separately below.
 */
add_action(
	'admin_menu',
	function () {
		if ( current_user_can( 'wpseo_manage_options' ) ) {
			return; // genuine Yoast managers keep it
		}
		global $menu, $submenu;
		if ( ! is_array( $menu ) ) {
			return;
		}
		foreach ( $menu as $i => $item ) {
			$slug = isset( $item[2] ) ? (string) $item[2] : '';
			if ( '' !== $slug && false !== strpos( $slug, 'wpseo' ) ) {
				unset( $menu[ $i ] );
				if ( isset( $submenu[ $slug ] ) ) {
					unset( $submenu[ $slug ] );
				}
			}
		}
	},
	9999
);

/** Drop Yoast's admin-bar item for the same users (more upsell surface). */
add_action(
	'wp_before_admin_bar_render',
	function () {
		if ( current_user_can( 'wpseo_manage_options' ) ) {
			return;
		}
		global $wp_admin_bar;
		if ( $wp_admin_bar ) {
			$wp_admin_bar->remove_node( 'wpseo-menu' );
		}
	}
);

/**
 * Hide Yoast's SEO metabox from anyone below Editor.
 *
 * Yoast's top-level menu is already invisible to them (it needs
 * `wpseo_manage_options`), but it also adds a `wpseo_meta` metabox to the post
 * editor for anyone who can edit a post — which is what a blog Author sees.
 * Blog contributors write copy; SEO is Marketing's job.
 */
add_action(
	'add_meta_boxes',
	function () {
		if ( current_user_can( 'publish_pages' ) ) {
			return; // Editor / Administrator keep it.
		}
		foreach ( array( 'post', 'page' ) as $screen ) {
			remove_meta_box( 'wpseo_meta', $screen, 'normal' );
		}
	},
	99
);

/**
 * Brentwood's own content types.
 *
 * These ship registered with `capability_type => post`, which means ANY role
 * holding post capabilities — Author, Contributor, our Draft Editor — could
 * create, edit and publish staff profiles, landing pages, courses, careers and
 * Brentwood 100 items. A blog contributor should never be able to do that.
 *
 * Giving them their own capability type (the same technique Kadence uses for
 * `kadence_element`) severs that link: post-level roles keep working on posts
 * and nothing else, and these types become Editor/Admin-only.
 *
 * Note this does NOT affect the teacher self-service editor — `/my-profile`
 * authorises by profile ownership (see inc/bw-my-profile.php), not by caps.
 */
const BW_CONTENT_POST_TYPES = array( 'staff', 'landing', 'course', 'livestream', 'bw_hundred', 'bw_career' );

/**
 * Re-register the Brentwood CPTs under a shared `bw_content` capability type.
 * Runs on register_post_type_args so it applies no matter who registers them
 * (these come from ACF), and survives ACF field-group edits.
 */
add_filter(
	'register_post_type_args',
	function ( $args, $post_type ) {
		if ( in_array( $post_type, BW_CONTENT_POST_TYPES, true ) ) {
			$args['capability_type'] = array( 'bw_content', 'bw_contents' );
			$args['map_meta_cap']    = true;
		}
		return $args;
	},
	10,
	2
);

/** The full primitive capability set for the bw_content types. */
function bw_roles_content_caps() {
	return array(
		'edit_bw_contents',
		'edit_others_bw_contents',
		'edit_private_bw_contents',
		'edit_published_bw_contents',
		'publish_bw_contents',
		'read_private_bw_contents',
		'delete_bw_contents',
		'delete_others_bw_contents',
		'delete_private_bw_contents',
		'delete_published_bw_contents',
		'create_bw_contents',
	);
}

/** Kadence Elements caps — so an Editor can manage the header/footer/mega-menus. */
function bw_roles_kadence_element_caps() {
	return array(
		'edit_kadence_elements',
		'edit_others_kadence_elements',
		'edit_private_kadence_elements',
		'edit_published_kadence_elements',
		'publish_kadence_elements',
		'read_private_kadence_elements',
		'delete_kadence_elements',
		'delete_others_kadence_elements',
		'delete_private_kadence_elements',
		'delete_published_kadence_elements',
	);
}

/**
 * Capabilities for "Site Editor".
 *
 * The model: **create and publish anything, but only your own work.**
 * They get the full lifecycle (create → publish → edit → delete) across posts,
 * pages AND the Brentwood content types (staff, landing, courses, careers,
 * livestreams, Brentwood 100) — but no `edit_others_*` / `delete_others_*`, so
 * WordPress's ownership mapping confines them to content they authored.
 *
 * This makes author-reassignment a delegation tool: Erin can hand a specific
 * page to a specific person by setting them as its author, and they can then
 * maintain exactly that page and nothing else.
 *
 * ── Why this is NOT a draft/approval role ────────────────────────────────────
 * It was originally built as "edit drafts, never publish". That turned out not
 * to be useful, because WordPress core offers no middle ground: without
 * `edit_published_pages` a user cannot touch a live page at all, and WITH it
 * their edit goes live instantly — there is no native "submit a revision to an
 * existing page for approval". A real approval workflow needs a plugin such as
 * Edit Flow / PublishPress. Parked pending Erin's call.
 */
function bw_roles_site_editor_caps() {
	return array(
		'read'                       => true,
		'upload_files'               => true,

		// Posts — full lifecycle, own only.
		'edit_posts'                 => true,
		'publish_posts'              => true,
		'edit_published_posts'       => true,
		'delete_posts'               => true,
		'delete_published_posts'     => true,

		// Pages — full lifecycle, own only.
		'edit_pages'                 => true,
		'publish_pages'              => true,
		'edit_published_pages'       => true,
		'delete_pages'               => true,
		'delete_published_pages'     => true,

		// Brentwood content types (staff / landing / course / livestream /
		// bw_hundred / bw_career) — full lifecycle, own only.
		'edit_bw_contents'           => true,
		'publish_bw_contents'        => true,
		'edit_published_bw_contents' => true,
		'delete_bw_contents'         => true,
		'delete_published_bw_contents' => true,

		// The boundary: never other people's content.
		'edit_others_posts'          => false,
		'edit_others_pages'          => false,
		'edit_others_bw_contents'    => false,
		'delete_others_posts'        => false,
		'delete_others_pages'        => false,
		'delete_others_bw_contents'  => false,
		// Site-wide taxonomy management stays with Editors.
		'manage_categories'          => false,
	);
}

/**
 * Capabilities for "Blog Manager".
 *
 * Jim Ganley runs the student blog — Laravel's publisher records show him
 * publishing 367 of the last two years' posts (next highest: 16). On the old CMS
 * he held blogs-editor + blogs-manager + blogs-publisher.
 *
 * Core's Author role only covers a user's OWN posts, so it couldn't cover the
 * ~250 older posts credited to other staff accounts. This adds `edit_others_*`
 * so he can maintain the whole blog — but strictly within posts: no pages, and
 * none of the Brentwood content types.
 */
function bw_roles_blog_manager_caps() {
	return array(
		'read'                   => true,
		'upload_files'           => true,
		// Own posts, full lifecycle.
		'edit_posts'             => true,
		'publish_posts'          => true,
		'edit_published_posts'   => true,
		'delete_posts'           => true,
		'delete_published_posts' => true,
		// Everyone else's posts too — this is the "manager" part.
		'edit_others_posts'      => true,
		'delete_others_posts'    => true,
		'edit_private_posts'     => true,
		'read_private_posts'     => true,
		// Blog taxonomy upkeep.
		'manage_categories'      => true,
		'moderate_comments'      => true,
		// Explicitly NOT pages or the Brentwood content types.
		'edit_pages'             => false,
		'publish_pages'          => false,
		'edit_others_pages'      => false,
		'edit_bw_contents'       => false,
		'publish_bw_contents'    => false,
	);
}

/**
 * Register / refresh the custom roles. Runs on init (cheap: a single option
 * read once the version matches).
 */
function bw_roles_register() {
	$installed = (int) get_option( 'bw_roles_version', 0 );
	$role      = get_role( 'bw_site_editor' );

	if ( $role && $installed === BW_ROLES_VERSION ) {
		return;
	}

	if ( ! get_role( 'bw_blog_manager' ) ) {
		add_role( 'bw_blog_manager', __( 'Blog Manager', 'kadence-child' ), bw_roles_blog_manager_caps() );
	} else {
		$bm = get_role( 'bw_blog_manager' );
		foreach ( bw_roles_blog_manager_caps() as $cap => $grant ) {
			if ( $grant ) { $bm->add_cap( $cap ); } else { $bm->remove_cap( $cap ); }
		}
	}

	if ( ! $role ) {
		add_role( 'bw_site_editor', __( 'Site Editor', 'kadence-child' ), bw_roles_site_editor_caps() );
	} else {
		// Re-assert every capability so edits to the list above take effect.
		foreach ( bw_roles_site_editor_caps() as $cap => $grant ) {
			if ( $grant ) {
				$role->add_cap( $cap );
			} else {
				$role->remove_cap( $cap );
			}
		}
	}

	// Editor = "Administrator without the clutter" (Rian, 2026-07-23): Editors
	// manage the user roster too — add/edit users and change roles. The
	// guardrails in inc/bw-user-management.php (which apply to ALL non-admins)
	// keep them out of administrator accounts and stop them creating admins, so
	// this is safe to hand to every Editor. Function/settings menus stay admin-
	// only, which is the "clutter" they don't want.
	$editor_role = get_role( 'editor' );
	if ( $editor_role && function_exists( 'bw_um_manager_caps' ) ) {
		foreach ( bw_um_manager_caps() as $cap ) {
			$editor_role->add_cap( $cap );
		}
	}

	// The Brentwood content types are Editor/Admin only. Granting the caps here
	// (rather than relying on capability_type => post) is what keeps Author and
	// the Draft Editor out of staff profiles, courses, careers, etc.
	foreach ( array( 'administrator', 'editor' ) as $slug ) {
		$r = get_role( $slug );
		if ( ! $r ) {
			continue;
		}
		foreach ( bw_roles_content_caps() as $cap ) {
			$r->add_cap( $cap );
		}
	}

	// Drop Kadence's own Forms / Queries / Query Cards from Editors — unused
	// here (the site runs Gravity Forms), and they only add menu noise. Cap
	// names are read from the registered post type rather than guessed, because
	// WordPress derives plurals naively (kadence_query -> kadence_querys).
	$editor_role = get_role( 'editor' );
	if ( $editor_role ) {
		foreach ( array( 'kadence_form', 'kadence_query', 'kadence_query_card' ) as $unused_type ) {
			$obj = get_post_type_object( $unused_type );
			if ( ! $obj ) {
				continue;
			}
			foreach ( (array) $obj->cap as $cap ) {
				// Only ever touch caps belonging to THAT type — never
				// kadence_element, which Editors are meant to keep.
				if ( is_string( $cap ) && false !== strpos( $cap, $unused_type ) ) {
					$editor_role->remove_cap( $cap );
				}
			}
		}
	}

	// Editors also manage Kadence Elements (header / footer / mega-menus) and
	// Gravity Forms, so Marketing can run the site chrome and the forms without
	// needing a full admin account. Menus/Widgets/Customizer are deliberately
	// NOT granted — those need `edit_theme_options`, which would also open the
	// whole Customizer (global colours, typography, layout).
	$editor = get_role( 'editor' );
	if ( $editor ) {
		foreach ( array_merge( bw_roles_kadence_element_caps(), bw_roles_gravityforms_caps() ) as $cap ) {
			$editor->add_cap( $cap );
		}
	}

	// Supersedes the original draft-only role. Move anyone still on the old
	// slug across, then drop it so it can't be assigned by accident.
	if ( get_role( 'bw_draft_editor' ) ) {
		foreach ( get_users( array( 'role' => 'bw_draft_editor' ) ) as $legacy_user ) {
			$legacy_user->set_role( 'bw_site_editor' );
		}
		remove_role( 'bw_draft_editor' );
	}

	update_option( 'bw_roles_version', BW_ROLES_VERSION, false );
}
add_action( 'init', 'bw_roles_register' );

/**
 * Tidy the admin menu for roles below Editor.
 *
 * These entries are already capability-gated (their inner pages refuse access),
 * but they still render as menu clutter for a blog author who has no use for
 * them. Editors and above are untouched.
 */
add_action(
	'admin_menu',
	function () {
		// Tools is clutter for a blog Author, but Editors need it — Redirection
		// lives under Tools.
		if ( ! current_user_can( 'publish_pages' ) ) {
			remove_menu_page( 'tools.php' );
		}

		// Kadence's own Forms / Queries / Query Cards / Form Entries screens are
		// unused here (the site runs Gravity Forms). Capabilities are dropped in
		// bw_roles_register(); this sweeps up any screen that isn't
		// capability-gated (e.g. Form Entries), while keeping Elements.
		if ( ! current_user_can( 'manage_options' ) ) {
			global $submenu;
			foreach ( array( 'kadence-blocks', 'kadence-blocks-home' ) as $parent ) {
				if ( empty( $submenu[ $parent ] ) ) {
					continue;
				}
				foreach ( $submenu[ $parent ] as $i => $item ) {
					$slug = isset( $item[2] ) ? (string) $item[2] : '';
					if ( false !== strpos( $slug, 'kadence_element' ) ) {
						continue; // keep Elements
					}
					if ( preg_match( '/kadence_form|kadence_query|entr/i', $slug ) ) {
						unset( $submenu[ $parent ][ $i ] );
					}
				}
			}
		}
	},
	999
);
