Unyson - Version 2.6.15

Version Description

  • Fixed #2380, #2397, #2212
Download this release

Release Info

Developer Unyson
Plugin Icon 128x128 Unyson
Version 2.6.15
Comparing to
See all releases

Code changes from version 2.6.14 to 2.6.15

framework/autoload.php CHANGED
@@ -107,6 +107,8 @@ function _fw_autoload_helper_classes($class) {
107
  'FW_Access_Key' => 'class-fw-access-key',
108
  'FW_WP_Filesystem' => 'class-fw-wp-filesystem',
109
  'FW_Form' => 'class-fw-form',
 
 
110
  'FW_Settings_Form' => 'class-fw-settings-form',
111
  'FW_Request' => 'class-fw-request',
112
  'FW_Session' => 'class-fw-session',
107
  'FW_Access_Key' => 'class-fw-access-key',
108
  'FW_WP_Filesystem' => 'class-fw-wp-filesystem',
109
  'FW_Form' => 'class-fw-form',
110
+ 'FW_Form_Not_Found_Exception' => 'exceptions/class-fw-form-not-found-exception',
111
+ 'FW_Form_Invalid_Submission_Exception' => 'exceptions/class-fw-form-invalid-submission-exception',
112
  'FW_Settings_Form' => 'class-fw-settings-form',
113
  'FW_Request' => 'class-fw-request',
114
  'FW_Session' => 'class-fw-session',
framework/helpers/class-fw-flash-messages.php CHANGED
@@ -210,7 +210,7 @@ class FW_Flash_Messages
210
  /**
211
  * Clear the FW_Flash_Messages messages
212
  *
213
- * @since 2.6.14
214
  */
215
  public static function _clear() {
216
  self::set_messages(array());
210
  /**
211
  * Clear the FW_Flash_Messages messages
212
  *
213
+ * @since 2.6.15
214
  */
215
  public static function _clear() {
216
  self::set_messages(array());
framework/helpers/class-fw-form.php CHANGED
@@ -9,32 +9,42 @@ class FW_Form {
9
  /**
10
  * Store all form ids created with this class
11
  * @var FW_Form[] {'form_id' => instance}
 
 
12
  */
13
  protected static $forms = array();
14
 
15
  /**
16
  * The id of the submitted form id
17
  * @var string
 
 
18
  */
19
  protected static $submitted_id;
20
 
21
  /**
22
  * Hidden input name that stores the form id
23
  * @var string
 
 
24
  */
25
  protected static $id_input_name = 'fwf';
26
 
27
  /**
28
  * Form id
29
  * @var string
 
 
30
  */
31
  protected $id;
32
 
33
  /**
34
  * Html attributes for <form> tag
35
  * @var array
 
 
36
  */
37
- protected $attr;
38
 
39
  /**
40
  * Found validation errors
@@ -51,20 +61,31 @@ class FW_Form {
51
  /**
52
  * If current request is the submit of this form
53
  * @var bool
 
 
54
  */
55
  protected $is_submitted;
56
 
57
  /**
58
  * @var bool
 
 
59
  */
60
  protected $validate_and_save_called = false;
61
 
 
 
 
 
 
62
  protected $callbacks = array(
63
  'render' => false,
64
  'validate' => false,
65
  'save' => false
66
  );
67
 
 
 
68
  /**
69
  * @param string $id Unique
70
  * @param array $data (optional)
@@ -76,50 +97,27 @@ class FW_Form {
76
  * )
77
  */
78
  public function __construct( $id, $data = array() ) {
79
- if ( isset( self::$forms[ $id ] ) ) {
 
80
  trigger_error( sprintf( __( 'Form with id "%s" was already defined', 'fw' ), $id ), E_USER_ERROR );
81
- }
82
-
83
- $this->id = $id;
84
-
85
- self::$forms[ $this->id ] =& $this;
86
-
87
- // prepare $this->attr
88
- {
89
- if ( ! isset( $data['attr'] ) || ! is_array( $data['attr'] ) ) {
90
- $data['attr'] = array();
91
- }
92
-
93
- $data['attr']['data-fw-form-id'] = $this->id;
94
-
95
- /** @deprecated */
96
- $data['attr']['class'] = 'fw_form_' . $this->id;
97
-
98
- if ( isset( $data['attr']['method'] ) ) {
99
- $data['attr']['method'] = strtolower( $data['attr']['method'] );
100
-
101
- $data['attr']['method'] = in_array( $data['attr']['method'], array( 'get', 'post' ) )
102
- ? $data['attr']['method']
103
- : 'post';
104
- } else {
105
- $data['attr']['method'] = 'post';
106
- }
107
 
108
- if ( ! isset( $data['attr']['action'] ) ) {
109
- $data['attr']['action'] = fw_current_url();
110
- }
111
-
112
- $this->attr = $data['attr'];
113
  }
114
 
115
- // prepare $this->callbacks
116
- {
117
- $this->callbacks = array(
118
- 'render' => empty( $data['render'] ) ? false : $data['render'],
119
- 'validate' => empty( $data['validate'] ) ? false : $data['validate'],
120
- 'save' => empty( $data['save'] ) ? false : $data['save'],
121
- );
122
- }
 
 
 
 
 
123
 
124
  if ( did_action( 'wp_loaded' ) ) {
125
  // in case if form instance was created after action
@@ -128,104 +126,40 @@ class FW_Form {
128
  // attach to an action before 'send_headers' action, to be able to do redirects
129
  add_action( 'wp_loaded', array( $this, '_validate_and_save' ), 101 );
130
  }
131
- }
132
-
133
- protected function validate() {
134
- if ( is_array( $this->errors ) ) {
135
- trigger_error( __METHOD__ . ' already called', E_USER_WARNING );
136
-
137
- return;
138
- }
139
-
140
- /**
141
- * Errors array {'input[name]' => 'Error message'}
142
- */
143
- $errors = array();
144
-
145
- /**
146
- * Call validate callback
147
- *
148
- * Callback must 'manually' extract input values from $_POST (or $_GET)
149
- */
150
- if ( $this->callbacks['validate'] ) {
151
- $errors = call_user_func_array( $this->callbacks['validate'], array( $errors ) );
152
-
153
- if ( ! is_array( $errors ) ) {
154
-
155
- $errors = array();
156
- }
157
- }
158
-
159
- /**
160
- * check nonce
161
- */
162
- if ( $this->attr['method'] == 'post' ) {
163
- $nonce_name = $this->get_nonce_name();
164
-
165
- if (
166
- ! isset( $_REQUEST[ $nonce_name ] )
167
- ||
168
- wp_verify_nonce( $_REQUEST[ $nonce_name ], 'submit_fwf' ) === false
169
- ) {
170
- $errors[ $nonce_name ] = __( 'Nonce verification failed', 'fw' );
171
- }
172
- }
173
 
174
- $this->errors = $errors;
175
  }
176
 
177
  /**
178
- * Some forms (like Forms extension frontend form) uses the same FW_Form instance for all sub-forms
179
- * and they must be differentiated somehow.
180
- * Fixes https://github.com/ThemeFuse/Unyson/issues/2033
181
- *
182
- * @param array $render_data
183
- *
184
  * @return string
185
- * @since 2.6.6
186
  */
187
- private function get_nonce_name( $render_data = array() ) {
188
- return '_nonce_' . md5( $this->id . apply_filters( 'fw:form:nonce-name-data', '', $this, $render_data ) );
189
  }
190
 
191
- protected function save() {
192
- $save_data = array(
193
- // you can set here a url for redirect after save
194
- 'redirect' => null
195
- );
196
-
197
- /**
198
- * Call save callback
199
- *
200
- * Callback must 'manually' extract input values from $_POST (or $_GET)
201
- */
202
- if ( $this->callbacks['save'] ) {
203
- $data = call_user_func_array( $this->callbacks['save'], array( $save_data ) );
204
-
205
- if ( ! is_array( $data ) ) {
206
- // fix if returned wrong data from callback
207
- $data = $save_data;
208
- }
209
-
210
- $save_data = $data;
211
 
212
- unset( $data );
213
  }
214
 
215
- if ( ! $this->is_ajax() ) {
216
- if ( isset( $save_data['redirect'] ) ) {
217
- wp_redirect( $save_data['redirect'] );
218
- exit;
219
- }
220
- }
221
 
222
- return $save_data;
223
  }
224
 
225
- protected function is_ajax() {
226
- return ( defined( 'DOING_AJAX' ) && DOING_AJAX )
227
- ||
228
- strtolower( fw_akg( 'HTTP_X_REQUESTED_WITH', $_SERVER ) ) == 'xmlhttprequest';
 
 
229
  }
230
 
231
  /**
@@ -233,94 +167,84 @@ class FW_Form {
233
  *
234
  * Note: This callback can abort script execution if save does redirect
235
  *
236
- * @return bool|null
237
  * @internal
238
  */
239
  public function _validate_and_save() {
240
- if ( $this->validate_and_save_called ) {
241
- trigger_error( __METHOD__ . ' already called', E_USER_WARNING );
242
 
243
- return null;
244
- } else {
245
- $this->validate_and_save_called = true;
246
  }
247
 
248
- if ( ! $this->is_submitted() || ! isset( $_POST[ $this->get_nonce_name() ] ) ) {
249
- return null;
250
- }
251
 
252
- $this->validate();
 
253
 
254
- if ( $this->is_ajax() ) {
255
- $json_data = array();
 
 
 
 
256
 
257
- if ( $this->is_valid() ) {
258
- $json_data['save_data'] = $this->save();
259
- } else {
260
- $json_data['errors'] = $this->get_errors();
 
 
 
 
 
 
261
  }
 
 
262
 
263
- /**
264
- * Transform flash messages structure from
265
- * array( 'type' => array( 'message_id' => array(...) ) )
266
- * to
267
- * array( 'type' => array( 'message_id' => 'Message' ) )
268
- */
269
- {
270
- $flash_messages = array();
 
 
 
 
 
 
 
 
271
 
272
- foreach ( FW_Flash_Messages::_get_messages( true ) as $type => $messages ) {
273
- $flash_messages[ $type ] = array();
274
 
275
- foreach ( $messages as $id => $message_data ) {
276
- $flash_messages[ $type ][ $id ] = $message_data['message'];
277
- }
278
- }
279
 
280
- $json_data['flash_messages'] = $flash_messages;
281
- }
282
 
283
- /**
284
- * Important!
285
- * We can't send form html in response:
286
- *
287
- * ob_start();
288
- * $this->render();
289
- * $json_data['html'] = ob_get_clean();
290
- *
291
- * because the render() method is not called within this class
292
- * but by the code that created and owns the $form,
293
- * and it's usually called with some custom data $this->render(array(...))
294
- * that it's impossible to know here which data is that.
295
- * If we will call $this->render(); without data, this may throw errors because
296
- * the render callback may expect some custom data.
297
- * Also it may be called or not, depending on the owner code inner logic.
298
- *
299
- * The only way to get the latest form html on ajax submit
300
- * is to make a new ajax GET to current page and extract form html from the response.
301
- */
302
 
303
- if ( $this->is_valid() ) {
304
- wp_send_json_success( $json_data );
305
- } else {
306
- wp_send_json_error( $json_data );
307
- }
308
- } else {
309
- if ( ! $this->is_valid() ) {
310
- return false;
311
- }
312
 
313
- $this->save();
 
 
 
 
 
 
 
314
  }
315
 
316
- return true;
317
- }
318
-
319
- /**
320
- * @return string
321
- */
322
- public function get_id() {
323
- return $this->id;
324
  }
325
 
326
  /**
@@ -331,11 +255,9 @@ class FW_Form {
331
  * @return array|string
332
  */
333
  public function attr( $name = null ) {
334
- if ( $name ) {
335
- return isset( $this->attr[ $name ] ) ? $this->attr[ $name ] : null;
336
- } else {
337
- return $this->attr;
338
- }
339
  }
340
 
341
  /**
@@ -354,15 +276,15 @@ class FW_Form {
354
  'html' => null,
355
  ),
356
  'data' => $data,
357
- 'attr' => $this->attr,
358
  );
359
 
360
- unset( $data );
361
 
362
- if ( $this->callbacks['render'] ) {
363
  ob_start();
364
 
365
- $data = call_user_func_array( $this->callbacks['render'], array( $render_data, $this ) );
366
 
367
  $html = ob_get_clean();
368
 
@@ -372,71 +294,11 @@ class FW_Form {
372
  }
373
 
374
  $render_data = $data;
375
-
376
- unset( $data );
377
  }
378
 
379
- // display form errors in frontend
380
- do {
381
- if ( is_admin() ) {
382
- // errors in admin side are displayed by a script at the end of this file
383
- break;
384
- }
385
-
386
- $submitted_form = FW_Form::get_submitted();
387
-
388
- if ( ! $submitted_form ) {
389
- break;
390
- }
391
-
392
- if ( $submitted_form->get_id() !== $this->get_id() ) {
393
- // the submitted form is not current form
394
- break;
395
- }
396
-
397
- unset( $submitted_form ); // not needed anymore, below will be used only with $this (because it's the same form)
398
-
399
- if ( ! isset( $_POST[ $this->get_nonce_name( $render_data ) ] ) ) {
400
- break;
401
- }
402
-
403
- if ( $this->is_valid() ) {
404
- break;
405
- }
406
-
407
- /**
408
- * Use this action to customize errors display in your theme
409
- */
410
- do_action( 'fw_form_display_errors_frontend', $this );
411
-
412
- if ( $this->errors_accessed() ) {
413
- // already displayed, prevent/cancel default display
414
- break;
415
- }
416
-
417
- $errors = $this->get_errors();
418
-
419
- if ( empty( $errors ) ) {
420
- break;
421
- }
422
-
423
- echo '<ul class="fw-form-errors">';
424
-
425
- foreach ( $errors as $input_name => $error_message ) {
426
- echo fw_html_tag(
427
- 'li',
428
- array(
429
- 'data-input-name' => $input_name,
430
- ),
431
- $error_message
432
- );
433
- }
434
-
435
- echo '</ul>';
436
-
437
- unset( $errors );
438
- } while ( false );
439
 
 
440
  echo '<form ' . fw_attr_to_html( $render_data['attr'] ) . ' >';
441
 
442
  do_action( 'fw_form_display:before', $this );
@@ -444,13 +306,11 @@ class FW_Form {
444
  echo fw_html_tag( 'input',
445
  array(
446
  'type' => 'hidden',
447
- 'name' => self::$id_input_name,
448
- 'value' => $this->id,
449
  ) );
450
 
451
- if ( $render_data['attr']['method'] == 'post' ) {
452
- wp_nonce_field( 'submit_fwf', $this->get_nonce_name( $render_data ) );
453
- }
454
 
455
  if ( ! empty( $render_data['attr']['action'] ) && $render_data['attr']['method'] == 'get' ) {
456
  /**
@@ -487,6 +347,8 @@ class FW_Form {
487
  do_action( 'fw_form_display:after', $this );
488
 
489
  echo '</form>';
 
 
490
  }
491
 
492
  /**
@@ -494,61 +356,226 @@ class FW_Form {
494
  * @return bool
495
  */
496
  public function is_submitted() {
497
- if ( is_null( $this->is_submitted ) ) {
498
- switch ( strtoupper( $this->attr( 'method' ) ) ) {
499
- case 'POST':
500
- $this->is_submitted = (
501
- isset( $_POST[ self::$id_input_name ] )
502
- &&
503
- FW_Request::POST( self::$id_input_name ) === $this->id
504
- );
505
- break;
506
- case 'GET':
507
- $this->is_submitted = (
508
- isset( $_GET[ self::$id_input_name ] )
509
- &&
510
- FW_Request::GET( self::$id_input_name ) === $this->id
511
- );
512
- break;
513
- default:
514
- $this->is_submitted = false;
515
- }
516
- }
517
-
518
- return $this->is_submitted;
519
  }
520
 
521
  /**
522
  * @return bool
523
  */
524
  public function is_valid() {
525
- if ( ! $this->validate_and_save_called ) {
526
- trigger_error( __METHOD__ . ' called before validation', E_USER_WARNING );
527
-
528
  return null;
529
  }
530
 
531
- return empty( $this->errors );
532
  }
533
 
534
  /**
535
- * Get validation errors
536
- * @return array
 
 
 
537
  */
538
- public function get_errors() {
539
- if ( ! $this->validate_and_save_called ) {
540
- trigger_error( __METHOD__ . ' called before validation', E_USER_WARNING );
 
541
 
542
- return array( '~' => true );
 
 
 
543
  }
544
 
545
- $this->errors_accessed = true;
 
546
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  return $this->errors;
548
  }
549
 
550
- public function errors_accessed() {
551
- return $this->errors_accessed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  }
553
 
554
  /**
@@ -556,24 +583,69 @@ class FW_Form {
556
  * @return FW_Form|false
557
  */
558
  public static function get_submitted() {
559
- if ( is_null( self::$submitted_id ) ) {
560
- // method called first time, search for submitted form
561
- do {
562
- foreach ( self::$forms as $form ) {
563
- if ( $form->is_submitted() ) {
564
- self::$submitted_id = $form->get_id();
565
- break 2;
566
- }
567
- }
568
 
569
- self::$submitted_id = false;
570
- } while ( false );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  }
572
 
573
- if ( is_string( self::$submitted_id ) ) {
574
- return self::$forms[ self::$submitted_id ];
575
- } else {
576
- return false;
577
  }
 
 
578
  }
579
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  /**
10
  * Store all form ids created with this class
11
  * @var FW_Form[] {'form_id' => instance}
12
+ *
13
+ * @deprecated 2.6.15 Use FW_Form::get_forms()
14
  */
15
  protected static $forms = array();
16
 
17
  /**
18
  * The id of the submitted form id
19
  * @var string
20
+ *
21
+ * @deprecated 2.6.15
22
  */
23
  protected static $submitted_id;
24
 
25
  /**
26
  * Hidden input name that stores the form id
27
  * @var string
28
+ *
29
+ * @deprecated 2.6.15 Use self::get_form_id_name()
30
  */
31
  protected static $id_input_name = 'fwf';
32
 
33
  /**
34
  * Form id
35
  * @var string
36
+ *
37
+ * @deprecated 2.6.15 Use $this->get_id()
38
  */
39
  protected $id;
40
 
41
  /**
42
  * Html attributes for <form> tag
43
  * @var array
44
+ *
45
+ * @deprecated 2.6.15 Use $this->attr()
46
  */
47
+ protected $attr = array();
48
 
49
  /**
50
  * Found validation errors
61
  /**
62
  * If current request is the submit of this form
63
  * @var bool
64
+ *
65
+ * @deprecated 2.6.15 Use $this->is_submitted()
66
  */
67
  protected $is_submitted;
68
 
69
  /**
70
  * @var bool
71
+ *
72
+ * @deprecated 2.6.15
73
  */
74
  protected $validate_and_save_called = false;
75
 
76
+ /**
77
+ * @var array
78
+ *
79
+ * @deprecated 2.6.15 Use $this->get_callbacks()
80
+ */
81
  protected $callbacks = array(
82
  'render' => false,
83
  'validate' => false,
84
  'save' => false
85
  );
86
 
87
+ private $request;
88
+
89
  /**
90
  * @param string $id Unique
91
  * @param array $data (optional)
97
  * )
98
  */
99
  public function __construct( $id, $data = array() ) {
100
+ try {
101
+ self::get_form( $id );
102
  trigger_error( sprintf( __( 'Form with id "%s" was already defined', 'fw' ), $id ), E_USER_ERROR );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
+ return;
105
+ } catch ( FW_Form_Not_Found_Exception $e ) {
 
 
 
106
  }
107
 
108
+ $this
109
+ // set id
110
+ ->set_id( $id )
111
+ // prepare callbacks
112
+ ->set_callbacks( array(
113
+ 'render' => fw_akg( 'render', $data, false ),
114
+ 'validate' => fw_akg( 'validate', $data, false ),
115
+ 'save' => fw_akg( 'save', $data, false ),
116
+ ) )
117
+ // prepare attributes
118
+ ->set_attr( (array) fw_akg( 'attr', $data, array() ) );
119
+
120
+ self::$forms[ $this->get_id() ] =& $this;
121
 
122
  if ( did_action( 'wp_loaded' ) ) {
123
  // in case if form instance was created after action
126
  // attach to an action before 'send_headers' action, to be able to do redirects
127
  add_action( 'wp_loaded', array( $this, '_validate_and_save' ), 101 );
128
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ add_action( 'fw_form_display:before_form', array( $this, '_action_fw_form_show_errors' ) );
131
  }
132
 
133
  /**
 
 
 
 
 
 
134
  * @return string
 
135
  */
136
+ public function get_id() {
137
+ return $this->id;
138
  }
139
 
140
+ /**
141
+ * Get validation errors
142
+ * @return array
143
+ */
144
+ public function get_errors() {
145
+ if ( ! $this->validate_and_save_called ) {
146
+ fw_print( debug_backtrace() );
147
+ trigger_error( __METHOD__ . ' called before validation', E_USER_WARNING );
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ return array( '~' => true );
150
  }
151
 
152
+ $this->errors_accessed = true;
 
 
 
 
 
153
 
154
+ return $this->_get_errors();
155
  }
156
 
157
+ public function get_callbacks() {
158
+ return $this->callbacks;
159
+ }
160
+
161
+ public function errors_accessed() {
162
+ return $this->errors_accessed;
163
  }
164
 
165
  /**
167
  *
168
  * Note: This callback can abort script execution if save does redirect
169
  *
 
170
  * @internal
171
  */
172
  public function _validate_and_save() {
 
 
173
 
174
+ if ( ! self::is_form_submitted( $this->get_id() ) || $this->validate_and_save_called ) {
175
+ return;
 
176
  }
177
 
178
+ $this->validate_and_save_called = true;
 
 
179
 
180
+ try {
181
+ $data = $this->submit( self::get_form_request( $this->get_id() ) );
182
 
183
+ if ( $this->_is_ajax() ) {
184
+ wp_send_json_success( array(
185
+ 'save_data' => $data,
186
+ 'flash_messages' => self::collect_flash_messages(),
187
+ ) );
188
+ }
189
 
190
+ if ( ( $redirect = fw_akg( 'redirect', $data ) ) ) {
191
+ wp_redirect( $redirect );
192
+ exit;
193
+ }
194
+ } catch ( FW_Form_Invalid_Submission_Exception $e ) {
195
+ if ( $this->_is_ajax() ) {
196
+ wp_send_json_error( array(
197
+ 'errors' => $this->get_errors(),
198
+ 'flash_messages' => self::collect_flash_messages()
199
+ ) );
200
  }
201
+ }
202
+ }
203
 
204
+ /**
205
+ * @param FW_Form $form
206
+ *
207
+ * @internal
208
+ *
209
+ * You can overwrite it in case you do not need the errors to be shown for your form
210
+ */
211
+ public function _action_fw_form_show_errors( $form ) {
212
+ if (
213
+ $form->get_id() != $this->get_id()
214
+ // errors in admin side are displayed by a script at the end of this file
215
+ || is_admin()
216
+ || ! $form->is_submitted()
217
+ || $form->is_valid()
218
+ || $form->errors_accessed()
219
+ ) {
220
 
221
+ return;
222
+ }
223
 
224
+ /**
225
+ * Use this action to customize errors display in your theme
226
+ */
227
+ do_action( 'fw_form_display_errors_frontend', $form );
228
 
229
+ $errors = $form->get_errors();
 
230
 
231
+ if ( empty( $errors ) ) {
232
+ return;
233
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
+ echo '<ul class="fw-form-errors">';
 
 
 
 
 
 
 
 
236
 
237
+ foreach ( $errors as $input_name => $error_message ) {
238
+ echo fw_html_tag(
239
+ 'li',
240
+ array(
241
+ 'data-input-name' => $input_name,
242
+ ),
243
+ $error_message
244
+ );
245
  }
246
 
247
+ echo '</ul>';
 
 
 
 
 
 
 
248
  }
249
 
250
  /**
255
  * @return array|string
256
  */
257
  public function attr( $name = null ) {
258
+ return $name !== null
259
+ ? fw_akg( $name, $this->attr )
260
+ : $this->attr;
 
 
261
  }
262
 
263
  /**
276
  'html' => null,
277
  ),
278
  'data' => $data,
279
+ 'attr' => $this->attr(),
280
  );
281
 
282
+ $html = '';
283
 
284
+ if ( $render_callback = fw_akg( 'render', $this->get_callbacks() ) ) {
285
  ob_start();
286
 
287
+ $data = call_user_func_array( $render_callback, array( $render_data, $this ) );
288
 
289
  $html = ob_get_clean();
290
 
294
  }
295
 
296
  $render_data = $data;
 
 
297
  }
298
 
299
+ do_action( 'fw_form_display:before_form', $this );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
+ // display form errors in frontend
302
  echo '<form ' . fw_attr_to_html( $render_data['attr'] ) . ' >';
303
 
304
  do_action( 'fw_form_display:before', $this );
306
  echo fw_html_tag( 'input',
307
  array(
308
  'type' => 'hidden',
309
+ 'name' => self::get_form_id_name(),
310
+ 'value' => $this->get_id(),
311
  ) );
312
 
313
+ wp_nonce_field( $this->get_nonce_action(), $this->get_nonce_name( $render_data ) );
 
 
314
 
315
  if ( ! empty( $render_data['attr']['action'] ) && $render_data['attr']['method'] == 'get' ) {
316
  /**
347
  do_action( 'fw_form_display:after', $this );
348
 
349
  echo '</form>';
350
+
351
+ do_action( 'fw_form_display:after_form', $this );
352
  }
353
 
354
  /**
356
  * @return bool
357
  */
358
  public function is_submitted() {
359
+ return $this->request !== null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  }
361
 
362
  /**
363
  * @return bool
364
  */
365
  public function is_valid() {
366
+ if ( ! $this->is_submitted() ) {
 
 
367
  return null;
368
  }
369
 
370
+ return count( $this->_get_errors() ) == 0;
371
  }
372
 
373
  /**
374
+ * @param array $request
375
+ *
376
+ * @throws FW_Form_Invalid_Submission_Exception
377
+ *
378
+ * @return mixed
379
  */
380
+ public function submit( array $request = array() ) {
381
+ $this->request = $request;
382
+ //Updated the deprecated member for those that extended the class and use it in code
383
+ $this->is_submitted = true;
384
 
385
+ $errors = $this->validate();
386
+
387
+ if ( ! empty( $errors ) ) {
388
+ throw new FW_Form_Invalid_Submission_Exception( $errors );
389
  }
390
 
391
+ return $this->save();
392
+ }
393
 
394
+ protected function get_default_attr() {
395
+ return array(
396
+ 'data-fw-form-id' => $this->get_id(),
397
+ 'method' => 'post',
398
+ 'action' => fw_current_url(),
399
+ 'class' => 'fw_form_' . $this->get_id()
400
+ );
401
+ }
402
+
403
+ /**
404
+ * @param array $attr
405
+ *
406
+ * @return $this
407
+ */
408
+ protected function set_attr( array $attr ) {
409
+ $this->attr = array_merge( $this->get_default_attr(), $attr );
410
+
411
+ return $this;
412
+ }
413
+
414
+ /**
415
+ * @param null $key
416
+ *
417
+ * @return array|mixed|null
418
+ *
419
+ * @since 2.6.15
420
+ */
421
+ protected function get_request( $key = null ) {
422
+ return $key === null ? (array) $this->request : fw_akg( $key, $this->request );
423
+ }
424
+
425
+ /**
426
+ * @return string|null
427
+ *
428
+ * @since 2.6.15
429
+ */
430
+ protected function get_nonce() {
431
+ return $this->get_request( $this->get_nonce_name() );
432
+ }
433
+
434
+ /**
435
+ * Returns forms errors without counting them as accessed
436
+ * @return array
437
+ */
438
+ protected function _get_errors() {
439
  return $this->errors;
440
  }
441
 
442
+ /**
443
+ * @return string
444
+ *
445
+ * @since 2.6.15
446
+ */
447
+ protected function get_nonce_action() {
448
+ return 'submit_fwf';
449
+ }
450
+
451
+ protected function check_nonce( $nonce ) {
452
+ return wp_verify_nonce( $nonce, $this->get_nonce_action() );
453
+ }
454
+
455
+ /**
456
+ * @return array
457
+ */
458
+ protected function validate() {
459
+ /**
460
+ * Errors array {'input[name]' => 'Error message'}
461
+ */
462
+ $errors = array();
463
+
464
+ if ( ! $this->check_nonce( $this->get_nonce() ) ) {
465
+ $errors[ $this->get_nonce_name() ] = __( 'Nonce verification failed', 'fw' );
466
+ }
467
+
468
+ /**
469
+ * Call validate callback
470
+ *
471
+ * Callback must 'manually' extract input values from $_POST (or $_GET)
472
+ */
473
+ if ( ( $validate = fw_akg( 'validate', $this->get_callbacks() ) ) ) {
474
+ $errors = (array) call_user_func( $validate, $errors );
475
+ }
476
+
477
+ return $this->set_errors( $errors )->_get_errors();
478
+ }
479
+
480
+ /**
481
+ * @return array|mixed
482
+ */
483
+ protected function save() {
484
+ $save_data = array(
485
+ // you can set here a url for redirect after save
486
+ 'redirect' => null
487
+ );
488
+
489
+ /**
490
+ * Call save callback
491
+ *
492
+ * Callback must 'manually' extract input values from $_POST (or $_GET)
493
+ */
494
+ if ( ( $save_callback = fw_akg( 'save', $this->get_callbacks() ) ) ) {
495
+ $data = call_user_func_array( $save_callback, array( $save_data ) );
496
+
497
+ if ( ! is_array( $data ) ) {
498
+ // fix if returned wrong data from callback
499
+ $data = $save_data;
500
+ }
501
+
502
+ $save_data = $data;
503
+
504
+ unset( $data );
505
+ }
506
+
507
+ return $save_data;
508
+ }
509
+
510
+ /**
511
+ * @return bool
512
+ *
513
+ * @deprecated 2.6.15
514
+ */
515
+ protected function is_ajax() {
516
+ return self::_is_ajax();
517
+ }
518
+
519
+ protected function set_id( $id ) {
520
+ $this->id = $id;
521
+
522
+ return $this;
523
+ }
524
+
525
+ /**
526
+ * @param array $callbacks
527
+ *
528
+ * @return $this
529
+ */
530
+ protected function set_callbacks( array $callbacks ) {
531
+ $this->callbacks = $callbacks;
532
+
533
+ return $this;
534
+ }
535
+
536
+ protected function set_errors( array $errors ) {
537
+ $this->errors = $errors;
538
+
539
+ return $this;
540
+ }
541
+
542
+ /**
543
+ * Some forms (like Forms extension frontend form) uses the same FW_Form instance for all sub-forms
544
+ * and they must be differentiated somehow.
545
+ * Fixes https://github.com/ThemeFuse/Unyson/issues/2033
546
+ *
547
+ * @param array $render_data
548
+ *
549
+ * @return string
550
+ * @since 2.6.6
551
+ */
552
+ private function get_nonce_name( $render_data = array() ) {
553
+ return '_nonce_' . md5( $this->id . apply_filters( 'fw:form:nonce-name-data', '', $this, $render_data ) );
554
+ }
555
+
556
+ /**
557
+ * @return FW_Form[]
558
+ *
559
+ * @since 2.6.15
560
+ */
561
+ public static function get_forms() {
562
+ return self::$forms;
563
+ }
564
+
565
+ /**
566
+ * @param $id
567
+ *
568
+ * @return FW_Form
569
+ * @throws FW_Form_Not_Found_Exception
570
+ *
571
+ * @since 2.6.15
572
+ */
573
+ public static function get_form( $id ) {
574
+ if ( ! isset( self::$forms[ $id ] ) ) {
575
+ throw new FW_Form_Not_Found_Exception( "FW_Form $id was not defined" );
576
+ }
577
+
578
+ return self::$forms[ $id ];
579
  }
580
 
581
  /**
583
  * @return FW_Form|false
584
  */
585
  public static function get_submitted() {
586
+ foreach ( self::get_forms() as $id => $form ) {
587
+ if ( self::is_form_submitted( $id ) ) {
588
+ return $form;
589
+ }
590
+ }
 
 
 
 
591
 
592
+ return false;
593
+ }
594
+
595
+ /**
596
+ * @return bool
597
+ *
598
+ * @since 2.6.15
599
+ */
600
+ public static function _is_ajax() {
601
+ return ( defined( 'DOING_AJAX' ) && DOING_AJAX )
602
+ ||
603
+ strtolower( fw_akg( 'HTTP_X_REQUESTED_WITH', $_SERVER ) ) == 'xmlhttprequest';
604
+ }
605
+
606
+ public static function get_form_request( $id ) {
607
+ if ( FW_Request::POST( self::get_form_id_name() ) == $id ) {
608
+ return FW_Request::POST();
609
  }
610
 
611
+ if ( FW_Request::GET( self::get_form_id_name() ) == $id ) {
612
+ return FW_Request::GET();
 
 
613
  }
614
+
615
+ return null;
616
  }
617
+
618
+ /**
619
+ * @return string
620
+ *
621
+ * @since 2.6.15
622
+ */
623
+ protected static function get_form_id_name() {
624
+ return 'fwf';
625
+ }
626
+
627
+ /**
628
+ * @param $id
629
+ *
630
+ * @return bool
631
+ *
632
+ * @since 2.6.15
633
+ */
634
+ protected static function is_form_submitted( $id ) {
635
+ return self::get_form_request( $id ) !== null;
636
+ }
637
+
638
+ private static function collect_flash_messages() {
639
+ $flash_messages = array();
640
+
641
+ foreach ( FW_Flash_Messages::_get_messages( true ) as $type => $messages ) {
642
+ $flash_messages[ $type ] = array();
643
+
644
+ foreach ( $messages as $id => $message_data ) {
645
+ $flash_messages[ $type ][ $id ] = $message_data['message'];
646
+ }
647
+ }
648
+
649
+ return $flash_messages;
650
+ }
651
+ }
framework/helpers/database.php CHANGED
@@ -174,7 +174,8 @@ class FW_Db_Options_Model_Post extends FW_Db_Options_Model {
174
 
175
  protected function _get_cache_key($key, $item_id, array $extra_data = array()) {
176
  if ($key === 'options') {
177
- return $this->get_post_type($item_id); // Cache options grouped by post-type, not by post id
 
178
  } else {
179
  return $this->get_post_id($item_id);
180
  }
174
 
175
  protected function _get_cache_key($key, $item_id, array $extra_data = array()) {
176
  if ($key === 'options') {
177
+ // Cache options grouped by post-type, not by post id
178
+ return ($post_type = $this->get_post_type($item_id)) ? $post_type : '?';
179
  } else {
180
  return $this->get_post_id($item_id);
181
  }
framework/helpers/exceptions/class-fw-form-invalid-submission-exception.php ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php if ( ! defined( 'FW' ) ) {
2
+ die( 'Forbidden' );
3
+ }
4
+
5
+ class FW_Form_Invalid_Submission_Exception extends Exception {
6
+
7
+ private $errors = array();
8
+
9
+ public function __construct( array $errors ) {
10
+ parent::__construct();
11
+
12
+ $this->set_errors( $errors );
13
+ }
14
+
15
+ public function get_errors() {
16
+ return $this->errors;
17
+ }
18
+
19
+ protected function set_errors( array $errors ) {
20
+ $this->errors = $errors;
21
+ }
22
+ }
framework/helpers/exceptions/class-fw-form-not-found-exception.php ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <?php if ( ! defined( 'FW' ) ) {
2
+ die( 'Forbidden' );
3
+ }
4
+
5
+ class FW_Form_Not_Found_Exception extends Exception {
6
+
7
+ }
framework/includes/option-types/color-picker/class-fw-option-type-color-picker.php CHANGED
@@ -78,7 +78,7 @@ class FW_Option_Type_Color_Picker extends FW_Option_Type
78
  // do not use `!is_null()` allow empty values https://github.com/ThemeFuse/Unyson/issues/2025
79
  !empty($input_value)
80
  &&
81
- !preg_match('/^#[a-f0-9]{3}([a-f0-9]{3})?$/i', $input_value)
82
  )
83
  ) {
84
  return (string)$option['value'];
78
  // do not use `!is_null()` allow empty values https://github.com/ThemeFuse/Unyson/issues/2025
79
  !empty($input_value)
80
  &&
81
+ !preg_match('/^#([a-f0-9]{3}){1,2}$/i', $input_value)
82
  )
83
  ) {
84
  return (string)$option['value'];
framework/includes/option-types/color-picker/static/js/scripts.js CHANGED
@@ -2,7 +2,7 @@ jQuery(document).ready(function($){
2
  var helpers = {
3
  optionClass: 'fw-option-type-color-picker',
4
  eventNamespace: '.fwOptionTypeColorPicker',
5
- colorRegex: /^#[a-f0-9]{3}([a-f0-9]{3})?$/i,
6
  localized: window._fw_option_type_color_picker_localized,
7
  /**
8
  * Return true if color is dark
2
  var helpers = {
3
  optionClass: 'fw-option-type-color-picker',
4
  eventNamespace: '.fwOptionTypeColorPicker',
5
+ colorRegex: /^#([a-f0-9]{3}){1,2}$/i,
6
  localized: window._fw_option_type_color_picker_localized,
7
  /**
8
  * Return true if color is dark
framework/includes/option-types/gradient/view.php CHANGED
@@ -15,7 +15,7 @@
15
  );
16
  }
17
 
18
- $color_regex = '/^#[a-f0-9]{6}$/i';
19
 
20
  ?>
21
  <div <?php echo fw_attr_to_html($div_attr) ?> >
15
  );
16
  }
17
 
18
+ $color_regex = '/^#([a-f0-9]{3}){1,2}$/i';
19
 
20
  ?>
21
  <div <?php echo fw_attr_to_html($div_attr) ?> >
framework/includes/option-types/multi-select/class-fw-option-type-multi-select.php CHANGED
@@ -79,6 +79,8 @@ if ( ! class_exists( 'FW_Option_Type_Multi_Select' ) ):
79
  'limit' => 100,
80
  ), $limits);
81
 
 
 
82
  /** @var WPDB $wpdb */
83
  global $wpdb;
84
 
@@ -112,7 +114,12 @@ if ( ! class_exists( 'FW_Option_Type_Multi_Select' ) ):
112
 
113
  $sql .= " LIMIT ". intval($limits['limit']);
114
 
115
- return $wpdb->get_results($wpdb->prepare($sql, $prepare), ARRAY_A);
 
 
 
 
 
116
  }
117
 
118
  private static function query_terms(array $limits) {
@@ -125,6 +132,8 @@ if ( ! class_exists( 'FW_Option_Type_Multi_Select' ) ):
125
  'limit' => 100,
126
  ), $limits);
127
 
 
 
128
  /** @var WPDB $wpdb */
129
  global $wpdb;
130
 
@@ -157,7 +166,12 @@ if ( ! class_exists( 'FW_Option_Type_Multi_Select' ) ):
157
 
158
  $sql .= " LIMIT ". intval($limits['limit']);
159
 
160
- return $wpdb->get_results($wpdb->prepare($sql, $prepare), ARRAY_A);
 
 
 
 
 
161
  }
162
 
163
  private static function query_users(array $limits) {
@@ -170,10 +184,12 @@ if ( ! class_exists( 'FW_Option_Type_Multi_Select' ) ):
170
  'limit' => 100,
171
  ), $limits);
172
 
 
 
173
  /** @var WPDB $wpdb */
174
  global $wpdb;
175
 
176
- $sql = "SELECT DISTINCT users.id val, users.user_nicename title"
177
  ." FROM $wpdb->users AS users, $wpdb->usermeta AS usermeta"
178
  ." WHERE usermeta.user_id = users.ID";
179
 
@@ -181,7 +197,7 @@ if ( ! class_exists( 'FW_Option_Type_Multi_Select' ) ):
181
  $prepare = array();
182
 
183
  if ($limits['id']) {
184
- $sql .= " AND users.id IN ( "
185
  . implode( ', ', array_fill( 1, count( $limits['id'] ), '%d' ) )
186
  . " ) ";
187
  $prepare = array_merge($prepare, $limits['id']);
@@ -207,7 +223,12 @@ if ( ! class_exists( 'FW_Option_Type_Multi_Select' ) ):
207
 
208
  $sql .= " LIMIT ". intval($limits['limit']);
209
 
210
- return $wpdb->get_results($wpdb->prepare($sql, $prepare), ARRAY_A);
 
 
 
 
 
211
  }
212
 
213
  /**
79
  'limit' => 100,
80
  ), $limits);
81
 
82
+ $limits['limit'] = max($limits['limit'], 1);
83
+
84
  /** @var WPDB $wpdb */
85
  global $wpdb;
86
 
114
 
115
  $sql .= " LIMIT ". intval($limits['limit']);
116
 
117
+ return $wpdb->get_results(
118
+ $prepare
119
+ ? $wpdb->prepare($sql, $prepare)
120
+ : $sql,
121
+ ARRAY_A
122
+ );
123
  }
124
 
125
  private static function query_terms(array $limits) {
132
  'limit' => 100,
133
  ), $limits);
134
 
135
+ $limits['limit'] = max($limits['limit'], 1);
136
+
137
  /** @var WPDB $wpdb */
138
  global $wpdb;
139
 
166
 
167
  $sql .= " LIMIT ". intval($limits['limit']);
168
 
169
+ return $wpdb->get_results(
170
+ $prepare
171
+ ? $wpdb->prepare($sql, $prepare)
172
+ : $sql,
173
+ ARRAY_A
174
+ );
175
  }
176
 
177
  private static function query_users(array $limits) {
184
  'limit' => 100,
185
  ), $limits);
186
 
187
+ $limits['limit'] = max($limits['limit'], 1);
188
+
189
  /** @var WPDB $wpdb */
190
  global $wpdb;
191
 
192
+ $sql = "SELECT DISTINCT users.ID AS val, users.user_nicename AS title"
193
  ." FROM $wpdb->users AS users, $wpdb->usermeta AS usermeta"
194
  ." WHERE usermeta.user_id = users.ID";
195
 
197
  $prepare = array();
198
 
199
  if ($limits['id']) {
200
+ $sql .= " AND users.ID IN ( "
201
  . implode( ', ', array_fill( 1, count( $limits['id'] ), '%d' ) )
202
  . " ) ";
203
  $prepare = array_merge($prepare, $limits['id']);
223
 
224
  $sql .= " LIMIT ". intval($limits['limit']);
225
 
226
+ return $wpdb->get_results(
227
+ $prepare
228
+ ? $wpdb->prepare($sql, $prepare)
229
+ : $sql,
230
+ ARRAY_A
231
+ );
232
  }
233
 
234
  /**
framework/includes/option-types/rgba-color-picker/class-fw-option-type-rgba-color-picker.php CHANGED
@@ -78,7 +78,7 @@ class FW_Option_Type_Rgba_Color_Picker extends FW_Option_Type {
78
  !empty($input_value)
79
  &&
80
  !(
81
- preg_match( '/^#[a-f0-9]{3}([a-f0-9]{3})?$/i', $input_value )
82
  ||
83
  preg_match( '/^rgba\( *([01]?\d\d?|2[0-4]\d|25[0-5]) *\, *([01]?\d\d?|2[0-4]\d|25[0-5]) *\, *([01]?\d\d?|2[0-4]\d|25[0-5]) *\, *(1|0|0?.\d+) *\)$/', $input_value )
84
  )
78
  !empty($input_value)
79
  &&
80
  !(
81
+ preg_match( '/^#([a-f0-9]{3}){1,2}$/i', $input_value )
82
  ||
83
  preg_match( '/^rgba\( *([01]?\d\d?|2[0-4]\d|25[0-5]) *\, *([01]?\d\d?|2[0-4]\d|25[0-5]) *\, *([01]?\d\d?|2[0-4]\d|25[0-5]) *\, *(1|0|0?.\d+) *\)$/', $input_value )
84
  )
framework/includes/option-types/rgba-color-picker/static/js/scripts.js CHANGED
@@ -35,7 +35,7 @@ jQuery(function($){
35
  var helpers = {
36
  optionClass: 'fw-option-type-rgba-color-picker',
37
  eventNamespace: '.fwOptionTypeRgbaColorPicker',
38
- hexColorRegex: /^#[a-f0-9]{3}([a-f0-9]{3})?$/i,
39
  localized: window._fw_option_type_rgba_color_picker_localized,
40
  increment: 0,
41
  isColorDark: function(rgbaColor) {
35
  var helpers = {
36
  optionClass: 'fw-option-type-rgba-color-picker',
37
  eventNamespace: '.fwOptionTypeRgbaColorPicker',
38
+ hexColorRegex: /^#([a-f0-9]{3}){1,2}$/i,
39
  localized: window._fw_option_type_rgba_color_picker_localized,
40
  increment: 0,
41
  isColorDark: function(rgbaColor) {
framework/includes/option-types/typography-v2/class-fw-option-type-typography-v2.php CHANGED
@@ -101,8 +101,8 @@ class FW_Option_Type_Typography_v2 extends FW_Option_Type {
101
  $default = $this->get_defaults();
102
  $values = array_merge( $default['value'], $option['value'], is_array($input_value) ? $input_value : array());
103
 
104
- if ( ! preg_match( '/^#[a-f0-9]{6}$/i', $values['color'] ) ) {
105
- $values = ( isset( $option['value']['color'] ) ) ? $option['value']['color'] : $default['value']['color'];
106
  }
107
 
108
  $components = array_merge( $default['components'], $option['components'] );
101
  $default = $this->get_defaults();
102
  $values = array_merge( $default['value'], $option['value'], is_array($input_value) ? $input_value : array());
103
 
104
+ if ( ! empty($values['color']) && ! preg_match( '/^#([a-f0-9]{3}){1,2}$/i', $values['color'] ) ) {
105
+ $values['color'] = isset( $option['value']['color'] ) ? $option['value']['color'] : $default['value']['color'];
106
  }
107
 
108
  $components = array_merge( $default['components'], $option['components'] );
framework/includes/option-types/typography/class-fw-option-type-typography.php CHANGED
@@ -112,7 +112,7 @@ class FW_Option_Type_Typography extends FW_Option_Type
112
  'size' => ( ! empty( $components['size'] ) ) ? ( isset( $input_value['size'] ) ) ? intval( $input_value['size'] ) : intval( $option['value']['size'] ) : false,
113
  'family' => ( ! empty( $components['family'] ) ) ? ( isset( $input_value['family'] ) ) ? $input_value['family'] : $option['value']['family'] : false,
114
  'style' => ( ! empty( $components['family'] ) ) ? ( isset( $input_value['style'] ) ) ? $input_value['style'] : $option['value']['style'] : false,
115
- 'color' => ( ! empty( $components['color'] ) ) ? ( isset( $input_value['color'] ) && preg_match( '/^#[a-f0-9]{6}$/i', $input_value['color'] ) ) ? $input_value['color'] : $option['value']['color'] : false,
116
  );
117
 
118
  return $values;
112
  'size' => ( ! empty( $components['size'] ) ) ? ( isset( $input_value['size'] ) ) ? intval( $input_value['size'] ) : intval( $option['value']['size'] ) : false,
113
  'family' => ( ! empty( $components['family'] ) ) ? ( isset( $input_value['family'] ) ) ? $input_value['family'] : $option['value']['family'] : false,
114
  'style' => ( ! empty( $components['family'] ) ) ? ( isset( $input_value['style'] ) ) ? $input_value['style'] : $option['value']['style'] : false,
115
+ 'color' => ( ! empty( $components['color'] ) ) ? ( isset( $input_value['color'] ) && preg_match( '/^#([a-f0-9]{3}){1,2}$/i', $input_value['color'] ) ) ? $input_value['color'] : $option['value']['color'] : false,
116
  );
117
 
118
  return $values;
framework/includes/option-types/wp-editor/static/scripts.js CHANGED
@@ -1,37 +1,61 @@
1
  (function ($, fwe) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  var init = function () {
4
  var $option = $(this),
5
  $textarea = $option.find('.wp-editor-area:first'),
6
- id = $option.attr('data-fw-editor-id'),
7
  $wrap = $textarea.closest('.wp-editor-wrap'),
8
- visualMode = (typeof $option.attr('data-mode') != 'undefined')
9
- ? ($option.attr('data-mode') == 'tinymce')
10
- : $wrap.hasClass('tmce-active'),
11
- hasAutoP = $option.attr('data-fw-has-autop') === 'true';
12
 
13
  /**
14
  * Dynamically set tinyMCEPreInit.mceInit and tinyMCEPreInit.qtInit
15
  * based on the data-fw-mce-settings and data-fw-qt-settings
16
  */
17
- var mceSettings = JSON.parse($option.attr('data-fw-mce-settings'));
18
- var qtSettings = JSON.parse($option.attr('data-fw-qt-settings'));
19
 
20
- tinyMCEPreInit.mceInit[ id ] = mceSettings;
21
- tinyMCEPreInit.qtInit[ id ] = qtSettings;
22
-
23
- /**
24
- * Set width
25
- */
26
  $option.closest('.fw-backend-option-input-type-wp-editor').addClass(
27
  'width-type-'+ ($option.attr('data-size') == 'large' ? 'full' : 'fixed')
28
  );
29
 
30
- /**
31
- * TinyMCE Editor
32
- * http://stackoverflow.com/a/21519323/1794248
33
- */
34
- if (mceSettings) {
35
  if (typeof tinyMCEPreInit.mceInit[ id ] == 'undefined') {
36
  console.error('Can\'t find "'+ id +'" in tinyMCEPreInit.mceInit');
37
  return;
@@ -40,6 +64,8 @@
40
  tinymce.execCommand('mceRemoveEditor', false, id);
41
 
42
  tinyMCEPreInit.mceInit[ id ].setup = function(ed) {
 
 
43
  ed.once('init', function (e) {
44
  var editor = e.target,
45
  id = editor.id;
@@ -49,10 +75,13 @@
49
  $textarea.trigger('change'); // fixes https://github.com/ThemeFuse/Unyson/issues/2273
50
  });
51
 
52
- /**
53
- * Fixes when wpautop is false
54
- */
55
  if (!editor.getParam('wpautop')) {
 
 
 
 
 
56
  editor
57
  .on('SaveContent', function(event){
58
  // Remove <p> in Visual mode
@@ -68,32 +97,23 @@
68
  });
69
  }
70
 
71
- /**
72
- * Quick Tags
73
- * http://stackoverflow.com/a/21519323/1794248
74
- */
75
- if (tinyMCEPreInit.qtInit[ id ]) {
76
- new QTags( tinyMCEPreInit.qtInit[ id ] );
77
 
78
- QTags._buttonsInit();
79
-
80
- if ($wrap.hasClass('html-active')) {
81
- $wrap.find('.switch-html').trigger('click');
82
- }
83
-
84
- $wrap.find('.switch-'+ (visualMode ? 'tmce' : 'html')).trigger('click');
85
-
86
- if (!hasAutoP && !visualMode) {
87
- $textarea.val(wp.editor.removep(editor.getContent()));
88
  }
89
  }
 
 
90
  });
91
  };
92
 
93
- if (!tinyMCEPreInit.mceInit[ id ].wpautop) {
94
- $textarea.val( wp.editor.autop($textarea.val()) );
95
- }
96
-
97
  try {
98
  tinymce.init( tinyMCEPreInit.mceInit[ id ] );
99
 
@@ -122,15 +142,7 @@
122
  }
123
  }
124
  } else {
125
- /**
126
- * Quick Tags
127
- * http://stackoverflow.com/a/21519323/1794248
128
- */
129
- if (tinyMCEPreInit.qtInit[ id ]) {
130
- new QTags( tinyMCEPreInit.qtInit[ id ] );
131
-
132
- QTags._buttonsInit();
133
- }
134
  }
135
  };
