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 = [] ) { $this->attributes = $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(); } $noResultsMessage = esc_html__( 'No posts/terms could be found.', 'all-in-one-seo-pack' ); if ( empty( $this->attributes['post_types'] ) && empty( $this->attributes['taxonomies'] ) ) { if ( $echo ) { echo $noResultsMessage; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } return $noResultsMessage; } // 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 .= ''; $hasPosts = false; $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; } $hasPosts = true; $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 .= '
'; } } $hasTerms = false; $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; } $hasTerms = true; $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 .= '
'; // Check if we actually were able to fetch any results. if ( ! $hasPosts && ! $hasTerms ) { $sitemap = $noResultsMessage; } 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 = ''; 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; } }