`, which is why an add-on must not call * add_submenu_page() of its own: two menu entries for one add-on is the confusion * this replaced. * * This is not the add-on catalog. The catalog is the static advertising list in * admin/views/partials/app-sidebar.php and lists products whether they are * installed or not. The two are built from different sources and never merged. * * See docs/addons.md for the contract add-on authors write against. */ class YMMHERO_Addons { const PAGE_SLUG = 'ymmhero-addons'; /** * Normalised map of slug => add-on. Entries missing a title or an uncallable * render are dropped rather than half-registered, so a broken add-on costs its * own screen and not the whole menu. */ public static function all() { $addons = array(); foreach ( (array) apply_filters( 'ymmhero_addons', array() ) as $slug => $addon ) { $slug = sanitize_key( $slug ); if ( '' === $slug || ! is_array( $addon ) ) { continue; } if ( empty( $addon['title'] ) || empty( $addon['render'] ) || ! is_callable( $addon['render'] ) ) { continue; } $addons[ $slug ] = array( 'title' => $addon['title'], 'icon' => empty( $addon['icon'] ) ? 'dashicons-admin-plugins' : $addon['icon'], 'render' => $addon['render'], 'capability' => empty( $addon['capability'] ) ? 'manage_woocommerce' : $addon['capability'], ); } return $addons; } public static function get( $slug ) { $addons = self::all(); $slug = sanitize_key( $slug ); return isset( $addons[ $slug ] ) ? $addons[ $slug ] : null; } public static function url( $slug ) { return admin_url( 'admin.php?page=' . self::PAGE_SLUG . '&addon=' . rawurlencode( $slug ) ); } /** * One screen for every add-on. The slug comes from the URL, so it is resolved * against the registered map before anything is rendered — an unknown slug is a * bad link, not an error worth a fatal. */ public static function render_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination/filter parameter, sanitized at the point of use; no state is changed. $slug = isset( $_GET['addon'] ) ? sanitize_key( wp_unslash( $_GET['addon'] ) ) : ''; $addon = self::get( $slug ); if ( ! $addon ) { $ymmhero_page = array( 'title' => __( 'Add-on not found', 'ymm-hero' ), 'subtitle' => __( 'This add-on is not installed, or is no longer active.', 'ymm-hero' ), 'rail' => true, ); require YMMHERO_PLUGIN_DIR . 'admin/views/partials/app-header.php'; require YMMHERO_PLUGIN_DIR . 'admin/views/partials/app-footer.php'; return; } if ( ! current_user_can( $addon['capability'] ) ) { wp_die( esc_html__( 'You do not have permission to manage this add-on.', 'ymm-hero' ) ); } $ymmhero_page = array( 'title' => $addon['title'], 'rail' => true, ); require YMMHERO_PLUGIN_DIR . 'admin/views/partials/app-header.php'; call_user_func( $addon['render'] ); require YMMHERO_PLUGIN_DIR . 'admin/views/partials/app-footer.php'; } }