136
 
1
  (function ($, fwe) {
2
+ var activeVisualMode = {},
3
+ /**
4
+ * Quick Tags
5
+ * http://stackoverflow.com/a/21519323/1794248
6
+ */
7
+ qTagsInit = function (id, $option, $wrap, $textarea, editor) {
8
+ if (!tinyMCEPreInit.qtInit[ id ]) {
9
+ return;
10
+ }
11
+
12
+ new QTags( tinyMCEPreInit.qtInit[ id ] );
13
+
14
+ QTags._buttonsInit();
15
+
16
+ if ($wrap.hasClass('html-active')) { // fixes glitch on init
17
+ $wrap.find('.switch-html:first').trigger('click');
18
+ }
19
+
20
+ var visualMode = (typeof activeVisualMode[ id ] != 'undefined')
21
+ ? activeVisualMode[ id ]
22
+ : (
23
+ (typeof $option.attr('data-mode') != 'undefined')
24
+ ? ($option.attr('data-mode') == 'tinymce')
25
+ : $wrap.hasClass('tmce-active')
26
+ );
27
+
28
+ $wrap.on('click', '.wp-switch-editor', function () {
29
+ activeVisualMode[ id ] = $(this).hasClass('switch-tmce');
30
+ });
31
+
32
+ $wrap.find('.switch-'+ (visualMode ? 'tmce' : 'html') +':first').trigger('click');
33
+
34
+ if (editor && !visualMode) {
35
+ $textarea.val(wp.editor.removep(editor.getContent()));
36
+ }
37
+ };
38
 
39
  var init = function () {
40
  var $option = $(this),
41
  $textarea = $option.find('.wp-editor-area:first'),
 
42
  $wrap = $textarea.closest('.wp-editor-wrap'),
43
+ id = $option.attr('data-fw-editor-id');
 
 
 
44
 
45
  /**
46
  * Dynamically set tinyMCEPreInit.mceInit and tinyMCEPreInit.qtInit
47
  * based on the data-fw-mce-settings and data-fw-qt-settings
48
  */
49
+ tinyMCEPreInit.mceInit[ id ] = JSON.parse($option.attr('data-fw-mce-settings'));
50
+ tinyMCEPreInit.qtInit[ id ] = JSON.parse($option.attr('data-fw-qt-settings'));
51
 
52
+ // Set width
 
 
 
 
 
53
  $option.closest('.fw-backend-option-input-type-wp-editor').addClass(
54
  'width-type-'+ ($option.attr('data-size') == 'large' ? 'full' : 'fixed')
55
  );
56
 
57
+ // TinyMCE Editor http://stackoverflow.com/a/21519323/1794248
58
+ if (tinyMCEPreInit.mceInit[ id ]) {
 
 
 
59
  if (typeof tinyMCEPreInit.mceInit[ id ] == 'undefined') {
60
  console.error('Can\'t find "'+ id +'" in tinyMCEPreInit.mceInit');
61
  return;
64
  tinymce.execCommand('mceRemoveEditor', false, id);
65
 
66
  tinyMCEPreInit.mceInit[ id ].setup = function(ed) {
67
+ var initialContent = $textarea.val(); // before \r\n were replaced
68
+
69
  ed.once('init', function (e) {
70
  var editor = e.target,
71
  id = editor.id;
75
  $textarea.trigger('change'); // fixes https://github.com/ThemeFuse/Unyson/issues/2273
76
  });
77
 
78
+ // Fixes when wpautop is false
 
 
79
  if (!editor.getParam('wpautop')) {
80
+ if (initialContent.indexOf('<p>') !== -1) {
81
+ initialContent = wp.editor.removep(initialContent);
82
+ }
83
+ editor.setContent(initialContent.replace(/\r?\n/g, '<br />'));
84
+
85
  editor
86
  .on('SaveContent', function(event){
87
  // Remove <p> in Visual mode
97
  });
98
  }
99
 
100
+ qTagsInit(id, $option, $wrap, $textarea, editor);
 
 
 
 
 
101
 
102
+ if (!editor.getParam('wpautop') && $wrap.hasClass('tmce-active')) {
103
+ /**
104
+ * fixes: when initialContent is with <p>
105
+ * if no changes are made in editor the <p> are not removed
106
+ */
107
+ {
108
+ $wrap.find('.switch-html:first').trigger('click');
109
+ $wrap.find('.switch-tmce:first').trigger('click');
 
 
110
  }
111
  }
112
+
113
+ initialContent = null; // free memory
114
  });
115
  };
116
 
 
 
 
 
117
  try {
118
  tinymce.init( tinyMCEPreInit.mceInit[ id ] );
119
 
142
  }
143
  }
144
  } else {
145
+ qTagsInit(id, $option, $wrap, $textarea);
 
 
 
 
 
 
 
 
146
  }
147
  };
