__( 'Not allowed.', '3pika-gallery' ) ), 403 ); } } /** * Decode a JSON string sent by the builder into an array of plain text values. * * Only a string is accepted. Every key and value of the decoded array is sanitized in clean(); * after that the caller validates it field by field (TPIKA_Post_Type::sanitize_images(), ::sanitize_filters(), * TPIKA_Settings::sanitize()), which casts IDs with absint(), colors with sanitize_hex_color() and checks * options against the list of allowed values. * * @param string $json JSON string, already unslashed. * @return array */ private function decode( $json ) { if ( ! is_string( $json ) || '' === $json ) { return array(); } $data = json_decode( $json, true ); return is_array( $data ) ? $this->clean( $data ) : array(); } /** * Sanitize a decoded array: keys with sanitize_key(), URLs with esc_url_raw(), other text with * sanitize_textarea_field(). Numbers and booleans keep their type, anything else is dropped. * * @param array $data Decoded data. * @param int $depth Nesting level (the builder never sends more than three). * @return array */ private function clean( $data, $depth = 0 ) { $out = array(); foreach ( $data as $key => $value ) { $key = is_int( $key ) ? $key : sanitize_key( $key ); if ( is_array( $value ) ) { if ( $depth < 4 ) { $out[ $key ] = $this->clean( $value, $depth + 1 ); } } elseif ( is_string( $value ) ) { $out[ $key ] = in_array( $key, array( 'link', 'url' ), true ) ? esc_url_raw( $value ) : sanitize_textarea_field( $value ); } elseif ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) { $out[ $key ] = $value; } } return $out; } public function save_gallery() { check_ajax_referer( 'tpika_admin', 'nonce' ); $this->guard(); $id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0; if ( $id && ( TPIKA_Post_Type::TYPE !== get_post_type( $id ) || ! current_user_can( 'edit_post', $id ) ) ) { wp_send_json_error( array( 'message' => __( 'Not allowed.', '3pika-gallery' ) ), 403 ); } // The three lists arrive as JSON strings: sanitized in decode(), then validated field by field. // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- see decode(). $raw_filters = isset( $_POST['filters'] ) && is_string( $_POST['filters'] ) ? wp_unslash( $_POST['filters'] ) : ''; $raw_images = isset( $_POST['images'] ) && is_string( $_POST['images'] ) ? wp_unslash( $_POST['images'] ) : ''; $raw_settings = isset( $_POST['settings'] ) && is_string( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : ''; // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : ''; $filters = TPIKA_Post_Type::sanitize_filters( $this->decode( $raw_filters ) ); $images = TPIKA_Post_Type::sanitize_images( $this->decode( $raw_images ), wp_list_pluck( $filters, 'id' ) ); $existing = $id ? TPIKA_Post_Type::get( $id ) : null; $base = $existing ? $existing['settings'] : TPIKA_Settings::get(); $input = $this->decode( $raw_settings ); $settings = TPIKA_Settings::sanitize( $input, $base ); // Keep global-only keys out of gallery meta. unset( $settings['load_assets_global'], $settings['delete_on_uninstall'] ); $result = TPIKA_Post_Type::save( $id, $title, $images, $filters, $settings ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( array( 'id' => $result, 'shortcode' => tpika_shortcode( $result ), 'editUrl' => tpika_admin_url( 'tpika-new', array( 'id' => $result ) ), 'previewUrl' => tpika_preview_url( $result ), 'images' => TPIKA_Post_Type::images_for_admin( $images ), 'filters' => $filters, ) ); } public function get_galleries() { check_ajax_referer( 'tpika_admin', 'nonce' ); $this->guard(); $out = array(); foreach ( tpika_gallery_options() as $id => $title ) { $out[] = array( 'id' => $id, 'title' => $title, ); } wp_send_json_success( $out ); } }