| @@ -1,120 +1,395 @@ | ||
| 1 | -<?php | |
| 2 | - | |
| 3 | - | |
| 4 | -namespace Jet_Form_Builder\Presets\Sources; | |
| 5 | - | |
| 6 | - | |
| 7 | -use Jet_Form_Builder\Classes\Tools; | |
| 8 | -use Jet_Form_Builder\Dev_Mode\Manager; | |
| 9 | -use Jet_Form_Builder\Exceptions\Preset_Exception; | |
| 10 | -use Jet_Form_Builder\Presets\Types\Base_Preset; | |
| 11 | - | |
| 12 | -abstract class Base_Source { | |
| 13 | - | |
| 14 | - protected $fields_map; | |
| 15 | - protected $field_data = array(); | |
| 16 | - protected $field_args; | |
| 17 | - protected $preset_data; | |
| 18 | - protected $field = '__condition__'; | |
| 19 | - protected $prop; | |
| 20 | - private $src; | |
| 21 | - | |
| 22 | - const FUNC_PREFIX = '_source__'; | |
| 23 | - const EMPTY = ''; | |
| 24 | - | |
| 25 | - abstract public function query_source(); | |
| 26 | - | |
| 27 | - public function __construct( $fields_map, $field_args, $preset_data ) { | |
| 28 | - $this->fields_map = $fields_map; | |
| 29 | - $this->field_args = $field_args; | |
| 30 | - $this->preset_data = $preset_data; | |
| 31 | - $this->field = $this->field_args['name']; | |
| 32 | - $this->field_data = $this->get_field_data(); | |
| 33 | - $this->prop = $this->get_prop(); | |
| 34 | - $this->src = $this->query_source(); | |
| 35 | - } | |
| 36 | - | |
| 37 | - | |
| 38 | - protected function get_field_data() { | |
| 39 | - if ( $this->has_field_in_map() ) { | |
| 40 | - return $this->fields_map[ $this->field ]; | |
| 41 | - } | |
| 42 | - } | |
| 43 | - | |
| 44 | - public function has_field_in_map() { | |
| 45 | - return ( isset( $this->fields_map[ $this->field ] ) && ( isset( $this->fields_map[ $this->field ]['prop'] ) || isset( $this->fields_map[ $this->field ]['key'] ) ) ); | |
| 46 | - } | |
| 47 | - | |
| 48 | - public function src() { | |
| 49 | - return $this->src; | |
| 50 | - } | |
| 51 | - | |
| 52 | - protected function can_get_preset() { | |
| 53 | - return ( ! empty( $this->src() ) && ! is_wp_error( $this->src() ) ); | |
| 54 | - } | |
| 55 | - | |
| 56 | - public function default_prop( string $prop ) { | |
| 57 | - $source = $this->src; | |
| 58 | - | |
| 59 | - if ( isset( $source->$prop ) ) { | |
| 60 | - return $source->$prop; | |
| 61 | - } elseif ( isset( $source->data ) && isset( $source->data->$prop ) ) { | |
| 62 | - return $source->data->$prop; | |
| 63 | - } | |
| 64 | - | |
| 65 | - return self::EMPTY; | |
| 66 | - } | |
| 67 | - | |
| 68 | - protected function get_prop() { | |
| 69 | - return ( ! empty( $this->field_data['prop'] ) ? $this->field_data['prop'] : 'post_title' ); | |
| 70 | - } | |
| 71 | - | |
| 72 | - public function get_result_on_prop() { | |
| 73 | - $func_name = self::FUNC_PREFIX . $this->prop; | |
| 74 | - | |
| 75 | - if ( is_callable( array( $this, $func_name ) ) ) { | |
| 76 | - return call_user_func( array( $this, $func_name ) ); | |
| 77 | - } | |
| 78 | - | |
| 79 | - return $this->default_prop( $this->prop ); | |
| 80 | - } | |
| 81 | - | |
| 82 | - final public function result() { | |
| 83 | - if ( ! $this->can_get_preset() ) { | |
| 84 | - return self::EMPTY; | |
| 85 | - } | |
| 86 | - | |
| 87 | - return $this->parse_result_value( $this->get_result_on_prop() ); | |
| 88 | - } | |
| 89 | - | |
| 90 | - | |
| 91 | - /** | |
| 92 | - * Try to get values from request if passed | |
| 93 | - * | |
| 94 | - * @param [type] $args [description] | |
| 95 | - * | |
| 96 | - * @return [type] [description] | |
| 97 | - */ | |
| 98 | - public function maybe_adjust_value() { | |
| 99 | - | |
| 100 | - $value = isset( $this->field_args['default'] ) ? $this->field_args['default'] : ''; | |
| 101 | - $request_val = ! empty( $_REQUEST['values'] ) ? $_REQUEST['values'] : array(); | |
| 102 | - | |
| 103 | - if ( isset( $request_val[ $this->field ] ) ) { | |
| 104 | - $value = $request_val[ $this->field ]; | |
| 105 | - } | |
| 106 | - | |
| 107 | - return $value; | |
| 108 | - | |
| 109 | - } | |
| 110 | - | |
| 111 | - public function parse_result_value( $value ) { | |
| 112 | - // Prepare value for date field | |
| 113 | - if ( 'date-field' === $this->field_args['type'] && Tools::is_valid_timestamp( $value ) ) { | |
| 114 | - $value = date_i18n( 'Y-m-d', $value ); | |
| 115 | - } | |
| 116 | - | |
| 117 | - return $value; | |
| 118 | - } | |
| 119 | - | |
| 120 | -} | |
| 1 | +<?php | |
| 2 | + | |
| 3 | + | |
| 4 | +namespace Jet_Form_Builder\Presets\Sources; | |
| 5 | + | |
| 6 | +use Jet_Form_Builder\Blocks\Types\Base; | |
| 7 | +use Jet_Form_Builder\Exceptions\Preset_Exception; | |
| 8 | +use Jet_Form_Builder\Presets\Preset_Manager; | |
| 9 | +use JFB_Modules\Rich_Content\Macros_Parser; | |
| 10 | + | |
| 11 | +// If this file is called directly, abort. | |
| 12 | +if ( ! defined( 'WPINC' ) ) { | |
| 13 | + die; | |
| 14 | +} | |
| 15 | + | |
| 16 | +abstract class Base_Source { | |
| 17 | + | |
| 18 | + protected $fields_map; | |
| 19 | + protected $field_data = array(); | |
| 20 | + protected $field_args; | |
| 21 | + protected $preset_data; | |
| 22 | + protected $field = '__condition__'; | |
| 23 | + protected $prop; | |
| 24 | + private $src; | |
| 25 | + | |
| 26 | + protected $permission; | |
| 27 | + | |
| 28 | + const FUNC_PREFIX = 'source__'; | |
| 29 | + | |
| 30 | + abstract public function query_source(); | |
| 31 | + | |
| 32 | + abstract public function get_id(); | |
| 33 | + | |
| 34 | + public function condition(): bool { | |
| 35 | + return true; | |
| 36 | + } | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * Field args getter | |
| 40 | + * | |
| 41 | + * @return array | |
| 42 | + */ | |
| 43 | + public function get_field_args() { | |
| 44 | + return $this->field_args; | |
| 45 | + } | |
| 46 | + | |
| 47 | + /** | |
| 48 | + * Fields map getter | |
| 49 | + * | |
| 50 | + * @return array | |
| 51 | + */ | |
| 52 | + public function get_fields_map() { | |
| 53 | + return $this->fields_map; | |
| 54 | + } | |
| 55 | + | |
| 56 | + /** | |
| 57 | + * @param $fields_map | |
| 58 | + * @param $preset_data | |
| 59 | + * @param $args | |
| 60 | + * | |
| 61 | + * @return $this | |
| 62 | + * @throws Preset_Exception | |
| 63 | + */ | |
| 64 | + public function init_source( $fields_map, $preset_data, $args ): Base_Source { | |
| 65 | + $this->field_args = $args; | |
| 66 | + $this->field = $args['name'] ?? ''; | |
| 67 | + $this->fields_map = $fields_map; | |
| 68 | + $this->preset_data = $preset_data; | |
| 69 | + $this->field_data = $this->get_field_data(); | |
| 70 | + $this->prop = $this->get_prop(); | |
| 71 | + | |
| 72 | + return $this; | |
| 73 | + } | |
| 74 | + | |
| 75 | + public function after_init(): Base_Source { | |
| 76 | + return $this; | |
| 77 | + } | |
| 78 | + | |
| 79 | + public function after_register() { | |
| 80 | + } | |
| 81 | + | |
| 82 | + public function on_sanitize(): bool { | |
| 83 | + return true; | |
| 84 | + } | |
| 85 | + | |
| 86 | + public function is_need_prop() { | |
| 87 | + return true; | |
| 88 | + } | |
| 89 | + | |
| 90 | + /** | |
| 91 | + * @return mixed | |
| 92 | + * @throws Preset_Exception | |
| 93 | + */ | |
| 94 | + public function maybe_query_source() { | |
| 95 | + if ( $this->prop ) { | |
| 96 | + $this->src = $this->query_source(); | |
| 97 | + | |
| 98 | + return $this; | |
| 99 | + } | |
| 100 | + | |
| 101 | + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped | |
| 102 | + throw new Preset_Exception( 'Empty `prop` in ' . get_class( $this ), $this->field_data ); | |
| 103 | + } | |
| 104 | + | |
| 105 | + /** | |
| 106 | + * @return mixed | |
| 107 | + * @throws Preset_Exception | |
| 108 | + */ | |
| 109 | + public function get_field_data() { | |
| 110 | + if ( $this->has_field_in_map() ) { | |
| 111 | + return $this->fields_map[ $this->field ]; | |
| 112 | + } | |
| 113 | + // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped | |
| 114 | + throw new Preset_Exception( | |
| 115 | + "Empty `fields_map['{$this->field}']` in " . get_class( $this ), | |
| 116 | + $this->fields_map | |
| 117 | + ); | |
| 118 | + // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped | |
| 119 | + } | |
| 120 | + | |
| 121 | + public function has_field_in_map() { | |
| 122 | + return ( isset( $this->fields_map[ $this->field ]['prop'] ) || isset( $this->fields_map[ $this->field ]['key'] ) ); | |
| 123 | + } | |
| 124 | + | |
| 125 | + /** | |
| 126 | + * @return mixed | |
| 127 | + */ | |
| 128 | + public function src() { | |
| 129 | + return $this->src; | |
| 130 | + } | |
| 131 | + | |
| 132 | + /** | |
| 133 | + * @return mixed | |
| 134 | + * @throws Preset_Exception | |
| 135 | + */ | |
| 136 | + public function safe_src() { | |
| 137 | + $this->throw_if_preset_not_available(); | |
| 138 | + | |
| 139 | + return $this->src(); | |
| 140 | + } | |
| 141 | + | |
| 142 | + /** | |
| 143 | + * @return bool | |
| 144 | + * @throws Preset_Exception | |
| 145 | + */ | |
| 146 | + protected function can_get_preset() { | |
| 147 | + return ( ! empty( $this->src() ) && ! is_wp_error( $this->src() ) ); | |
| 148 | + } | |
| 149 | + | |
| 150 | + /** | |
| 151 | + * Whether this source allows the `restricted: false` opt-out (see | |
| 152 | + * has_permission()) to skip can_get_preset() at all. A source can override | |
| 153 | + * this to return false when its capability must never be made public by a | |
| 154 | + * form's stored, trusted preset configuration. | |
| 155 | + * | |
| 156 | + * Preset_Source_Options_Page intentionally uses the default: a form author | |
| 157 | + * can explicitly publish the selected Options Page field by switching | |
| 158 | + * "Restrict access" off. Request-provided preset data cannot do so because | |
| 159 | + * it never receives the trusted-origin marker. See issues-tracker #20359. | |
| 160 | + * | |
| 161 | + * @return bool | |
| 162 | + */ | |
| 163 | + protected function allows_restriction_bypass(): bool { | |
| 164 | + return true; | |
| 165 | + } | |
| 166 | + | |
| 167 | + /** | |
| 168 | + * The `restricted: false` opt-out below only takes effect when both: | |
| 169 | + * - `_trusts_restriction_flag` is true - a flag set programmatically by | |
| 170 | + * Base_Preset::get_source() (never taken from preset_data itself, so | |
| 171 | + * an attacker can't forge it) from whatever the CALL SITE declared via | |
| 172 | + * Base_Preset::trust_restriction_flag(). Only origins that are | |
| 173 | + * admin-authored declare trust: the form's own preset meta | |
| 174 | + * (General_Preset), a field block's attributes | |
| 175 | + * (Preset_Manager::get_field_value()) and the settings parsed via | |
| 176 | + * jet_fb_parse_dynamic_trusted() (validation rules, date min/max, | |
| 177 | + * conditional blocks, action conditions, dynamic value). Parsing a | |
| 178 | + * submitted field value - jet_fb_parse_dynamic() via Rich_Content - | |
| 179 | + * never does, so a `restricted` flag smuggled through a submitted | |
| 180 | + * value is ignored there. | |
| 181 | + * - allows_restriction_bypass() is true for this source (see above). | |
| 182 | + * | |
| 183 | + * This closes the access-control bypass reported in issues-tracker | |
| 184 | + * #20359 while preserving the documented "Restrict access" editor | |
| 185 | + * toggle for admin-configured presets. | |
| 186 | + * | |
| 187 | + * `jet-form-builder/preset-sanitize` runs on BOTH paths - the opt-out | |
| 188 | + * result as well as can_get_preset() - so an integrator can always | |
| 189 | + * further restrict access, including on presets whose author switched | |
| 190 | + * the toggle off. Returning true from it does not grant access that | |
| 191 | + * can_get_preset() denied on the checked path, because the filter only | |
| 192 | + * ever sees an already-computed decision. | |
| 193 | + * | |
| 194 | + * @return bool | |
| 195 | + * @throws Preset_Exception | |
| 196 | + */ | |
| 197 | + protected function has_permission(): bool { | |
| 198 | + if ( is_null( $this->permission ) ) { | |
| 199 | + $this->permission = apply_filters( | |
| 200 | + 'jet-form-builder/preset-sanitize', | |
| 201 | + $this->allows_restriction_bypass_for_data() ? true : $this->can_get_preset(), | |
| 202 | + $this | |
| 203 | + ); | |
| 204 | + } | |
| 205 | + | |
| 206 | + return $this->permission; | |
| 207 | + } | |
| 208 | + | |
| 209 | + /** | |
| 210 | + * Whether the `restricted: false` opt-out applies to this preset data. | |
| 211 | + * | |
| 212 | + * All four conditions must hold, see has_permission() above. | |
| 213 | + * | |
| 214 | + * @return bool | |
| 215 | + */ | |
| 216 | + private function allows_restriction_bypass_for_data(): bool { | |
| 217 | + return ( | |
| 218 | + ! empty( $this->preset_data['_trusts_restriction_flag'] ) && | |
| 219 | + $this->allows_restriction_bypass() && | |
| 220 | + array_key_exists( 'restricted', $this->preset_data ) && | |
| 221 | + ! $this->preset_data['restricted'] | |
| 222 | + ); | |
| 223 | + } | |
| 224 | + | |
| 225 | + /** | |
| 226 | + * @throws Preset_Exception | |
| 227 | + */ | |
| 228 | + final protected function throw_if_preset_not_available() { | |
| 229 | + if ( ! $this->has_permission() ) { | |
| 230 | + throw new Preset_Exception( static::class . '::can_get_preset return FALSE' ); | |
| 231 | + } | |
| 232 | + } | |
| 233 | + | |
| 234 | + | |
| 235 | + protected function get_prop() { | |
| 236 | + if ( ! $this->is_need_prop() ) { | |
| 237 | + return true; | |
| 238 | + } | |
| 239 | + | |
| 240 | + return ( ! empty( $this->field_data['prop'] ) ? $this->field_data['prop'] : false ); | |
| 241 | + } | |
| 242 | + | |
| 243 | + /** | |
| 244 | + * Public prop getter | |
| 245 | + * | |
| 246 | + * @return string | |
| 247 | + */ | |
| 248 | + public function get_prop_name() { | |
| 249 | + return $this->prop; | |
| 250 | + } | |
| 251 | + | |
| 252 | + /** | |
| 253 | + * @return false|mixed | |
| 254 | + * @throws Preset_Exception | |
| 255 | + */ | |
| 256 | + public function get_result_on_prop() { | |
| 257 | + if ( ! $this->is_need_prop() ) { | |
| 258 | + return $this->src(); | |
| 259 | + } | |
| 260 | + | |
| 261 | + $extra = $this->get_extra_fields(); | |
| 262 | + | |
| 263 | + if ( empty( $extra ) ) { | |
| 264 | + return $this->get_current_value(); | |
| 265 | + } | |
| 266 | + | |
| 267 | + $value = array(); | |
| 268 | + | |
| 269 | + foreach ( $extra as $name => $field ) { | |
| 270 | + $this->before_query_extra_field( $field ); | |
| 271 | + | |
| 272 | + $value[ $name ] = $this->get_current_value(); | |
| 273 | + } | |
| 274 | + | |
| 275 | + return $value; | |
| 276 | + } | |
| 277 | + | |
| 278 | + protected function before_query_extra_field( $field ) { | |
| 279 | + $this->field_data['key'] = $field; | |
| 280 | + } | |
| 281 | + | |
| 282 | + /** | |
| 283 | + * @return false|mixed | |
| 284 | + * @throws Preset_Exception | |
| 285 | + */ | |
| 286 | + private function get_current_value() { | |
| 287 | + $func_name = self::FUNC_PREFIX . $this->prop; | |
| 288 | + | |
| 289 | + if ( is_callable( array( $this, $func_name ) ) ) { | |
| 290 | + $result = call_user_func( array( $this, $func_name ) ); | |
| 291 | + } else { | |
| 292 | + $result = $this->default_prop( $this->prop ); | |
| 293 | + } | |
| 294 | + | |
| 295 | + return apply_filters( | |
| 296 | + 'jet-form-builder/preset/source/value', | |
| 297 | + $result, | |
| 298 | + $this | |
| 299 | + ); | |
| 300 | + } | |
| 301 | + | |
| 302 | + private function get_extra_fields(): array { | |
| 303 | + try { | |
| 304 | + $extra = $this->get_field_object()->get_extra_fields( $this ); | |
| 305 | + } catch ( Preset_Exception $exception ) { | |
| 306 | + return array(); | |
| 307 | + } | |
| 308 | + | |
| 309 | + $extra = apply_filters( 'jet-form-builder/preset/extra-fields', $extra, $this ); | |
| 310 | + | |
| 311 | + $parser = ( new Macros_Parser() )->set_replacements( | |
| 312 | + array( | |
| 313 | + 'key' => $this->field_data['key'] ?? '', | |
| 314 | + 'prop' => $this->prop, | |
| 315 | + ) | |
| 316 | + ); | |
| 317 | + | |
| 318 | + foreach ( $extra as $index => $field ) { | |
| 319 | + $extra[ $index ] = $parser->parse_macros( $field ); | |
| 320 | + } | |
| 321 | + | |
| 322 | + return $extra; | |
| 323 | + } | |
| 324 | + | |
| 325 | + /** | |
| 326 | + * @param string $prop | |
| 327 | + * | |
| 328 | + * @return mixed | |
| 329 | + * @throws Preset_Exception | |
| 330 | + */ | |
| 331 | + public function default_prop( string $prop ) { | |
| 332 | + $source = $this->src; | |
| 333 | + | |
| 334 | + if ( isset( $source->$prop ) ) { | |
| 335 | + return $source->$prop; | |
| 336 | + } elseif ( isset( $source->data ) && isset( $source->data->$prop ) ) { | |
| 337 | + return $source->data->$prop; | |
| 338 | + } | |
| 339 | + | |
| 340 | + if ( ! is_object( $source ) ) { | |
| 341 | + throw new Preset_Exception( "Source isn't object" ); | |
| 342 | + } | |
| 343 | + | |
| 344 | + throw new Preset_Exception( | |
| 345 | + esc_html( "Can't get value from " . get_class( $source ) ) | |
| 346 | + ); | |
| 347 | + } | |
| 348 | + | |
| 349 | + | |
| 350 | + /** | |
| 351 | + * @return mixed | |
| 352 | + * @throws Preset_Exception | |
| 353 | + */ | |
| 354 | + final public function result() { | |
| 355 | + $this->throw_if_preset_not_available(); | |
| 356 | + | |
| 357 | + return $this->parse_result_value( $this->get_result_on_prop() ); | |
| 358 | + } | |
| 359 | + | |
| 360 | + public function parse_result_value( $value ) { | |
| 361 | + if ( ! isset( $this->field_args['type'] ) ) { | |
| 362 | + return $value; | |
| 363 | + } | |
| 364 | + | |
| 365 | + return Preset_Manager::instance()->prepare_result( $this->field_args['type'], $value ); | |
| 366 | + } | |
| 367 | + | |
| 368 | + | |
| 369 | + /** | |
| 370 | + * @return Base | |
| 371 | + * @throws Preset_Exception | |
| 372 | + */ | |
| 373 | + public function get_field_object(): Base { | |
| 374 | + $type = $this->field_args['type'] ?? false; | |
| 375 | + $block = jet_form_builder()->blocks->get_field_by_name( $type ); | |
| 376 | + | |
| 377 | + if ( ! $block ) { | |
| 378 | + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped | |
| 379 | + throw new Preset_Exception( 'Undefined block_type: ' . $type, $this->field_args ); | |
| 380 | + } | |
| 381 | + | |
| 382 | + $block->block_attrs = $this->field_args; | |
| 383 | + | |
| 384 | + return $block; | |
| 385 | + } | |
| 386 | + | |
| 387 | + /** | |
| 388 | + * @return mixed | |
| 389 | + * @throws Preset_Exception | |
| 390 | + */ | |
| 391 | + public function get_expected_format() { | |
| 392 | + return $this->get_field_object()->expected_preset_type()[0] ?? 'raw'; | |
| 393 | + } | |
| 394 | + | |
| 395 | +} | |