148
 
framework/manifest.php CHANGED
@@ -4,4 +4,4 @@ $manifest = array();
4
 
5
  $manifest['name'] = __('Unyson', 'fw');
6
 
7
- $manifest['version'] = '2.6.14';
4
 
5
  $manifest['name'] = __('Unyson', 'fw');
6
 
7
+ $manifest['version'] = '2.6.15';
readme.txt CHANGED
@@ -3,7 +3,7 @@ Contributors: unyson
3
  Tags: page builder, shortcodes, backup, seo, breadcrumbs, portfolio, framework
4
  Requires at least: 4.4
5
  Tested up to: 4.7
6
- Stable tag: 2.6.14
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
@@ -22,6 +22,7 @@ A simple and easy way to build a powerful website.
22
  **Features include:**
23
 
24
  * **Drag & Drop Page Builder.** Create countless pages using the content and media shortcodes.
 
25
  * **Sliders.** To make you life even easier we have already built in 3 of them that support images and videos.
26
  * **Mega Menu.** User-friendly drop down menu that will let you easily create highly customized menu configurations.
27
  * **Sidebars.** This module will let your users customize WordPress pages with dynamic sidebars.
@@ -31,7 +32,6 @@ A simple and easy way to build a powerful website.
31
  * **SEO.** SEO settings at finger tips without installing further plugins.
