robotstxt-documentation-mar.../robotstxt-documentation-markdown-map.php
2026-06-08 12:31:51 +00:00

349 lines
11 KiB
PHP

<?php
/**
* Mapping Functions
*
* @package RobotsTxt\DocumentationMarkdown
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Get all mappings
*
* @since 1.0.0
*
* @return list<MappingData> Array of mappings.
*/
function robotstxt_docmd_get_all_mappings(): array {
$args = array(
'post_type' => 'robotstxt_map',
'posts_per_page' => -1,
'post_status' => 'publish',
'orderby' => 'title',
'order' => 'ASC',
);
$query = new WP_Query( $args );
$mappings = array();
foreach ( $query->posts as $post ) {
if ( ! ( $post instanceof WP_Post ) ) {
continue;
}
$mapping = robotstxt_docmd_get_mapping( $post->ID );
if ( $mapping ) {
$mappings[] = $mapping;
}
}
wp_reset_postdata();
return $mappings;
}
/**
* Get single mapping
*
* @since 1.0.0
*
* @param int $mapping_id Mapping post ID.
* @return MappingData|null Mapping data or null.
*/
function robotstxt_docmd_get_mapping( int $mapping_id ): ?array {
$post = get_post( $mapping_id );
if ( ! $post || 'robotstxt_map' !== $post->post_type ) {
return null;
}
$repo_owner_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_repo_owner', true );
$repo_name_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_repo_name', true );
$file_path_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_file_path', true );
$branch_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_branch', true );
$repo_owner = is_string( $repo_owner_raw ) ? $repo_owner_raw : '';
$repo_name = is_string( $repo_name_raw ) ? $repo_name_raw : '';
$file_path = is_string( $file_path_raw ) ? $file_path_raw : '';
$branch = is_string( $branch_raw ) && '' !== $branch_raw ? $branch_raw : 'main';
$post_type_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_target_post_type', true );
$sync_frequency_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_sync_frequency', true );
$sync_status_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_sync_status', true );
$last_sync_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_last_sync', true );
$target_post_id_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_target_post_id', true );
$target_author_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_target_author', true );
$target_parent_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_target_parent', true );
$target_order_raw = get_post_meta( $mapping_id, '_robotstxt_docmd_target_order', true );
return array(
'id' => $mapping_id,
'title' => $post->post_title,
'repo_owner' => $repo_owner,
'repo_name' => $repo_name,
'file_path' => $file_path,
'branch' => $branch,
'github_url' => sprintf( 'https://github.com/%s/%s/blob/%s/%s', $repo_owner, $repo_name, $branch, $file_path ),
'target_post_type' => is_string( $post_type_raw ) ? $post_type_raw : '',
'target_post_id' => is_numeric( $target_post_id_raw ) ? absint( $target_post_id_raw ) : 0,
'target_author' => is_numeric( $target_author_raw ) ? absint( $target_author_raw ) : 0,
'target_parent' => is_numeric( $target_parent_raw ) ? absint( $target_parent_raw ) : 0,
'target_order' => is_numeric( $target_order_raw ) ? absint( $target_order_raw ) : 0,
'sync_enabled' => (bool) get_post_meta( $mapping_id, '_robotstxt_docmd_sync_enabled', true ),
'sync_frequency' => is_string( $sync_frequency_raw ) ? $sync_frequency_raw : 'daily',
'sync_status' => is_string( $sync_status_raw ) && '' !== $sync_status_raw ? $sync_status_raw : 'never',
'last_sync' => is_string( $last_sync_raw ) ? $last_sync_raw : '',
);
}
/**
* Create mapping
*
* @since 1.0.0
*
* @param array $data Mapping input data.
* @phpstan-param MappingInput $data
* @return int|WP_Error Mapping ID or error.
*/
function robotstxt_docmd_create_mapping( array $data ): int|WP_Error {
// Check for duplicates.
if ( robotstxt_docmd_is_file_mapped( $data['repo_owner'], $data['repo_name'], $data['file_path'], $data['branch'] ) ) {
return new WP_Error( 'duplicate_mapping', __( 'This file is already mapped', 'robotstxt-documentation-markdown' ) );
}
// Create mapping post.
$post_id = wp_insert_post(
array(
'post_title' => $data['title'],
'post_type' => 'robotstxt_map',
'post_status' => 'publish',
),
true
);
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
// Save metadata.
update_post_meta( $post_id, '_robotstxt_docmd_repo_owner', $data['repo_owner'] );
update_post_meta( $post_id, '_robotstxt_docmd_repo_name', $data['repo_name'] );
update_post_meta( $post_id, '_robotstxt_docmd_file_path', $data['file_path'] );
update_post_meta( $post_id, '_robotstxt_docmd_branch', $data['branch'] );
update_post_meta( $post_id, '_robotstxt_docmd_target_post_type', $data['target_post_type'] );
update_post_meta( $post_id, '_robotstxt_docmd_target_author', $data['target_author'] );
update_post_meta( $post_id, '_robotstxt_docmd_target_parent', $data['target_parent'] );
update_post_meta( $post_id, '_robotstxt_docmd_target_order', $data['target_order'] );
update_post_meta( $post_id, '_robotstxt_docmd_sync_enabled', $data['sync_enabled'] ? 1 : 0 );
update_post_meta( $post_id, '_robotstxt_docmd_sync_frequency', $data['sync_frequency'] );
if ( ! empty( $data['target_post_id'] ) ) {
update_post_meta( $post_id, '_robotstxt_docmd_target_post_id', $data['target_post_id'] );
}
// Schedule cron if enabled.
if ( $data['sync_enabled'] && 'never' !== $data['sync_frequency'] ) {
robotstxt_docmd_schedule_mapping_sync( $post_id, $data['sync_frequency'] );
}
return $post_id;
}
/**
* Update mapping
*
* @since 1.0.0
*
* @param int $mapping_id Mapping ID.
* @param array $data Mapping input data.
* @phpstan-param MappingInput $data
* @return bool True on success.
*/
function robotstxt_docmd_update_mapping( int $mapping_id, array $data ): bool {
// Update post title.
wp_update_post(
array(
'ID' => $mapping_id,
'post_title' => $data['title'],
)
);
// Update metadata.
update_post_meta( $mapping_id, '_robotstxt_docmd_repo_owner', $data['repo_owner'] );
update_post_meta( $mapping_id, '_robotstxt_docmd_repo_name', $data['repo_name'] );
update_post_meta( $mapping_id, '_robotstxt_docmd_file_path', $data['file_path'] );
update_post_meta( $mapping_id, '_robotstxt_docmd_branch', $data['branch'] );
update_post_meta( $mapping_id, '_robotstxt_docmd_target_post_type', $data['target_post_type'] );
update_post_meta( $mapping_id, '_robotstxt_docmd_target_author', $data['target_author'] );
update_post_meta( $mapping_id, '_robotstxt_docmd_target_parent', $data['target_parent'] );
update_post_meta( $mapping_id, '_robotstxt_docmd_target_order', $data['target_order'] );
update_post_meta( $mapping_id, '_robotstxt_docmd_sync_enabled', $data['sync_enabled'] ? 1 : 0 );
update_post_meta( $mapping_id, '_robotstxt_docmd_sync_frequency', $data['sync_frequency'] );
if ( ! empty( $data['target_post_id'] ) ) {
update_post_meta( $mapping_id, '_robotstxt_docmd_target_post_id', $data['target_post_id'] );
}
// Update cron schedule.
robotstxt_docmd_unschedule_mapping_sync( $mapping_id );
if ( $data['sync_enabled'] && 'never' !== $data['sync_frequency'] ) {
robotstxt_docmd_schedule_mapping_sync( $mapping_id, $data['sync_frequency'] );
}
return true;
}
/**
* Delete mapping
*
* @since 1.0.0
*
* @param int $mapping_id Mapping ID.
* @return bool True on success.
*/
function robotstxt_docmd_delete_mapping( int $mapping_id ): bool {
robotstxt_docmd_unschedule_mapping_sync( $mapping_id );
wp_delete_post( $mapping_id, true );
return true;
}
/**
* Check if file is already mapped
*
* @since 1.0.0
*
* @param string $owner Repository owner.
* @param string $repo Repository name.
* @param string $file_path File path.
* @param string $branch Branch name.
* @return bool True if mapped.
*/
function robotstxt_docmd_is_file_mapped( string $owner, string $repo, string $file_path, string $branch ): bool {
$args = array(
'post_type' => 'robotstxt_map',
'posts_per_page' => 1,
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_robotstxt_docmd_repo_owner',
'value' => $owner,
),
array(
'key' => '_robotstxt_docmd_repo_name',
'value' => $repo,
),
array(
'key' => '_robotstxt_docmd_file_path',
'value' => $file_path,
),
array(
'key' => '_robotstxt_docmd_branch',
'value' => $branch,
),
),
);
$query = new WP_Query( $args );
return $query->have_posts();
}
/**
* Sync mapping (pull content from GitHub and update WordPress)
*
* @since 1.0.0
*
* @param int $mapping_id Mapping ID.
* @return bool|WP_Error True on success or error.
*/
function robotstxt_docmd_sync_mapping( int $mapping_id ): bool|WP_Error {
$mapping = robotstxt_docmd_get_mapping( $mapping_id );
if ( ! $mapping ) {
return new WP_Error( 'invalid_mapping', __( 'Mapping not found', 'robotstxt-documentation-markdown' ) );
}
// Update status.
update_post_meta( $mapping_id, '_robotstxt_docmd_sync_status', 'pending' );
// Get settings.
$raw_settings = get_option( 'robotstxt_docmd_settings' );
$settings = is_array( $raw_settings ) ? $raw_settings : array();
$token_raw = $settings['github_token'] ?? '';
$token = is_string( $token_raw ) ? $token_raw : '';
if ( empty( $token ) ) {
update_post_meta( $mapping_id, '_robotstxt_docmd_sync_status', 'error' );
return new WP_Error( 'no_token', __( 'GitHub token not configured', 'robotstxt-documentation-markdown' ) );
}
// Get file content from GitHub.
$content = robotstxt_docmd_get_file_content(
$mapping['repo_owner'],
$mapping['repo_name'],
$mapping['file_path'],
$mapping['branch'],
$token
);
if ( is_wp_error( $content ) ) {
update_post_meta( $mapping_id, '_robotstxt_docmd_sync_status', 'error' );
return $content;
}
// Convert Markdown to HTML using CommonMark.
// Strip raw HTML from Markdown to prevent stored XSS via compromised upstream repos.
$converter = new \League\CommonMark\CommonMarkConverter(
array(
'html_input' => 'strip',
'allow_unsafe_links' => false,
)
);
$html_content = $converter->convert( $content )->getContent();
// Create or update WordPress post.
if ( empty( $mapping['target_post_id'] ) ) {
// Create new post.
$post_id = wp_insert_post(
array(
'post_title' => $mapping['title'],
'post_content' => $html_content,
'post_type' => $mapping['target_post_type'],
'post_status' => 'publish',
'post_author' => $mapping['target_author'],
'post_parent' => $mapping['target_parent'],
'menu_order' => $mapping['target_order'],
),
true
);
if ( is_wp_error( $post_id ) ) {
update_post_meta( $mapping_id, '_robotstxt_docmd_sync_status', 'error' );
return $post_id;
}
// Save the post ID for future syncs.
update_post_meta( $mapping_id, '_robotstxt_docmd_target_post_id', $post_id );
} else {
// Update existing post.
$update_result = wp_update_post(
array(
'ID' => $mapping['target_post_id'],
'post_content' => $html_content,
),
true
);
if ( is_wp_error( $update_result ) ) {
update_post_meta( $mapping_id, '_robotstxt_docmd_sync_status', 'error' );
return $update_result;
}
}
// Update sync status.
update_post_meta( $mapping_id, '_robotstxt_docmd_sync_status', 'success' );
update_post_meta( $mapping_id, '_robotstxt_docmd_last_sync', current_time( 'mysql' ) );
return true;
}