diff --git a/.gitignore b/.gitignore index 90ec22b..114f179 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,11 @@ +.sass-cache +.DS_Store +.thumbsdb .svn +npm-debug.log +node_modules +npm-debug.log +bower_components +.idea + +/vendor diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f15009e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,115 @@ +# Travis CI (MIT License) configuration file for Options Importer +# @link https://travis-ci.org/ + +# Declare project language. +# @link http://about.travis-ci.org/docs/user/languages/php/ +language: php + +# Specify when Travis should build. +branches: + only: + - master + - /^release-v.*$/ + +services: + - mysql + +cache: + directories: + - $HOME/.composer/cache + - ./vendor + +matrix: + include: + - php: '5.3' + env: WP_VERSION=3.8 + dist: precise + - php: '5.6' + env: WP_VERSION=3.8 + - php: '7.0' + env: WP_VERSION=latest + - php: '7.3' + env: WP_VERSION=latest WP_TRAVISCI=phpcs PHP_LINT=1 WP_PHPCS=1 + - php: '7.4' + env: WP_VERSION=nightly + fast_finish: true + allow_failures: + - php: '7.4' + +# Prepare your build for testing. +# Failures in this section will result in build status 'errored'. +before_script: + # Turn off Xdebug. See https://core.trac.wordpress.org/changeset/40138. + - phpenv config-rm xdebug.ini || echo "Xdebug not available" + - export OG_DIR="$(pwd)" + + - export PATH="$HOME/.composer/vendor/bin:$PATH" + + # Couple the PHPUnit version to the PHP version. + - | + case "$TRAVIS_PHP_VERSION" in + 7.*) + echo "Using PHPUnit 6.1" + composer global require "phpunit/phpunit=6.1.*" + ;; + *) + echo "Using PHPUnit 4.8" + composer global require "phpunit/phpunit=4.8.*" + ;; + esac + + - | + if [[ ! -z "$WP_VERSION" ]] ; then + bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + fi + + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + # Composer Install + travis_retry composer install + export PATH=$PATH:`pwd`/vendor/bin/ + fi + + - phpenv rehash + + # For debugging. + - pwd + - which phpunit + - phpunit --version + - echo $PATH + +# Run test script commands. +# Default is specific to project language. +# All commands must exit with code 0 on success. Anything else is considered failure. +script: + # Search for PHP syntax errors. + # + # Only need to run this once per PHP version. + - | + if [[ "$PHP_LINT" == "1" ]] ; then + find . -type "f" -iname "*.php" -not -path "./vendor/*" | xargs -L "1" php -l + fi + + # WordPress Coding Standards. + # + # These are the same across PHP and WordPress, so we need to run them only once. + # + # @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + # @link http://pear.php.net/package/PHP_CodeSniffer/ + - | + if [[ "$WP_PHPCS" == "1" ]] ; then + phpcs -n + fi + + # Run the plugins's unit tests, both in single and multisite. + - | + if [[ ! -z "$WP_VERSION" ]] ; then + phpunit --version + phpunit + phpunit -c multisite.xml + fi + +# Receive notifications for build results. +# @link http://docs.travis-ci.com/user/notifications/#Email-notifications +notifications: + email: false diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100644 index 0000000..73bb4c7 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then + WP_TESTS_TAG="tags/$WP_VERSION" +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi + +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p /tmp/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip + unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ + mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz + tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i .bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/class-wp-options-importer.php b/class-wp-options-importer.php new file mode 100644 index 0000000..21475b4 --- /dev/null +++ b/class-wp-options-importer.php @@ -0,0 +1,935 @@ + +

+ +

+ true ); + } + + return $args; + } + + /** + * Export options as a JSON file if that's what the user wants to do. + * + * @param array $args The export arguments. + */ + public function export_wp( $args ) { + if ( ! empty( $args['options'] ) ) { + + $sitename = sanitize_key( get_bloginfo( 'name' ) ); + if ( ! empty( $sitename ) ) { + $sitename .= '.'; + } + + if ( function_exists( 'wp_date' ) ) { + $date = wp_date( 'Y-m-d' ); + } else { + $date = gmdate( 'Y-m-d' ); + } + + $filename = $sitename . 'wp_options.' . $date . '.json'; + + header( 'Content-Description: File Transfer' ); + header( 'Content-Disposition: attachment; filename=' . $filename ); + header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ), true ); + + $export_options = $this->get_export_options(); + + if ( ! empty( $export_options ) ) { + $json_pretty_print = defined( 'JSON_PRETTY_PRINT' ) ? JSON_PRETTY_PRINT : null; + + echo wp_json_encode( + array( + 'version' => self::VERSION, + 'options' => $export_options, + 'no_autoload' => $this->get_export_options_no_autoload(), + ), + $json_pretty_print + ); + } + + // Exit. + exit; + } + } + + /** + * Gets all of the export options and their values for the export file. + * + * @return array Any array of options to export. + */ + public function get_export_options() { + global $wpdb; + + $option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + "SELECT DISTINCT `option_name` + FROM $wpdb->options + WHERE `option_name` NOT LIKE '_transient_%' + AND `option_name` NOT LIKE '_site_transient_%'" + ); + + if ( ! empty( $option_names ) ) { + + /** + * Filters options that are in the denylist to be exported. + * + * @param array The deny list options. + */ + $denylist = apply_filters( 'options_export_denylist', array() ); + + // Backwards compat for legacy filter name. + $denylist = apply_filters( 'options_export_blacklist', $denylist ); + + $export_options = array(); + + // We're going to use a random hash as our default, to know if something is set or not. + $hash = '048f8580e913efe41ca7d402cc51e848'; + foreach ( $option_names as $option_name ) { + + // Skip if in the deny list. + if ( in_array( $option_name, $denylist, true ) ) { + continue; + } + + // Allow an installation to define a regular expression export denylist for security purposes. It's entirely possible + // that sensitive data might be installed in an option, or you may not want anyone to even know that a key exists. + // For instance, if you run a multsite installation, you could add in an mu-plugin: + // define( 'WP_OPTION_EXPORT_DENYLIST_REGEX', '/^(mailserver_(login|pass|port|url))$/' ); + // to ensure that none of your sites could export your mailserver settings. + if ( defined( 'WP_OPTION_EXPORT_DENYLIST_REGEX' ) && preg_match( WP_OPTION_EXPORT_DENYLIST_REGEX, $option_name ) ) { + continue; + } + + // Backwards compat for legacy constant name. + if ( defined( 'WP_OPTION_EXPORT_BLACKLIST_REGEX' ) && preg_match( WP_OPTION_EXPORT_BLACKLIST_REGEX, $option_name ) ) { + continue; + } + + $option_value = get_option( $option_name, $hash ); + + // Only export the setting if it's present. + if ( $option_value !== $hash ) { + $export_options[ $option_name ] = maybe_serialize( $option_value ); + } + } + + return $export_options; + } + + return array(); + } + + /** + * Gets all of the export option names with autoload disabled. + * + * @return array Array of option names that have autoload disabled. + */ + public function get_export_options_no_autoload() { + global $wpdb; + + $no_autoload = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + "SELECT DISTINCT `option_name` + FROM $wpdb->options + WHERE `option_name` NOT LIKE '_transient_%' + AND `option_name` NOT LIKE '_site__transient_%' + AND `autoload`='no'" + ); + + if ( empty( $no_autoload ) ) { + $no_autoload = array(); + } + + return (array) $no_autoload; + } + + /** + * Registered callback function for the Options Importer + * + * Manages the three separate stages of the import process. + */ + public function dispatch() { + $this->header(); + + if ( empty( $_GET['step'] ) ) { + $_GET['step'] = 0; + } + + switch ( intval( $_GET['step'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + case 0: + $this->greet(); + break; + case 1: + check_admin_referer( 'import-upload' ); + + if ( $this->handle_upload() ) { + $this->pre_import(); + } else { + echo '

