222 lines
6.7 KiB
PHP
222 lines
6.7 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* Where each URL lands in the name's bucket.
|
||
|
|
*
|
||
|
|
* WordPress URLs are directories: `/hello-world/`. Object storage has no
|
||
|
|
* directories and no index resolution of its own, so every such URL becomes
|
||
|
|
* `hello-world/index.html` and the resolver's directory-index rescue does the
|
||
|
|
* rest. URLs that already name a file — `/robots.txt`, `/wp-content/…/app.css`
|
||
|
|
* — keep their own name.
|
||
|
|
*
|
||
|
|
* The one rule this file exists to enforce is that the mapping is total and
|
||
|
|
* reversible enough to be safe: anything that cannot be represented as a
|
||
|
|
* gateway-legal path returns '' rather than being coerced into some other
|
||
|
|
* file's name. A path collision in a static export is silent data loss — one
|
||
|
|
* page overwrites another and nobody notices until a reader hits the wrong
|
||
|
|
* article.
|
||
|
|
*
|
||
|
|
* @package SiriusPress
|
||
|
|
*/
|
||
|
|
|
||
|
|
defined( 'ABSPATH' ) || exit;
|
||
|
|
|
||
|
|
final class SPE_Mapper {
|
||
|
|
|
||
|
|
/** Extensions treated as assets rather than pages. */
|
||
|
|
const ASSET_EXTENSIONS = array(
|
||
|
|
'css', 'js', 'mjs', 'json', 'map',
|
||
|
|
'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'ico',
|
||
|
|
'woff', 'woff2', 'ttf', 'eot', 'otf',
|
||
|
|
'mp3', 'mp4', 'webm', 'ogg', 'wav', 'pdf', 'zip', 'txt', 'xml',
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Bucket path for a site-relative URL path.
|
||
|
|
*
|
||
|
|
* @param string $url_path e.g. '/blog/hello/' or '/wp-content/x.css'.
|
||
|
|
* @return string '' when the path cannot be exported.
|
||
|
|
*/
|
||
|
|
public static function path_for_url_path( $url_path ) {
|
||
|
|
$home_path = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH );
|
||
|
|
$url_path = '/' . ltrim( (string) $url_path, '/' );
|
||
|
|
|
||
|
|
// A site installed in a subdirectory serves its own root at that
|
||
|
|
// prefix; the export has the name's root, so the prefix comes off.
|
||
|
|
if ( '' !== $home_path && '/' !== $home_path && 0 === strpos( $url_path, $home_path ) ) {
|
||
|
|
$url_path = '/' . ltrim( substr( $url_path, strlen( $home_path ) ), '/' );
|
||
|
|
}
|
||
|
|
|
||
|
|
$url_path = rawurldecode( $url_path );
|
||
|
|
$trimmed = trim( $url_path, '/' );
|
||
|
|
|
||
|
|
if ( '' === $trimmed ) {
|
||
|
|
return 'index.html';
|
||
|
|
}
|
||
|
|
|
||
|
|
// A URL naming a file keeps its name; a URL naming a directory gets an
|
||
|
|
// index.html inside it. "Names a file" means the last segment has an
|
||
|
|
// extension AND the URL did not end in a slash — `/about.us/` is a
|
||
|
|
// page called "about.us", not a file.
|
||
|
|
$slash = strrpos( $trimmed, '/' );
|
||
|
|
$last = false === $slash ? $trimmed : substr( $trimmed, $slash + 1 );
|
||
|
|
$path = ( false !== strpos( $last, '.' ) && '/' !== substr( $url_path, -1 ) )
|
||
|
|
? $trimmed
|
||
|
|
: $trimmed . '/index.html';
|
||
|
|
|
||
|
|
// Everything the gateway would reject is better refused here, where
|
||
|
|
// there is still a chance to leave the original URL alone.
|
||
|
|
return SP_Gateway::clean_path( $path );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Bucket path for a post, page or any public post type. */
|
||
|
|
public static function path_for_post( $post ) {
|
||
|
|
$post = get_post( $post );
|
||
|
|
if ( ! $post ) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
$permalink = get_permalink( $post );
|
||
|
|
if ( ! $permalink ) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
return self::path_for_url_path( (string) wp_parse_url( $permalink, PHP_URL_PATH ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function is_asset( $path ) {
|
||
|
|
$ext = strtolower( (string) pathinfo( $path, PATHINFO_EXTENSION ) );
|
||
|
|
return in_array( $ext, self::ASSET_EXTENSIONS, true ) && 'html' !== $ext;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Everything a full export should cover.
|
||
|
|
*
|
||
|
|
* @return array<string,array{url:string,kind:string}> path => descriptor.
|
||
|
|
*/
|
||
|
|
public static function full_site() {
|
||
|
|
$targets = array();
|
||
|
|
|
||
|
|
$add = function ( $url, $kind = 'page' ) use ( &$targets ) {
|
||
|
|
if ( ! $url ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
$path = self::path_for_url_path( (string) wp_parse_url( $url, PHP_URL_PATH ) );
|
||
|
|
if ( '' === $path ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
$targets[ $path ] = array(
|
||
|
|
'url' => $url,
|
||
|
|
'kind' => $kind,
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
$add( home_url( '/' ) );
|
||
|
|
|
||
|
|
// Every published thing with a public permalink.
|
||
|
|
$types = get_post_types( array( 'public' => true ), 'names' );
|
||
|
|
unset( $types['attachment'] );
|
||
|
|
$posts = get_posts(
|
||
|
|
array(
|
||
|
|
'post_type' => array_values( $types ),
|
||
|
|
'post_status' => 'publish',
|
||
|
|
'numberposts' => -1,
|
||
|
|
'fields' => 'ids',
|
||
|
|
'suppress_filters' => false,
|
||
|
|
)
|
||
|
|
);
|
||
|
|
foreach ( $posts as $post_id ) {
|
||
|
|
$add( get_permalink( $post_id ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
// Archives people actually navigate through. Date archives are left
|
||
|
|
// out by default: on a long-running blog they multiply into hundreds
|
||
|
|
// of near-identical pages that nothing links to.
|
||
|
|
foreach ( get_taxonomies( array( 'public' => true ), 'names' ) as $taxonomy ) {
|
||
|
|
$terms = get_terms(
|
||
|
|
array(
|
||
|
|
'taxonomy' => $taxonomy,
|
||
|
|
'hide_empty' => true,
|
||
|
|
)
|
||
|
|
);
|
||
|
|
if ( is_wp_error( $terms ) ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
foreach ( $terms as $term ) {
|
||
|
|
$link = get_term_link( $term );
|
||
|
|
if ( ! is_wp_error( $link ) ) {
|
||
|
|
$add( $link, 'archive' );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_for_posts' ) ) {
|
||
|
|
$add( get_permalink( (int) get_option( 'page_for_posts' ) ), 'archive' );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Filters the full list of export targets.
|
||
|
|
*
|
||
|
|
* @param array $targets path => {url, kind}.
|
||
|
|
*/
|
||
|
|
return apply_filters( 'sirius_press_export_targets', $targets );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The paths that go stale when one post changes.
|
||
|
|
*
|
||
|
|
* A post edit does not only change the post: it changes the home page, the
|
||
|
|
* archives it belongs to, and the posts either side of it if the theme
|
||
|
|
* shows previous/next links. Being generous here costs a few uploads;
|
||
|
|
* being stingy leaves a reader looking at a stale index.
|
||
|
|
*
|
||
|
|
* @return array<string,array{url:string,kind:string}>
|
||
|
|
*/
|
||
|
|
public static function affected_by_post( $post ) {
|
||
|
|
$post = get_post( $post );
|
||
|
|
$targets = array();
|
||
|
|
if ( ! $post ) {
|
||
|
|
return $targets;
|
||
|
|
}
|
||
|
|
|
||
|
|
$add = function ( $url, $kind = 'page' ) use ( &$targets ) {
|
||
|
|
if ( ! $url || is_wp_error( $url ) ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
$path = SPE_Mapper::path_for_url_path( (string) wp_parse_url( $url, PHP_URL_PATH ) );
|
||
|
|
if ( '' !== $path ) {
|
||
|
|
$targets[ $path ] = array(
|
||
|
|
'url' => $url,
|
||
|
|
'kind' => $kind,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if ( 'publish' === $post->post_status ) {
|
||
|
|
$add( get_permalink( $post ) );
|
||
|
|
}
|
||
|
|
$add( home_url( '/' ) );
|
||
|
|
|
||
|
|
if ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_for_posts' ) ) {
|
||
|
|
$add( get_permalink( (int) get_option( 'page_for_posts' ) ), 'archive' );
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ( get_object_taxonomies( $post->post_type, 'names' ) as $taxonomy ) {
|
||
|
|
$tax = get_taxonomy( $taxonomy );
|
||
|
|
if ( ! $tax || ! $tax->public ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
foreach ( (array) get_the_terms( $post, $taxonomy ) as $term ) {
|
||
|
|
if ( $term instanceof WP_Term ) {
|
||
|
|
$add( get_term_link( $term ), 'archive' );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Filters which paths a post change invalidates.
|
||
|
|
*
|
||
|
|
* @param array $targets
|
||
|
|
* @param WP_Post $post
|
||
|
|
*/
|
||
|
|
return apply_filters( 'sirius_press_affected_paths', $targets, $post );
|
||
|
|
}
|
||
|
|
}
|