=== Pure Demo Importer ===
Contributors: themepure
Tags: demo, demo importer, one-click import, starter sites, wxr
Requires at least: 5.6
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.0.12
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Drop-in demo importer/exporter for WordPress themes.

== Description ==

Pure Demo Importer adds a **Demo Importer** admin page to any WordPress theme that registers itself with the plugin. It can:

* Export the current site's content (content.xml WXR), widgets (widgets.wie) and customizer settings (customizer.dat) into a single ZIP.
* Import any of the above on a fresh WordPress install, with auto-install and activation of required plugins from the WordPress.org repo.
* Show a live system-requirements check before import with copy-paste fix instructions.
* Optionally reset the site (delete posts/widgets/customizer mods) before importing.

The plugin itself ships **no demo config**. Themes and core plugins register their demos by hooking the `pure_demo_importer_register` action — see "Registering demos" below.

== Installation ==

1. Upload the `pure-demo-importer` folder to `/wp-content/plugins/`.
2. Activate "Pure Demo Importer" from the Plugins screen.
3. From your theme's `functions.php` (or a core plugin's bootstrap), hook the `pure_demo_importer_register` action and call `ThemePure_Demo_Importer::register()` with your config. See the "Registering demos" section for a full example.

== Registering demos ==

The plugin fires a `pure_demo_importer_register` action on `after_setup_theme:20`. Hook into it and call `ThemePure_Demo_Importer::register()` with your config:

`
add_action( 'pure_demo_importer_register', function () {
    ThemePure_Demo_Importer::register( array(
        'theme_name' => 'My Theme',
        'theme_slug' => 'my-theme',          // drives the filter name
        'menu_slug'  => 'my-theme-demo',
        'menu_title' => 'My Theme Importer',
        'menu_icon'  => 'dashicons-download',
        'demos'      => require __DIR__ . '/demo-config/demo.php',
    ) );
} );
`

== Filter hook: extending the demos array ==

After the initial `::register()` call, the demos array is **filterable per theme** via the filter `{theme_slug}_demo_importer_demos`. The filter name is built from your `theme_slug` — if you registered with `'theme_slug' => 'consora'`, the filter is `consora_demo_importer_demos`. The filter passes two args: `( array $demos, array $config )`.

Use this filter to **add, remove, or modify** demos after the initial register call — without touching the theme's static `demo.php` config.

= When the filter fires =

Inside `ThemePure_Demo_Importer::get_demos()` — every time the admin page (or any AJAX step) reads the demos array. Filter callbacks can be added anywhere that runs **before** the user opens the Demo Importer page (e.g. `functions.php`, plugin bootstrap, `after_setup_theme`).

= Where to add the filter callback =

| Location                                              | When to use                                                              |
|-------------------------------------------------------|--------------------------------------------------------------------------|
| Theme's `demo-config/demo-import.php`                  | Most demos are static — this is just a small extension.                  |
| A separate file (`theme/demo-config/extra-demos.php`)  | Keep dynamic / conditional demos isolated from the static config.        |
| A site-specific plugin (`mu-plugins/site-demos.php`)   | The extra demos are project-specific and shouldn't ship with the theme.  |

= Example 1: Append an extra demo (most common) =

From any plugin or `functions.php`, after the theme's register call:

`
add_filter( 'consora_demo_importer_demos', function ( $demos, $config ) {
    $demos[] = array(
        'slug'               => 'home-bonus',
        'name'               => 'Bonus Homepage',
        'page_preview_image' => 'https://wp.themepure.net/consora/sample-data/preview-image/thumb-bonus.jpg',
        'preview_live_url'   => 'https://wp.themepure.net/consora/home-bonus',
        'required_plugins'   => array(
            array( 'slug' => 'elementor', 'name' => 'Elementor' ),
        ),
        'content_xml_url'    => 'https://wp.themepure.net/consora/sample-data/sample-data/bonus.xml',
        'home_page'          => 'Home Bonus',
        'source_url'         => 'https://wp.themepure.net/consora/',
    );
    return $demos;
}, 10, 2 );
`

= Example 2: Remove a demo conditionally =

Hide demos that depend on a plugin which isn't active (e.g. WooCommerce-only demos):