' . esc_html__( 'Return to File Upload', 'wp-options-importer' ) . '

'; + } + + break; + case 2: + check_admin_referer( 'import-wordpress-options' ); + + $this->file_id = ! empty( $_POST['import_id'] ) ? intval( $_POST['import_id'] ) : 0; + $this->import_data = get_transient( $this->transient_key() ); + + if ( false !== $this->import_data ) { + $this->import(); + } + + break; + } + + $this->footer(); + } + + /** + * Start the options import page HTML. + */ + private function header() { + echo '
'; + echo '

' . esc_html__( 'Import WordPress Options', 'wp-options-importer' ) . '

'; + } + + /** + * End the options import page HTML. + */ + private function footer() { + echo '
'; + } + + /** + * Display introductory text and file upload form. + */ + private function greet() { + echo '
'; + echo '

' . esc_html__( 'Howdy! Upload your WordPress options JSON file and we’ll import the desired data. You’ll have a chance to review the data prior to import.', 'wp-options-importer' ) . '

'; + echo '

' . esc_html__( 'Choose a JSON (.json) file to upload, then click Upload file and import.', 'wp-options-importer' ) . '

'; + wp_import_upload_form( 'admin.php?import=wp-options-import&step=1' ); + echo '
'; + } + + + /** + * Handles the JSON upload and initial parsing of the file to prepare for + * displaying author import options + * + * @return bool False if error uploading or invalid file, true otherwise + */ + private function handle_upload() { + $file = wp_import_handle_upload(); + + if ( isset( $file['error'] ) ) { + return $this->error_message( + esc_html__( 'Sorry, there has been an error.', 'wp-options-importer' ), + esc_html( $file['error'] ) + ); + } + + if ( ! isset( $file['file'], $file['id'] ) ) { + return $this->error_message( + esc_html__( 'Sorry, there has been an error.', 'wp-options-importer' ), + esc_html__( 'The file did not upload properly. Please try again.', 'wp-options-importer' ) + ); + } + + $this->file_id = intval( $file['id'] ); + + if ( ! file_exists( $file['file'] ) ) { + wp_import_cleanup( $this->file_id ); + return $this->error_message( + esc_html__( 'Sorry, there has been an error.', 'wp-options-importer' ), + /* translators: 1. filename */ + sprintf( esc_html__( 'The export file could not be found at %s. It is likely that this was caused by a permissions problem.', 'wp-options-importer' ), esc_html( $file['file'] ) ) + ); + } + + if ( ! is_file( $file['file'] ) ) { + wp_import_cleanup( $this->file_id ); + return $this->error_message( + esc_html__( 'Sorry, there has been an error.', 'wordpress-importer' ), + esc_html__( 'The path is not a file, please try again.', 'wordpress-importer' ) + ); + } + + // Get the file source URL. + $file_url = wp_get_attachment_url( $this->file_id ); + + // Unable to get the file URL. + if ( empty( $file_url ) ) { + wp_import_cleanup( $this->file_id ); + return $this->error_message( + esc_html__( 'Sorry, there has been an error.', 'wordpress-importer' ), + esc_html__( 'Unable to fetch the file URL.', 'wordpress-importer' ) + ); + } + + if ( function_exists( 'vip_safe_wp_remote_get' ) ) { + $file_contents = vip_safe_wp_remote_get( $file_url ); + } else { + $file_contents = wp_remote_get( $file_url ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + } + + // Invalid file or file contents. + if ( is_wp_error( $file_contents ) || empty( $file_contents['body'] ) ) { + wp_import_cleanup( $this->file_id ); + return $this->error_message( + esc_html__( 'Sorry, there has been an error.', 'wordpress-importer' ), + esc_html__( 'Unable to fetch the file contents.', 'wordpress-importer' ) + ); + } + + $this->import_data = json_decode( $file_contents['body'], true ); + + set_transient( $this->transient_key(), $this->import_data, DAY_IN_SECONDS ); + wp_import_cleanup( $this->file_id ); + + return $this->run_data_check(); + } + + + /** + * Get an array of known options which we would want checked by default when importing. + * + * @return array + */ + public function get_allowlist_options() { + /** + * Filters the allowed options to be imported. + * + * @param array The allowlist of options to be imported. + */ + $allowlist = apply_filters( 'options_import_allowlist', $this->get_default_import_options() ); + + // Backwards compat for legacy filter name. + $allowlist = apply_filters( 'options_import_whitelist', $allowlist ); + + return $allowlist; + } + + /** + * Gets an array of default options to import. + * + * @return array An array of option names. + */ + public function get_default_import_options() { + return array( + // 'active_plugins', + 'admin_email', + 'advanced_edit', + 'avatar_default', + 'avatar_rating', + 'blacklist_keys', + 'blogdescription', + 'blogname', + 'blog_charset', + 'blog_public', + 'blog_upload_space', + 'category_base', + 'category_children', + 'close_comments_days_old', + 'close_comments_for_old_posts', + 'comments_notify', + 'comments_per_page', + 'comment_max_links', + 'comment_moderation', + 'comment_order', + 'comment_registration', + 'comment_whitelist', + 'comment_previously_approved', + 'cron', + // 'current_theme', + 'date_format', + 'default_category', + 'default_comments_page', + 'default_comment_status', + 'default_email_category', + 'default_link_category', + 'default_pingback_flag', + 'default_ping_status', + 'default_post_format', + 'default_role', + 'disallowed_keys', + 'gmt_offset', + 'gzipcompression', + 'hack_file', + 'html_type', + 'image_default_align', + 'image_default_link_type', + 'image_default_size', + 'large_size_h', + 'large_size_w', + 'links_recently_updated_append', + 'links_recently_updated_prepend', + 'links_recently_updated_time', + 'links_updated_date_format', + 'link_manager_enabled', + 'mailserver_login', + 'mailserver_pass', + 'mailserver_port', + 'mailserver_url', + 'medium_size_h', + 'medium_size_w', + 'moderation_keys', + 'moderation_notify', + 'ms_robotstxt', + 'ms_robotstxt_sitemap', + 'nav_menu_options', + 'page_comments', + 'page_for_posts', + 'page_on_front', + 'permalink_structure', + 'ping_sites', + 'posts_per_page', + 'posts_per_rss', + 'recently_activated', + 'recently_edited', + 'require_name_email', + 'rss_use_excerpt', + 'show_avatars', + 'show_on_front', + 'sidebars_widgets', + 'start_of_week', + 'sticky_posts', + // 'stylesheet', + 'subscription_options', + 'tag_base', + // 'template', + 'theme_switched', + 'thread_comments', + 'thread_comments_depth', + 'thumbnail_crop', + 'thumbnail_size_h', + 'thumbnail_size_w', + 'timezone_string', + 'time_format', + 'uninstall_plugins', + 'uploads_use_yearmonth_folders', + 'upload_path', + 'upload_url_path', + 'users_can_register', + 'use_balanceTags', + 'use_smilies', + 'use_trackback', + 'widget_archives', + 'widget_categories', + 'widget_image', + 'widget_meta', + 'widget_nav_menu', + 'widget_recent-comments', + 'widget_recent-posts', + 'widget_rss', + 'widget_rss_links', + 'widget_search', + 'widget_text', + 'widget_top-posts', + 'WPLANG', + ); + } + + /** + * Get an array of denylist options which we never want to import. + * + * @return array The import denylist. + */ + public function get_denylist_options() { + /** + * Filters the denylist of options to import. + * + * @param array The options denylist. + */ + $denylist = apply_filters( 'options_import_denylist', array() ); + + // Backwards compat for legacy filter name. + $denylist = apply_filters( 'options_import_blacklist', $denylist ); + + return $denylist; + } + + + /** + * Provide the user with a choice of which options to import from the JSON + * file, pre-selecting known options. + */ + private function pre_import() { + $allowlist = $this->get_allowlist_options(); + + // Allow others to prevent their options from importing. + $denylist = $this->get_denylist_options(); + + ?> + + +
+ + + +

