[ app route, title, capability ]. * * Every page renders the same React app. Only Overview appears in the WordPress * menu; the app's own sidebar links to the rest, so there is one navigation. * * @return array */ public static function pages() { return array( 'karma-docs' => array( 'dashboard', __( 'Overview', 'karma-docs' ), 'edit_posts' ), 'karma-docs-builder' => array( 'builder', __( 'Builder', 'karma-docs' ), 'edit_posts' ), 'karma-docs-articles' => array( 'articles', __( 'Articles', 'karma-docs' ), 'edit_posts' ), 'karma-docs-categories' => array( 'categories', __( 'Categories', 'karma-docs' ), 'edit_posts' ), 'karma-docs-trash' => array( 'trash', __( 'Trash', 'karma-docs' ), 'edit_posts' ), 'karma-docs-analytics' => array( 'analytics', __( 'Analytics', 'karma-docs' ), Capabilities::ANALYTICS ), 'karma-docs-feedback' => array( 'feedback', __( 'Feedback', 'karma-docs' ), Capabilities::ANALYTICS ), 'karma-docs-reviews' => array( 'reviews', __( 'Reviews', 'karma-docs' ), 'edit_posts' ), 'karma-docs-versions' => array( 'versions', __( 'Versions', 'karma-docs' ), Capabilities::STRUCTURE ), 'karma-docs-kbs' => array( 'kbs', __( 'Knowledge Bases', 'karma-docs' ), Capabilities::STRUCTURE ), 'karma-docs-design' => array( 'design', __( 'Design', 'karma-docs' ), Capabilities::SETTINGS ), 'karma-docs-settings' => array( 'settings', __( 'Settings', 'karma-docs' ), Capabilities::SETTINGS ), 'karma-docs-tools' => array( 'tools', __( 'Tools', 'karma-docs' ), Capabilities::SETTINGS ), 'karma-docs-setup' => array( 'setup', __( 'Setup', 'karma-docs' ), Capabilities::SETTINGS ), ); } /** * Admin URL of an app route. * * @param string $route Route name (e.g. "articles"). * @param array $query Extra query args. * @return string */ public static function url( $route, array $query = array() ) { foreach ( self::pages() as $slug => $page ) { if ( $page[0] === $route ) { return add_query_arg( $query, admin_url( 'admin.php?page=' . $slug ) ); } } return admin_url( 'admin.php?page=karma-docs' ); } /** * Registers the menu: one top-level entry with Overview and Add article. * * The other app pages are registered under it too, so their URLs, capability * checks and menu highlighting work as usual, and trim_submenu() hides them. */ public function menu() { $label = self::menu_label(); add_menu_page( $label, $label, 'edit_posts', 'karma-docs', array( $this, 'render' ), 'data:image/svg+xml;base64,' . base64_encode( '' ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions 26 ); foreach ( self::pages() as $slug => $page ) { add_submenu_page( 'karma-docs', $page[1], $page[1], $page[2], $slug, array( $this, 'render' ) ); } add_submenu_page( 'karma-docs', __( 'Add article', 'karma-docs' ), __( 'Add article', 'karma-docs' ), 'edit_posts', 'post-new.php?post_type=' . PostTypes::ARTICLE ); } /** * Hides the app pages other than Overview (the app's sidebar links to them), * removes the "All Articles" item WordPress adds for the post type (the app's * Articles page replaces it) and keeps Overview first. */ public function trim_submenu() { global $submenu; if ( empty( $submenu['karma-docs'] ) ) { return; } $pages = self::pages(); $items = array(); foreach ( $submenu['karma-docs'] as $item ) { if ( 'edit.php?post_type=' . PostTypes::ARTICLE === $item[2] ) { continue; } if ( 'karma-docs' !== $item[2] && isset( $pages[ $item[2] ] ) ) { // Index 4 is the item's CSS class; core's .hidden is display: none. $item[4] = 'hidden'; } $items[] = $item; } usort( $items, static function ( $a, $b ) { return (int) ( 'karma-docs' === $b[2] ) - (int) ( 'karma-docs' === $a[2] ); } ); $submenu['karma-docs'] = $items; // phpcs:ignore WordPress.WP.GlobalVariablesOverride } /** * Keeps the menu open on app pages, article and category screens. * * @param string $parent_file Parent. * @return string */ public function parent_file( $parent_file ) { $screen = get_current_screen(); if ( $this->current_page() || ( $screen && ( PostTypes::ARTICLE === $screen->post_type || Taxonomies::CATEGORY === $screen->taxonomy ) ) ) { return 'karma-docs'; } return $parent_file; } /** * Highlights Overview on app pages and Add article in the new-article editor. * * @param string $submenu_file Submenu file. * @return string */ public function submenu_file( $submenu_file ) { $screen = get_current_screen(); if ( $screen && PostTypes::ARTICLE === $screen->post_type && 'add' === $screen->action ) { return 'post-new.php?post_type=' . PostTypes::ARTICLE; } if ( $this->current_page() || ( $screen && ( PostTypes::ARTICLE === $screen->post_type || Taxonomies::CATEGORY === $screen->taxonomy ) ) ) { return 'karma-docs'; } return $submenu_file; } /** * Sends the WordPress Articles list to the app's Articles (or Trash) page. * Add &kdocs_classic=1 to reach the classic list. */ public function redirect_articles_list() { // phpcs:disable WordPress.Security.NonceVerification $type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : ''; if ( PostTypes::ARTICLE !== $type || isset( $_GET['kdocs_classic'] ) || isset( $_GET['action'] ) || ! empty( $_POST ) ) { return; } $status = isset( $_GET['post_status'] ) ? sanitize_key( wp_unslash( $_GET['post_status'] ) ) : ''; // phpcs:enable wp_safe_redirect( self::url( 'trash' === $status ? 'trash' : 'articles' ) ); exit; } /** * Sends the WordPress categories screen to the app's Categories page. * Add &kdocs_classic=1 to reach the classic screen. */ public function redirect_categories_screen() { // phpcs:disable WordPress.Security.NonceVerification $taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : ''; if ( Taxonomies::CATEGORY !== $taxonomy || isset( $_GET['kdocs_classic'] ) || ! empty( $_POST ) ) { return; } // phpcs:enable wp_safe_redirect( self::url( 'categories' ) ); exit; } /** * Whether the current screen is one of the app pages. * * @return string|false Page slug. */ private function current_page() { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification return isset( self::pages()[ $page ] ) ? $page : false; } /** * Renders the app root. */ public function render() { echo '
'; } /** * Body class. * * @param string $classes Classes. * @return string */ public function body_class( $classes ) { if ( $this->current_page() ) { $classes .= ' kdocs-admin-page'; } return $classes; } /** * Loads the app bundle. */ public function enqueue() { $page = $this->current_page(); if ( ! $page ) { return; } $asset_file = KDOCS_PATH . 'build/admin.asset.php'; if ( ! file_exists( $asset_file ) ) { return; } $asset = require $asset_file; wp_enqueue_script( 'kdocs-admin', KDOCS_URL . 'build/admin.js', $asset['dependencies'], $asset['version'], true ); wp_enqueue_style( 'kdocs-admin', KDOCS_URL . 'build/admin.css', array(), $asset['version'] ); wp_style_add_data( 'kdocs-admin', 'rtl', 'replace' ); wp_set_script_translations( 'kdocs-admin', 'karma-docs', KDOCS_PATH . 'languages' ); wp_enqueue_media(); $pages = self::pages(); $roles = array(); foreach ( wp_roles()->get_names() as $key => $name ) { $roles[] = array( 'value' => $key, 'label' => translate_user_role( $name ), ); } $counts = wp_count_posts( PostTypes::ARTICLE ); $data = array( 'version' => KDOCS_VERSION, 'route' => $pages[ $page ][0], 'restNs' => 'karma-docs/v1', 'isPro' => Features::is_pro(), 'features' => Features::map(), 'kbLimit' => Features::kb_limit(), 'upgradeUrl' => Features::upgrade_url(), 'adminUrl' => admin_url(), 'docsUrl' => get_post_type_archive_link( PostTypes::ARTICLE ), 'newArticle' => admin_url( 'post-new.php?post_type=' . PostTypes::ARTICLE ), 'classicList' => admin_url( 'edit.php?post_type=' . PostTypes::ARTICLE . '&kdocs_classic=1' ), 'permalinks' => admin_url( 'options-permalink.php' ), 'brand' => self::menu_label(), 'roles' => $roles, 'prettyLinks' => (bool) get_option( 'permalink_structure' ), 'docsSlug' => Rewrites::base(), 'caps' => array( 'manage' => \KarmaDocs\Rest\Routes::can_manage(), 'settings' => \KarmaDocs\Rest\Routes::can_admin(), 'analytics' => \KarmaDocs\Rest\Routes::can_analytics(), ), 'user' => array( 'id' => get_current_user_id(), 'name' => wp_get_current_user()->display_name, ), 'reviewDays' => (int) Settings::get( 'review_interval', 180 ), 'setupDone' => (bool) get_option( 'kdocs_setup_done' ), 'trashCount' => (int) ( $counts->trash ?? 0 ), 'reviewCount' => (int) ( $counts->pending ?? 0 ), 'algolia' => 'algolia' === Settings::get( 'search_provider' ), 'iconsUrl' => add_query_arg( 'ver', (string) @filemtime( \KarmaDocs\Icons::file() ), KDOCS_URL . 'assets/icons.json' ), // phpcs:ignore WordPress.PHP.NoSilencedErrors ); wp_add_inline_script( 'kdocs-admin', 'window.kdocsAdmin = ' . wp_json_encode( $data ) . ';', 'before' ); } /** * Plugin row links. * * @param array $links Links. * @return array */ public function action_links( $links ) { $own = array( '' . esc_html__( 'Builder', 'karma-docs' ) . '', '' . esc_html__( 'Settings', 'karma-docs' ) . '', ); if ( ! Features::is_pro() ) { $own[] = '' . esc_html__( 'Go Pro', 'karma-docs' ) . ''; } return array_merge( $own, $links ); } /** * KB select markup. * * @param int $selected Selected KB. */ private function kb_select( $selected ) { echo ''; wp_nonce_field( 'kdocs_category_kb', 'kdocs_category_kb_nonce' ); } /** * Add-category form field. */ public function category_add_field() { echo '
'; $this->kb_select( 0 ); echo '
'; } /** * Edit-category form field. * * @param \WP_Term $term Term. */ public function category_edit_field( $term ) { echo ''; $this->kb_select( Taxonomies::category_kb( $term->term_id ) ); echo ''; } /** * Saves the category's knowledge base from the classic screens. * * @param int $term_id Term. */ public function save_category_kb( $term_id ) { if ( ! isset( $_POST['kdocs_category_kb_nonce'], $_POST['kdocs_kb'] ) || ! wp_verify_nonce( sanitize_key( $_POST['kdocs_category_kb_nonce'] ), 'kdocs_category_kb' ) ) { return; } $kb = absint( $_POST['kdocs_kb'] ); if ( $kb ) { update_term_meta( $term_id, 'kdocs_kb', $kb ); } } /** * Category list columns. * * @param array $columns Columns. * @return array */ public function category_columns( $columns ) { $out = array(); foreach ( $columns as $key => $label ) { $out[ $key ] = $label; if ( 'name' === $key ) { $out['kdocs_kb'] = __( 'Knowledge Base', 'karma-docs' ); } } return $out; } /** * Category column content. * * @param string $content Content. * @param string $column Column. * @param int $term_id Term. * @return string */ public function category_column( $content, $column, $term_id ) { if ( 'kdocs_kb' === $column ) { $kb = get_term( Taxonomies::category_kb( $term_id ), Taxonomies::KB ); return ( $kb && ! is_wp_error( $kb ) ) ? esc_html( $kb->name ) : '—'; } return $content; } }