robotstxt-documentation-mar.../robotstxt-documentation-markdown.php
2026-08-24 11:42:10 +00:00

1763 lines
64 KiB
PHP

<?php
/**
* Plugin Name: Documentation Markdown (by ROBOTSTXT)
* Plugin URI: https://www.robotstxt.software/plugins/robotstxt-documentation-markdown/
* Description: Synchronizes Markdown documentation from GitHub repositories to WordPress pages and posts automatically.
* Version: 1.2.2
* Requires at least: 4.2
* Requires PHP: 8.0
* Security: robotstxt@robotstxt.es
* Author: ROBOTSTXT
* Author URI: https://www.robotstxt.software/
* Text Domain: robotstxt-documentation-markdown
* Domain Path: /languages
* License: GPL-3.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
* Update URI: https://www.robotstxt.software/plugins/robotstxt-documentation-markdown/
*
* @package RobotsTxt\DocumentationMarkdown
* @since 1.0.0
*/
// Security check.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Define plugin constants.
define( 'ROBOTSTXT_DOCMD_VERSION', '1.2.2' );
define( 'ROBOTSTXT_DOCMD_PLUGIN_FILE', __FILE__ );
define( 'ROBOTSTXT_DOCMD_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'ROBOTSTXT_DOCMD_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'ROBOTSTXT_DOCMD_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
// Load Composer dependencies.
if ( file_exists( ROBOTSTXT_DOCMD_PLUGIN_DIR . 'vendor/autoload.php' ) ) {
require_once ROBOTSTXT_DOCMD_PLUGIN_DIR . 'vendor/autoload.php';
}
// Load plugin files.
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.
register_activation_hook( __FILE__, 'robotstxt_docmd_activate' );
register_deactivation_hook( __FILE__, 'robotstxt_docmd_deactivate' );
// Initialize plugin.
add_action( 'init', 'robotstxt_docmd_init' );
add_action( 'admin_menu', 'robotstxt_docmd_register_admin_menu' );
add_action( 'admin_enqueue_scripts', 'robotstxt_docmd_enqueue_admin_assets' );
add_action( 'admin_init', 'robotstxt_docmd_handle_admin_actions' );
add_action( 'admin_notices', 'robotstxt_docmd_admin_notice_manager_plugin' );
/**
* Plugin activation
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_activate() {
// Register CPT.
robotstxt_docmd_register_post_types();
// Flush rewrite rules.
flush_rewrite_rules();
// Set default options.
if ( ! get_option( 'robotstxt_docmd_settings' ) ) {
add_option(
'robotstxt_docmd_settings',
array(
'github_url' => '',
'github_token' => '',
),
'',
false // Do not autoload — contains encrypted token.
);
}
}
/**
* Plugin deactivation
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_deactivate() {
flush_rewrite_rules();
robotstxt_docmd_unschedule_all_crons();
}
/**
* Initialize plugin
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_init() {
// Register CPT.
robotstxt_docmd_register_post_types();
}
/**
* Register Custom Post Type for mappings
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_register_post_types() {
register_post_type(
'robotstxt_map',
array(
'labels' => array(
'name' => __( 'Doc Mappings', 'robotstxt-documentation-markdown' ),
'singular_name' => __( 'Doc Mapping', 'robotstxt-documentation-markdown' ),
),
'public' => false,
'publicly_queryable' => false,
'show_ui' => false,
'show_in_menu' => false,
'show_in_nav_menus' => false,
'show_in_admin_bar' => false,
'capability_type' => 'post',
'hierarchical' => false,
'supports' => array( 'title' ),
'has_archive' => false,
'rewrite' => false,
'can_export' => true,
'delete_with_user' => false,
)
);
}
/**
* Register admin menu
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_register_admin_menu() {
// Main menu.
add_menu_page(
__( 'Documentation', 'robotstxt-documentation-markdown' ),
__( 'Documentation', 'robotstxt-documentation-markdown' ),
'edit_pages',
'robotstxt-docmd-mappings',
'robotstxt_docmd_render_mappings_page',
'dashicons-media-document',
30
);
// Mappings (replaces auto-generated first item).
add_submenu_page(
'robotstxt-docmd-mappings',
__( 'Mappings', 'robotstxt-documentation-markdown' ),
__( 'Mappings', 'robotstxt-documentation-markdown' ),
'edit_pages',
'robotstxt-docmd-mappings',
'robotstxt_docmd_render_mappings_page'
);
// Discover Files.
add_submenu_page(
'robotstxt-docmd-mappings',
__( 'Discover Files', 'robotstxt-documentation-markdown' ),
__( 'Discover Files', 'robotstxt-documentation-markdown' ),
'edit_pages',
'robotstxt-docmd-discover',
'robotstxt_docmd_render_discover_page'
);
// Add New (manual).
add_submenu_page(
'robotstxt-docmd-mappings',
__( 'Add New', 'robotstxt-documentation-markdown' ),
__( 'Add New', 'robotstxt-documentation-markdown' ),
'edit_pages',
'robotstxt-docmd-add-mapping',
'robotstxt_docmd_render_add_mapping_page'
);
// Settings.
add_submenu_page(
'robotstxt-docmd-mappings',
__( 'Settings', 'robotstxt-documentation-markdown' ),
__( 'Settings', 'robotstxt-documentation-markdown' ),
'edit_pages',
'robotstxt-docmd-settings',
'robotstxt_docmd_render_settings_page'
);
}
/**
* Enqueue admin assets
*
* @since 1.0.0
*
* @param string $hook Current admin page hook.
* @return void
*/
function robotstxt_docmd_enqueue_admin_assets( $hook ) {
// Only on our pages.
if ( false === strpos( $hook, 'robotstxt-docmd' ) ) {
return;
}
wp_enqueue_style(
'robotstxt-docmd-admin',
ROBOTSTXT_DOCMD_PLUGIN_URL . 'assets/css/admin.css',
array(),
ROBOTSTXT_DOCMD_VERSION
);
}
/**
* Handle admin actions (sync, delete from Mappings page)
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_handle_admin_actions() {
if ( ! current_user_can( 'edit_pages' ) ) {
return;
}
// Handle mapping form submission (POST).
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( isset( $_POST['robotstxt_docmd_nonce'] ) && isset( $_POST['title'] ) ) {
if ( wp_verify_nonce( sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'robotstxt_docmd_nonce' ) ) ), 'robotstxt_docmd_save_mapping' ) ) {
robotstxt_docmd_handle_save_mapping();
// The function above will redirect and exit.
}
}
// Handle settings form submission (POST).
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( isset( $_POST['robotstxt_docmd_nonce'] ) && isset( $_POST['github_url'] ) ) {
if ( wp_verify_nonce( sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'robotstxt_docmd_nonce' ) ) ), 'robotstxt_docmd_save_settings' ) ) {
robotstxt_docmd_handle_save_settings();
// The function above will redirect and exit.
}
}
// Handle debug actions (only on settings page).
// phpcs:disable WordPress.Security.NonceVerification.Recommended
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' ) ) );
switch ( $debug_action ) {
case 'test_github':
check_admin_referer( 'robotstxt_docmd_debug_test_github' );
robotstxt_docmd_debug_test_github();
return;
case 'test_token':
check_admin_referer( 'robotstxt_docmd_debug_test_token' );
robotstxt_docmd_debug_test_token();
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;
case 'clear_cache':
check_admin_referer( 'robotstxt_docmd_debug_clear_cache' );
robotstxt_docmd_debug_clear_cache();
return;
case 'fix_crons':
check_admin_referer( 'robotstxt_docmd_debug_fix_crons' );
robotstxt_docmd_debug_fix_crons();
return;
}
}
// Handle GET actions (sync, delete) on mappings page.
if ( ! isset( $_GET['page'] ) || 'robotstxt-docmd-mappings' !== $_GET['page'] ) {
return;
}
if ( ! isset( $_GET['action'] ) || ! isset( $_GET['mapping_id'] ) ) {
return;
}
$action = sanitize_key( wp_unslash( robotstxt_docmd_input_string( $_GET, 'action' ) ) );
$mapping_id = robotstxt_docmd_input_int( $_GET, 'mapping_id' );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( 'sync' === $action ) {
check_admin_referer( 'robotstxt_docmd_sync_' . $mapping_id );
$result = robotstxt_docmd_sync_mapping( $mapping_id );
if ( is_wp_error( $result ) ) {
wp_safe_redirect( add_query_arg( 'error', rawurlencode( $result->get_error_message() ), admin_url( 'admin.php?page=robotstxt-docmd-mappings' ) ) );
} else {
wp_safe_redirect( add_query_arg( 'message', 'sync_complete', admin_url( 'admin.php?page=robotstxt-docmd-mappings' ) ) );
}
exit;
}
if ( 'delete' === $action ) {
check_admin_referer( 'robotstxt_docmd_delete_' . $mapping_id );
robotstxt_docmd_delete_mapping( $mapping_id );
wp_safe_redirect( add_query_arg( 'message', 'mapping_deleted', admin_url( 'admin.php?page=robotstxt-docmd-mappings' ) ) );
exit;
}
}
/**
* Render Mappings page
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_render_mappings_page() {
if ( ! current_user_can( 'edit_pages' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions.', 'robotstxt-documentation-markdown' ) );
}
// Get all mappings.
$mappings = robotstxt_docmd_get_all_mappings();
?>
<div class="wrap robotstxt-docmd-mappings">
<h1 class="wp-heading-inline">
<?php esc_html_e( 'Documentation Mappings', 'robotstxt-documentation-markdown' ); ?>
</h1>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=robotstxt-docmd-add-mapping' ) ); ?>" class="page-title-action">
<?php esc_html_e( 'Add New', 'robotstxt-documentation-markdown' ); ?>
</a>
<hr class="wp-header-end">
<?php robotstxt_docmd_render_admin_notices(); ?>
<?php if ( empty( $mappings ) ) : ?>
<div class="robotstxt-docmd-empty">
<p><?php esc_html_e( 'No mappings found. Create your first mapping to get started!', 'robotstxt-documentation-markdown' ); ?></p>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=robotstxt-docmd-discover' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Discover Files', 'robotstxt-documentation-markdown' ); ?>
</a>
</div>
<?php else : ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php esc_html_e( 'Title', 'robotstxt-documentation-markdown' ); ?></th>
<th><?php esc_html_e( 'GitHub File', 'robotstxt-documentation-markdown' ); ?></th>
<th><?php esc_html_e( 'WordPress Page', 'robotstxt-documentation-markdown' ); ?></th>
<th><?php esc_html_e( 'Status', 'robotstxt-documentation-markdown' ); ?></th>
<th><?php esc_html_e( 'Actions', 'robotstxt-documentation-markdown' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $mappings as $mapping ) : ?>
<tr>
<td><strong><?php echo esc_html( $mapping['title'] ); ?></strong></td>
<td>
<a href="<?php echo esc_url( $mapping['github_url'] ); ?>" target="_blank" rel="noopener noreferrer">
<code><?php echo esc_html( $mapping['file_path'] ); ?></code>
</a>
</td>
<td>
<?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>
<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>
<td>
<?php echo wp_kses_post( robotstxt_docmd_get_status_badge( $mapping['sync_status'] ) ); ?>
</td>
<td>
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=robotstxt-docmd-mappings&action=sync&mapping_id=' . $mapping['id'] ), 'robotstxt_docmd_sync_' . $mapping['id'] ) ); ?>" class="button button-small">
<?php esc_html_e( 'Sync Now', 'robotstxt-documentation-markdown' ); ?>
</a>
<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 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; ?>
</tbody>
</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
}
/**
* Render Discover Files page
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_render_discover_page() {
if ( ! current_user_can( 'edit_pages' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions.', 'robotstxt-documentation-markdown' ) );
}
// Get settings.
$raw_settings = get_option( 'robotstxt_docmd_settings', array() );
$settings = is_array( $raw_settings ) ? $raw_settings : array();
$github_url = robotstxt_docmd_input_string( $settings, 'github_url' );
$github_token = robotstxt_docmd_input_string( $settings, 'github_token' );
// Parse GitHub URL.
$repo_data = robotstxt_docmd_parse_github_url( $github_url );
if ( is_wp_error( $repo_data ) || empty( $github_token ) ) {
?>
<div class="wrap">
<h1><?php esc_html_e( 'Discover Files', 'robotstxt-documentation-markdown' ); ?></h1>
<div class="notice notice-error">
<p>
<?php
printf(
/* translators: %s: settings page link */
esc_html__( 'Please configure your GitHub repository in %s first.', 'robotstxt-documentation-markdown' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) ) . '">' . esc_html__( 'Settings', 'robotstxt-documentation-markdown' ) . '</a>'
);
?>
</p>
</div>
</div>
<?php
return;
}
// Get branch from query string.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$branch = sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_GET, 'branch', 'main' ) ) );
// 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 ) {
check_admin_referer( 'robotstxt_docmd_discover_refresh' );
delete_transient( $cache_key );
$was_cached = false;
}
// Get files from GitHub.
$files = robotstxt_docmd_get_all_markdown_files( $repo_data['owner'], $repo_data['repo'], $branch, $github_token );
?>
<div class="wrap">
<h1><?php esc_html_e( 'Discover Files', 'robotstxt-documentation-markdown' ); ?></h1>
<?php robotstxt_docmd_render_admin_notices(); ?>
<p>
<strong><?php esc_html_e( 'Repository:', 'robotstxt-documentation-markdown' ); ?></strong>
<a href="<?php echo esc_url( $github_url ); ?>" target="_blank" rel="noopener noreferrer">
<?php echo esc_html( $repo_data['owner'] . '/' . $repo_data['repo'] ); ?>
</a>
</p>
<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">
</label>
<button type="submit" class="button"><?php esc_html_e( 'Change Branch', 'robotstxt-documentation-markdown' ); ?></button>
<button type="submit" name="refresh" value="1" class="button button-primary">
<?php esc_html_e( 'Refresh Files from GitHub', 'robotstxt-documentation-markdown' ); ?>
</button>
</form>
<?php if ( $refresh ) : ?>
<div class="notice notice-info">
<p><strong><?php esc_html_e( 'Cache cleared. Files reloaded from GitHub.', 'robotstxt-documentation-markdown' ); ?></strong></p>
</div>
<?php endif; ?>
<div class="notice notice-info" style="background: #f0f0f1; border-left-color: #646970;">
<p>
<strong><?php esc_html_e( 'Debug Info:', 'robotstxt-documentation-markdown' ); ?></strong><br>
<?php
echo esc_html(
sprintf(
/* translators: 1: repository owner, 2: repository name, 3: branch name */
__( 'Repository: %1$s/%2$s | Branch: %3$s', 'robotstxt-documentation-markdown' ),
$repo_data['owner'],
$repo_data['repo'],
$branch
)
);
?>
<br>
<?php
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>';
}
?>
</p>
</div>
<?php if ( is_wp_error( $files ) ) : ?>
<div class="notice notice-error">
<p><?php echo esc_html( $files->get_error_message() ); ?></p>
</div>
<?php elseif ( empty( $files ) ) : ?>
<p><?php esc_html_e( 'No markdown files found.', 'robotstxt-documentation-markdown' ); ?></p>
<?php else : ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php esc_html_e( 'File Path', 'robotstxt-documentation-markdown' ); ?></th>
<th><?php esc_html_e( 'Size', 'robotstxt-documentation-markdown' ); ?></th>
<th><?php esc_html_e( 'Status', 'robotstxt-documentation-markdown' ); ?></th>
<th><?php esc_html_e( 'Actions', 'robotstxt-documentation-markdown' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $files as $file ) : ?>
<?php
$is_mapped = robotstxt_docmd_is_file_mapped( $repo_data['owner'], $repo_data['repo'], $file['path'], $branch );
?>
<tr>
<td><code><?php echo esc_html( $file['path'] ); ?></code></td>
<?php
$file_size_str = size_format( $file['size'], 2 );
?>
<td><?php echo esc_html( false !== $file_size_str ? $file_size_str : '0 B' ); ?></td>
<td>
<?php if ( $is_mapped ) : ?>
<span class="status-badge status-mapped"><?php esc_html_e( 'Mapped', 'robotstxt-documentation-markdown' ); ?></span>
<?php else : ?>
<span class="status-badge status-unmapped"><?php esc_html_e( 'Unmapped', 'robotstxt-documentation-markdown' ); ?></span>
<?php endif; ?>
</td>
<td>
<?php if ( $is_mapped ) : ?>
<button class="button button-small" disabled><?php esc_html_e( 'Already Mapped', 'robotstxt-documentation-markdown' ); ?></button>
<?php else : ?>
<a href="
<?php
echo esc_url(
add_query_arg(
array(
'page' => 'robotstxt-docmd-add-mapping',
'owner' => $repo_data['owner'],
'repo' => $repo_data['repo'],
'branch' => $branch,
'file_path' => rawurlencode( $file['path'] ),
),
admin_url( 'admin.php' )
)
);
?>
" class="button button-primary button-small">
<?php esc_html_e( 'Map', 'robotstxt-documentation-markdown' ); ?>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}
/**
* Render Add/Edit Mapping page
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_render_add_mapping_page() {
if ( ! current_user_can( 'edit_pages' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions.', 'robotstxt-documentation-markdown' ) );
}
// Get mapping ID if editing.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$mapping_id = robotstxt_docmd_input_int( $_GET, 'mapping_id' );
$mapping = $mapping_id ? robotstxt_docmd_get_mapping( $mapping_id ) : null;
// Pre-fill from discover page or defaults.
$defaults = array(
'title' => '',
'repo_owner' => sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_GET, 'owner' ) ) ),
'repo_name' => sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_GET, 'repo' ) ) ),
'file_path' => urldecode( sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_GET, 'file_path' ) ) ) ),
'branch' => sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_GET, 'branch', 'main' ) ) ),
// phpcs:enable WordPress.Security.NonceVerification.Recommended
'target_post_type' => 'page',
'target_post_id' => 0,
'target_author' => get_current_user_id(),
'target_parent' => 0,
'target_order' => 0,
'sync_enabled' => true,
'sync_frequency' => 'daily',
);
$data = $mapping ? $mapping : $defaults;
// Auto-generate title from file path.
if ( empty( $data['title'] ) && ! empty( $data['file_path'] ) ) {
$data['title'] = robotstxt_docmd_generate_title_from_path( $data['file_path'] );
}
?>
<div class="wrap robotstxt-docmd-edit-mapping">
<h1><?php echo $mapping_id ? esc_html__( 'Edit Mapping', 'robotstxt-documentation-markdown' ) : esc_html__( 'Add New Mapping', 'robotstxt-documentation-markdown' ); ?></h1>
<?php robotstxt_docmd_render_admin_notices(); ?>
<form method="post" action="">
<?php wp_nonce_field( 'robotstxt_docmd_save_mapping', 'robotstxt_docmd_nonce' ); ?>
<?php if ( $mapping_id ) : ?>
<input type="hidden" name="mapping_id" value="<?php echo esc_attr( (string) $mapping_id ); ?>">
<?php endif; ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="title"><?php esc_html_e( 'Mapping Title', 'robotstxt-documentation-markdown' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="text" id="title" name="title" value="<?php echo esc_attr( $data['title'] ); ?>" class="regular-text" required>
</td>
</tr>
<tr>
<th scope="row">
<label for="repo_owner"><?php esc_html_e( 'Repository Owner', 'robotstxt-documentation-markdown' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="text" id="repo_owner" name="repo_owner" value="<?php echo esc_attr( $data['repo_owner'] ); ?>" class="regular-text" required>
<p class="description"><?php esc_html_e( 'GitHub username or organization', 'robotstxt-documentation-markdown' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="repo_name"><?php esc_html_e( 'Repository Name', 'robotstxt-documentation-markdown' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="text" id="repo_name" name="repo_name" value="<?php echo esc_attr( $data['repo_name'] ); ?>" class="regular-text" required>
</td>
</tr>
<tr>
<th scope="row">
<label for="file_path"><?php esc_html_e( 'File Path', 'robotstxt-documentation-markdown' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="text" id="file_path" name="file_path" value="<?php echo esc_attr( $data['file_path'] ); ?>" class="regular-text" required>
<p class="description"><?php esc_html_e( 'Path to markdown file (e.g., README.md or docs/api.md)', 'robotstxt-documentation-markdown' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="branch"><?php esc_html_e( 'Branch', 'robotstxt-documentation-markdown' ); ?></label>
</th>
<td>
<input type="text" id="branch" name="branch" value="<?php echo esc_attr( $data['branch'] ); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Target WordPress Content', 'robotstxt-documentation-markdown' ); ?></th>
<td>
<label>
<input type="radio" name="target_type" value="new" <?php checked( empty( $data['target_post_id'] ) ); ?>>
<?php esc_html_e( 'Create New Page/Post', 'robotstxt-documentation-markdown' ); ?>
</label>
<br><br>
<div id="new-post-options" style="margin-left: 25px;">
<label>
<?php esc_html_e( 'Post Type:', 'robotstxt-documentation-markdown' ); ?>
<select name="target_post_type">
<?php
$post_types = get_post_types( array( 'public' => true ), 'objects' );
foreach ( $post_types as $post_type ) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr( $post_type->name ),
selected( $data['target_post_type'], $post_type->name, false ),
esc_html( $post_type->labels->singular_name )
);
}
?>
</select>
</label>
<br><br>
<label>
<?php esc_html_e( 'Parent Page:', 'robotstxt-documentation-markdown' ); ?>
<?php
wp_dropdown_pages(
array(
'name' => 'target_parent',
'show_option_none' => esc_html__( '(no parent)', 'robotstxt-documentation-markdown' ),
'option_none_value' => '0',
'selected' => absint( $data['target_parent'] ),
)
);
?>
</label>
<br><br>
<label>
<?php esc_html_e( 'Author:', 'robotstxt-documentation-markdown' ); ?>
<?php
wp_dropdown_users(
array(
'name' => 'target_author',
'selected' => $data['target_author'],
)
);
?>
</label>
<br><br>
<label>
<?php esc_html_e( 'Order:', 'robotstxt-documentation-markdown' ); ?>
<input type="number" name="target_order" value="<?php echo esc_attr( (string) $data['target_order'] ); ?>" class="small-text" min="0" step="1">
<p class="description"><?php esc_html_e( 'Page order (menu_order). Lower numbers appear first. Default: 0', 'robotstxt-documentation-markdown' ); ?></p>
</label>
</div>
<br>
<label>
<input type="radio" name="target_type" value="existing" <?php checked( ! empty( $data['target_post_id'] ) ); ?>>
<?php esc_html_e( 'Update Existing Content', 'robotstxt-documentation-markdown' ); ?>
</label>
<div id="existing-post-options" style="margin-left: 25px; margin-top: 10px;">
<label>
<?php esc_html_e( 'Select Page/Post:', 'robotstxt-documentation-markdown' ); ?>
<?php
// 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' => 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 ( $options as $option ) {
printf(
'<option value="%d" %s>%s</option>',
esc_attr( (string) $option['id'] ),
selected( $data['target_post_id'], $option['id'], false ),
esc_html( $option['label'] )
);
}
?>
</select>
</label>
</div>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Sync Settings', 'robotstxt-documentation-markdown' ); ?></th>
<td>
<label>
<input type="checkbox" name="sync_enabled" value="1" <?php checked( $data['sync_enabled'] ); ?>>
<?php esc_html_e( 'Enable automatic sync', 'robotstxt-documentation-markdown' ); ?>
</label>
<br><br>
<label>
<?php esc_html_e( 'Frequency:', 'robotstxt-documentation-markdown' ); ?>
<select name="sync_frequency">
<option value="never" <?php selected( $data['sync_frequency'], 'never' ); ?>><?php esc_html_e( 'Manual only', 'robotstxt-documentation-markdown' ); ?></option>
<option value="hourly" <?php selected( $data['sync_frequency'], 'hourly' ); ?>><?php esc_html_e( 'Hourly', 'robotstxt-documentation-markdown' ); ?></option>
<option value="twicedaily" <?php selected( $data['sync_frequency'], 'twicedaily' ); ?>><?php esc_html_e( 'Twice Daily', 'robotstxt-documentation-markdown' ); ?></option>
<option value="daily" <?php selected( $data['sync_frequency'], 'daily' ); ?>><?php esc_html_e( 'Daily', 'robotstxt-documentation-markdown' ); ?></option>
<option value="weekly" <?php selected( $data['sync_frequency'], 'weekly' ); ?>><?php esc_html_e( 'Weekly', 'robotstxt-documentation-markdown' ); ?></option>
</select>
</label>
</td>
</tr>
</table>
<?php submit_button( $mapping_id ? __( 'Update Mapping', 'robotstxt-documentation-markdown' ) : __( 'Create Mapping', 'robotstxt-documentation-markdown' ) ); ?>
</form>
</div>
<script>
// Simple toggle for target type options
(function() {
var newOptions = document.getElementById('new-post-options');
var existingOptions = document.getElementById('existing-post-options');
var radios = document.querySelectorAll('input[name="target_type"]');
function toggleOptions() {
var selectedType = document.querySelector('input[name="target_type"]:checked').value;
if (selectedType === 'new') {
newOptions.style.display = 'block';
existingOptions.style.display = 'none';
} else {
newOptions.style.display = 'none';
existingOptions.style.display = 'block';
}
}
radios.forEach(function(radio) {
radio.addEventListener('change', toggleOptions);
});
toggleOptions();
})();
</script>
<?php
}
/**
* Handle mapping form submission
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_handle_save_mapping() {
if ( ! current_user_can( 'edit_pages' ) ) {
return;
}
// Verify nonce.
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'robotstxt_docmd_nonce' ) ) ), 'robotstxt_docmd_save_mapping' ) ) {
wp_die( esc_html__( 'Security check failed.', 'robotstxt-documentation-markdown' ) );
}
$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' => $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' ),
'sync_enabled' => ! empty( $_POST['sync_enabled'] ),
'sync_frequency' => sanitize_key( wp_unslash( robotstxt_docmd_input_string( $_POST, 'sync_frequency', 'daily' ) ) ),
);
$target_type = sanitize_key( wp_unslash( robotstxt_docmd_input_string( $_POST, 'target_type', 'new' ) ) );
if ( 'existing' === $target_type ) {
$data['target_post_id'] = robotstxt_docmd_input_int( $_POST, 'target_post_id' );
}
if ( $mapping_id ) {
$result = robotstxt_docmd_update_mapping( $mapping_id, $data );
$message = 'mapping_updated';
} else {
$result = robotstxt_docmd_create_mapping( $data );
$message = 'mapping_created';
}
if ( is_wp_error( $result ) ) {
wp_safe_redirect( add_query_arg( 'error', rawurlencode( $result->get_error_message() ), admin_url( 'admin.php?page=robotstxt-docmd-add-mapping' ) ) );
} else {
wp_safe_redirect( add_query_arg( 'message', $message, admin_url( 'admin.php?page=robotstxt-docmd-mappings' ) ) );
}
exit;
}
/**
* Render admin notices
*
* Shows success/error messages and debug results.
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_render_admin_notices() {
// Check for regular message parameter.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$message = sanitize_key( wp_unslash( robotstxt_docmd_input_string( $_GET, 'message' ) ) );
$success_messages = array(
'settings_saved' => __( 'Settings saved successfully.', 'robotstxt-documentation-markdown' ),
'mapping_created' => __( 'Mapping created successfully.', 'robotstxt-documentation-markdown' ),
'mapping_updated' => __( 'Mapping updated successfully.', 'robotstxt-documentation-markdown' ),
'mapping_deleted' => __( 'Mapping deleted.', 'robotstxt-documentation-markdown' ),
'sync_complete' => __( 'Sync completed successfully.', 'robotstxt-documentation-markdown' ),
);
if ( isset( $success_messages[ $message ] ) ) {
echo '<div class="notice notice-success is-dismissible"><p>';
echo esc_html( $success_messages[ $message ] );
echo '</p></div>';
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$error_text = sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_GET, 'error' ) ) );
if ( '' !== $error_text ) {
echo '<div class="notice notice-error is-dismissible"><p>';
echo esc_html( $error_text );
echo '</p></div>';
}
// Check for debug results.
$debug_result = get_transient( 'robotstxt_docmd_debug_result' );
if ( is_array( $debug_result )
&& isset( $debug_result['type'], $debug_result['message'] )
&& is_string( $debug_result['type'] )
&& is_string( $debug_result['message'] )
) {
delete_transient( 'robotstxt_docmd_debug_result' );
$notice_class = 'notice-' . $debug_result['type'];
echo '<div class="notice ' . esc_attr( $notice_class ) . ' is-dismissible">';
echo '<p><strong>' . wp_kses_post( $debug_result['message'] ) . '</strong></p>';
if ( ! empty( $debug_result['details'] ) && is_array( $debug_result['details'] ) ) {
echo '<ul style="list-style: disc; margin-left: 20px;">';
foreach ( $debug_result['details'] as $detail ) {
if ( is_string( $detail ) ) {
echo '<li>' . wp_kses_post( $detail ) . '</li>';
}
}
echo '</ul>';
}
echo '</div>';
}
}
/**
* Render Settings page
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_render_settings_page() {
if ( ! current_user_can( 'edit_pages' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions.', 'robotstxt-documentation-markdown' ) );
}
$raw_settings = get_option( 'robotstxt_docmd_settings', array() );
$settings = is_array( $raw_settings ) ? $raw_settings : array();
?>
<div class="wrap robotstxt-docmd-settings">
<h1><?php esc_html_e( 'Documentation Settings', 'robotstxt-documentation-markdown' ); ?></h1>
<?php robotstxt_docmd_render_admin_notices(); ?>
<?php if ( ! robotstxt_docmd_is_manager_active() ) : ?>
<?php robotstxt_docmd_render_manager_notice( false ); ?>
<?php endif; ?>
<form method="post" action="">
<?php wp_nonce_field( 'robotstxt_docmd_save_settings', 'robotstxt_docmd_nonce' ); ?>
<h2><?php esc_html_e( 'GitHub Repository', 'robotstxt-documentation-markdown' ); ?></h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="github_url"><?php esc_html_e( 'Repository URL', 'robotstxt-documentation-markdown' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="url" id="github_url" name="github_url" value="<?php echo esc_attr( robotstxt_docmd_input_string( $settings, 'github_url' ) ); ?>" class="regular-text" required>
<p class="description"><?php esc_html_e( 'Full GitHub repository URL (e.g., https://github.com/owner/repo)', 'robotstxt-documentation-markdown' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="github_token"><?php esc_html_e( 'Access Token', 'robotstxt-documentation-markdown' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="password" id="github_token" name="github_token" value="" class="regular-text" placeholder="<?php esc_attr_e( 'Enter new token to update', 'robotstxt-documentation-markdown' ); ?>">
<p class="description"><?php esc_html_e( 'GitHub Personal Access Token with "repo" scope', 'robotstxt-documentation-markdown' ); ?></p>
<?php if ( ! empty( robotstxt_docmd_input_string( $settings, 'github_token' ) ) ) : ?>
<p class="description"><strong><?php esc_html_e( 'Token is currently set. Leave blank to keep existing token.', 'robotstxt-documentation-markdown' ); ?></strong></p>
<?php endif; ?>
</td>
</tr>
</table>
<h2><?php esc_html_e( 'Plugin Settings', 'robotstxt-documentation-markdown' ); ?></h2>
<table class="form-table">
<tr>
<th scope="row">
<?php esc_html_e( 'Uninstall Options', 'robotstxt-documentation-markdown' ); ?>
</th>
<td>
<label>
<input type="checkbox" name="delete_on_uninstall" value="1" <?php checked( ! empty( $settings['delete_on_uninstall'] ) ); ?>>
<?php esc_html_e( 'Delete all plugin data when uninstalling', 'robotstxt-documentation-markdown' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'When enabled, all mappings, settings, and plugin data will be permanently deleted when you uninstall the plugin. Created WordPress pages/posts will NOT be deleted.', 'robotstxt-documentation-markdown' ); ?>
<br>
<strong><?php esc_html_e( 'Default: Disabled (data is preserved)', 'robotstxt-documentation-markdown' ); ?></strong>
</p>
</td>
</tr>
</table>
<?php submit_button( __( 'Save Settings', 'robotstxt-documentation-markdown' ) ); ?>
</form>
<?php if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) : ?>
<hr>
<h2><?php esc_html_e( 'Debug Tools', 'robotstxt-documentation-markdown' ); ?></h2>
<p class="description">
<?php esc_html_e( 'This section is only visible when WP_DEBUG is enabled. Use these tools to test and troubleshoot the plugin.', 'robotstxt-documentation-markdown' ); ?>
</p>
<!-- GitHub Connection Tests -->
<div style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin: 20px 0;">
<h3><?php esc_html_e( 'GitHub Connection Tests', 'robotstxt-documentation-markdown' ); ?></h3>
<p>
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=robotstxt-docmd-settings&debug_action=test_github' ), 'robotstxt_docmd_debug_test_github' ) ); ?>" class="button">
<?php esc_html_e( 'Test GitHub Repository Connection', 'robotstxt-documentation-markdown' ); ?>
</a>
<span class="description">
<?php esc_html_e( 'Tests if the plugin can access the configured repository.', 'robotstxt-documentation-markdown' ); ?>
</span>
</p>
<p>
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=robotstxt-docmd-settings&debug_action=test_token' ), 'robotstxt_docmd_debug_test_token' ) ); ?>" class="button">
<?php esc_html_e( 'Test GitHub Token', 'robotstxt-documentation-markdown' ); ?>
</a>
<span class="description">
<?php esc_html_e( 'Validates that the GitHub token is working and shows rate limit info.', 'robotstxt-documentation-markdown' ); ?>
</span>
</p>
</div>
<!-- Scheduled Cron Jobs -->
<div style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin: 20px 0;">
<h3><?php esc_html_e( 'Scheduled Cron Jobs', 'robotstxt-documentation-markdown' ); ?></h3>
<?php
$mappings = robotstxt_docmd_get_all_mappings();
if ( empty( $mappings ) ) {
echo '<p>' . esc_html__( 'No mappings configured yet.', 'robotstxt-documentation-markdown' ) . '</p>';
} else {
echo '<table class="widefat">';
echo '<thead><tr>';
echo '<th>' . esc_html__( 'Mapping', 'robotstxt-documentation-markdown' ) . '</th>';
echo '<th>' . esc_html__( 'Sync Enabled', 'robotstxt-documentation-markdown' ) . '</th>';
echo '<th>' . esc_html__( 'Frequency', 'robotstxt-documentation-markdown' ) . '</th>';
echo '<th>' . esc_html__( 'Next Run', 'robotstxt-documentation-markdown' ) . '</th>';
echo '<th>' . esc_html__( 'Last Sync', 'robotstxt-documentation-markdown' ) . '</th>';
echo '<th>' . esc_html__( 'Actions', 'robotstxt-documentation-markdown' ) . '</th>';
echo '</tr></thead><tbody>';
foreach ( $mappings as $mapping ) {
$hook = 'robotstxt_docmd_sync_event';
$args = array( $mapping['id'] );
$timestamp = wp_next_scheduled( $hook, $args );
echo '<tr>';
echo '<td><strong>' . esc_html( $mapping['title'] ) . '</strong><br>';
echo '<small>' . esc_html( $mapping['repo_owner'] . '/' . $mapping['repo_name'] . '/' . $mapping['file_path'] ) . '</small></td>';
echo '<td>' . ( $mapping['sync_enabled'] ? '✓ ' . esc_html__( 'Yes', 'robotstxt-documentation-markdown' ) : '✗ ' . esc_html__( 'No', 'robotstxt-documentation-markdown' ) ) . '</td>';
echo '<td>' . esc_html( $mapping['sync_frequency'] ) . '</td>';
echo '<td>';
if ( $timestamp ) {
$gmt_offset_raw = get_option( 'gmt_offset', 0 );
$gmt_offset_sec = is_numeric( $gmt_offset_raw ) ? (int) ( (float) $gmt_offset_raw * HOUR_IN_SECONDS ) : 0;
echo esc_html( gmdate( 'Y-m-d H:i:s', $timestamp + $gmt_offset_sec ) );
} else {
echo '<em>' . esc_html__( 'Not scheduled', 'robotstxt-documentation-markdown' ) . '</em>';
}
echo '</td>';
echo '<td>' . ( $mapping['last_sync'] ? esc_html( $mapping['last_sync'] ) : '<em>' . esc_html__( 'Never', 'robotstxt-documentation-markdown' ) . '</em>' ) . '</td>';
echo '<td>';
echo '<a href="' . esc_url( wp_nonce_url( admin_url( 'admin.php?page=robotstxt-docmd-settings&debug_action=run_cron&mapping_id=' . $mapping['id'] ), 'robotstxt_docmd_debug_run_cron_' . $mapping['id'] ) ) . '" class="button button-small">';
echo esc_html__( 'Run Now', 'robotstxt-documentation-markdown' );
echo '</a>';
echo '</td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '<p style="margin-top: 15px;">';
echo '<a href="' . esc_url( wp_nonce_url( admin_url( 'admin.php?page=robotstxt-docmd-settings&debug_action=fix_crons' ), 'robotstxt_docmd_debug_fix_crons' ) ) . '" class="button button-secondary">';
echo esc_html__( 'Fix All Cron Jobs', 'robotstxt-documentation-markdown' );
echo '</a> ';
echo '<span class="description">';
echo esc_html__( 'Unschedule and reschedule all cron jobs. Use this if crons show "Not scheduled" incorrectly.', 'robotstxt-documentation-markdown' );
echo '</span>';
echo '</p>';
}
?>
</div>
<!-- Cache Management -->
<div style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin: 20px 0;">
<h3><?php esc_html_e( 'Cache Management', 'robotstxt-documentation-markdown' ); ?></h3>
<p>
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=robotstxt-docmd-settings&debug_action=clear_cache' ), 'robotstxt_docmd_debug_clear_cache' ) ); ?>" class="button">
<?php esc_html_e( 'Clear All Plugin Caches', 'robotstxt-documentation-markdown' ); ?>
</a>
<span class="description">
<?php esc_html_e( 'Clears all transients used by the plugin.', 'robotstxt-documentation-markdown' ); ?>
</span>
</p>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Handle settings form submission
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_handle_save_settings() {
if ( ! current_user_can( 'edit_pages' ) ) {
return;
}
// Verify nonce.
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'robotstxt_docmd_nonce' ) ) ), 'robotstxt_docmd_save_settings' ) ) {
wp_die( esc_html__( 'Security check failed.', 'robotstxt-documentation-markdown' ) );
}
$github_url = sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'github_url' ) ) );
$github_token = sanitize_text_field( wp_unslash( robotstxt_docmd_input_string( $_POST, 'github_token' ) ) );
$raw_settings = get_option( 'robotstxt_docmd_settings', array() );
$settings = is_array( $raw_settings ) ? $raw_settings : array();
$settings['github_url'] = $github_url;
// Only update token if a new one was provided.
if ( ! empty( $github_token ) ) {
$settings['github_token'] = robotstxt_docmd_encrypt_token( $github_token );
}
// Save uninstall option.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$settings['delete_on_uninstall'] = ! empty( $_POST['delete_on_uninstall'] ) ? 1 : 0;
update_option( 'robotstxt_docmd_settings', $settings, false );
wp_safe_redirect( add_query_arg( 'message', 'settings_saved', admin_url( 'admin.php?page=robotstxt-docmd-settings' ) ) );
exit;
}
/**
* Debug: Test GitHub repository connection
*
* Tests if the configured repository can be accessed.
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_debug_test_github() {
if ( ! current_user_can( 'edit_pages' ) ) {
wp_die( esc_html__( 'Permission denied.', 'robotstxt-documentation-markdown' ) );
}
$raw_settings = get_option( 'robotstxt_docmd_settings', array() );
$settings = is_array( $raw_settings ) ? $raw_settings : array();
$repo_url = robotstxt_docmd_input_string( $settings, 'github_url' );
$token = robotstxt_docmd_input_string( $settings, 'github_token' );
$result = array(
'type' => 'error',
'message' => '',
'details' => array(),
);
if ( empty( $repo_url ) ) {
$result['message'] = __( 'No GitHub repository URL configured. Please configure it in Settings first.', 'robotstxt-documentation-markdown' );
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
if ( empty( $token ) ) {
$result['message'] = __( 'No GitHub token configured. Please configure it in Settings first.', 'robotstxt-documentation-markdown' );
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
// Decrypt token.
$token = robotstxt_docmd_decrypt_token( $token );
// Parse repository URL.
if ( ! preg_match( '#github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$#', $repo_url, $matches ) ) {
$result['message'] = __( 'Invalid GitHub repository URL format.', 'robotstxt-documentation-markdown' );
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
$owner = $matches[1];
$repo = $matches[2];
// Test repository access.
$api_url = sprintf( 'https://api.github.com/repos/%s/%s', $owner, $repo );
$response = wp_remote_get(
$api_url,
array(
'headers' => array(
'Authorization' => 'token ' . $token,
'Accept' => 'application/vnd.github.v3+json',
),
'timeout' => 15,
)
);
$status_code = wp_remote_retrieve_response_code( $response );
if ( is_wp_error( $response ) ) {
$result['message'] = sprintf(
/* translators: %s: error message */
__( 'Connection Failed: %s', 'robotstxt-documentation-markdown' ),
$response->get_error_message()
);
} elseif ( 200 === $status_code ) {
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$full_name = is_array( $body ) && isset( $body['full_name'] ) && is_string( $body['full_name'] ) ? $body['full_name'] : 'N/A';
$repo_name = is_array( $body ) && isset( $body['name'] ) && is_string( $body['name'] ) ? $body['name'] : 'N/A';
$is_private = is_array( $body ) && ! empty( $body['private'] );
$default_branch = is_array( $body ) && isset( $body['default_branch'] ) && is_string( $body['default_branch'] ) ? $body['default_branch'] : 'N/A';
$result['type'] = 'success';
$result['message'] = sprintf(
/* translators: %s: repository full name */
__( 'Successfully connected to repository: %s', 'robotstxt-documentation-markdown' ),
$full_name
);
$result['details'][] = sprintf(
'<strong>%s:</strong> %s',
__( 'Name', 'robotstxt-documentation-markdown' ),
$repo_name
);
$result['details'][] = sprintf(
'<strong>%s:</strong> %s',
__( 'Private', 'robotstxt-documentation-markdown' ),
$is_private ? __( 'Yes', 'robotstxt-documentation-markdown' ) : __( 'No', 'robotstxt-documentation-markdown' )
);
$result['details'][] = sprintf(
'<strong>%s:</strong> %s',
__( 'Default Branch', 'robotstxt-documentation-markdown' ),
$default_branch
);
} elseif ( 404 === $status_code ) {
$result['message'] = __( 'Repository Not Found: The repository does not exist or your token does not have access to it.', 'robotstxt-documentation-markdown' );
} elseif ( 401 === $status_code ) {
$result['message'] = __( 'Authentication Failed: Your GitHub token is invalid or expired.', 'robotstxt-documentation-markdown' );
} else {
$result['message'] = sprintf(
/* translators: %d: HTTP status code */
__( 'Unexpected Error - HTTP Status Code: %d', 'robotstxt-documentation-markdown' ),
$status_code
);
}
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
/**
* Debug: Test GitHub token
*
* Validates the GitHub token and shows rate limit information.
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_debug_test_token() {
if ( ! current_user_can( 'edit_pages' ) ) {
wp_die( esc_html__( 'Permission denied.', 'robotstxt-documentation-markdown' ) );
}
$raw_settings = get_option( 'robotstxt_docmd_settings', array() );
$settings = is_array( $raw_settings ) ? $raw_settings : array();
$token = robotstxt_docmd_input_string( $settings, 'github_token' );
$result = array(
'type' => 'error',
'message' => '',
'details' => array(),
);
if ( empty( $token ) ) {
$result['message'] = __( 'No GitHub token configured. Please configure it in Settings first.', 'robotstxt-documentation-markdown' );
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
// Decrypt token.
$token = robotstxt_docmd_decrypt_token( $token );
// Test token by checking rate limit.
$api_url = 'https://api.github.com/rate_limit';
$response = wp_remote_get(
$api_url,
array(
'headers' => array(
'Authorization' => 'token ' . $token,
'Accept' => 'application/vnd.github.v3+json',
),
'timeout' => 15,
)
);
$status_code = wp_remote_retrieve_response_code( $response );
if ( is_wp_error( $response ) ) {
$result['message'] = sprintf(
/* translators: %s: error message */
__( 'Connection Failed: %s', 'robotstxt-documentation-markdown' ),
$response->get_error_message()
);
} elseif ( 200 === $status_code ) {
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$result['type'] = 'success';
$result['message'] = __( 'Token Valid! Your GitHub token is working correctly.', 'robotstxt-documentation-markdown' );
$resources = is_array( $body ) && isset( $body['resources'] ) && is_array( $body['resources'] ) ? $body['resources'] : null;
$core = is_array( $resources ) && isset( $resources['core'] ) && is_array( $resources['core'] ) ? $resources['core'] : null;
if ( null !== $core ) {
$remaining = isset( $core['remaining'] ) && is_int( $core['remaining'] ) ? $core['remaining'] : 0;
$limit = isset( $core['limit'] ) && is_int( $core['limit'] ) ? $core['limit'] : 0;
$reset = isset( $core['reset'] ) && is_int( $core['reset'] ) ? $core['reset'] : 0;
$result['details'][] = sprintf(
'<strong>%s:</strong> %d / %d',
__( 'Requests Remaining', 'robotstxt-documentation-markdown' ),
$remaining,
$limit
);
if ( $reset > 0 ) {
$gmt_offset_raw = get_option( 'gmt_offset', 0 );
$gmt_offset_sec = is_numeric( $gmt_offset_raw ) ? (int) ( (float) $gmt_offset_raw * HOUR_IN_SECONDS ) : 0;
$reset_time = gmdate( 'Y-m-d H:i:s', $reset + $gmt_offset_sec );
$result['details'][] = sprintf(
'<strong>%s:</strong> %s',
__( 'Resets At', 'robotstxt-documentation-markdown' ),
$reset_time
);
}
if ( $remaining < 100 ) {
$result['type'] = 'warning';
$result['message'] = __( 'Token Valid but Low Rate Limit! You are running low on GitHub API requests. Consider waiting for the rate limit to reset.', 'robotstxt-documentation-markdown' );
}
}
} elseif ( 401 === $status_code ) {
$result['message'] = __( 'Authentication Failed: Your GitHub token is invalid or expired.', 'robotstxt-documentation-markdown' );
} else {
$result['message'] = sprintf(
/* translators: %d: HTTP status code */
__( 'Unexpected Error - HTTP Status Code: %d', 'robotstxt-documentation-markdown' ),
$status_code
);
}
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
/**
* Debug: Run cron manually
*
* Executes synchronization for a specific mapping immediately.
*
* @since 1.0.0
*
* @param int $mapping_id Mapping ID to sync.
* @return void
*/
function robotstxt_docmd_debug_run_cron( int $mapping_id ) {
if ( ! current_user_can( 'edit_pages' ) ) {
wp_die( esc_html__( 'Permission denied.', 'robotstxt-documentation-markdown' ) );
}
$mapping = robotstxt_docmd_get_mapping( $mapping_id );
$result = array(
'type' => 'error',
'message' => '',
'details' => array(),
);
if ( ! $mapping ) {
$result['message'] = __( 'Mapping not found.', 'robotstxt-documentation-markdown' );
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
// Run the sync.
$sync_result = robotstxt_docmd_sync_mapping( $mapping_id );
if ( is_wp_error( $sync_result ) ) {
$result['message'] = sprintf(
/* translators: 1: mapping title, 2: error message */
__( 'Sync Failed for "%1$s": %2$s', 'robotstxt-documentation-markdown' ),
$mapping['title'],
$sync_result->get_error_message()
);
} else {
// Reload mapping to get updated info.
$mapping = robotstxt_docmd_get_mapping( $mapping_id );
if ( ! $mapping ) {
$result['message'] = __( 'Sync completed but mapping data could not be reloaded.', 'robotstxt-documentation-markdown' );
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
$result['type'] = 'success';
$result['message'] = sprintf(
/* translators: %s: mapping title */
__( 'Sync Successful for "%s"', 'robotstxt-documentation-markdown' ),
$mapping['title']
);
$result['details'][] = sprintf(
'<strong>%s:</strong> %s',
__( 'Repository', 'robotstxt-documentation-markdown' ),
$mapping['repo_owner'] . '/' . $mapping['repo_name'] . '/' . $mapping['file_path']
);
$result['details'][] = sprintf(
'<strong>%s:</strong> %s',
__( 'Last Sync', 'robotstxt-documentation-markdown' ),
$mapping['last_sync']
);
if ( ! empty( $mapping['target_post_id'] ) ) {
$edit_url = get_edit_post_link( $mapping['target_post_id'] ) ?? '';
$result['details'][] = sprintf(
'<strong>%s:</strong> <a href="%s">%s</a>',
__( 'Target Post', 'robotstxt-documentation-markdown' ),
esc_url( $edit_url ),
__( 'Edit Post', 'robotstxt-documentation-markdown' )
);
}
}
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
/**
* Debug: Clear all plugin caches
*
* Deletes all transients used by the plugin.
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_debug_clear_cache() {
if ( ! current_user_can( 'edit_pages' ) ) {
wp_die( esc_html__( 'Permission denied.', 'robotstxt-documentation-markdown' ) );
}
global $wpdb;
$result = array(
'type' => 'error',
'message' => '',
'details' => array(),
);
// Delete all plugin transients.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query acceptable for cache clearing operation.
$deleted = $wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE %s
OR option_name LIKE %s",
$wpdb->esc_like( '_transient_robotstxt_docmd_' ) . '%',
$wpdb->esc_like( '_transient_timeout_robotstxt_docmd_' ) . '%'
)
);
if ( false !== $deleted ) {
$result['type'] = 'success';
$result['message'] = sprintf(
/* translators: %d: number of cache entries deleted */
__( 'Cache Cleared! Deleted %d cache entries.', 'robotstxt-documentation-markdown' ),
$deleted
);
} else {
$result['message'] = __( 'Failed to clear cache. Please check database permissions.', 'robotstxt-documentation-markdown' );
}
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
/**
* Debug: Fix cron jobs
*
* Reschedules all cron jobs for mappings that have sync enabled.
*
* @since 1.0.0
*
* @return void
*/
function robotstxt_docmd_debug_fix_crons() {
if ( ! current_user_can( 'edit_pages' ) ) {
wp_die( esc_html__( 'Permission denied.', 'robotstxt-documentation-markdown' ) );
}
$result = array(
'type' => 'success',
'message' => '',
'details' => array(),
);
$mappings = robotstxt_docmd_get_all_mappings();
$fixed = 0;
$skipped = 0;
if ( empty( $mappings ) ) {
$result['type'] = 'warning';
$result['message'] = __( 'No mappings found to fix.', 'robotstxt-documentation-markdown' );
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
foreach ( $mappings as $mapping ) {
// Unschedule existing cron.
robotstxt_docmd_unschedule_mapping_sync( $mapping['id'] );
// Reschedule if sync is enabled.
if ( $mapping['sync_enabled'] && 'never' !== $mapping['sync_frequency'] ) {
robotstxt_docmd_schedule_mapping_sync( $mapping['id'], $mapping['sync_frequency'] );
++$fixed;
$result['details'][] = sprintf(
'✓ %s - %s',
$mapping['title'],
$mapping['sync_frequency']
);
} else {
++$skipped;
}
}
$result['message'] = sprintf(
/* translators: 1: number of crons fixed, 2: number of crons skipped */
__( 'Cron Jobs Fixed! Rescheduled %1$d cron jobs, skipped %2$d disabled mappings.', 'robotstxt-documentation-markdown' ),
$fixed,
$skipped
);
set_transient( 'robotstxt_docmd_debug_result', $result, 60 );
wp_safe_redirect( admin_url( 'admin.php?page=robotstxt-docmd-settings' ) );
exit;
}
/**
* Check whether the Manager (by ROBOTSTXT) plugin is installed and active
*
* Updates for this plugin are delivered through the Manager plugin. This
* helper detects whether it is installed and active: it uses the ecosystem
* presence constant (Manager 1.6.2+) and falls back to a plugin-list scan
* for older Manager versions.
*
* @since 1.2.1
* @since 1.2.2 Switched to the ROBOTSTXT_MANAGER_NOTICED presence constant
* with a plugin-list scan fallback for Manager < 1.6.2.
*
* @return bool True if Manager (by ROBOTSTXT) is active, false otherwise.
*/
function robotstxt_docmd_is_manager_active(): bool {
if ( defined( 'ROBOTSTXT_MANAGER_NOTICED' ) && ROBOTSTXT_MANAGER_NOTICED ) {
return true;
}
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
foreach ( get_plugins() as $file => $data ) {
$slug = dirname( $file );
if ( '.' === $slug ) {
$slug = basename( $file, '.php' );
}
if ( 'robotstxt-manager' === $slug ) {
return is_plugin_active( $file );
}
}
return false;
}
/**
* Render the Manager (by ROBOTSTXT) recommendation notice
*
* Shared markup for the plugins list page (dismissible) and the plugin
* settings page (not dismissible). Only rendered when Manager is not active.
*
* @since 1.2.1
*
* @param bool $dismissible Whether the notice should be dismissible.
* @return void
*/
function robotstxt_docmd_render_manager_notice( bool $dismissible ): void {
echo '<div class="notice notice-warning' . ( $dismissible ? ' is-dismissible' : '' ) . '"><p>';
echo wp_kses(
sprintf(
/* translators: %s: Manager (by ROBOTSTXT) plugin URL. */
__( 'To receive automatic updates, the <a href="%s">Manager (by ROBOTSTXT)</a> plugin must be installed and active.', 'robotstxt-documentation-markdown' ),
esc_url( 'https://www.robotstxt.software/plugins/robotstxt-manager/' )
),
array(
'a' => array(
'href' => true,
),
)
);
echo '</p></div>';
}
/**
* Show the Manager (by ROBOTSTXT) notice on the plugins list page
*
* @since 1.2.1
*
* @return void
*/
function robotstxt_docmd_admin_notice_manager_plugin(): void {
global $pagenow;
if ( 'plugins.php' !== $pagenow ) {
return;
}
if ( ! current_user_can( 'edit_pages' ) || robotstxt_docmd_is_manager_active() ) {
return;
}
robotstxt_docmd_render_manager_notice( true );
}