+

+ +
+
+

+ +
+

+

+ + | + | +

+ + + + + + + + + + import_data['options'] as $option_name => $option_value ) : ?> + + + + + + + + + + + + + + + + +
 
/>
+
+ +

+

+ + +

+

+ +
+

+
+ + +
+ run_data_check() ) { + if ( empty( $_POST['settings']['which_options'] ) ) { + $this->error_message( esc_html__( 'The posted data does not appear intact. Please try again.', 'wp-options-importer' ) ); + $this->pre_import(); + return; + } + + // Determine which options to import. + $which_options = sanitize_text_field( wp_unslash( $_POST['settings']['which_options'] ) ); + + // Specific options to import. + $specific_options = array(); + + if ( 'specific' === $which_options ) { + if ( empty( $_POST['options'] ) ) { + $this->error_message( esc_html__( 'There do not appear to be any options to import. Did you select any?', 'wp-options-importer' ) ); + $this->pre_import(); + return; + } + + $specific_options = array_map( 'sanitize_text_field', wp_unslash( $_POST['options'] ) ); + } + + // Get the options to import. + $options_to_import = $this->get_options_to_import( $which_options, $specific_options ); + + $override = ( ! empty( $_POST['settings']['override'] ) && '1' === $_POST['settings']['override'] ); + + foreach ( (array) $options_to_import as $option_name ) { + if ( isset( $this->import_data['options'][ $option_name ] ) ) { + + // Import the option. + $this->import_option( $option_name, $override ); + + } elseif ( 'specific' === $which_options ) { + /* translators: 1. option name */ + echo "\n

" . sprintf( esc_html__( 'Failed to import option `%s`; it does not appear to be in the import file.', 'wp-options-importer' ), esc_html( $option_name ) ) . '

'; + } + } + + $this->clean_up(); + echo '

' . esc_html__( 'All done. That was easy.', 'wp-options-importer' ) . ' ' . esc_html__( 'Have fun!', 'wp-options-importer' ) . '

'; + } + } + + /** + * Gets the options to import. + * + * @param string $which_options Which options should be imported. + * @param array $specific_options An array of specific option names to import. + * @return array $options_to_import An array of option names to import. + */ + public function get_options_to_import( $which_options, $specific_options ) { + $options_to_import = array(); + + if ( 'all' === $which_options ) { + $options_to_import = array_keys( $this->import_data['options'] ); + } elseif ( 'default' === $which_options ) { + $options_to_import = $this->get_allowlist_options(); + } elseif ( 'specific' === $which_options ) { + $options_to_import = $specific_options; + } + + return $options_to_import; + } + + /** + * Imports an option after perform checks. + * + * @param string $name The option name. + * @param bool $override Whether or not to override the current option. + * @return bool|\WP_Error True on success, otherwise a \WP_Error object on failure. + */ + public function import_option( $name, $override ) { + $hash = '048f8580e913efe41ca7d402cc51e848'; + + // Allow others to prevent their options from importing. + $denylist = $this->get_denylist_options(); + + if ( in_array( $name, $denylist, true ) ) { + /* translators: 1. option name */ + return new \WP_Error( 'skipped', sprintf( __( 'Skipped option `%s` because this WordPress installation does not allow it.', 'wp-options-importer' ), $name ) ); + } + + // As an absolute last resort for security purposes, allow an installation to define a regular expression + // denylist. For instance, if you run a multsite installation, you could add in an mu-plugin: + // define( 'WP_OPTION_IMPORT_BLACKLIST_REGEX', '/^(home|siteurl)$/' ); + // to ensure that none of your sites could change their own url using this tool. + if ( + ( defined( 'WP_OPTION_IMPORT_DENYLIST_REGEX' ) && preg_match( WP_OPTION_IMPORT_DENYLIST_REGEX, $name ) ) + || ( defined( 'WP_OPTION_IMPORT_BLACKLIST_REGEX' ) && preg_match( WP_OPTION_IMPORT_BLACKLIST_REGEX, $name ) ) + ) { + /* translators: 1. option name */ + return new \WP_Error( 'skipped', sprintf( __( 'Skipped option `%s` because this WordPress installation does not allow it.', 'wp-options-importer' ), $name ) ); + } + + if ( ! $override ) { + // We're going to use a random hash as our default, to know if something is set or not. + $old_value = get_option( $name, $hash ); + + // Only import the setting if it's not present. + if ( $old_value !== $hash ) { + /* translators: 1. option name */ + return new \WP_Error( 'skipped', sprintf( __( 'Skipped option `%s` because it currently exists.', 'wp-options-importer' ), $name ) ); + } + } + + $option_value = maybe_unserialize( $this->import_data['options'][ $name ] ); + + if ( in_array( $name, $this->import_data['no_autoload'], true ) ) { + + if ( false === delete_option( $name ) ) { + /* translators: 1. option name */ + return new \WP_Error( 'error', sprintf( __( 'Failed deleting option `%s`.', 'wp-options-importer' ), $name ) ); + } + + if ( false === add_option( $name, $option_value, '', 'no' ) ) { + /* translators: 1. option name */ + return new \WP_Error( 'error', sprintf( __( 'Failed adding option `%s`.', 'wp-options-importer' ), $name ) ); + } + } else { + + if ( false === update_option( $name, $option_value ) ) { + /* translators: 1. option name */ + return new \WP_Error( 'error', sprintf( __( 'Failed updating option `%s`.', 'wp-options-importer' ), $name ) ); + } + } + + return true; + } + + /** + * Run a series of checks to ensure we're working with a valid JSON export. + * + * @return bool true if the file and data appear valid, false otherwise. + */ + private function run_data_check() { + if ( empty( $this->import_data['version'] ) ) { + $this->clean_up(); + return $this->error_message( esc_html__( 'Sorry, there has been an error. This file may not contain data or is corrupt.', 'wp-options-importer' ) ); + } + + if ( $this->import_data['version'] < $this->min_version ) { + $this->clean_up(); + /* translators: 1. file version */ + return $this->error_message( sprintf( esc_html__( 'This JSON file (version %s) is not supported by this version of the importer. Please update the plugin on the source, or download an older version of the plugin to this installation.', 'wp-options-importer' ), intval( $this->import_data['version'] ) ) ); + } + + if ( $this->import_data['version'] > self::VERSION ) { + $this->clean_up(); + /* translators: 1. file version */ + return $this->error_message( sprintf( esc_html__( 'This JSON file (version %s) is from a newer version of this plugin and may not be compatible. Please update this plugin.', 'wp-options-importer' ), intval( $this->import_data['version'] ) ) ); + } + + if ( empty( $this->import_data['options'] ) ) { + $this->clean_up(); + return $this->error_message( esc_html__( 'Sorry, there has been an error. This file appears valid, but does not seem to have any options.', 'wp-options-importer' ) ); + } + + return true; + } + + /** + * Gets the transient key name. + * + * @return string The transient key name. + */ + private function transient_key() { + return sprintf( $this->transient_key, $this->file_id ); + } + + /** + * Deletes the transient. + */ + private function clean_up() { + delete_transient( $this->transient_key() ); + } + + /** + * A helper method to keep DRY with our error messages. Note that the error messages + * must be escaped prior to being passed to this method (this allows us to send HTML). + * + * @param string $message The main message to output. + * @param string $details Optional. Additional details. + * @return bool false + */ + private function error_message( $message, $details = '' ) { + echo '

' . esc_html( $message ) . ''; + + if ( ! empty( $details ) ) { + echo '
' . wp_kses_post( $details ); + } + + echo '

'; + + return false; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..befb428 --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "alleyinteractive/options-importer", + "type": "wordpress-plugin", + "keywords": ["wordpress", "plugin"], + "authors": [ + { + "name": "Alley Interactive", + "email": "noreply@alleyinteractive.com" + } + ], + "require": { + "composer/installers": "~1.0" + }, + "require-dev": { + "squizlabs/php_codesniffer": "3.*", + "wp-coding-standards/wpcs": "2.*", + "automattic/vipwpcs": "2.*" + }, + "scripts": { + "post-install-cmd": "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs,vendor/automattic/vipwpcs", + "post-update-cmd" : "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs,vendor/automattic/vipwpcs" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..fa479a5 --- /dev/null +++ b/composer.lock @@ -0,0 +1,289 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "8f0b377e7fb23ad29cce9334245992fa", + "packages": [ + { + "name": "composer/installers", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/b93bcf0fa1fccb0b7d176b0967d969691cd74cca", + "reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "replace": { + "roundcube/plugin-installer": "*", + "shama/baton": "*" + }, + "require-dev": { + "composer/composer": "1.6.* || 2.0.*@dev", + "composer/semver": "1.0.* || 2.0.*@dev", + "phpunit/phpunit": "^4.8.36", + "sebastian/comparator": "^1.2.4", + "symfony/process": "^2.3" + }, + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Craft", + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "MantisBT", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Thelia", + "Whmcs", + "WolfCMS", + "agl", + "aimeos", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "joomla", + "known", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "mediawiki", + "modulework", + "modx", + "moodle", + "osclass", + "phpbb", + "piwik", + "ppi", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "sylius", + "symfony", + "typo3", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "time": "2020-04-07T06:57:05+00:00" + } + ], + "packages-dev": [ + { + "name": "automattic/vipwpcs", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Automattic/VIP-Coding-Standards.git", + "reference": "03e75ddd0261b675dece60fb67fc2e9c6af4ad35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/03e75ddd0261b675dece60fb67fc2e9c6af4ad35", + "reference": "03e75ddd0261b675dece60fb67fc2e9c6af4ad35", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.5.5", + "wp-coding-standards/wpcs": "^2.3" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "phpcompatibility/php-compatibility": "^9", + "phpunit/phpunit": "^4 || ^5 || ^6 || ^7" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will manage the PHPCS 'installed_paths' automatically." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/Automattic/VIP-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress VIP minimum coding conventions", + "keywords": [ + "phpcs", + "standards", + "wordpress" + ], + "time": "2020-07-07T07:48:04+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.5.5", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6", + "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2020-04-17T01:09:41+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7da1894633f168fe244afc6de00d141f27517b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", + "reference": "7da1894633f168fe244afc6de00d141f27517b62", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.3.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", + "phpcompatibility/php-compatibility": "^9.0", + "phpcsstandards/phpcsdevtools": "^1.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "wordpress" + ], + "time": "2020-05-13T23:57:56+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/multisite.xml b/multisite.xml new file mode 100644 index 0000000..4a64bf4 --- /dev/null +++ b/multisite.xml @@ -0,0 +1,25 @@ + + + + + + + ./tests/ + + + + + ./ + + ./tests/ + + + + diff --git a/options-importer.php b/options-importer.php index c726c81..5ab5659 100644 --- a/options-importer.php +++ b/options-importer.php @@ -1,739 +1,26 @@ setup(); - } - return self::$instance; - } - - - /** - * Initialize the singleton. - * - * @return void - */ - public function setup() { - add_action( 'export_filters', array( $this, 'export_filters' ) ); - add_filter( 'export_args', array( $this, 'export_args' ) ); - add_action( 'export_wp', array( $this, 'export_wp' ) ); - add_action( 'admin_init', array( $this, 'register_importer' ) ); - } - - - /** - * Register our importer. - * - * @return void - */ - public function register_importer() { - if ( function_exists( 'register_importer' ) ) { - register_importer( 'wp-options-import', esc_html__( 'Options', 'wp-options-importer' ), esc_html__( 'Import wp_options from a JSON file', 'wp-options-importer' ), array( $this, 'dispatch' ) ); - } - } - - - /** - * Add a radio option to export options. - * - * @return void - */ - public function export_filters() { - ?> -

- true ); - } - return $args; - } - - - /** - * Export options as a JSON file if that's what the user wants to do. - * - * @param array $args The export arguments. - * @return void - */ - public function export_wp( $args ) { - if ( ! empty( $args['options'] ) ) { - global $wpdb; - - $sitename = sanitize_key( get_bloginfo( 'name' ) ); - if ( ! empty( $sitename ) ) { - $sitename .= '.'; - } - $filename = $sitename . 'wp_options.' . date( 'Y-m-d' ) . '.json'; - - header( 'Content-Description: File Transfer' ); - header( 'Content-Disposition: attachment; filename=' . $filename ); - header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ), true ); - - $option_names = $wpdb->get_col( "SELECT DISTINCT `option_name` FROM $wpdb->options WHERE `option_name` NOT LIKE '_transient_%'" ); - if ( ! empty( $option_names ) ) { - - // Allow others to be able to exclude their options from exporting - $blacklist = apply_filters( 'options_export_blacklist', array() ); - - $export_options = array(); - // we're going to use a random hash as our default, to know if something is set or not - $hash = '048f8580e913efe41ca7d402cc51e848'; - foreach ( $option_names as $option_name ) { - if ( in_array( $option_name, $blacklist ) ) { - continue; - } - - // Allow an installation to define a regular expression export blacklist for security purposes. It's entirely possible - // that sensitive data might be installed in an option, or you may not want anyone to even know that a key exists. - // For instance, if you run a multsite installation, you could add in an mu-plugin: - // define( 'WP_OPTION_EXPORT_BLACKLIST_REGEX', '/^(mailserver_(login|pass|port|url))$/' ); - // to ensure that none of your sites could export your mailserver settings. - if ( defined( 'WP_OPTION_EXPORT_BLACKLIST_REGEX' ) && preg_match( WP_OPTION_EXPORT_BLACKLIST_REGEX, $option_name ) ) { - continue; - } - - $option_value = get_option( $option_name, $hash ); - // only export the setting if it's present - if ( $option_value !== $hash ) { - $export_options[ $option_name ] = maybe_serialize( $option_value ); - } - } - - $no_autoload = $wpdb->get_col( "SELECT DISTINCT `option_name` FROM $wpdb->options WHERE `option_name` NOT LIKE '_transient_%' AND `autoload`='no'" ); - if ( empty( $no_autoload ) ) { - $no_autoload = array(); - } - - $JSON_PRETTY_PRINT = defined( 'JSON_PRETTY_PRINT' ) ? JSON_PRETTY_PRINT : null; - echo json_encode( array( 'version' => self::VERSION, 'options' => $export_options, 'no_autoload' => $no_autoload ), $JSON_PRETTY_PRINT ); - } - - exit; - } - } - - - /** - * Registered callback function for the Options Importer - * - * Manages the three separate stages of the import process. - * - * @return void - */ - public function dispatch() { - $this->header(); - - if ( empty( $_GET['step'] ) ) { - $_GET['step'] = 0; - } - - switch ( intval( $_GET['step'] ) ) { - case 0: - $this->greet(); - break; - case 1: - check_admin_referer( 'import-upload' ); - if ( $this->handle_upload() ) { - $this->pre_import(); - } else { - echo '

' . esc_html__( 'Return to File Upload', 'wp-options-importer' ) . '

'; - } - break; - case 2: - check_admin_referer( 'import-wordpress-options' ); - $this->file_id = intval( $_POST['import_id'] ); - if ( false !== ( $this->import_data = get_transient( $this->transient_key() ) ) ) { - $this->import(); - } - break; - } - - $this->footer(); - } - - - /** - * Start the options import page HTML. - * - * @return void - */ - private function header() { - echo '
'; - echo '

' . esc_html__( 'Import WordPress Options', 'wp-options-importer' ) . '

'; - } - - - /** - * End the options import page HTML. - * - * @return void - */ - private function footer() { - echo '
'; - } - - - /** - * Display introductory text and file upload form. - * - * @return void - */ - private function greet() { - echo '
'; - echo '

'.esc_html__( 'Howdy! Upload your WordPress options JSON file and we’ll import the desired data. You’ll have a chance to review the data prior to import.', 'wp-options-importer' ).'

'; - echo '

'.esc_html__( 'Choose a JSON (.json) file to upload, then click Upload file and import.', 'wp-options-importer' ).'

'; - wp_import_upload_form( 'admin.php?import=wp-options-import&step=1' ); - echo '
'; - } - - - /** - * Handles the JSON upload and initial parsing of the file to prepare for - * displaying author import options - * - * @return bool False if error uploading or invalid file, true otherwise - */ - private function handle_upload() { - $file = wp_import_handle_upload(); - - if ( isset( $file['error'] ) ) { - return $this->error_message( - esc_html__( 'Sorry, there has been an error.', 'wp-options-importer' ), - esc_html( $file['error'] ) - ); - } - - if ( ! isset( $file['file'], $file['id'] ) ) { - return $this->error_message( - esc_html__( 'Sorry, there has been an error.', 'wp-options-importer' ), - esc_html__( 'The file did not upload properly. Please try again.', 'wp-options-importer' ) - ); - } - - $this->file_id = intval( $file['id'] ); - - if ( ! file_exists( $file['file'] ) ) { - wp_import_cleanup( $this->file_id ); - return $this->error_message( - esc_html__( 'Sorry, there has been an error.', 'wp-options-importer' ), - sprintf( esc_html__( 'The export file could not be found at %s. It is likely that this was caused by a permissions problem.', 'wp-options-importer' ), esc_html( $file['file'] ) ) - ); - } - - if ( ! is_file( $file['file'] ) ) { - wp_import_cleanup( $this->file_id ); - return $this->error_message( - esc_html__( 'Sorry, there has been an error.', 'wordpress-importer' ), - esc_html__( 'The path is not a file, please try again.', 'wordpress-importer' ) - ); - } - - $file_contents = file_get_contents( $file['file'] ); - $this->import_data = json_decode( $file_contents, true ); - set_transient( $this->transient_key(), $this->import_data, DAY_IN_SECONDS ); - wp_import_cleanup( $this->file_id ); - - return $this->run_data_check(); - } - - - /** - * Get an array of known options which we would want checked by default when importing. - * - * @return array - */ - private function get_whitelist_options() { - return apply_filters( 'options_import_whitelist', array( - // 'active_plugins', - 'admin_email', - 'advanced_edit', - 'avatar_default', - 'avatar_rating', - 'blacklist_keys', - 'blogdescription', - 'blogname', - 'blog_charset', - 'blog_public', - 'blog_upload_space', - 'category_base', - 'category_children', - 'close_comments_days_old', - 'close_comments_for_old_posts', - 'comments_notify', - 'comments_per_page', - 'comment_max_links', - 'comment_moderation', - 'comment_order', - 'comment_registration', - 'comment_whitelist', - 'cron', - // 'current_theme', - 'date_format', - 'default_category', - 'default_comments_page', - 'default_comment_status', - 'default_email_category', - 'default_link_category', - 'default_pingback_flag', - 'default_ping_status', - 'default_post_format', - 'default_role', - 'gmt_offset', - 'gzipcompression', - 'hack_file', - 'html_type', - 'image_default_align', - 'image_default_link_type', - 'image_default_size', - 'large_size_h', - 'large_size_w', - 'links_recently_updated_append', - 'links_recently_updated_prepend', - 'links_recently_updated_time', - 'links_updated_date_format', - 'link_manager_enabled', - 'mailserver_login', - 'mailserver_pass', - 'mailserver_port', - 'mailserver_url', - 'medium_size_h', - 'medium_size_w', - 'moderation_keys', - 'moderation_notify', - 'ms_robotstxt', - 'ms_robotstxt_sitemap', - 'nav_menu_options', - 'page_comments', - 'page_for_posts', - 'page_on_front', - 'permalink_structure', - 'ping_sites', - 'posts_per_page', - 'posts_per_rss', - 'recently_activated', - 'recently_edited', - 'require_name_email', - 'rss_use_excerpt', - 'show_avatars', - 'show_on_front', - 'sidebars_widgets', - 'start_of_week', - 'sticky_posts', - // 'stylesheet', - 'subscription_options', - 'tag_base', - // 'template', - 'theme_switched', - 'thread_comments', - 'thread_comments_depth', - 'thumbnail_crop', - 'thumbnail_size_h', - 'thumbnail_size_w', - 'timezone_string', - 'time_format', - 'uninstall_plugins', - 'uploads_use_yearmonth_folders', - 'upload_path', - 'upload_url_path', - 'users_can_register', - 'use_balanceTags', - 'use_smilies', - 'use_trackback', - 'widget_archives', - 'widget_categories', - 'widget_image', - 'widget_meta', - 'widget_nav_menu', - 'widget_recent-comments', - 'widget_recent-posts', - 'widget_rss', - 'widget_rss_links', - 'widget_search', - 'widget_text', - 'widget_top-posts', - 'WPLANG', - ) ); - } - - - /** - * Get an array of blacklisted options which we never want to import. - * - * @return array - */ - private function get_blacklist_options() { - return apply_filters( 'options_import_blacklist', array() ); - } - - - /** - * Provide the user with a choice of which options to import from the JSON - * file, pre-selecting known options. - * - * @return void - */ - private function pre_import() { - $whitelist = $this->get_whitelist_options(); - - // Allow others to prevent their options from importing - $blacklist = $this->get_blacklist_options(); - - ?> - - -
- - - -

-

- -
-
-

- -
-

-

- - | - | -

- - - - - - - - - - import_data['options'] as $option_name => $option_value ) : ?> - - - - - - - - - - - - - - - - -
 
