Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/quick-playground/client-demo-filters.php
+++ b/quick-playground/client-demo-filters.php
@@ -4,22 +4,15 @@
$temp = wp_upload_dir();
$file_path = $temp['path'].'/'.$choice['choice'];
$profile = get_option("qckply_profile");
- $qckply_sync_code = get_option('qckply_sync_code');
$sync_origin = get_option('qckply_sync_origin');
$remote_url = $sync_origin.'/wp-json/quickplayground/v1/upload_image/'.$profile.'?t='.time();
update_option('qckply_upload_image_path',$file_path);
update_option('qckply_upload_image_url',$remote_url);
$data = $choice;
- $data['sync_code'] = $qckply_sync_code;
$data['base64'] = base64_encode(file_get_contents($file_path));
update_option('qckply_upload_image_path_64',substr($data['base64'],0,10));
$data['filename'] = $choice['basename'];
- $request = array(
- 'body' => json_encode($data),
- 'headers' => array(
- 'Content-Type' => 'application/json',
- ));
- $response = wp_remote_post( $remote_url, $request);
+ $response = qckply_sync_remote_post($remote_url, $data);
if ( ! is_wp_error( $response ) ) {
$body = json_decode( wp_remote_retrieve_body( $response ), true );
update_option('qckply_upload_image_resized_result',var_export($body,true));
--- a/quick-playground/client-qckply_data.php
+++ b/quick-playground/client-qckply_data.php
@@ -41,12 +41,8 @@
$starter_top = qckply_top_ids();
$diff = array_diff_assoc($true_top,$starter_top);
$sync_origin = get_option('qckply_sync_origin');
- $remote_url = $sync_origin.'/wp-json/quickplayground/v1/sync_ids?t='.time();
+ $profile = get_option('qckply_profile', 'default');
+ $remote_url = $sync_origin.'/wp-json/quickplayground/v1/sync_ids/'.$profile.'?t='.time();
$updata['top_ids'] = $true_top;
- $request = array(
- 'body' => json_encode($updata),
- 'headers' => array(
- 'Content-Type' => 'application/json',
- ));
- $response = wp_remote_post( $remote_url, $request);
+ $response = qckply_sync_remote_post($remote_url, $updata);
}
--- a/quick-playground/client-save-images.php
+++ b/quick-playground/client-save-images.php
@@ -41,11 +41,9 @@
}
update_option('qckply_upload_image_path_scaled',$filename);
$profile = get_option("qckply_profile");
- $qckply_sync_code = get_option('qckply_sync_code');
$sync_origin = get_option('qckply_sync_origin');
$remote_url = $sync_origin.'/wp-json/quickplayground/v1/upload_image/'.$profile.'?t='.time();
update_option('qckply_upload_image_url',$remote_url);
- $updata['sync_code'] = $qckply_sync_code;
$imgcontent = file_get_contents($filename);
$updata['base64'] = base64_encode($imgcontent);
$updata['top_id'] = $wpdb->get_var($wpdb->prepare('select ID from %i ORDER BY ID DESC',$wpdb->posts));
@@ -69,12 +67,7 @@
echo '<p>Error encoding to base64</p>';
return;
}
- $request = array(
- 'body' => json_encode($updata),
- 'headers' => array(
- 'Content-Type' => 'application/json',
- ));
- $response = wp_remote_post( $remote_url, $request);
+ $response = qckply_sync_remote_post($remote_url, $updata);
update_option('qckply_upload_image_raw_result',$response);
if ( ! is_wp_error( $response ) ) {
$response_data = json_decode( wp_remote_retrieve_body( $response ), true );
--- a/quick-playground/client-save-playground.php
+++ b/quick-playground/client-save-playground.php
@@ -1,8 +1,107 @@
<?php
+function qckply_clear_sync_token() {
+ delete_option('qckply_sync_token');
+ delete_option('qckply_sync_token_expires');
+}
+
+function qckply_get_sync_token($force_refresh = false) {
+ $sync_code = get_option('qckply_sync_code');
+ $sync_origin = get_option('qckply_sync_origin');
+ $profile = get_option('qckply_profile', 'default');
+ $token = get_option('qckply_sync_token', '');
+ $expires = intval(get_option('qckply_sync_token_expires', 0));
+
+ if(!$force_refresh && !empty($token) && $expires > (time() + MINUTE_IN_SECONDS)) {
+ return $token;
+ }
+
+ if(empty($sync_code) || empty($sync_origin) || empty($profile)) {
+ qckply_clear_sync_token();
+ return '';
+ }
+
+ $response = wp_remote_post(
+ $sync_origin.'/wp-json/quickplayground/v1/authorize_sync/'.$profile,
+ [
+ 'body' => wp_json_encode(['sync_code' => $sync_code]),
+ 'headers' => ['Content-Type' => 'application/json'],
+ 'timeout' => 20,
+ ]
+ );
+
+ if(is_wp_error($response)) {
+ qckply_clear_sync_token();
+ return '';
+ }
+
+ if(200 !== wp_remote_retrieve_response_code($response)) {
+ qckply_clear_sync_token();
+ return '';
+ }
+
+ $payload = json_decode(wp_remote_retrieve_body($response), true);
+
+ if(empty($payload['sync_token']) || empty($payload['expires'])) {
+ qckply_clear_sync_token();
+ return '';
+ }
+
+ update_option('qckply_sync_token', sanitize_text_field($payload['sync_token']));
+ update_option('qckply_sync_token_expires', intval($payload['expires']));
+
+ return sanitize_text_field($payload['sync_token']);
+}
+
+function qckply_prepare_sync_payload($payload, $force_refresh = false) {
+ $sync_token = qckply_get_sync_token($force_refresh);
+
+ if(empty($sync_token)) {
+ return false;
+ }
+
+ if(!is_array($payload)) {
+ $payload = [];
+ }
+
+ unset($payload['sync_code']);
+ $payload['sync_token'] = $sync_token;
+
+ return $payload;
+}
+
+function qckply_sync_remote_post($url, $payload) {
+ $prepared = qckply_prepare_sync_payload($payload);
+
+ if(false === $prepared) {
+ return new WP_Error('qckply_sync_token_missing', __('Unable to authorize Quick Playground sync.', 'quick-playground'));
+ }
+
+ $body = qckply_json_outgoing(wp_json_encode($prepared));
+ $response = wp_remote_post($url, [
+ 'body' => $body,
+ 'headers' => ['Content-Type' => 'application/json'],
+ ]);
+
+ $status_code = wp_remote_retrieve_response_code($response);
+
+ if(!is_wp_error($response) && in_array($status_code, [401, 403], true)) {
+ $prepared = qckply_prepare_sync_payload($payload, true);
+
+ if(false !== $prepared) {
+ $body = qckply_json_outgoing(wp_json_encode($prepared));
+ $response = wp_remote_post($url, [
+ 'body' => $body,
+ 'headers' => ['Content-Type' => 'application/json'],
+ ]);
+ }
+ }
+
+ return $response;
+}
+
function qckply_clone_save_playground($clone) {
global $wpdb;
- $qckply_sync_code = $clone['sync_code'];;
$profile = get_option("qckply_profile",'default');
printf('<h2>Saving Playground Data for %s</h2>',esc_html($profile));
$sync_origin = get_option('qckply_sync_origin');
@@ -49,11 +148,8 @@
}
else
printf('<h3>Save %d Posts and %s Related Items</h3><p>Sample JSON: %s</p>',empty($clone['posts']) ? 0 : sizeof($clone['posts']),empty($clone['related']) ? 0 : sizeof($clone['related']),esc_html(substr($json,0,300)));
- $json = qckply_json_outgoing($json,$sync_origin.$site_dir);
- $response = wp_remote_post($save_posts_url, array(
- 'body' => $json,
- 'headers' => array('Content-Type' => 'application/json')
- ));
+ $payload = json_decode($json, true);
+ $response = qckply_sync_remote_post($save_posts_url, $payload);
$status_code = wp_remote_retrieve_response_code( $response );
if(is_wp_error($response)) {
echo '<p>Error: '.esc_html( htmlentities($response->get_error_message()) ).'</p>';
@@ -67,7 +163,7 @@
echo '<p>If you see this repeatedly, please report the issue via <a href="https://wordpress.org/support/plugin/quick-playground/">https://wordpress.org/support/plugin/quick-playground/</a></p>';
$file = 'quickplayground_posts_'.$profile.'.json';
$filename = trailingslashit($local_directories['site_uploads']).$file;
- file_put_contents($filename,$json);
+ file_put_contents($filename, qckply_json_outgoing(wp_json_encode($payload)));
return;
}
else {
@@ -96,13 +192,8 @@
$clone['settings'][$option] = $v;
}
$clone = apply_filters('qckply_clone_save_settings',$clone);
- $clone['sync_code'] = $qckply_sync_code;
printf('<h3>Saving %d Settings</h3>',esc_html(sizeof($clone['settings'])));
- $clone = qckply_json_outgoing(json_encode($clone),$sync_origin.$site_dir);
- $response = wp_remote_post($save_settings_url, array(
- 'body' => $clone,
- 'headers' => array('Content-Type' => 'application/json')
- ));
+ $response = qckply_sync_remote_post($save_settings_url, $clone);
if(is_wp_error($response)) {
echo '<p>Error: '.esc_html( htmlentities($response->get_error_message()) ).'</p>';
@@ -164,17 +255,12 @@
*/
$clone = qckply_custom_tables_clone([]);
- $clone['sync_code'] = $qckply_sync_code;
printf('<h3>Checking for Custom Table Data</h3><p>%s</p>',esc_html( var_export(!empty($clone['custom_tables']),true)) );
if(!empty($clone['custom_tables']))
{
echo '<h3>Saving Custom Table Data</h3>';
- $clone = qckply_json_outgoing(json_encode($clone),$sync_origin.$site_dir);
- $response = wp_remote_post($save_custom_url, array(
- 'body' => $clone,
- 'headers' => array('Content-Type' => 'application/json')
- ));
+ $response = qckply_sync_remote_post($save_custom_url, $clone);
if(is_wp_error($response)) {
echo '<p>Error: '.esc_html( htmlentities($response->get_error_message()) ).'</p>';
@@ -249,6 +335,7 @@
}
$qckply_sync_code = sanitize_text_field( wp_unslash ( $_POST['qckply_sync_code'] ) );
update_option('qckply_sync_code',$qckply_sync_code);
+ qckply_clear_sync_token();
printf(__('<p>Sync Code %s saved. <a href="%s">Reload page</a>.</p>', 'quick-playground'), esc_html($qckply_sync_code), esc_attr($action));
}
}
@@ -283,9 +370,13 @@
if($no_cache) $imgurl .= '&nocache=1';
$sync_date = get_option('qckply_sync_date');
$action = admin_url('admin.php?page=qckply_save');
+ if(empty(qckply_get_sync_token())) {
+ echo '<div class="notice notice-error"><p>'.esc_html__('Unable to authorize sync with the live website. Re-enter the sync code and try again.', 'quick-playground').'</p></div>';
+ qckply_clear_sync_token();
+ return;
+ }
$clone = qckply_posts();
error_log('play_posts '.sizeof($clone['posts']));
- $clone['sync_code'] = $qckply_sync_code;
qckply_clone_save_playground($clone);
set_transient('qckply_messages_updated',false);
}
--- a/quick-playground/expro-api.php
+++ b/quick-playground/expro-api.php
@@ -40,18 +40,7 @@
* @return bool True if allowed.
*/
public function get_items_permissions_check($request) {
- $profile = $request['profile'];
- $code = qckply_cloning_code($profile);
- if(empty($code))
- return false;
- $params = $request->get_json_params();
- $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
- if($code == $sync_code)
- return true;
- else {
- set_transient('invalid_sync_code',$sync_code);
- return false;
- }
+ return qckply_require_sync_session($request);
}
/**
@@ -148,7 +137,7 @@
$namespace = 'quickplayground/v1';
- $path = 'sync_ids';
+ $path = 'sync_ids/(?P<profile>[a-z0-9_]+)';
register_rest_route( $namespace, '/' . $path, [
@@ -175,7 +164,7 @@
* @return bool True if allowed.
*/
public function get_items_permissions_check($request) {
- return 'https://playground.wordpress.net/' == $_SERVER['HTTP_REFERER'];
+ return qckply_require_sync_session($request);
}
/**
@@ -244,18 +233,7 @@
* @return bool True if allowed.
*/
public function get_items_permissions_check($request) {
- $profile = $request['profile'];
- $code = qckply_cloning_code($profile);
- if(empty($code))
- return false;
- $params = $request->get_json_params();
- $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
- if($code == $sync_code)
- return true;
- else {
- set_transient('invalid_sync_code',$sync_code);
- return false;
- }
+ return qckply_require_sync_session($request);
}
/**
@@ -327,18 +305,7 @@
* @return bool True if allowed.
*/
public function get_items_permissions_check($request) {
- $profile = $request['profile'];
- $code = qckply_cloning_code($profile);
- if(empty($code))
- return false;
- $params = $request->get_json_params();
- $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
- if($code == $sync_code)
- return true;
- else {
- set_transient('invalid_sync_code',$sync_code);
- return false;
- }
+ return qckply_require_sync_session($request);
}
/**
@@ -411,18 +378,7 @@
* @return bool True if allowed.
*/
public function get_items_permissions_check($request) {
- $profile = $request['profile'];
- $code = qckply_cloning_code($profile);
- if(empty($code))
- return false;
- $params = $request->get_json_params();
- $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
- if($code == $sync_code)
- return true;
- else {
- set_transient('invalid_sync_code',$sync_code);
- return false;
- }
+ return qckply_require_sync_session($request);
}
/**
@@ -502,18 +458,7 @@
* @return bool True if allowed.
*/
public function get_items_permissions_check($request) {
- $profile = $request['profile'];
- $code = qckply_cloning_code($profile);
- if(empty($code))
- return false;
- $params = $request->get_json_params();
- $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
- if($code == $sync_code)
- return true;
- else {
- set_transient('invalid_sync_code',$sync_code);
- return false;
- }
+ return qckply_require_sync_session($request);
}
/**
@@ -533,12 +478,7 @@
$qckply_uploads_url = $qckply_directories['uploads_url'];
$qckply_site_uploads_url = $qckply_directories['site_uploads_url'];
$params = $request->get_json_params();
- $code = get_option('qckply_sync_code_'.$profile,'');
- $params = $request->get_json_params();
- $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
- $sync_response['sync_code'] = $sync_code;
- $sync_response['correct_code'] = $code;
- $filename = sanitize_text_field($params['filename']);
+ $filename = empty($params['filename']) ? '' : sanitize_file_name(wp_basename($params['filename']));
$last_image = get_transient('qckply_last_image_uploaded');
if($last_image == $filename) {
$sync_response['message'] = 'duplicate image';
@@ -556,10 +496,41 @@
return $response;
}
else {
+ $filedata = base64_decode($params['base64'], true);
+ $image_info = false;
+
+ if(false !== $filedata) {
+ $image_info = @getimagesizefromstring($filedata);
+ }
+
+ $allowed_mimes = apply_filters('qckply_allowed_upload_mimes', [
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/gif' => 'gif',
+ 'image/webp' => 'webp',
+ ]);
+
+ if(false === $filedata || empty($image_info['mime']) || empty($allowed_mimes[$image_info['mime']])) {
+ $sync_response['message'] = 'invalid image data';
+ ob_clean();
+ return new WP_REST_Response($sync_response, 400, [
+ 'Access-Control-Allow-Origin' => '*',
+ 'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
+ 'Access-Control-Allow-Headers' => 'Content-Type',
+ ]);
+ }
+
+ $base_name = pathinfo($filename, PATHINFO_FILENAME);
+ $base_name = sanitize_file_name($base_name);
+
+ if(empty($base_name)) {
+ $base_name = 'playground-image';
+ }
+
+ $filename = wp_unique_filename($qckply_site_uploads, $base_name.'.'.$allowed_mimes[$image_info['mime']]);
set_transient('qckply_last_image_uploaded',$filename);
- $filedata = base64_decode($params['base64']);
- $newpath = $qckply_site_uploads.'/'.$filename;
- $newurl = $qckply_site_uploads_url.'/'.$filename;
+ $newpath = trailingslashit($qckply_site_uploads).$filename;
+ $newurl = trailingslashit($qckply_site_uploads_url).$filename;
$saved = file_put_contents($newpath,$filedata);
$parent_id = isset($params['post_parent']) ? intval($params['post_parent']) : 0;
$sync_response['message'] = 'saving to '.$newpath .' '.var_export($saved,true);
@@ -587,6 +558,56 @@
return $response;
}
}
+
+class Quick_Playground_Authorize_Sync extends WP_REST_Controller {
+
+ public function register_routes() {
+
+ $namespace = 'quickplayground/v1';
+
+ $path = 'authorize_sync/(?P<profile>[a-z0-9_-]+)';
+
+ register_rest_route( $namespace, '/' . $path, [
+
+ array(
+
+ 'methods' => 'POST',
+
+ 'callback' => array( $this, 'get_items' ),
+
+ 'permission_callback' => '__return_true'
+
+ ),
+
+ ]);
+
+ }
+
+ public function get_items($request) {
+ $profile = sanitize_text_field($request['profile']);
+ $params = $request->get_json_params();
+ $supplied_code = empty($params['sync_code']) ? '' : sanitize_text_field($params['sync_code']);
+ $stored_code = qckply_get_sync_code($profile);
+
+ if(empty($stored_code)) {
+ return new WP_Error('qckply_sync_disabled', __('Quick Playground sync is not enabled for this profile.', 'quick-playground'), ['status' => 403]);
+ }
+
+ if(empty($supplied_code) || !hash_equals($stored_code, $supplied_code)) {
+ set_transient('invalid_sync_code', $supplied_code, HOUR_IN_SECONDS);
+ return new WP_Error('qckply_sync_code_invalid', __('Quick Playground sync code is invalid.', 'quick-playground'), ['status' => 403]);
+ }
+
+ $session = qckply_issue_sync_session($profile);
+ $headers = [
+ 'Access-Control-Allow-Origin' => '*',
+ 'Access-Control-Allow-Methods' => 'POST, OPTIONS',
+ 'Access-Control-Allow-Headers' => 'Content-Type, Authorization',
+ ];
+
+ return new WP_REST_Response($session, 200, $headers);
+ }
+}
/**
* REST controller for saving custom table content in the playground.
*/
@@ -624,18 +645,7 @@
* @return bool True if allowed.
*/
public function get_items_permissions_check($request) {
- $profile = $request['profile'];
- $code = qckply_cloning_code($profile);
- if(empty($code))
- return false;
- $params = $request->get_json_params();
- $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
- if($code == $sync_code)
- return true;
- else {
- set_transient('invalid_sync_code',$sync_code);
- return false;
- }
+ return qckply_require_sync_session($request);
}
/**
@@ -701,18 +711,7 @@
* @return bool True if allowed.
*/
public function get_items_permissions_check($request) {
- $profile = $request['profile'];
- $code = qckply_cloning_code($profile);
- if(empty($code))
- return false;
- $params = $request->get_json_params();
- $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
- if($code == $sync_code)
- return true;
- else {
- set_transient('invalid_sync_code',$sync_code);
- return false;
- }
+ return qckply_require_sync_session($request);
}
/**
@@ -812,6 +811,8 @@
}
add_action('rest_api_init', function () {
+ $hook = new Quick_Playground_Authorize_Sync();
+ $hook->register_routes();
$hook = new Quick_Playground_Save_Posts();
$hook->register_routes();
$hook = new Quick_Playground_Save_Settings();
--- a/quick-playground/expro-filters.php
+++ b/quick-playground/expro-filters.php
@@ -90,18 +90,138 @@
return;
}
+function qckply_get_sync_code($profile) {
+ return (string) get_option('qckply_sync_code_'.$profile, '');
+}
+
+function qckply_get_sync_sessions_option_name($profile) {
+ return 'qckply_sync_sessions_'.$profile;
+}
+
+function qckply_get_sync_sessions($profile) {
+ $sessions = get_option(qckply_get_sync_sessions_option_name($profile), []);
+
+ if(!is_array($sessions)) {
+ $sessions = [];
+ }
+
+ $now = time();
+ $changed = false;
+
+ foreach($sessions as $session_id => $session) {
+ if(empty($session['expires']) || intval($session['expires']) <= $now) {
+ unset($sessions[$session_id]);
+ $changed = true;
+ }
+ }
+
+ if($changed) {
+ update_option(qckply_get_sync_sessions_option_name($profile), $sessions, false);
+ }
+
+ return $sessions;
+}
+
+function qckply_clear_sync_sessions($profile) {
+ delete_option(qckply_get_sync_sessions_option_name($profile));
+}
+
+function qckply_issue_sync_session($profile) {
+ $sessions = qckply_get_sync_sessions($profile);
+ $session_id = wp_generate_password(20, false, false);
+ $session_secret = wp_generate_password(48, false, false);
+ $ttl = absint(apply_filters('qckply_sync_session_ttl', 15 * MINUTE_IN_SECONDS, $profile));
+
+ if($ttl < MINUTE_IN_SECONDS) {
+ $ttl = MINUTE_IN_SECONDS;
+ }
+
+ $expires = time() + $ttl;
+
+ $sessions[$session_id] = [
+ 'token_hash' => wp_hash_password($session_secret),
+ 'created' => time(),
+ 'expires' => $expires,
+ ];
+
+ update_option(qckply_get_sync_sessions_option_name($profile), $sessions, false);
+
+ return [
+ 'sync_token' => $session_id.'.'.$session_secret,
+ 'expires' => $expires,
+ 'expires_in' => max(0, $expires - time()),
+ ];
+}
+
+function qckply_get_request_sync_token($request) {
+ $params = $request->get_json_params();
+
+ if(is_array($params) && !empty($params['sync_token'])) {
+ return sanitize_text_field($params['sync_token']);
+ }
+
+ $authorization = $request->get_header('authorization');
+
+ if(!empty($authorization) && preg_match('/^Bearers+(.+)$/i', $authorization, $matches)) {
+ return sanitize_text_field($matches[1]);
+ }
+
+ return '';
+}
+
+function qckply_verify_sync_session($profile, $sync_token) {
+ if(empty($sync_token)) {
+ return new WP_Error('qckply_sync_token_missing', __('Quick Playground sync authorization is required.', 'quick-playground'), ['status' => 403]);
+ }
+
+ $parts = explode('.', $sync_token, 2);
+
+ if(2 !== count($parts) || empty($parts[0]) || empty($parts[1])) {
+ return new WP_Error('qckply_sync_token_invalid', __('Quick Playground sync authorization is invalid.', 'quick-playground'), ['status' => 403]);
+ }
+
+ list($session_id, $session_secret) = $parts;
+ $sessions = qckply_get_sync_sessions($profile);
+
+ if(empty($sessions[$session_id])) {
+ return new WP_Error('qckply_sync_token_expired', __('Quick Playground sync authorization has expired.', 'quick-playground'), ['status' => 403]);
+ }
+
+ $session = $sessions[$session_id];
+
+ if(empty($session['token_hash']) || !wp_check_password($session_secret, $session['token_hash'])) {
+ return new WP_Error('qckply_sync_token_invalid', __('Quick Playground sync authorization is invalid.', 'quick-playground'), ['status' => 403]);
+ }
+
+ return true;
+}
+
+function qckply_require_sync_session($request, $profile = '') {
+ if(empty($profile) && !empty($request['profile'])) {
+ $profile = sanitize_text_field($request['profile']);
+ }
+
+ if(empty($profile)) {
+ return new WP_Error('qckply_sync_profile_missing', __('Quick Playground sync profile is required.', 'quick-playground'), ['status' => 400]);
+ }
+
+ return qckply_verify_sync_session($profile, qckply_get_request_sync_token($request));
+}
+
function qckply_cloning_code($profile) {
$code = '';
if(!empty($_POST['set_sync_code']) && '1' == $_POST['set_sync_code'] && wp_verify_nonce( $_POST['playground'], 'quickplayground' )) {
$code = wp_generate_password(20, false, false);
update_option('qckply_sync_code_'.$profile,$code);
+ qckply_clear_sync_sessions($profile);
}
elseif(isset($_POST['set_sync_code'])) {
delete_option('qckply_sync_code_'.$profile);
+ qckply_clear_sync_sessions($profile);
$code = '';
}
else {
- $code = get_option('qckply_sync_code_'.$profile,'');
+ $code = qckply_get_sync_code($profile);
}
return $code;
}
--- a/quick-playground/expro-quickplayground-sync.php
+++ b/quick-playground/expro-quickplayground-sync.php
@@ -6,10 +6,39 @@
* Displays a preview of proposed changes and, upon approval, applies changes to posts, meta, terms, taxonomies, and relationships.
*/
function qckply_sync() {
- if(!empty($_POST) && !wp_verify_nonce( sanitize_text_field( wp_unslash ( $_REQUEST['playground'])), 'quickplayground' ) )
- {
- echo '<h2>'.esc_html__('Security Error','quick-playground').'</h2>';
- return;
+ $sync_capability = apply_filters('qckply_sync_approval_capability', 'manage_options');
+ if(!current_user_can($sync_capability)) {
+ wp_die(esc_html__('You are not allowed to approve Quick Playground sync changes.', 'quick-playground'), 403);
+ }
+
+ if(!empty($_POST)) {
+ $settings_request = isset($_POST['import_settings']) || isset($_POST['import_setting']);
+ $posts_request = isset($_POST['preview_button']) || isset($_POST['import_button']) || isset($_POST['import_from']) || isset($_POST['new_posts']);
+ $switch_request = isset($_POST['switch_theme']);
+
+ if($settings_request) {
+ $nonce = isset($_POST['qckply_sync_settings_nonce']) ? sanitize_text_field(wp_unslash($_POST['qckply_sync_settings_nonce'])) : '';
+ if(!wp_verify_nonce($nonce, 'qckply_sync_settings')) {
+ echo '<h2>'.esc_html__('Security Error','quick-playground').'</h2>';
+ return;
+ }
+ }
+
+ if($posts_request) {
+ $nonce = isset($_POST['qckply_sync_posts_nonce']) ? sanitize_text_field(wp_unslash($_POST['qckply_sync_posts_nonce'])) : '';
+ if(!wp_verify_nonce($nonce, 'qckply_sync_posts')) {
+ echo '<h2>'.esc_html__('Security Error','quick-playground').'</h2>';
+ return;
+ }
+ }
+
+ if($switch_request) {
+ $nonce = isset($_POST['qckply_sync_switch_theme_nonce']) ? sanitize_text_field(wp_unslash($_POST['qckply_sync_switch_theme_nonce'])) : '';
+ if(!wp_verify_nonce($nonce, 'qckply_sync_switch_theme')) {
+ echo '<h2>'.esc_html__('Security Error','quick-playground').'</h2>';
+ return;
+ }
+ }
}
global $wpdb;
@@ -22,7 +51,15 @@
$playground_imported_items = sizeof($playground_imported);
$profile = (empty($_REQUEST['profile'])) ? 'default' : sanitize_text_field($_REQUEST['profile']);
+ $saved = ['posts' => [], 'settings' => [], 'related' => []];
$shown = [];
+ $audit_counts = [
+ 'settings_imported' => 0,
+ 'posts_inserted' => 0,
+ 'posts_updated' => 0,
+ 'meta_updated' => 0,
+ 'taxonomies_updated' => 0,
+ ];
$pp = get_option('qckply_profiles',array('default'));
$getnonce = wp_create_nonce('quickplayground');
@@ -64,17 +101,51 @@
else {
echo '<p>Theme is not installed locally: '.esc_html($new_stylesheet).'</p>';
}
- }
+
+ qckply_sync_audit_log('switch_theme', $profile, [
+ 'requested_stylesheet' => $new_stylesheet,
+ 'theme_exists' => $theme->exists(),
+ ]);
+ }
+
+ $allowed_settings = apply_filters('qckply_sync_allowed_settings', [
+ 'blogname',
+ 'blogdescription',
+ 'timezone_string',
+ 'date_format',
+ 'time_format',
+ 'start_of_week',
+ 'show_on_front',
+ 'page_on_front',
+ 'page_for_posts',
+ 'posts_per_page',
+ 'permalink_structure',
+ 'default_comment_status',
+ 'default_ping_status',
+ 'thumbnail_size_w',
+ 'thumbnail_size_h',
+ 'thumbnail_crop',
+ 'medium_size_w',
+ 'medium_size_h',
+ 'large_size_w',
+ 'large_size_h',
+ ]);
if(isset($_POST['import_setting'])) {
foreach($_POST['import_setting'] as $option_name) {
$option_name = sanitize_text_field($option_name);
+ if(!in_array($option_name, $allowed_settings, true)) {
+ continue;
+ }
if(isset($saved['settings'][$option_name])) {
$option_value = $saved['settings'][$option_name];
update_option($option_name,maybe_unserialize($option_value));
+ $audit_counts['settings_imported']++;
printf('<p>Imported setting %s</p>',esc_html($option_name));
}
}
+
+ qckply_sync_audit_log('import_settings', $profile, $audit_counts);
}
if(isset($_POST['import_from'])) {
$terms_checked = [];
@@ -92,8 +163,10 @@
if(!empty($playground_imported[$iddate])) {
printf('<p>Updating previously imported imported %s</p>',esc_html($saved_post['post_title']));
$saved_post['ID'] = $playground_imported[$iddate];
- if(!empty($_POST['import_button']))
+ if(!empty($_POST['import_button'])) {
wp_update_post($saved_post);
+ $audit_counts['posts_updated']++;
+ }
}
elseif(in_array($id,$new_posts)) {
printf('<p>New: %s</p>',esc_html($saved_post['post_title']));
@@ -103,6 +176,7 @@
{
$id = wp_insert_post($new_post);
$playground_imported[$iddate] = $id;
+ $audit_counts['posts_inserted']++;
}
else
{
@@ -114,6 +188,7 @@
if(!empty($_POST['import_button']))
{
wp_update_post($saved_post);
+ $audit_counts['posts_updated']++;
}
}
$metadata = empty($saved['related']['p'.$saved_post['ID']]['postmeta']) ? [] : $saved['related']['p'.$saved_post['ID']]['postmeta'];
@@ -122,6 +197,7 @@
if(!empty($_POST['import_button']))
{
update_post_meta($id,$meta['meta_key'],$meta['meta_value']);
+ $audit_counts['meta_updated']++;
}
if(!empty($_POST['preview_button']) || !empty($_POST['show_details'])) {
printf('<p>Updated post %d meta %s: %s</p>',esc_html($id),esc_html($meta['meta_key']),esc_html($meta['meta_value']));
@@ -138,6 +214,7 @@
{
//insert new taxonomy terms if needed, do not delete existing terms
wp_set_post_terms($id, $tax_terms, $taxonomy, false);
+ $audit_counts['taxonomies_updated']++;
}
if(!empty($_POST['preview_button']) || !empty($_POST['show_details'])) {
printf('<p>Set terms for taxonomy %s: %s</p>',esc_html($taxonomy),esc_html(implode(', ',$tax_terms)) );
@@ -152,6 +229,14 @@
// Save the updated playground import data
update_option('playground_imported',$playground_imported);
}
+
+ if(isset($_POST['preview_button'])) {
+ qckply_sync_audit_log('preview_content', $profile, $audit_counts);
+ }
+
+ if(isset($_POST['import_button'])) {
+ qckply_sync_audit_log('import_content', $profile, $audit_counts);
+ }
}
$theme_options = ['default'=>'','changed'=>''];
$internal_settings = ['copy_blogs','copy_events','origin_template','cache_created'];
@@ -166,6 +251,8 @@
$theme_options['default'] = $option_value;
if(in_array($option_name,$internal_settings) || strpos($option_name,'qckply_') === 0 || strpos($option_name,'quickplay_') === 0)
continue;
+ if(!in_array($option_name, $allowed_settings, true))
+ continue;
$current_value = get_option($option_name);
if($option_value != $current_value) {
$settings_output .= sprintf('<p><input type="checkbox" name="import_setting[]" value="%s" /> Import: %s</p><pre>%s</pre>',esc_attr($option_name),esc_html($option_name),esc_html(var_export($option_value,true)));
@@ -181,7 +268,8 @@
else {
printf('<h1>Saved Data from Playground "%s"</h1>',esc_html($profile));
printf('<form method="post" action="%s">',esc_attr($action));
- wp_nonce_field('quickplayground','playground',true,true);
+ wp_nonce_field('qckply_sync_posts','qckply_sync_posts_nonce',true,true);
+ wp_nonce_field('qckply_sync_switch_theme','qckply_sync_switch_theme_nonce',true,true);
$taxtracker = [];
foreach($saved['posts'] as $index => $saved_post) {
$out = '';
@@ -248,7 +336,7 @@
if(!empty($settings_output)) {
printf('<h2>Settings to import from Playground "%s"</h2>',esc_html($profile));
printf('<form method="post" action="%s">',esc_attr($action));
- wp_nonce_field('quickplayground','playground',true,true);
+ wp_nonce_field('qckply_sync_settings','qckply_sync_settings_nonce',true,true);
echo $settings_output;
printf('<input type="hidden" name="profile" value="%s" >',esc_attr($profile));
submit_button('Import Selected Settings','primary','import_settings',false);
@@ -267,3 +355,31 @@
}
return $output;
}
+
+function qckply_sync_audit_log($event, $profile, $details = []) {
+ $log = get_option('qckply_sync_audit_log', []);
+
+ if(!is_array($log)) {
+ $log = [];
+ }
+
+ $entry = [
+ 'time' => gmdate('c'),
+ 'event' => sanitize_text_field($event),
+ 'profile' => sanitize_text_field($profile),
+ 'user_id' => get_current_user_id(),
+ 'details' => is_array($details) ? $details : ['raw' => (string) $details],
+ ];
+
+ $log[] = $entry;
+ $max_items = absint(apply_filters('qckply_sync_audit_log_limit', 100));
+ if($max_items < 10) {
+ $max_items = 10;
+ }
+
+ if(count($log) > $max_items) {
+ $log = array_slice($log, (count($log) - $max_items));
+ }
+
+ update_option('qckply_sync_audit_log', $log, false);
+}
--- a/quick-playground/quick-playground.php
+++ b/quick-playground/quick-playground.php
@@ -3,11 +3,11 @@
* Plugin Name: Quick Playground
* Plugin URI: https://quickplayground.com
* Description: Preview your content in different themes or test plugins using WordPress Playground. Quickly create Theme and Plugin demo, testing, and staging websites.
- * Version: 1.3.1
+ * Version: 1.3.2
* Author: David F. Carr
-* License: GPL2
-* Text Domain: quick-playground
-* Domain Path: /languages
+ * License: GPL2
+ * Text Domain: quick-playground
+ * Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
require_once('includes.php');