32
  * **Feedback.** We've added a way for users to submit reviews and ratings for events, projects, etc.
33
  * **Events.** It's pretty simple to use and it has Calendar and Map shortcodes.
34
- * **Backup & Demo Content.** Create an automated backup schedule, import demo content or even create a demo content archive for migration purposes.
35
 
36
  **Get involved**
37
 
@@ -85,6 +85,9 @@ Yes; Unyson will work with any theme.
85
 
86
  == Changelog ==
87
 
 
 
 
88
  = 2.6.14 =
89
  * Fixed infinite loop when php memory limit is `-1`
90
  * Minor changes
3
  Tags: page builder, shortcodes, backup, seo, breadcrumbs, portfolio, framework
4
  Requires at least: 4.4
5
  Tested up to: 4.7
6
+ Stable tag: 2.6.15
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
22
  **Features include:**
23
 
24
  * **Drag & Drop Page Builder.** Create countless pages using the content and media shortcodes.
25
+ * **Backup & Demo Content.** Create an automated backup schedule, import demo content or even create a demo content archive for migration purposes.
26
  * **Sliders.** To make you life even easier we have already built in 3 of them that support images and videos.
27
  * **Mega Menu.** User-friendly drop down menu that will let you easily create highly customized menu configurations.
28
  * **Sidebars.** This module will let your users customize WordPress pages with dynamic sidebars.
