query = new Query; } /** * Returns the attributes. * * @since 4.1.3 * * @param array $attributes The user-defined attributes * @return array The defaults with user-defined attributes merged. */ public function getAttributes( $attributes = [] ) { aioseo()->sitemap->type = 'html'; $defaults = [ 'label_tag' => 'h4', 'show_label' => true, 'order' => aioseo()->options->sitemap->html->sortDirection, 'order_by' => aioseo()->options->sitemap->html->sortOrder, 'nofollow_links' => false, 'publication_date' => aioseo()->options->sitemap->html->publicationDate, 'archives' => aioseo()->options->sitemap->html->compactArchives, 'post_types' => aioseo()->sitemap->helpers->includedPostTypes(), 'taxonomies' => aioseo()->sitemap->helpers->includedTaxonomies(), 'excluded_posts' => [], 'excluded_terms' => [], 'is_admin' => false ]; $attributes = shortcode_atts( $defaults, $attributes ); $attributes['show_label'] = filter_var( $attributes['show_label'], FILTER_VALIDATE_BOOLEAN ); $attributes['nofollow_links'] = filter_var( $attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN ); $attributes['is_admin'] = filter_var( $attributes['is_admin'], FILTER_VALIDATE_BOOLEAN ); return $attributes; } /** * Formats the publish date according to what's set under Settings > General. * * @since 4.1.3 * * @param string $date The date that should be formatted. * @return string The formatted date. */ private function formatDate( $date ) { $dateFormat = apply_filters( 'aioseo_html_sitemap_date_format', get_option( 'date_format' ) ); return date_i18n( $dateFormat, strtotime( $date ) ); } /** * Returns the posts of a given post type that should be included. * * @since 4.1.3 * * @param string $postType The post type. * @param array $additionalArgs Additional arguments for the post query (optional). * @return array The post entries. */ private function posts( $postType, $additionalArgs = [] ) { $posts = $this->query->posts( $postType, $additionalArgs ); if ( ! $posts ) { return []; } $entries = []; foreach ( $posts as $post ) { $entry = [ 'id' => $post->ID, 'title' => $post->post_title, 'loc' => get_permalink( $post->ID ), 'date' => $this->formatDate( $post->post_date_gmt ), 'parent' => ! empty( $post->post_parent ) ? $post->post_parent : null ]; $entries[] = $entry; } return $entries; } /** * Returns the terms of a given taxonomy that should be included. * * @since 4.1.3 * * @param string $taxonomy The taxonomy name. * @param array $additionalArgs Additional arguments for the query (optional). * @return array The term entries. */ private function terms( $taxonomy, $additionalArgs = [] ) { $terms = $this->query->terms( $taxonomy, $additionalArgs ); if ( ! $terms ) { return []; } $entries = []; foreach ( $terms as $term ) { $entries[] = [ 'id' => $term->term_id, 'title' => $term->name, 'loc' => get_term_link( $term->term_id ), 'parent' => ! empty( $term->parent ) ? $term->parent : null ]; } return $entries; } /** * Outputs the sitemap to the frontend. * * @since 4.1.3 * * @param bool $echo Whether the sitemap should be printed to the screen. * @param array $attributes The shortcode attributes. * @return string|void The HTML sitemap. */ public function output( $echo = true, $attributes = [] ) { if ( ! aioseo()->options->sitemap->html->enable ) { return; } aioseo()->sitemap->type = 'html'; if ( filter_var( $attributes['archives'], FILTER_VALIDATE_BOOLEAN ) ) { return ( new CompactArchive() )->output( $attributes, $echo ); } if ( ! empty( $attributes['default'] ) ) { $attributes = $this->getAttributes(); } // Setting this allows us to use the helper functions of the general sitemap. $this->attributes = $attributes; if ( empty( $this->attributes['post_types'] ) && empty( $this->attributes['taxonomies'] ) ) { $message = esc_html__( 'No posts/terms could be found.', 'all-in-one-seo-pack' ); if ( $echo ) { echo $message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } return $message; } // TODO: Consider moving all remaining HTML code below to a dedicated view instead of printing it in PHP. $sitemap = sprintf( '
', ! $this->attributes['show_label'] ? ' labels-hidden' : '' ); $sitemap .= ''; $postTypes = $this->getIncludedObjects( $this->attributes['post_types'] ); foreach ( $postTypes as $postType ) { if ( 'attachment' === $postType ) { continue; } // Check if post type is still registered. if ( ! in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true ) ) { continue; } $posts = $this->posts( $postType, $attributes ); if ( empty( $posts ) ) { continue; } $postTypeObject = get_post_type_object( $postType ); $label = ! empty( $postTypeObject->label ) ? $postTypeObject->label : ucfirst( $postType ); $sitemap .= '
'; $sitemap .= $this->generateLabel( $label ); if ( is_post_type_hierarchical( $postType ) ) { $sitemap .= $this->generateHierarchicalList( $posts ) . '
'; if ( $this->attributes['show_label'] ) { $sitemap .= '
'; } continue; } $sitemap .= $this->generateList( $posts ); if ( $this->attributes['show_label'] ) { $sitemap .= '
'; } } $taxonomies = $this->getIncludedObjects( $this->attributes['taxonomies'], false ); foreach ( $taxonomies as $taxonomy ) { // Check if post type is still registered. if ( ! in_array( $taxonomy, aioseo()->helpers->getPublicTaxonomies( true ), true ) ) { continue; } $terms = $this->terms( $taxonomy, $attributes ); if ( empty( $terms ) ) { continue; } $taxonomyObject = get_taxonomy( $taxonomy ); $label = ! empty( $taxonomyObject->label ) ? $taxonomyObject->label : ucfirst( $taxonomy ); $sitemap .= '
'; $sitemap .= $this->generateLabel( $label ); if ( is_taxonomy_hierarchical( $taxonomy ) ) { $sitemap .= $this->generateHierarchicalList( $terms ) . '
'; if ( $this->attributes['show_label'] ) { $sitemap .= '
'; } continue; } $sitemap .= $this->generateList( $terms ); if ( $this->attributes['show_label'] ) { $sitemap .= '
'; } } $sitemap .= '
'; if ( $echo ) { echo $sitemap; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } return $sitemap; } /** * Generates the label for a section of the sitemap. * * @since 4.1.3 * * @param string $label The label. * @return string The HTML code for the label. */ private function generateLabel( $label ) { $labelTag = ! empty( $this->attributes['label_tag'] ) ? $this->attributes['label_tag'] : 'h4'; return $this->attributes['show_label'] ? sprintf( '<%2$s>%1$s', esc_attr( $label ), wp_kses_post( $labelTag ) ) : ''; } /** * Generates the HTML for a non-hierarchical list of objects. * * @since 4.1.3 * * @param array $objects The object. * @return string The HTML code. */ private function generateList( $objects ) { $list = ''; } /** * Generates a list item for an object (without the closing tag). * We cannot close it as the caller might need to generate a hierarchical structure inside the list item. * * @since 4.1.3 * * @param array $objects The object. * @return string The HTML code. */ private function generateListItem( $object ) { $li = ''; if ( ! empty( $object['title'] ) ) { $li .= '
  • '; // add nofollow to the link. if ( filter_var( $this->attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN ) ) { $li .= sprintf( '', esc_url( $object['loc'] ), 'rel="nofollow"', $this->attributes['is_admin'] ? 'target="_blank"' : '' ); } else { $li .= sprintf( '', esc_url( $object['loc'] ), $this->attributes['is_admin'] ? 'target="_blank"' : '' ); } $li .= sprintf( '%s', esc_attr( $object['title'] ) ); // add publication date on the list item. if ( ! empty( $object['date'] ) && filter_var( $this->attributes['publication_date'], FILTER_VALIDATE_BOOLEAN ) ) { $li .= sprintf( ' (%s)', esc_attr( $object['date'] ) ); } $li .= ''; } return $li; } /** * Generates the HTML for a hierarchical list of objects. * * @since 4.1.3 * * @param array $objects The objects. * @return string The HTML of the hierarchical objects section. */ private function generateHierarchicalList( $objects ) { if ( empty( $objects ) ) { return ''; } $objects = $this->buildHierarchicalTree( $objects ); $list = ''; return $list; } /** * Recursive helper function for generateHierarchicalList(). * Generates hierarchical structure for objects with child objects. * * @since 4.1.3 * * @param array $object The object. * @return string The HTML code of the hierarchical tree. */ private function generateHierarchicalTree( $object ) { static $nestedLevel = 0; $tree = ''; $nestedLevel--; } } return $tree; } /** * Builds the structure for hierarchical objects that have a parent. * * @since 4.1.3 * * @param array $objects The list of hierarchical objects. * @param int $parent ID of the parent node. * @return array Multidimensional array with the hierarchical structure. */ private function buildHierarchicalTree( $objects ) { $objects = json_decode( wp_json_encode( $objects ) ); foreach ( $objects as $index => $child ) { if ( $child->parent ) { foreach ( $objects as $parent ) { // Find the parent among the other objects. if ( (int) $child->parent === (int) $parent->id ) { $parent->children[] = $child; unset( $objects[ $index ] ); continue 2; } // If one of the objects already has children, try to recursively find the parent for the current child among those children. if ( ! empty( $parent->children ) ) { list( $children, $found ) = $this->findParentAmongChildren( $parent->children, $child ); if ( $found ) { $parent->children = $children; unset( $objects[ $index ] ); continue 2; } } } } } $objects = array_values( json_decode( wp_json_encode( $objects ), true ) ); return $objects; } /** * Recursive helper function for buildHierarchicalTree(). * Finds the parent for child objects whose parent is a child of another object. * * @since 4.1.3 * * @param array $parentChildren The child objects of the potential parent object. * @param array $child The child object. * @return array The parent's children + whether the parent was found. */ private function findParentAmongChildren( $parentChildren, $child ) { $found = false; foreach ( $parentChildren as $parentChild ) { if ( (int) $child->parent === (int) $parentChild->id ) { $parentChild->children[] = $child; $found = true; break; } if ( ! empty( $parentChild->children ) ) { return $this->findParentAmongChildren( $parentChild->children, $child ); } } return [ $parentChildren, $found ]; } /** * Returns the names of the included post types or taxonomies. * * @since 4.1.3 * * @param array|string $objects The included post types/taxonomies. * @param boolean $arePostTypes Whether the objects are post types. * @return array The names of the included post types/taxonomies. */ private function getIncludedObjects( $objects, $arePostTypes = true ) { if ( is_array( $objects ) ) { return $objects; } $exploded = explode( ',', $objects ); if ( ! empty( $exploded ) ) { $objects = array_map( function( $object ) { return trim( $object ); }, $exploded ); $publicObjects = $arePostTypes ? aioseo()->helpers->getPublicPostTypes( true ) : aioseo()->helpers->getPublicTaxonomies( true ); $objects = array_filter( $objects, function( $object ) use ( $publicObjects ) { return in_array( $object, $publicObjects, true ); }); } return $objects; } }