{form, page, menu: {type: classic|block, id, item?}} */ const OPTION = 'beon_notify_test_forms'; /** * The test form made for a form plugin, while its form or page is still there. * * @return array{form: string, page: int, menu: array}|null */ public static function get( $source ) { $all = get_option( self::OPTION, array() ); $made = is_array( $all ) ? ( $all[ $source ] ?? null ) : null; if ( ! is_array( $made ) ) { return null; } $page = get_post( (int) ( $made['page'] ?? 0 ) ); $page = $page && 'page' === $page->post_type && 'trash' !== $page->post_status ? (int) $page->ID : 0; $form = self::form( $source, (string) ( $made['form'] ?? '' ) ); if ( ! $page && ! $form ) { self::forget( $source ); return null; } return array( 'form' => $form ? (string) $made['form'] : '', 'page' => $page, 'menu' => (array) ( $made['menu'] ?? array() ), ); } /** The form as BeOn Chat lists it, only while it's still one we made (title "BeOn test – …"). */ private static function form( $source, $form_id ) { $sources = Beon_Notify_Forms::active_sources(); if ( '' === $form_id || ! isset( $sources[ $source ] ) ) { return null; } $forms = $sources[ $source ]->forms(); $form = $forms[ $form_id ] ?? null; return $form && 0 === strpos( (string) $form['title'], 'BeOn test' ) ? $form : null; } private static function remember( $source, array $made ) { $all = get_option( self::OPTION, array() ); $all = is_array( $all ) ? $all : array(); $all[ $source ] = $made; update_option( self::OPTION, $all, false ); } private static function forget( $source ) { $all = get_option( self::OPTION, array() ); if ( is_array( $all ) && isset( $all[ $source ] ) ) { unset( $all[ $source ] ); update_option( self::OPTION, $all, false ); } } /** * @return array{form: string, page: int}|WP_Error */ public static function create( $source ) { $catalog = Beon_Notify_Forms::catalog(); $sources = Beon_Notify_Forms::active_sources(); if ( ! isset( $sources[ $source ], $catalog[ $source ] ) ) { return new WP_Error( 'beon_notify_inactive', __( 'That form plugin isn’t active.', 'beon-chat' ) ); } if ( self::get( $source ) ) { return new WP_Error( 'beon_notify_exists', __( 'There’s already a test form for this plugin. Delete it first.', 'beon-chat' ) ); } $title = sprintf( 'BeOn test – %s', $catalog[ $source ]['name'] ); $method = 'create_' . $source; try { $made = self::$method( $title ); } catch ( Throwable $e ) { return new WP_Error( 'beon_notify_create_failed', $e->getMessage() ); } if ( is_wp_error( $made ) ) { return $made; } // Make sure the form really shows up for BeOn Chat before saying it worked. $found = $sources[ $source ]->forms(); if ( ! isset( $found[ (string) $made['form'] ] ) ) { if ( ! empty( $made['page'] ) ) { wp_delete_post( $made['page'], true ); } return new WP_Error( 'beon_notify_not_found', __( 'The form plugin didn’t keep the new form.', 'beon-chat' ) ); } $made['form'] = (string) $made['form']; $made['menu'] = $made['page'] ? self::add_to_menu( (int) $made['page'], $title ) : array( 'type' => 'none' ); self::remember( $source, $made ); return $made; } /** * Deletes the test form, its page, its menu link and the messages set up for it. * * @return array{rules: int} */ public static function delete( $source ) { $made = self::get( $source ); if ( ! $made ) { return array( 'rules' => 0 ); } if ( '' !== $made['form'] ) { $method = 'delete_' . $source; try { self::$method( $made['form'] ); } catch ( Throwable $e ) { beon_notify_log( 'Couldn’t delete the test form: ' . $e->getMessage(), 'error' ); } } self::remove_from_menu( $made['menu'], $made['page'] ); if ( $made['page'] ) { wp_delete_post( $made['page'], true ); } // Its messages would only point at a form that's gone. $rules = 0; $settings = beon_notify_settings(); $key = $source . ':' . $made['form']; foreach ( $settings['form_rules'] as $i => $rule ) { if ( '' !== $made['form'] && $rule['form'] === $key ) { unset( $settings['form_rules'][ $i ] ); ++$rules; } } if ( $rules ) { $settings['form_rules'] = array_values( $settings['form_rules'] ); $settings['form_rules_shown'] = 1; // Saving keeps the old rules unless the list was "shown" (edited). update_option( Beon_Notify_Settings::OPTION, $settings ); } self::forget( $source ); return array( 'rules' => $rules ); } /** * Links the page from the menu visitors see: the classic menu in the theme's main * location, or the header's Navigation block in a block theme. * * @return array{type: string, id?: int, item?: int, name?: string} type: classic | block | pages (the theme lists every page) | none */ private static function add_to_menu( $page, $title ) { if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) { $nav = self::header_navigation(); if ( ! $nav ) { return array( 'type' => 'pages' ); // No saved menu: the Navigation block lists the pages. } if ( false !== strpos( $nav->post_content, '', wp_json_encode( array( 'label' => $title, 'type' => 'page', 'id' => $page, 'url' => get_permalink( $page ), 'kind' => 'post-type', ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) ); $saved = wp_update_post( array( 'ID' => $nav->ID, 'post_content' => wp_slash( rtrim( $nav->post_content ) . "\n" . $link ), ), true ); return is_wp_error( $saved ) ? array( 'type' => 'none' ) : array( 'type' => 'block', 'id' => (int) $nav->ID, 'name' => $nav->post_title, ); } $menu = self::main_menu(); if ( ! $menu ) { // No menu in the theme's locations: themes then list the pages themselves. return array( 'type' => get_registered_nav_menus() ? 'pages' : 'none' ); } $item = wp_update_nav_menu_item( $menu->term_id, 0, array( 'menu-item-title' => $title, 'menu-item-object' => 'page', 'menu-item-object-id' => $page, 'menu-item-type' => 'post_type', 'menu-item-status' => 'publish', ) ); return is_wp_error( $item ) || ! $item ? array( 'type' => 'none' ) : array( 'type' => 'classic', 'id' => (int) $menu->term_id, 'item' => (int) $item, 'name' => $menu->name, ); } private static function remove_from_menu( array $menu, $page ) { $type = $menu['type'] ?? ''; if ( 'classic' === $type && ! empty( $menu['item'] ) && 'nav_menu_item' === get_post_type( (int) $menu['item'] ) ) { wp_delete_post( (int) $menu['item'], true ); } elseif ( 'block' === $type && $page ) { $nav = get_post( (int) ( $menu['id'] ?? 0 ) ); if ( $nav && 'wp_navigation' === $nav->post_type ) { $content = preg_replace( '/\s*/', '', $nav->post_content ); if ( null !== $content && $content !== $nav->post_content ) { wp_update_post( array( 'ID' => $nav->ID, 'post_content' => wp_slash( $content ), ) ); } } } } /** The classic menu in the theme's main location (primary, main, menu-1… or else the first one used). */ private static function main_menu() { $locations = array_filter( get_nav_menu_locations() ); foreach ( array( 'primary', 'main', 'main-menu', 'primary-menu', 'menu-1', 'header', 'header-menu', 'top' ) as $name ) { if ( isset( $locations[ $name ] ) ) { $menu = wp_get_nav_menu_object( $locations[ $name ] ); if ( $menu ) { return $menu; } } } foreach ( $locations as $id ) { $menu = wp_get_nav_menu_object( $id ); if ( $menu ) { return $menu; } } return null; } /** * The Navigation the block theme's header shows: the one its header names, else the * newest published one (what the Navigation block falls back to). */ private static function header_navigation() { $header = function_exists( 'get_block_template' ) ? get_block_template( get_stylesheet() . '//header', 'wp_template_part' ) : null; if ( $header && preg_match( '/