set_supported_post_types();
add_filter('post_row_actions', array($this, 'add_export_action'), 10, 2);
add_filter('page_row_actions', array($this, 'add_export_action'), 10, 2);
add_action('admin_init', array($this, 'handle_export_request'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
add_action('admin_menu', array($this, 'add_admin_menu'));
}
private function set_supported_post_types()
{
$post_types = get_post_types(array('public' => true), 'names');
if (isset($post_types['attachment'])) {
unset($post_types['attachment']);
}
$this->supported_post_types = apply_filters('pageexli_supported_post_types', $post_types);
$saved_post_types = get_option('pageexli_post_types', array());
if (! empty($saved_post_types)) {
$this->supported_post_types = array_intersect($this->supported_post_types, $saved_post_types);
}
}
private function is_supported_post_type($post_type)
{
return in_array($post_type, $this->supported_post_types, true);
}
public function add_export_action($actions, $post)
{
if (! current_user_can('administrator') || ! $this->is_supported_post_type($post->post_type)) {
return $actions;
}
$export_url = wp_nonce_url(
admin_url('admin.php?action=pageexli_export_post&post_id=' . absint($post->ID)),
'pageexli_export_post_' . $post->ID
);
/* translators: %s: Post type label (e.g. "page" or "post") */
$title_text = sprintf(
// translators: %s is the post type name (e.g., "page", "post")
__('Export this %s', 'alenjoseph-page-exporter-lite'),
$post->post_type
);
$actions['pageexli_export'] = sprintf(
'%s',
esc_url($export_url),
esc_attr($title_text),
esc_html__('Export', 'alenjoseph-page-exporter-lite')
);
return $actions;
}
public function handle_export_request()
{
if (! isset($_GET['action']) || sanitize_key(wp_unslash($_GET['action'])) !== 'pageexli_export_post') {
return;
}
// Validate post_id existence
if (! isset($_GET['post_id'])) {
wp_die(esc_html__('Invalid post ID.', 'alenjoseph-page-exporter-lite'));
}
// Properly sanitize and validate $_GET['post_id']
$post_id_raw = sanitize_text_field(wp_unslash($_GET['post_id']));
// Validate numeric value before conversion
if (! is_numeric($post_id_raw)) {
wp_die(esc_html__('Invalid post ID.', 'alenjoseph-page-exporter-lite'));
}
$post_id = absint($post_id_raw);
if (! isset($_GET['_wpnonce'])) {
wp_die(esc_html__('Missing security token.', 'alenjoseph-page-exporter-lite'));
}
$nonce = sanitize_text_field(wp_unslash($_GET['_wpnonce']));
if (! wp_verify_nonce($nonce, 'pageexli_export_post_' . $post_id)) {
wp_die(esc_html__('Security check failed.', 'alenjoseph-page-exporter-lite'));
}
if (! current_user_can('administrator')) {
wp_die(esc_html__('You do not have permission to export posts.', 'alenjoseph-page-exporter-lite'));
}
$post = get_post($post_id);
if (! $post || ! $this->is_supported_post_type($post->post_type)) {
wp_die(esc_html__('Invalid or unsupported post type.', 'alenjoseph-page-exporter-lite'));
}
$this->export_post($post);
}
private function export_post($post)
{
$attachments = get_attached_media('', $post->ID);
$post_type_obj = get_post_type_object($post->post_type);
$post_type_label = $post_type_obj ? strtolower($post_type_obj->labels->singular_name) : $post->post_type;
$filename = sanitize_file_name($post_type_label . '-' . ($post->post_name ? $post->post_name : $post->ID)) . '.xml';
// Send headers
if (! headers_sent()) {
header('Content-Type: application/xml; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output is a well-formed export XML
echo $this->generate_xml($post, $attachments);
exit;
}
private function generate_xml($post, $attachments)
{
$sitename = get_bloginfo('name');
$site_url = get_bloginfo('url');
$xml = '' . "\n";
$xml .= '' . "\n\n";
$xml .= '' . "\n\n";
$xml .= "\n";
$xml .= " " . $this->cdata($sitename) . "\n";
$xml .= " " . esc_url($site_url) . "\n";
$xml .= " " . $this->cdata(get_bloginfo('description')) . "\n";
$xml .= " " . gmdate('D, d M Y H:i:s +0000') . "\n";
$xml .= " " . get_bloginfo('language') . "\n";
$xml .= " 1.2\n";
$xml .= " " . esc_url($site_url) . "\n";
$xml .= " " . esc_url($site_url) . "\n\n";
if ($post->post_type === 'post') {
$xml .= $this->get_categories_xml($post);
$xml .= $this->get_tags_xml($post);
}
$xml .= $this->get_custom_taxonomies_xml($post);
$xml .= $this->get_post_xml($post);
foreach ($attachments as $attachment) {
$xml .= $this->get_attachment_xml($attachment);
}
$xml .= "\n";
return $xml;
}
private function get_categories_xml($post)
{
$xml = '';
$categories = get_the_category($post->ID);
if ($categories && ! is_wp_error($categories)) {
foreach ($categories as $category) {
$parent_slug = $category->parent ? get_category($category->parent)->slug : '';
$xml .= " \n";
$xml .= " {$category->term_id}\n";
$xml .= " " . $this->cdata($category->slug) . "\n";
$xml .= " " . $this->cdata($parent_slug) . "\n";
$xml .= " " . $this->cdata($category->name) . "\n";
$xml .= " \n";
}
}
return $xml;
}
private function get_tags_xml($post)
{
$xml = '';
$tags = get_the_tags($post->ID);
if ($tags && ! is_wp_error($tags)) {
foreach ($tags as $tag) {
$xml .= " \n";
$xml .= " {$tag->term_id}\n";
$xml .= " " . $this->cdata($tag->slug) . "\n";
$xml .= " " . $this->cdata($tag->name) . "\n";
$xml .= " \n";
}
}
return $xml;
}
private function get_custom_taxonomies_xml($post)
{
$xml = '';
$taxonomies = get_object_taxonomies($post->post_type, 'objects');
foreach ($taxonomies as $taxonomy) {
if (in_array($taxonomy->name, array('category', 'post_tag'), true)) {
continue;
}
$terms = get_the_terms($post->ID, $taxonomy->name);
if ($terms && ! is_wp_error($terms)) {
foreach ($terms as $term) {
$parent_slug = $term->parent ? get_term($term->parent)->slug : '';
$xml .= " \n";
$xml .= " {$term->term_id}\n";
$xml .= " " . $this->cdata($taxonomy->name) . "\n";
$xml .= " " . $this->cdata($term->slug) . "\n";
$xml .= " " . $this->cdata($parent_slug) . "\n";
$xml .= " " . $this->cdata($term->name) . "\n";
$xml .= " \n";
}
}
}
return $xml;
}
private function get_post_xml($post)
{
$pub_date = gmdate('D, d M Y H:i:s +0000', strtotime($post->post_date));
$post_date = $post->post_date;
$post_date_gmt = $post->post_date_gmt;
$xml = " - \n";
$xml .= " " . $this->cdata($post->post_title) . "\n";
$xml .= " " . esc_url(get_permalink($post->ID)) . "\n";
$xml .= " $pub_date\n";
$xml .= " " . $this->cdata(get_the_author_meta('login', $post->post_author)) . "\n";
$xml .= " " . esc_url(get_permalink($post->ID)) . "\n";
$xml .= " \n";
$xml .= " " . $this->cdata($post->post_content) . "\n";
$xml .= " " . $this->cdata($post->post_excerpt) . "\n";
$xml .= " {$post->ID}\n";
$xml .= " " . $this->cdata($post_date) . "\n";
$xml .= " " . $this->cdata($post_date_gmt) . "\n";
$xml .= " " . $this->cdata($post->comment_status) . "\n";
$xml .= " " . $this->cdata($post->ping_status) . "\n";
$xml .= " " . $this->cdata($post->post_name) . "\n";
$xml .= " " . $this->cdata($post->post_status) . "\n";
$xml .= " {$post->post_parent}\n";
$xml .= " {$post->menu_order}\n";
$xml .= " " . $this->cdata($post->post_type) . "\n";
$xml .= " " . $this->cdata($post->post_password) . "\n";
$xml .= " " . (is_sticky($post->ID) ? '1' : '0') . "\n";
$taxonomies = get_object_taxonomies($post->post_type);
foreach ($taxonomies as $taxonomy) {
if (in_array($taxonomy, array('category', 'post_tag'), true)) {
continue;
}
$terms = get_the_terms($post->ID, $taxonomy);
if ($terms && ! is_wp_error($terms)) {
foreach ($terms as $term) {
$xml .= ' ' . $this->cdata($term->name) . '' . "\n";
}
}
}
$custom_fields = get_post_meta($post->ID);
foreach ($custom_fields as $key => $values) {
$include_field = true;
if (strpos($key, '_') === 0) {
$acf_patterns = array(
'/^_[a-z0-9_]+$/',
'/^_wp_page_template$/',
'/^_thumbnail_id$/',
'/^_edit_lock$/',
'/^_edit_last$/',
'/^_wp_attachment_metadata$/',
'/^_wp_attached_file$/'
);
$include_field = false;
foreach ($acf_patterns as $pattern) {
if (preg_match($pattern, $key)) {
$include_field = true;
break;
}
}
}
if (! $include_field) {
continue;
}
foreach ($values as $value) {
$xml .= " \n";
$xml .= " " . $this->cdata($key) . "\n";
$xml .= " " . $this->cdata($value) . "\n";
$xml .= " \n";
}
}
$xml .= "
\n\n";
return $xml;
}
private function get_attachment_xml($attachment)
{
$pub_date = gmdate('D, d M Y H:i:s +0000', strtotime($attachment->post_date));
$xml = " - \n";
$xml .= " " . $this->cdata($attachment->post_title) . "\n";
$xml .= " " . esc_url(wp_get_attachment_url($attachment->ID)) . "\n";
$xml .= " $pub_date\n";
$xml .= " " . $this->cdata(get_the_author_meta('login', $attachment->post_author)) . "\n";
$xml .= " " . esc_url(wp_get_attachment_url($attachment->ID)) . "\n";
$xml .= " \n";
$xml .= " " . $this->cdata($attachment->post_content) . "\n";
$xml .= " " . $this->cdata($attachment->post_excerpt) . "\n";
$xml .= " {$attachment->ID}\n";
$xml .= " " . $this->cdata($attachment->post_date) . "\n";
$xml .= " " . $this->cdata($attachment->post_date_gmt) . "\n";
$xml .= " " . $this->cdata($attachment->comment_status) . "\n";
$xml .= " " . $this->cdata($attachment->ping_status) . "\n";
$xml .= " " . $this->cdata($attachment->post_name) . "\n";
$xml .= " " . $this->cdata($attachment->post_status) . "\n";
$xml .= " {$attachment->post_parent}\n";
$xml .= " {$attachment->menu_order}\n";
$xml .= " " . $this->cdata($attachment->post_type) . "\n";
$xml .= " " . $this->cdata($attachment->post_password) . "\n";
$xml .= " 0\n";
$attachment_url = wp_get_attachment_url($attachment->ID);
$xml .= " " . $this->cdata($attachment_url) . "\n";
$attachment_meta = wp_get_attachment_metadata($attachment->ID);
if ($attachment_meta) {
$xml .= " \n";
$xml .= " _wp_attachment_metadata\n";
$xml .= " " . $this->cdata(serialize($attachment_meta)) . "\n";
$xml .= " \n";
}
$custom_fields = get_post_meta($attachment->ID);
foreach ($custom_fields as $key => $values) {
if ($key === '_wp_attachment_metadata') {
continue;
}
foreach ($values as $value) {
$xml .= " \n";
$xml .= " " . $this->cdata($key) . "\n";
$xml .= " " . $this->cdata($value) . "\n";
$xml .= " \n";
}
}
$xml .= "
\n\n";
return $xml;
}
public function add_admin_menu()
{
add_options_page(
__('Page Exporter Lite Settings', 'alenjoseph-page-exporter-lite'),
__('Page Exporter Lite', 'alenjoseph-page-exporter-lite'),
'manage_options',
'alenjoseph-page-exporter-lite',
array($this, 'settings_page')
);
}
public function settings_page()
{
if (isset($_POST['submit'])) {
check_admin_referer('pageexli_settings');
$post_types = array();
// Properly sanitize $_POST['pel_post_types']
if (isset($_POST['pageexli_post_types']) && is_array($_POST['pageexli_post_types'])) {
// Sanitize the entire array immediately
$post_types_raw = array_map('sanitize_text_field', wp_unslash($_POST['pageexli_post_types']));
// Verify it's an array
if (is_array($post_types_raw)) {
foreach ($post_types_raw as $pt) {
// Additional validation: ensure it's a valid post type
if (post_type_exists($pt)) {
$post_types[] = $pt;
}
}
}
}
update_option('pageexli_post_types', $post_types);
echo '
' . esc_html__('Settings saved.', 'alenjoseph-page-exporter-lite') . '
';
$this->set_supported_post_types();
}
$all_post_types = get_post_types(array('public' => true), 'objects');
if (isset($all_post_types['attachment'])) {
unset($all_post_types['attachment']);
}
$selected_post_types = get_option('pageexli_post_types', array_keys($all_post_types));
?>
';
}
public function enqueue_scripts($hook)
{
if ('edit.php' !== $hook) {
return;
}
$screen = get_current_screen();
if (! $this->is_supported_post_type($screen->post_type)) {
return;
}
wp_enqueue_style(
'alenjoseph-page-exporter-lite',
PAGEEXLI_PLUGIN_URL . 'admin/admin.css',
array(),
PAGEEXLI_PLUGIN_VERSION
);
wp_enqueue_script(
'alenjoseph-page-exporter-lite',
PAGEEXLI_PLUGIN_URL . 'admin/admin.js',
array('jquery'),
PAGEEXLI_PLUGIN_VERSION,
true
);
wp_localize_script(
'alenjoseph-page-exporter-lite',
'pageexli_ajax',
array(
'nonce' => wp_create_nonce('pageexli_nonce'),
'exporting' => esc_html__('Exporting...', 'alenjoseph-page-exporter-lite'),
'export_complete' => esc_html__('Export complete!', 'alenjoseph-page-exporter-lite'),
'export_error' => esc_html__('Export failed. Please try again.', 'alenjoseph-page-exporter-lite'),
)
);
}
}
new PAGEEXLI_Page_Exporter();
register_activation_hook(__FILE__, 'pageexli_activation');
function pageexli_activation()
{
if (version_compare(get_bloginfo('version'), '6.0', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
wp_die(esc_html__('Page Exporter Lite requires WordPress 6.0 or higher.', 'alenjoseph-page-exporter-lite'));
}
$default = array('post', 'page');
add_option('pageexli_post_types', $default);
}
register_deactivation_hook(__FILE__, 'pageexli_deactivation');
function pageexli_deactivation()
{
delete_option('pageexli_post_types');
}