`
add_filter( 'consora_demo_importer_demos', function ( $demos, $config ) {
    // Hide the WooCommerce demo when Woo isn't active.
    if ( ! class_exists( 'WooCommerce' ) ) {
        $demos = array_values( array_filter( $demos, function ( $d ) {
            return $d['slug'] !== 'home-05';
        } ) );
    }
    return $demos;
}, 10, 2 );
`

= Example 3: Mutate every demo at once =

Switch the CDN host across every demo, or force-add a plugin to every demo's required-plugins list:

`
add_filter( 'consora_demo_importer_demos', function ( $demos, $config ) {
    foreach ( $demos as &$demo ) {
        // Force-add a plugin to every demo.
        $demo['required_plugins'][] = array(
            'slug' => 'wordpress-seo',
            'name' => 'Yoast SEO',
        );
        // Swap the CDN host.
        if ( isset( $demo['content_xml_url'] ) ) {
            $demo['content_xml_url'] = str_replace(
                'wp.themepure.net',
                'cdn.themepure.net',
                $demo['content_xml_url']
            );
        }
    }
    unset( $demo );
    return $demos;
}, 10, 2 );
`

= Replacing the demos array entirely =

You can also call `::register()` with `'demos' => array()` and populate the entire list via the filter — useful if your demos are generated dynamically (e.g. fetched from a remote API at boot).

== Other available hooks ==

= Lifecycle actions =

* `themepure_di_before_import` — fires before reset / import begins. Args: `( $demo )`.
* `themepure_di_after_import` — fires once all imports complete. Args: `( $demo )`. Use for WooCommerce page IDs, custom kits, theme options.
* `themepure_di_before_step` / `themepure_di_after_step` — per-step hooks. Args: `( $step, $demo, $arg )`.

= Reset =

* `themepure_di_reset_preserve_plugins` — filter the list of plugin basenames to keep active across a reset. Default: `[ 'pure-demo-importer/pure-demo-importer.php' ]`. Append any plugin that registers demos so the demos remain visible after the post-reset page reload.

= Downloader =

* `themepure_di_download_timeout` — remote download timeout in seconds (default 300).
* `themepure_di_http_bypass_args` — tweak browser-style headers used when a host 403s the default WP user-agent.

= WXR import =

* `themepure_di_wp_import_fetch_attachments` — skip media sideload during WXR import (default true).
* `themepure_di_wp_import_allow_create_users` — allow WP_Import to create users from XML authors (default false).

= Logger =

* `themepure_di_logging_enabled` — disable site-wide import logging:

`
add_filter( 'themepure_di_logging_enabled', '__return_false' );
`

Logs are written to a private `wp-content/uploads/themepure-di-logs-{random}/import.log` and rotated at ~2 MB. The random suffix is generated once per site so the log cannot be fetched by guessing its URL.

== Frequently Asked Questions ==

= The plugin says "No demos configured" — what now? =

Either your theme/core plugin hasn't called `ThemePure_Demo_Importer::register()`, or it called it with an empty `demos` array. See the "Registering demos" section above for the correct hook + call pattern. If you want to populate demos via filter only (no `register()` array), use the `{theme_slug}_demo_importer_demos` filter described above.

= Will reset delete my Pure Demo Importer plugin? =

No. The reset always preserves Pure Demo Importer itself — deactivating the plugin running the reset would terminate the request mid-flight. Use the `themepure_di_reset_preserve_plugins` filter to preserve additional plugins.

= Does this work without the WordPress Importer plugin installed? =

Yes. Pure Demo Importer vendors the full WordPress Importer plugin internally at `vendor/wordpress-importer/`. There is no runtime dependency on the upstream plugin being installed.

= Where is the import log? =

`wp-content/uploads/themepure-di-logs-{random}/import.log` — rotated at ~2 MB. Disable with `add_filter( 'themepure_di_logging_enabled', '__return_false' );`.

== External services ==

Pure Demo Importer ships with zero demos configured out of the box (`demos => array()`) and makes no external connections on its own. It is a drop-in engine: a WordPress theme or plugin integrates with it by registering one or more "demo" entries (each with its own name and URLs) via the `pure_demo_importer_register` action. Once a theme has registered real demos, the following outbound requests can happen — always as the direct result of the site administrator clicking a button in the Pure Demo Importer admin screen, never automatically or in the background:

