term_id ) !== (int) $kb->term_id ) { return array( 'error' => '404' ); } $vars['kdocs_category'] = $slug; return $vars; } if ( ! empty( $vars['kdocs_article'] ) ) { $id = $kb ? self::find_article_by_path( $vars['kdocs_article'], (int) $kb->term_id ) : 0; if ( ! $id ) { return array( 'error' => '404', 'kdocs_lost' => wp_basename( trim( $vars['kdocs_article'], '/' ) ), ); } unset( $vars['kdocs_article'], $vars['name'] ); $vars['p'] = $id; $vars['post_type'] = PostTypes::ARTICLE; } return $vars; } /** * Walks a slug path (parent/child/grandchild) inside a knowledge base. * * @param string $path Path. * @param int $kb_id Knowledge base term id. * @return int Post id or 0. */ public static function find_article_by_path( $path, $kb_id ) { global $wpdb; $segments = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) ); $parent = 0; foreach ( $segments as $index => $segment ) { $slug = sanitize_title_for_query( rawurldecode( $segment ) ); if ( 0 === $index ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT p.ID FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE p.post_type = %s AND p.post_name = %s AND p.post_status NOT IN ('trash','auto-draft','inherit') AND tt.taxonomy = %s AND tt.term_id = %d ORDER BY (p.post_parent = 0) DESC, p.ID ASC LIMIT 1", PostTypes::ARTICLE, $slug, Taxonomies::KB, $kb_id ) ); // The first segment is the top-most article in the path. It may itself be nested // deeper only when a shortened link is used; we accept it for resilience. } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_name = %s AND post_parent = %d AND post_status NOT IN ('trash','auto-draft','inherit') LIMIT 1", PostTypes::ARTICLE, $slug, $parent ) ); } if ( ! $id ) { return 0; } $parent = $id; } return $parent; } /** * Avoids loading every article for archive-style pages: our templates run their own queries. * * @param \WP_Query $query Query. */ public function lighten_archive_queries( $query ) { if ( is_admin() || ! $query->is_main_query() ) { return; } if ( $query->is_post_type_archive( PostTypes::ARTICLE ) || $query->is_tax( array( Taxonomies::KB, Taxonomies::CATEGORY ) ) ) { $query->set( 'posts_per_page', 1 ); $query->set( 'no_found_rows', true ); } } /** * 301-redirects old article URLs after an article was moved in the builder. */ public function redirect_moved_articles() { global $wp; // Articles reached through a partial / outdated path go to their canonical URL. if ( self::pretty() && is_singular( PostTypes::ARTICLE ) && ! is_preview() ) { $canonical = get_permalink( get_queried_object_id() ); $requested = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ), PHP_URL_PATH ) : ''; $expected = (string) wp_parse_url( $canonical, PHP_URL_PATH ); if ( $requested && $expected && untrailingslashit( rawurldecode( $requested ) ) !== untrailingslashit( rawurldecode( $expected ) ) ) { $query = isset( $_SERVER['QUERY_STRING'] ) && '' !== $_SERVER['QUERY_STRING'] ? '?' . sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) ) : ''; wp_safe_redirect( $canonical . $query, 301 ); exit; } return; } if ( ! is_404() || empty( $wp->query_vars['kdocs_lost'] ) ) { return; } $posts = get_posts( array( 'post_type' => PostTypes::ARTICLE, 'name' => sanitize_title( $wp->query_vars['kdocs_lost'] ), 'posts_per_page' => 1, 'post_status' => 'publish', ) ); if ( $posts ) { wp_safe_redirect( get_permalink( $posts[0] ), 301 ); exit; } } /** * Builds pretty article permalinks. * * @param string $link Link. * @param \WP_Post $post Post. * @return string */ public function article_link( $link, $post ) { if ( PostTypes::ARTICLE !== $post->post_type ) { return $link; } $kb = Taxonomies::article_kb( $post ); if ( ! self::pretty() || ! $kb || empty( $post->post_name ) || in_array( $post->post_status, array( 'draft', 'pending', 'auto-draft' ), true ) ) { return add_query_arg( array( 'post_type' => PostTypes::ARTICLE, 'p' => $post->ID, ), home_url( '/' ) ); } $slugs = array( $post->post_name ); foreach ( get_post_ancestors( $post ) as $ancestor ) { $slugs[] = get_post_field( 'post_name', $ancestor ); } $path = implode( '/', array_reverse( $slugs ) ); return user_trailingslashit( home_url( self::base() . '/' . $kb->slug . '/' . $path ) ); } /** * KB / category links. * * @param string $link Link. * @param \WP_Term $term Term. * @param string $taxonomy Taxonomy. * @return string */ public function term_link( $link, $term, $taxonomy ) { if ( ! self::pretty() ) { return $link; } if ( Taxonomies::KB === $taxonomy ) { return user_trailingslashit( home_url( self::base() . '/' . $term->slug ) ); } if ( Taxonomies::CATEGORY === $taxonomy ) { $kb = get_term( Taxonomies::category_kb( $term->term_id ), Taxonomies::KB ); if ( ! $kb || is_wp_error( $kb ) ) { return $link; } $slugs = array( $term->slug ); foreach ( get_ancestors( $term->term_id, Taxonomies::CATEGORY, 'taxonomy' ) as $ancestor ) { $a = get_term( $ancestor, Taxonomies::CATEGORY ); if ( $a && ! is_wp_error( $a ) ) { $slugs[] = $a->slug; } } $path = implode( '/', array_reverse( $slugs ) ); return user_trailingslashit( home_url( self::base() . '/' . $kb->slug . '/' . Settings::get( 'category_base', 'category' ) . '/' . $path ) ); } return $link; } /** * Docs home link. * * @param string $link Link. * @param string $post_type Post type. * @return string */ public function archive_link( $link, $post_type ) { if ( PostTypes::ARTICLE === $post_type && self::pretty() ) { return user_trailingslashit( home_url( self::base() ) ); } return $link; } }