self::FORMAT, 'version' => self::VERSION, 'generator' => (array) \AccessibleForms\Extensions::filter( 'accessible_forms_export_generator', array( 'free' => \AccessibleForms\VERSION, 'extension_api' => \AccessibleForms\API_VERSION, ) ), 'created_at' => gmdate( 'Y-m-d\TH:i:s\Z' ), 'contains' => array_values( array_merge( $forms ? array( 'forms' ) : array(), $settings ? array( 'settings' ) : array(), array_keys( $extra ) ) ), 'forms' => array_values( $forms ), ); if ( $settings ) { $package['settings'] = $settings; } foreach ( $extra as $name => $section ) { if ( ! isset( $package[ $name ] ) ) { $package[ $name ] = $section; } } $package['checksum'] = self::checksum( $package ); return $package; } /** Pretty, readable JSON. */ public static function encode( array $package ) { return (string) wp_json_encode( $package, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); } /** Refuse a download that this importer cannot read, without truncating anything. */ public static function download( array $package ) { $json = self::encode( $package ); $decoded = self::decode( $json ); return is_wp_error( $decoded ) ? $decoded : $json; } /** * Read a file. Values are left unsanitized here; the importer sanitizes every * form through Schema::sanitize() and every setting through the settings rules. * * @param mixed $json File contents. * @return array|\WP_Error Package with `checksum_ok` and `source_version` added. */ public static function decode( $json ) { if ( ! is_string( $json ) || '' === trim( $json ) ) { return new \WP_Error( 'empty', __( 'Choose an Accessible Forms export file.', 'accessible-forms-by-accessible-org' ) ); } if ( strlen( $json ) > self::MAX_BYTES ) { return new \WP_Error( 'size', __( 'This file is larger than 1 MB. Export fewer forms at a time.', 'accessible-forms-by-accessible-org' ) ); } // A UTF-8 byte order mark from some editors is not part of the JSON. $json = preg_replace( '/^\xEF\xBB\xBF/', '', $json ); $data = json_decode( $json, true, self::MAX_DEPTH ); if ( ! is_array( $data ) ) { return new \WP_Error( 'json', __( 'This file is not valid JSON. Export it again, without editing it.', 'accessible-forms-by-accessible-org' ) ); } // The checksum covers the file as written, before values are cleaned. $checksum_ok = is_string( $data['checksum'] ?? null ) && hash_equals( self::checksum( $data ), $data['checksum'] ); $data = Schema::clean_input( $data ); if ( self::FORMAT !== ( $data['format'] ?? '' ) ) { return new \WP_Error( 'format', __( 'This is not an Accessible Forms export file.', 'accessible-forms-by-accessible-org' ) ); } $version = $data['version'] ?? 0; if ( 1 === $version ) { return self::from_version_1( $data ); } if ( ! is_int( $version ) || $version < 1 ) { return new \WP_Error( 'format', __( 'This is not a supported Accessible Forms export.', 'accessible-forms-by-accessible-org' ) ); } if ( $version > self::VERSION ) { return new \WP_Error( 'newer', __( 'This file was made by a newer version of Accessible Forms. Update the plugin on this site, then import it.', 'accessible-forms-by-accessible-org' ) ); } $forms = $data['forms'] ?? array(); if ( ! is_array( $forms ) || ! array_is_list( $forms ) ) { return new \WP_Error( 'format', __( 'The forms in this file are not readable.', 'accessible-forms-by-accessible-org' ) ); } if ( count( $forms ) > self::MAX_FORMS ) { return new \WP_Error( 'count', __( 'This file has more than 100 forms. Export fewer forms at a time.', 'accessible-forms-by-accessible-org' ) ); } foreach ( $forms as $form ) { if ( ! is_array( $form ) || ! is_array( $form['schema'] ?? null ) ) { return new \WP_Error( 'format', __( 'A form in this file is not readable.', 'accessible-forms-by-accessible-org' ) ); } } if ( isset( $data['settings'] ) && ! is_array( $data['settings'] ) ) { unset( $data['settings'] ); } $data['checksum_ok'] = $checksum_ok; $data['source_version'] = $version; return $data; } /** * SHA-256 of the file's content in a canonical order, without the checksum * itself and the values added when reading. */ public static function checksum( array $package ) { unset( $package['checksum'], $package['checksum_ok'], $package['source_version'] ); return hash( 'sha256', (string) wp_json_encode( self::canonical( $package ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) ); } private static function canonical( $value ) { if ( ! is_array( $value ) ) { return $value; } if ( ! array_is_list( $value ) ) { ksort( $value, SORT_STRING ); } return array_map( array( self::class, 'canonical' ), $value ); } /** A Pro 1.x file holds one form's configuration and nothing else. */ private static function from_version_1( array $data ) { if ( ! is_array( $data['schema'] ?? null ) ) { return new \WP_Error( 'format', __( 'This is not a supported Accessible Forms export.', 'accessible-forms-by-accessible-org' ) ); } $heading = is_string( $data['schema']['heading'] ?? null ) ? $data['schema']['heading'] : ''; return array( 'format' => self::FORMAT, 'version' => self::VERSION, 'contains' => array( 'forms' ), 'forms' => array( array( 'key' => '', 'title' => $heading, 'schema' => $data['schema'], ), ), // Version 1 had no checksum; there is nothing to compare. 'checksum_ok' => true, 'source_version' => 1, ); } }