32
  * **SEO.** SEO settings at finger tips without installing further plugins.
33
  * **Feedback.** We've added a way for users to submit reviews and ratings for events, projects, etc.
34
  * **Events.** It's pretty simple to use and it has Calendar and Map shortcodes.
 
35
 
36
  **Get involved**
37
 
85
 
86
  == Changelog ==
87
 
88
+ = 2.6.15 =
89
+ * Fixed [#2380](https://github.com/ThemeFuse/Unyson/issues/2380), [#2397](https://github.com/ThemeFuse/Unyson/issues/2397), [#2212](https://github.com/ThemeFuse/Unyson/issues/2212#issuecomment-278954170)
90
+
91
  = 2.6.14 =
92
  * Fixed infinite loop when php memory limit is `-1`
93
  * Minor changes
unyson.php CHANGED
@@ -3,7 +3,7 @@
3
  * Plugin Name: Unyson
4
  * Plugin URI: http://unyson.io/
5
  * Description: A free drag & drop framework that comes with a bunch of built in extensions that will help you develop premium themes fast & easy.
6
- * Version: 2.6.14
7
  * Author: ThemeFuse
8
  * Author URI: http://themefuse.com
9
  * License: GPL2+
3
  * Plugin Name: Unyson
4
  * Plugin URI: http://unyson.io/
5
  * Description: A free drag & drop framework that comes with a bunch of built in extensions that will help you develop premium themes fast & easy.
6
+ * Version: 2.6.15
7
  * Author: ThemeFuse
8
  * Author URI: http://themefuse.com
9
  * License: GPL2+