/>nullempty stringfalse
-
- -

-

- - -

-

- -
-

-
- - -
- run_data_check() ) { - if ( empty( $_POST['settings']['which_options'] ) ) { - $this->error_message( esc_html__( 'The posted data does not appear intact. Please try again.', 'wp-options-importer' ) ); - $this->pre_import(); - return; - } - - $options_to_import = array(); - if ( 'all' == $_POST['settings']['which_options'] ) { - $options_to_import = array_keys( $this->import_data['options'] ); - } elseif ( 'default' == $_POST['settings']['which_options'] ) { - $options_to_import = $this->get_whitelist_options(); - } elseif ( 'specific' == $_POST['settings']['which_options'] ) { - if ( empty( $_POST['options'] ) ) { - $this->error_message( esc_html__( 'There do not appear to be any options to import. Did you select any?', 'wp-options-importer' ) ); - $this->pre_import(); - return; - } - - $options_to_import = $_POST['options']; - } - - $override = ( ! empty( $_POST['settings']['override'] ) && '1' === $_POST['settings']['override'] ); - - $hash = '048f8580e913efe41ca7d402cc51e848'; - - // Allow others to prevent their options from importing - $blacklist = $this->get_blacklist_options(); - - foreach ( (array) $options_to_import as $option_name ) { - if ( isset( $this->import_data['options'][ $option_name ] ) ) { - if ( in_array( $option_name, $blacklist ) ) { - echo "\n

" . sprintf( esc_html__( 'Skipped option `%s` because a plugin or theme does not allow it to be imported.', 'wp-options-importer' ), esc_html( $option_name ) ) . '

'; - continue; - } - - // As an absolute last resort for security purposes, allow an installation to define a regular expression - // blacklist. For instance, if you run a multsite installation, you could add in an mu-plugin: - // define( 'WP_OPTION_IMPORT_BLACKLIST_REGEX', '/^(home|siteurl)$/' ); - // to ensure that none of your sites could change their own url using this tool. - if ( defined( 'WP_OPTION_IMPORT_BLACKLIST_REGEX' ) && preg_match( WP_OPTION_IMPORT_BLACKLIST_REGEX, $option_name ) ) { - echo "\n

" . sprintf( esc_html__( 'Skipped option `%s` because this WordPress installation does not allow it.', 'wp-options-importer' ), esc_html( $option_name ) ) . '

'; - continue; - } - - if ( ! $override ) { - // we're going to use a random hash as our default, to know if something is set or not - $old_value = get_option( $option_name, $hash ); - - // only import the setting if it's not present - if ( $old_value !== $hash ) { - echo "\n

" . sprintf( esc_html__( 'Skipped option `%s` because it currently exists.', 'wp-options-importer' ), esc_html( $option_name ) ) . '

'; - continue; - } - } - - $option_value = maybe_unserialize( $this->import_data['options'][ $option_name ] ); - if ( in_array( $option_name, $this->import_data['no_autoload'] ) ) { - delete_option( $option_name ); - add_option( $option_name, $option_value, '', 'no' ); - } else { - update_option( $option_name, $option_value ); - } - } elseif ( 'specific' == $_POST['settings']['which_options'] ) { - echo "\n

" . sprintf( esc_html__( 'Failed to import option `%s`; it does not appear to be in the import file.', 'wp-options-importer' ), esc_html( $option_name ) ) . '

'; - } - } - - $this->clean_up(); - echo '

' . esc_html__( 'All done. That was easy.', 'wp-options-importer' ) . ' ' . esc_html__( 'Have fun!', 'wp-options-importer' ) . '' . '

'; - } - } - - - /** - * Run a series of checks to ensure we're working with a valid JSON export. - * - * @return bool true if the file and data appear valid, false otherwise. - */ - private function run_data_check() { - if ( empty( $this->import_data['version'] ) ) { - $this->clean_up(); - return $this->error_message( esc_html__( 'Sorry, there has been an error. This file may not contain data or is corrupt.', 'wp-options-importer' ) ); - } - - if ( $this->import_data['version'] < $this->min_version ) { - $this->clean_up(); - return $this->error_message( sprintf( esc_html__( 'This JSON file (version %s) is not supported by this version of the importer. Please update the plugin on the source, or download an older version of the plugin to this installation.', 'wp-options-importer' ), intval( $this->import_data['version'] ) ) ); - } - - if ( $this->import_data['version'] > self::VERSION ) { - $this->clean_up(); - return $this->error_message( sprintf( esc_html__( 'This JSON file (version %s) is from a newer version of this plugin and may not be compatible. Please update this plugin.', 'wp-options-importer' ), intval( $this->import_data['version'] ) ) ); - } - - if ( empty( $this->import_data['options'] ) ) { - $this->clean_up(); - return $this->error_message( esc_html__( 'Sorry, there has been an error. This file appears valid, but does not seem to have any options.', 'wp-options-importer' ) ); - } - - return true; - } - - - private function transient_key() { - return sprintf( $this->transient_key, $this->file_id ); - } - - - private function clean_up() { - delete_transient( $this->transient_key() ); - } - - - /** - * A helper method to keep DRY with our error messages. Note that the error messages - * must be escaped prior to being passed to this method (this allows us to send HTML). - * - * @param string $message The main message to output. - * @param string $details Optional. Additional details. - * @return bool false - */ - private function error_message( $message, $details = '' ) { - echo '

' . $message . ''; - if ( ! empty( $details ) ) { - echo '
' . $details; - } - echo '

'; - return false; - } +/** + * Plugin Name: WP Options Importer + * Plugin URI: https://github.com/alleyinteractive/options-importer + * Description: Export and import WordPress Options + * Version: 7 + * Author: Matthew Boynes + * Author URI: https://alley.co/ + * + * @package Options_Importer + */ + +if ( ! class_exists( 'WP_Options_Importer' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-options-importer.php'; } -WP_Options_Importer::instance(); +/** + * Creates and setups up the main singleton class instance. + */ +function options_import_setup_main_class() { + // Create and the singleton instance. + WP_Options_Importer::instance()->setup(); -endif; \ No newline at end of file + return false; +} +add_filter( 'plugins_loaded', 'options_import_setup_main_class' ); diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..9a8e4b5 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,24 @@ + + + Sniffs for the coding standards of the Options Importer plugin + + + + + + + + + . + + + */vendor/ + */tests/ + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..811a992 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + ./tests/ + + + + + ./ + + ./tests/ + + + + diff --git a/readme.txt b/readme.txt index db12424..078f2e1 100644 --- a/readme.txt +++ b/readme.txt @@ -2,7 +2,7 @@ Contributors: mboynes,alleyinteractive Tags: options, importer, exporter, export, import, migrate, settings, wp_options Requires at least: 3.8 -Tested up to: 3.9 +Tested up to: 5.5 Stable tag: 7 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -43,7 +43,7 @@ manually select those which you need to import. = I'm the author of [some plugin]. Can you add my settings to the default list? = -No, but you can! We provide a filter, `options_import_whitelist` for you to add +No, but you can! We provide a filter, `options_import_allowlist` for you to add your options to the default list. Here's an example one might add to their plugin: @@ -51,17 +51,17 @@ plugin: $options[] = 'my_awesome_plugin'; return $options; } - add_filter( 'options_import_whitelist', 'my_awesome_plugin_options' ); + add_filter( 'options_import_allowlist', 'my_awesome_plugin_options' ); Similarly, if you don't want someone to ever import an option, you can add it -to the blacklist using the `options_import_blacklist` filter. As above, it +to the denylist using the `options_import_denylist` filter. As above, it would look something like this: - function my_awesome_plugin_blacklist_options( $options ) { + function my_awesome_plugin_denylist_options( $options ) { $options[] = 'my_awesome_plugin_edit_lock'; return $options; } - add_filter( 'options_import_blacklist', 'my_awesome_plugin_blacklist_options' ); + add_filter( 'options_import_denylist', 'my_awesome_plugin_denylist_options' ); = I operate a multisite network and some options should *never* be able to be exported or imported by the site owner. Can I prevent that? = @@ -69,21 +69,21 @@ You have two options for both exports and imports. **Imports** -First, you can use the `options_import_blacklist` filter +First, you can use the `options_import_denylist` filter and add any options to that array (which is empty by default). If your users have access to theme or plugin code, this isn't 100% safe, because they could -override your blacklist using the same filter. In those cases, there's an +override your denylist using the same filter. In those cases, there's an emergency ripcord where you can disable options from ever being imported. To -use this, define the constant `WP_OPTION_IMPORT_BLACKLIST_REGEX` (you'll +use this, define the constant `WP_OPTION_IMPORT_DENYLIST_REGEX` (you'll probably want to do this in an mu-plugin) and set it to a regular expression. Anything matching this expression will be skipped. For example: - define( 'WP_OPTION_IMPORT_BLACKLIST_REGEX', '/^(home|siteurl)$/' ); + define( 'WP_OPTION_IMPORT_DENYLIST_REGEX', '/^(home|siteurl)$/' ); **Exports** -Exactly the same as with imports. The filter is `options_export_blacklist`, -and the constant is `WP_OPTION_EXPORT_BLACKLIST_REGEX`. +Exactly the same as with imports. The filter is `options_export_denylist`, +and the constant is `WP_OPTION_EXPORT_DENYLIST_REGEX`. == Screenshots == @@ -99,7 +99,13 @@ uncheck those which you don't want to include. == Changelog == = 7 = -* Use escaped variants of `_e()` and `__()` +* SECURITY: Add proper escaping to all echo functions +* SECURITY: Add nonce checks +* SECURITY: Sanitize option name values during import +* ENHANCEMENT: Use wp_remote_get instead of file_get_contents +* INFO: Deprecate the use of blacklist and whitlelist in favor of denylist and allowlist +* INFO: Move class into new file +* INFO: Enable phpcs against the WordPress standard = 6 = * Remove multisite site-specific exclusions @@ -128,4 +134,4 @@ uncheck those which you don't want to include. == Upgrade Notice == = 5 = -**Breaking:** Changed the `options_export_exclude` filter to `options_export_blacklist` to be consistent with imports. \ No newline at end of file +**Breaking:** Changed the `options_export_exclude` filter to `options_export_blacklist` to be consistent with imports. diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..138e867 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,40 @@ +get_export_options(); + + $this->assertNotEmpty( $export_options ); + + // Set a custom option. + $option_value = rand_str(); + update_option( 'custom_option', $option_value ); + + // Check custom value is in export. + $export_options = WP_Options_Importer::instance()->get_export_options(); + $this->assertEquals( $option_value, $export_options['custom_option'] ); + } + + /** + * Tests the different ways to import options. + */ + function test_get_options_to_import() { + // Stub test data. + $test_options = array( + 'option_1' => rand_str(), + 'option_2' => rand_str(), + 'option_3' => rand_str(), + ); + WP_Options_Importer::instance()->import_data['options'] = $test_options; + + // All options. + $this->assertEquals( + array_keys( $test_options ), + array_values( WP_Options_Importer::instance()->get_options_to_import( 'all', array() ) ) + ); + + // Default options. + $this->assertEquals( + array_values( WP_Options_Importer::instance()->get_allowlist_options() ), + array_values( WP_Options_Importer::instance()->get_options_to_import( 'default', array() ) ) + ); + + // Specific options. + $this->assertEquals( + array_keys( array( 'option_1' => true, 'option_2' => true ) ), + array_values( WP_Options_Importer::instance()->get_options_to_import( 'specific', array( 'option_1', 'option_2' ) ) ) + ); + } + + /** + * Tests importing a single option. + */ + function test_import_option() { + $option_name = 'import_option'; + $option_value = rand_str(); + WP_Options_Importer::instance()->import_data['options'][ $option_name ] = $option_value; + WP_Options_Importer::instance()->import_data['no_autoload'] = array(); + + // Import the option. + $this->assertTrue( WP_Options_Importer::instance()->import_option( $option_name, true ) ); + $this->assertEquals( get_option( $option_name ), $option_value ); + + add_filter( 'options_import_denylist', function ( $denylist ) { return array_merge( $denylist, array( 'import_option' ) ); } ); + $this->assertInstanceOf( '\WP_Error', WP_Options_Importer::instance()->import_option( $option_name, true ) ); + + // Backwards support for old filter name. + add_filter( 'options_import_denylist', '__return_empty_array' ); + add_filter( 'options_import_blacklist', function ( $denylist ) { return array_merge( $denylist, array( 'import_option' ) ); } ); + $this->assertInstanceOf( '\WP_Error', WP_Options_Importer::instance()->import_option( $option_name, true ) ); + } + + /** + * Tests getting the allowlist option names filter. + */ + function test_get_allowlist_options_filter() { + add_filter( 'options_import_allowlist', function ( $allowlist ) { return array_merge( $allowlist, array( 'custom_option_allowlist' ) ); } ); + $this->assertTrue( in_array( 'custom_option_allowlist', WP_Options_Importer::instance()->get_allowlist_options(), true ) ); + + // Backwards support for old filter name. + add_filter( 'options_import_whitelist', function ( $allowlist ) { return array_merge( $allowlist, array( 'custom_option_whitelist' ) ); } ); + $this->assertTrue( in_array( 'custom_option_whitelist', WP_Options_Importer::instance()->get_allowlist_options(), true ) ); + } + + /** + * Tests getting the denylist option names filter. + */ + function test_get_denylist_options_filter() { + add_filter( 'options_import_denylist', function ( $denylist ) { return array_merge( $denylist, array( 'custom_option_denylist' ) ); } ); + $this->assertTrue( in_array( 'custom_option_denylist', WP_Options_Importer::instance()->get_denylist_options(), true ) ); + + // Backwards support for old filter name. + add_filter( 'options_import_blacklist', function ( $denylist ) { return array_merge( $denylist, array( 'custom_option_blacklist' ) ); } ); + $this->assertTrue( in_array( 'custom_option_blacklist', WP_Options_Importer::instance()->get_denylist_options(), true ) ); + } + + /** + * Tests getting the options to export with deny list filter. + */ + function test_get_export_options_denylist() { + // Set a custom option. + $option_value = rand_str(); + update_option( 'custom_option', $option_value ); + + // Check custom value is in export. + $export_options = WP_Options_Importer::instance()->get_export_options(); + $this->assertEquals( $option_value, $export_options['custom_option'] ); + + // Add the value to the deny list. + add_filter( 'options_export_denylist', function() { return array( 'custom_option' ); } ); + + // Ensure the value does not exist in the export. + $export_options = WP_Options_Importer::instance()->get_export_options(); + $this->assertFalse( isset( $export_options['custom_option'] ) ); + + // Test legacy filer name. + add_filter( 'options_export_denylist', '__return_empty_array' ); + add_filter( 'options_export_blacklist', function() { return array( 'custom_option' ); } ); + + // Ensure the value does not exist in the export. + $export_options = WP_Options_Importer::instance()->get_export_options(); + $this->assertFalse( isset( $export_options['custom_option'] ) ); + } + + /** + * Tests getting the options to export with deny list constant. + */ + function test_get_export_options_denylist_constant() { + // Set a custom option. + $option_value = rand_str(); + update_option( 'custom_option_denylist_regex', $option_value ); + + // Check custom value is in export. + $export_options = WP_Options_Importer::instance()->get_export_options(); + $this->assertEquals( $option_value, $export_options['custom_option_denylist_regex'] ); + + // Add the value to the deny list. + define( 'WP_OPTION_EXPORT_DENYLIST_REGEX', '/^custom_option_denylist_regex$/' ); + + // Ensure the value does not exist in the export. + $export_options = WP_Options_Importer::instance()->get_export_options(); + $this->assertFalse( isset( $export_options['custom_option_denylist_regex'] ) ); + + // Test legacy filer name. + define( 'WP_OPTION_EXPORT_BLACKLIST_REGEX', '/^custom_option_blacklist_regex$/' ); + + // Ensure the value does not exist in the export. + $export_options = WP_Options_Importer::instance()->get_export_options(); + $this->assertFalse( isset( $export_options['custom_option_blacklist_regex'] ) ); + } +}