* **Demo content download.** When the admin clicks "Import" for a specific demo, the plugin downloads that demo's content files (WXR XML, widgets `.wie`/JSON, Customizer `.dat`, an optional Elementor kit `.zip`) from the URL(s) the integrating theme configured for that demo. This is a plain HTTP GET of a static file; no site data, personal data, or tracking parameters are sent — only a standard request for the file at that URL. The host is whatever demo server the theme author operates or points to; this plugin does not operate its own demo-content service and cannot provide a single Terms of Service / Privacy Policy link, since the host is defined entirely by the integrating theme, not by this plugin.
* **Plugin installation.** When the admin clicks "Install" next to a recommended plugin, the plugin is resolved and downloaded from the official WordPress.org Plugin Directory using WordPress core's own `plugins_api()` and `Plugin_Upgrader` — the same mechanism used by Add New Plugin in wp-admin. See https://wordpress.org/about/privacy/ for WordPress.org's own privacy policy. No plugin code is ever installed from any other source.
* **WooCommerce variation-swatch image (optional).** If the integrating theme's demo config includes a `variation_settings_url`, the plugin fetches that single URL after import to configure WooCommerce product-variation swatches. Same nature as the demo content download above: a static file fetch to a URL the theme author configured, triggered only by the admin's own import action.

No analytics, telemetry, or usage tracking of any kind is performed by this plugin.

== Changelog ==

= 1.0.12 =
* Security: removed the SQL-pack import/export subsystem. It executed statements from a downloaded `.sql` dump verbatim, which is an arbitrary-SQL path and has no place in a WordPress.org release. Normal WXR / widgets / customizer importing is unchanged.
* Security: removed the bundled regex-based SVG "sanitizer" and stopped allowlisting `svg` / `svgz` or forcing a MIME type from the filename. SVG in demo content is now refused during import unless a proven sanitizer library (enshrined/svg-sanitize, as bundled by Safe SVG) is installed.
* Security: all XML now loads through a single hardened path — `LIBXML_NONET`, no entity substitution, DOCTYPE refused, document size capped, libxml error state always restored. The deprecated `libxml` entity-loader toggle is gone.
* Security: `WP_Import::import()` can no longer read arbitrary fields from the original request. `$_POST` is replaced with a minimal allowlisted array for the duration of the call and restored in a `finally` block, along with filters, deferred counting, cache invalidation and output buffers.
* Security: every outbound download now goes through one safe downloader — http/https only, loopback / private / link-local / unique-local blocked for IPv4 and IPv6, every redirect hop revalidated, redirect and response-size caps, partial files cleaned up on failure.
* Security: the selective-reset payload is now limited by byte size and nesting depth before decoding.
* Security: private upload directories are now named with a per-site random suffix and protected with Apache 2.4-compatible rules, a `web.config` and an `index.php` stub, instead of Apache-2.2-only `.htaccess` syntax that could 500 a directory on Apache 2.4.
* Removed all `set_time_limit()` and `memory_limit` `ini_set()` calls. Imports rely on bounded AJAX chunks; memory headroom is requested once via WordPress's own `wp_raise_memory_limit( 'admin' )`.
* Fixed a PHP 8.4 implicit-nullable parameter deprecation.
* Removed the `tools/build-sql-pack.php` developer script from the distributed package.

= 1.0.11 =
* Recommended-plugin installs are now resolved exclusively from the WordPress.org Plugin Directory — no other source (remote or local) is ever used.

= 1.0.0 =
* Initial release.
* AJAX queue-driven import with progress UI.
* Single-shot and chunked WXR import paths.
* Vendored WordPress Importer (no runtime dependency on the upstream plugin).
* Widgets, customizer, metafields, Elementor kit, URL replacement steps.
* Reset site with self-preservation + filterable preserve-list.
* Auto page reload after successful reset.

== Upgrade Notice ==

= 1.0.12 =
Security release. SQL-pack import/export and the built-in SVG sanitizer have been removed; XML parsing, outbound downloads and request handling are hardened. Normal WXR / widgets / customizer imports are unaffected.

= 1.0.11 =
Recommended-plugin installs are now resolved exclusively from the WordPress.org Plugin Directory.

= 1.0.0 =
First public release.
