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%2$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 = '';
foreach ( $objects as $object ) {
$list .= $this->generateListItem( $object ) . '';
}
return $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 = '';
foreach ( $objects as $object ) {
$list .= $this->generateListItem(
$object,
[
'publication_date' => $this->attributes['publication_date'],
'nofollow_links' => $this->attributes['nofollow_links']
]
);
if ( ! empty( $object['children'] ) ) {
$list .= $this->generateHierarchicalTree( $object );
}
$list .= '
';
}
$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 = '';
foreach ( $object['children'] as $child ) {
$nestedLevel++;
$tree .= $this->generateListItem(
$child,
[
'publication_date' => $this->attributes['publication_date'],
'nofollow_links' => $this->attributes['nofollow_links']
]
);
if ( ! empty( $child['children'] ) ) {
$tree .= $this->generateHierarchicalTree( $child );
}
// Because the list is closed off after the loop ends, we must keep track of the nest level so that all nested lists are closed.
for ( $i = 0; $i < $nestedLevel; $i++ ) {
$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;
}
}