This commit is contained in:
Javier Casares 2026-08-08 08:53:08 +00:00
commit 7d3fc1969d
52 changed files with 1776 additions and 295 deletions

View file

@ -3,8 +3,8 @@
* Plugin Name: Documentation Markdown (by ROBOTSTXT)
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-documentation-markdown
* Description: Synchronizes Markdown documentation from GitHub repositories to WordPress pages and posts automatically.
* Version: 1.1.1
* Requires at least: 6.7
* Version: 1.2.0
* Requires at least: 6.8
* Requires PHP: 8.0
* Security: robotstxt@robotstxt.es
* Author: ROBOTSTXT
@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
}
// Define plugin constants.
define( 'ROBOTSTXT_DOCMD_VERSION', '1.1.1' );
define( 'ROBOTSTXT_DOCMD_VERSION', '1.2.0' );
define( 'ROBOTSTXT_DOCMD_PLUGIN_FILE', __FILE__ );
define( 'ROBOTSTXT_DOCMD_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'ROBOTSTXT_DOCMD_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
@ -41,6 +41,7 @@ if ( file_exists( ROBOTSTXT_DOCMD_PLUGIN_DIR . 'vendor/autoload.php' ) ) {
require_once ROBOTSTXT_DOCMD_PLUGIN_DIR . 'robotstxt-documentation-markdown-functions.php';
require_once ROBOTSTXT_DOCMD_PLUGIN_DIR . 'robotstxt-documentation-markdown-github.php';
require_once ROBOTSTXT_DOCMD_PLUGIN_DIR . 'robotstxt-documentation-markdown-map.php';
require_once ROBOTSTXT_DOCMD_PLUGIN_DIR . 'robotstxt-documentation-markdown-content.php';
require_once ROBOTSTXT_DOCMD_PLUGIN_DIR . 'robotstxt-documentation-markdown-cron.php';
// Activation/Deactivation hooks.
@ -254,35 +255,34 @@ function robotstxt_docmd_handle_admin_actions() {
if ( isset( $_GET['page'] ) && 'robotstxt-docmd-settings' === $_GET['page'] && isset( $_GET['debug_action'] ) ) {
$debug_action = sanitize_key( wp_unslash( robotstxt_docmd_input_string( $_GET, 'debug_action' ) ) );
if ( 'test_github' === $debug_action ) {
check_admin_referer( 'robotstxt_docmd_debug_test_github' );
robotstxt_docmd_debug_test_github();
return;
}
switch ( $debug_action ) {
case 'test_github':
check_admin_referer( 'robotstxt_docmd_debug_test_github' );
robotstxt_docmd_debug_test_github();
return;
if ( 'test_token' === $debug_action ) {
check_admin_referer( 'robotstxt_docmd_debug_test_token' );
robotstxt_docmd_debug_test_token();
return;
}
case 'test_token':
check_admin_referer( 'robotstxt_docmd_debug_test_token' );
robotstxt_docmd_debug_test_token();
return;
if ( 'run_cron' === $debug_action && isset( $_GET['mapping_id'] ) ) {
$mapping_id = robotstxt_docmd_input_int( $_GET, 'mapping_id' );
check_admin_referer( 'robotstxt_docmd_debug_run_cron_' . $mapping_id );
robotstxt_docmd_debug_run_cron( $mapping_id );
return;
}
case 'run_cron':
if ( isset( $_GET['mapping_id'] ) ) {
$mapping_id = robotstxt_docmd_input_int( $_GET, 'mapping_id' );
check_admin_referer( 'robotstxt_docmd_debug_run_cron_' . $mapping_id );
robotstxt_docmd_debug_run_cron( $mapping_id );
}
return;
if ( 'clear_cache' === $debug_action ) {
check_admin_referer( 'robotstxt_docmd_debug_clear_cache' );
robotstxt_docmd_debug_clear_cache();
return;
}
case 'clear_cache':
check_admin_referer( 'robotstxt_docmd_debug_clear_cache' );
robotstxt_docmd_debug_clear_cache();
return;
if ( 'fix_crons' === $debug_action ) {
check_admin_referer( 'robotstxt_docmd_debug_fix_crons' );
robotstxt_docmd_debug_fix_crons();
return;
case 'fix_crons':
check_admin_referer( 'robotstxt_docmd_debug_fix_crons' );
robotstxt_docmd_debug_fix_crons();
return;
}
}
@ -374,11 +374,23 @@ function robotstxt_docmd_render_mappings_page() {
</a>
</td>
<td>
<?php if ( ! empty( $mapping['target_post_id'] ) ) : ?>
<a href="<?php echo esc_url( (string) get_permalink( (int) $mapping['target_post_id'] ) ); ?>" target="_blank" rel="noopener noreferrer">
<?php echo esc_html( (string) get_the_title( (int) $mapping['target_post_id'] ) ); ?>
<?php
if ( ! empty( $mapping['target_post_id'] ) ) :
$target_id = (int) $mapping['target_post_id'];
$permalink = get_permalink( $target_id );
$uri = get_page_uri( $target_id );
$post_type = get_post_type( $target_id );
?>
<a href="<?php echo esc_url( (string) $permalink ); ?>" target="_blank" rel="noopener noreferrer">
<?php echo esc_html( (string) get_the_title( $target_id ) ); ?>
</a>
<?php else : ?>
<br><small>
<code><?php echo esc_html( '/' . ( is_string( $uri ) ? $uri : '' ) ); ?></code>
<span class="description"><?php echo esc_html( is_string( $post_type ) ? $post_type : '' ); ?> #<?php echo esc_html( (string) $target_id ); ?></span>
</small>
<?php
else :
?>
<em><?php esc_html_e( 'Not created yet', 'robotstxt-documentation-markdown' ); ?></em>
<?php endif; ?>
</td>
@ -392,9 +404,9 @@ function robotstxt_docmd_render_mappings_page() {
<a href="<?php echo esc_url( admin_url( 'admin.php?page=robotstxt-docmd-add-mapping&mapping_id=' . $mapping['id'] ) ); ?>" class="button button-small">
<?php esc_html_e( 'Edit', 'robotstxt-documentation-markdown' ); ?>
</a>
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=robotstxt-docmd-mappings&action=delete&mapping_id=' . $mapping['id'] ), 'robotstxt_docmd_delete_' . $mapping['id'] ) ); ?>" class="button button-small button-link-delete" onclick="return confirm('<?php echo esc_js( __( 'Are you sure?', 'robotstxt-documentation-markdown' ) ); ?>');">
<?php esc_html_e( 'Delete', 'robotstxt-documentation-markdown' ); ?>
</a>
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=robotstxt-docmd-mappings&action=delete&mapping_id=' . $mapping['id'] ), 'robotstxt_docmd_delete_' . $mapping['id'] ) ); ?>" class="button button-small button-link-delete robotstxt-docmd-confirm" data-confirm="<?php esc_attr_e( 'Are you sure?', 'robotstxt-documentation-markdown' ); ?>">
<?php esc_html_e( 'Delete', 'robotstxt-documentation-markdown' ); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
@ -402,6 +414,19 @@ function robotstxt_docmd_render_mappings_page() {
</table>
<?php endif; ?>
</div>
<script>
// Delegated confirmation for delete actions (replaces inline onclick).
(function() {
var links = document.querySelectorAll('.robotstxt-docmd-confirm');
links.forEach(function(link) {
link.addEventListener('click', function(e) {
if (!confirm(link.getAttribute('data-confirm'))) {
e.preventDefault();
}
});
});
})();
</script>
<?php
}
@ -450,12 +475,20 @@ function robotstxt_docmd_render_discover_page() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$branch = sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_GET, 'branch', 'main' ) ) );
// Check if refresh requested.
// Cache key for this repository + branch.
$cache_key = sprintf( 'robotstxt_docmd_files_%s_%s_%s', $repo_data['owner'], $repo_data['repo'], $branch );
// Detect cache state BEFORE the fetch below repopulates it, so the
// "cached vs fresh" indicator reflects where the displayed data came from.
$was_cached = false !== get_transient( $cache_key );
// Check if refresh requested (CSRF-protected; forces a fresh GitHub API call).
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$refresh = '1' === robotstxt_docmd_input_string( $_GET, 'refresh' );
if ( $refresh ) {
$cache_key = sprintf( 'robotstxt_docmd_files_%s_%s_%s', $repo_data['owner'], $repo_data['repo'], $branch );
check_admin_referer( 'robotstxt_docmd_discover_refresh' );
delete_transient( $cache_key );
$was_cached = false;
}
// Get files from GitHub.
@ -476,6 +509,7 @@ function robotstxt_docmd_render_discover_page() {
<form method="get" style="margin-bottom: 20px;">
<input type="hidden" name="page" value="robotstxt-docmd-discover">
<?php wp_nonce_field( 'robotstxt_docmd_discover_refresh', '_wpnonce', false ); ?>
<label>
<?php esc_html_e( 'Branch:', 'robotstxt-documentation-markdown' ); ?>
<input type="text" name="branch" value="<?php echo esc_attr( $branch ); ?>" class="regular-text">
@ -508,9 +542,7 @@ function robotstxt_docmd_render_discover_page() {
?>
<br>
<?php
$cache_key = sprintf( 'robotstxt_docmd_files_%s_%s_%s', $repo_data['owner'], $repo_data['repo'], $branch );
$cached_value = get_transient( $cache_key );
if ( false !== $cached_value ) {
if ( $was_cached ) {
echo '<span style="color: green;">✓ ' . esc_html__( 'Using cached data (24 hour cache)', 'robotstxt-documentation-markdown' ) . '</span>';
} else {
echo '<span style="color: orange;">⟲ ' . esc_html__( 'Fetching fresh data from GitHub API', 'robotstxt-documentation-markdown' ) . '</span>';
@ -761,25 +793,50 @@ function robotstxt_docmd_render_add_mapping_page() {
<label>
<?php esc_html_e( 'Select Page/Post:', 'robotstxt-documentation-markdown' ); ?>
<?php
$all_posts = get_posts(
// Only query public, sync-relevant post types (excludes the
// internal robotstxt_map CPT and attachments).
$selectable_types = get_post_types( array( 'public' => true ), 'names' );
unset( $selectable_types['attachment'] );
$existing_posts = get_posts(
array(
'post_type' => 'any',
'posts_per_page' => 100,
'post_type' => array_values( $selectable_types ),
// phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page -- Deliberate cap for the admin "existing content" dropdown; query uses no_found_rows for performance.
'posts_per_page' => 200,
'orderby' => 'title',
'order' => 'ASC',
'no_found_rows' => true,
'ignore_sticky_posts' => true,
)
);
// Build a path-based label per post so nested and duplicate
// pages are distinguishable, then sort by path to group the tree.
$options = array();
foreach ( $existing_posts as $existing ) {
$uri = get_page_uri( $existing );
$path = ( is_string( $uri ) && '' !== $uri ) ? $uri : $existing->post_name;
$depth = substr_count( $path, '/' );
$label = str_repeat( '— ', $depth )
. $existing->post_title
. ' — /' . $path
. ' (#' . (string) $existing->ID . ', ' . $existing->post_type . ')';
$options[ $path . "\0" . (string) $existing->ID ] = array(
'id' => $existing->ID,
'label' => $label,
);
}
ksort( $options );
?>
<select name="target_post_id">
<option value="0"><?php esc_html_e( 'Select a page...', 'robotstxt-documentation-markdown' ); ?></option>
<?php
foreach ( $all_posts as $post ) {
foreach ( $options as $option ) {
printf(
'<option value="%d" %s>%s (%s)</option>',
esc_attr( (string) $post->ID ),
selected( $data['target_post_id'], $post->ID, false ),
esc_html( $post->post_title ),
esc_html( $post->post_type )
'<option value="%d" %s>%s</option>',
esc_attr( (string) $option['id'] ),
selected( $data['target_post_id'], $option['id'], false ),
esc_html( $option['label'] )
);
}
?>
@ -863,13 +920,20 @@ function robotstxt_docmd_handle_save_mapping() {
$mapping_id = robotstxt_docmd_input_int( $_POST, 'mapping_id' );
// Validate target post type against registered public post types; fall back to 'page'.
$target_post_type = sanitize_key( wp_unslash( robotstxt_docmd_input_string( $_POST, 'target_post_type', 'page' ) ) );
$public_post_types = get_post_types( array( 'public' => true ), 'names' );
if ( ! in_array( $target_post_type, $public_post_types, true ) ) {
$target_post_type = 'page';
}
$data = array(
'title' => sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'title' ) ) ),
'repo_owner' => sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'repo_owner' ) ) ),
'repo_name' => sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'repo_name' ) ) ),
'file_path' => sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'file_path' ) ) ),
'branch' => sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'branch', 'main' ) ) ),
'target_post_type' => sanitize_key( wp_unslash( robotstxt_docmd_input_string( $_POST, 'target_post_type', 'page' ) ) ),
'target_post_type' => $target_post_type,
'target_author' => robotstxt_docmd_input_int( $_POST, 'target_author', get_current_user_id() ),
'target_parent' => robotstxt_docmd_input_int( $_POST, 'target_parent' ),
'target_order' => robotstxt_docmd_input_int( $_POST, 'target_order' ),