Posts 2 Posts - Version 1.3.1

Version Description

  • sanitize connection fields values on save, preventing security exploits
  • improved connection field default value handling
  • added 'default_cb' as an optional key when defining connection fields
  • fixed parameter order for 'p2p_admin_box_show' filter
  • pass the current post ID to the 'p2p_new_post_args' filter
Download this release

Release Info

Developer scribu
Plugin Icon wp plugin Posts 2 Posts
Version 1.3.1
Comparing to
See all releases

Code changes from version 1.3 to 1.3.1

admin/box-factory.php CHANGED
@@ -23,7 +23,7 @@ class P2P_Box_Factory {
23
  $box_args = array();
24
  }
25
 
26
- foreach ( array( 'fields', 'can_create_post' ) as $key ) {
27
  if ( isset( $args[ $key ] ) ) {
28
  $box_args[ $key ] = _p2p_pluck( $args, $key );
29
  }
@@ -41,23 +41,12 @@ class P2P_Box_Factory {
41
  $box_args = (object) wp_parse_args( $box_args, array(
42
  'show' => 'any',
43
  'context' => 'side',
44
- 'fields' => array(),
45
  'can_create_post' => true
46
  ) );
47
 
48
  if ( !$box_args->show )
49
  return false;
50
 
51
- foreach ( $box_args->fields as &$field_args ) {
52
- if ( !is_array( $field_args ) )
53
- $field_args = array( 'title' => $field_args );
54
-
55
- $field_args['type'] = _p2p_get_field_type( $field_args );
56
-
57
- if ( 'checkbox' == $field_args['type'] && !isset( $field_args['values'] ) )
58
- $field_args['values'] = array( true => ' ' );
59
- }
60
-
61
  self::$box_args[$p2p_type] = $box_args;
62
 
63
  return true;
@@ -122,17 +111,21 @@ class P2P_Box_Factory {
122
  if ( 'revision' == $post->post_type || defined( 'DOING_AJAX' ) )
123
  return;
124
 
125
- // Custom fields
126
- if ( isset( $_POST['p2p_meta'] ) ) {
127
- foreach ( $_POST['p2p_meta'] as $p2p_id => $data ) {
 
 
128
  $connection = p2p_get_connection( $p2p_id );
129
 
130
- $fields = self::$box_args[$connection->p2p_type]->fields;
131
 
132
  foreach ( $fields as $key => &$field ) {
133
  $field['name'] = $key;
134
  }
135
 
 
 
136
  scbForms::update_meta( $fields, $data, $p2p_id, 'p2p' );
137
  }
138
  }
23
  $box_args = array();
24
  }
25
 
26
+ foreach ( array( 'can_create_post' ) as $key ) {
27
  if ( isset( $args[ $key ] ) ) {
28
  $box_args[ $key ] = _p2p_pluck( $args, $key );
29
  }
41
  $box_args = (object) wp_parse_args( $box_args, array(
42
  'show' => 'any',
43
  'context' => 'side',
 
44
  'can_create_post' => true
45
  ) );
46
 
47
  if ( !$box_args->show )
48
  return false;
49
 
 
 
 
 
 
 
 
 
 
 
50
  self::$box_args[$p2p_type] = $box_args;
51
 
52
  return true;
111
  if ( 'revision' == $post->post_type || defined( 'DOING_AJAX' ) )
112
  return;
113
 
114
+ if ( isset( $_POST['p2p_connections'] ) ) {
115
+ // Loop through the hidden fields instead of through $_POST['p2p_meta'] because empty checkboxes send no data.
116
+ foreach ( $_POST['p2p_connections'] as $p2p_id ) {
117
+ $data = scbForms::get_value( array( 'p2p_meta', $p2p_id ), $_POST, array() );
118
+
119
  $connection = p2p_get_connection( $p2p_id );
120
 
121
+ $fields = p2p_type( $connection->p2p_type )->fields;
122
 
123
  foreach ( $fields as $key => &$field ) {
124
  $field['name'] = $key;
125
  }
126
 
127
+ $data = scbForms::validate_post_data( $fields, $data );
128
+
129
  scbForms::update_meta( $fields, $data, $p2p_id, 'p2p' );
130
  }
131
  }
admin/box.js CHANGED
@@ -25,7 +25,7 @@
25
  jQuery('.p2p-search input[placeholder]').each(setVal).focus(clearVal).blur(setVal);
26
  }
27
  return jQuery('.p2p-box').each(function() {
28
- var $connections, $createButton, $createInput, $metabox, $searchInput, $spinner, $viewAll, PostsTab, ajax_request, append_connection, clear_connections, create_connection, delete_connection, maybe_hide_table, refresh_candidates, row_ajax_request, searchTab, switch_to_tab;
29
  $metabox = jQuery(this);
30
  $connections = $metabox.find('.p2p-connections');
31
  $spinner = jQuery('<img>', {
@@ -128,7 +128,10 @@
128
  $td.html($spinner.show());
129
  return ajax_request(data, callback);
130
  };
131
- maybe_hide_table = function($table) {
 
 
 
132
  if (!$table.find('tbody tr').length) {
133
  return $table.hide();
134
  }
@@ -168,11 +171,10 @@
168
  $td = $self.closest('td');
169
  data = {
170
  subaction: 'disconnect',
171
- p2p_id: $self.data('p2p_id')
172
  };
173
  row_ajax_request($td, data, function(response) {
174
- $td.closest('tr').remove();
175
- maybe_hide_table($connections);
176
  return refresh_candidates(response);
177
  });
178
  return false;
@@ -188,11 +190,10 @@
188
  };
189
  row_ajax_request($td, data, function(response) {
190
  append_connection(response);
191
- if ($metabox.data('prevent_duplicates')) {
192
- $td.closest('tr').remove();
193
- return maybe_hide_table($td.closest('table'));
194
- } else {
195
  return $td.html($self);
 
 
196
  }
197
  });
198
  return false;
25
  jQuery('.p2p-search input[placeholder]').each(setVal).focus(clearVal).blur(setVal);
26
  }
27
  return jQuery('.p2p-box').each(function() {
28
+ var $connections, $createButton, $createInput, $metabox, $searchInput, $spinner, $viewAll, PostsTab, ajax_request, append_connection, clear_connections, create_connection, delete_connection, refresh_candidates, remove_row, row_ajax_request, searchTab, switch_to_tab;
29
  $metabox = jQuery(this);
30
  $connections = $metabox.find('.p2p-connections');
31
  $spinner = jQuery('<img>', {
128
  $td.html($spinner.show());
129
  return ajax_request(data, callback);
130
  };
131
+ remove_row = function($td) {
132
+ var $table;
133
+ $table = $td.closest('table');
134
+ $td.closest('tr').remove();
135
  if (!$table.find('tbody tr').length) {
136
  return $table.hide();
137
  }
171
  $td = $self.closest('td');
172
  data = {
173
  subaction: 'disconnect',
174
+ p2p_id: $self.closest('td').find('input').val()
175
  };
176
  row_ajax_request($td, data, function(response) {
177
+ remove_row($td);
 
178
  return refresh_candidates(response);
179
  });
180
  return false;
190
  };
191
  row_ajax_request($td, data, function(response) {
192
  append_connection(response);
193
+ if ($metabox.data('duplicate_connections')) {
 
 
 
194
  return $td.html($self);
195
+ } else {
196
+ return remove_row($td);
197
  }
198
  });
199
  return false;
admin/box.php CHANGED
@@ -49,7 +49,7 @@ class P2P_Box {
49
  'title' => new $title_class( $this->labels->singular_name ),
50
  );
51
 
52
- foreach ( $this->args->fields as $key => $data ) {
53
  $this->columns[ 'meta-' . $key ] = new P2P_Field_Generic( $key, $data );
54
  }
55
 
@@ -90,7 +90,7 @@ class P2P_Box {
90
  protected function render_data_attributes() {
91
  $data_attr = array(
92
  'p2p_type' => $this->ctype->name,
93
- 'prevent_duplicates' => $this->ctype->prevent_duplicates,
94
  'cardinality' => $this->ctype->get_opposite( 'cardinality' ),
95
  'direction' => $this->ctype->get_direction()
96
  );
@@ -239,7 +239,9 @@ class P2P_Box {
239
  'post_type' => $this->ctype->get_opposite( 'side' )->post_type[0]
240
  );
241
 
242
- $args = apply_filters( 'p2p_new_post_args', $args, $this->ctype );
 
 
243
 
244
  $this->safe_connect( wp_insert_post( $args ) );
245
  }
@@ -322,7 +324,7 @@ class P2P_Box {
322
  public function check_capability() {
323
  $show = $this->ctype->get_opposite( 'side' )->check_capability();
324
 
325
- return apply_filters( 'p2p_admin_box_show', $show, $GLOBALS['post'], $this->ctype );
326
  }
327
  }
328
 
49
  'title' => new $title_class( $this->labels->singular_name ),
50
  );
51
 
52
+ foreach ( $this->ctype->fields as $key => $data ) {
53
  $this->columns[ 'meta-' . $key ] = new P2P_Field_Generic( $key, $data );
54
  }
55
 
90
  protected function render_data_attributes() {
91
  $data_attr = array(
92
  'p2p_type' => $this->ctype->name,
93
+ 'duplicate_connections' => $this->ctype->duplicate_connections,
94
  'cardinality' => $this->ctype->get_opposite( 'cardinality' ),
95
  'direction' => $this->ctype->get_direction()
96
  );
239
  'post_type' => $this->ctype->get_opposite( 'side' )->post_type[0]
240
  );
241
 
242
+ $from = absint( $_POST['from'] );
243
+
244
+ $args = apply_filters( 'p2p_new_post_args', $args, $this->ctype, $from );
245
 
246
  $this->safe_connect( wp_insert_post( $args ) );
247
  }
324
  public function check_capability() {
325
  $show = $this->ctype->get_opposite( 'side' )->check_capability();
326
 
327
+ return apply_filters( 'p2p_admin_box_show', $show, $this->ctype, $GLOBALS['post'] );
328
  }
329
  }
330
 
admin/fields.php CHANGED
@@ -81,15 +81,10 @@ class P2P_Field_Generic implements P2P_Field {
81
  );
82
 
83
  if ( isset( $this->data['values'] ) )
84
- $args['value'] = $this->data['values'];
85
 
86
- if ( isset( $this->data['default'] ) ) {
87
- $args['default'] = $this->data['default'];
88
- if ( 'checkbox' == $args['type'] )
89
- $args['default'] = (array) $args['default'];
90
- } elseif ( 'select' == $args['type'] ) {
91
  $args['text'] = '';
92
- }
93
 
94
  return scbForms::input_from_meta( $args, $p2p_id, 'p2p' );
95
  }
81
  );
82
 
83
  if ( isset( $this->data['values'] ) )
84
+ $args['values'] = $this->data['values'];
85
 
86
+ if ( 'select' == $args['type'] )
 
 
 
 
87
  $args['text'] = '';
 
88
 
89
  return scbForms::input_from_meta( $args, $p2p_id, 'p2p' );
90
  }
admin/templates/column-delete.html CHANGED
@@ -1 +1,2 @@
1
- <a href="#" data-p2p_id="{{p2p_id}}" title="{{{title}}}"></a>
 
1
+ <a href="#" title="{{{title}}}"></a>
2
+ <input type="hidden" name="p2p_connections[]" value="{{p2p_id}}" />
core/api.php CHANGED
@@ -21,7 +21,7 @@
21
  *
22
  * - 'cardinality' - string How many connection can each post have: 'one-to-many', 'many-to-one' or 'many-to-many'. Default: 'many-to-many'
23
  *
24
- * - 'prevent_duplicates' - bool Whether to disallow duplicate connections between the same two posts. Default: true.
25
  *
26
  * - 'self_connections' - bool Whether to allow a post to connect to itself. Default: false.
27
  *
@@ -64,6 +64,10 @@ function p2p_register_connection_type( $args ) {
64
  $args['name'] = _p2p_pluck( $args, 'id' );
65
  }
66
 
 
 
 
 
67
  if ( isset( $args['show_ui'] ) ) {
68
  $args['admin_box'] = array(
69
  'show' => _p2p_pluck( $args, 'show_ui' )
21
  *
22
  * - 'cardinality' - string How many connection can each post have: 'one-to-many', 'many-to-one' or 'many-to-many'. Default: 'many-to-many'
23
  *
24
+ * - 'duplicate_connections' - bool Whether to allow more than one connection between the same two posts. Default: false.
25
  *
26
  * - 'self_connections' - bool Whether to allow a post to connect to itself. Default: false.
27
  *
64
  $args['name'] = _p2p_pluck( $args, 'id' );
65
  }
66
 
67
+ if ( isset( $args['prevent_duplicates'] ) ) {
68
+ $args['duplicate_connections'] = !$args['prevent_duplicates'];
69
+ }
70
+
71
  if ( isset( $args['show_ui'] ) ) {
72
  $args['admin_box'] = array(
73
  'show' => _p2p_pluck( $args, 'show_ui' )
core/directed-type.php CHANGED
@@ -171,7 +171,7 @@ class P2P_Directed_Connection_Type {
171
  ) ) );
172
  }
173
 
174
- if ( $this->prevent_duplicates ) {
175
  _p2p_append( $to_exclude, $this->get_connections( array(
176
  'from' => $item_id,
177
  'fields' => 'object_id'
@@ -202,7 +202,7 @@ class P2P_Directed_Connection_Type {
202
  if ( !$this->self_connections && $from == $to )
203
  return new WP_Error( 'self_connection', 'Connection between an element and itself is not allowed.' );
204
 
205
- if ( $this->prevent_duplicates && $this->get_p2p_id( $from, $to ) )
206
  return new WP_Error( 'duplicate_connection', 'Duplicate connections are not allowed.' );
207
 
208
  if ( 'one' == $this->get_opposite( 'cardinality' ) && $this->connection_exists( compact( 'from' ) ) )
@@ -211,11 +211,31 @@ class P2P_Directed_Connection_Type {
211
  if ( 'one' == $this->get_current( 'cardinality' ) && $this->connection_exists( compact( 'to' ) ) )
212
  return new WP_Error( 'cardinality_current', 'Cardinality problem.' );
213
 
214
- return $this->create_connection( array(
215
  'from' => $from,
216
  'to' => $to,
217
  'meta' => array_merge( $meta, $this->data )
218
  ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  }
220
 
221
  /**
171
  ) ) );
172
  }
173
 
174
+ if ( !$this->duplicate_connections ) {
175
  _p2p_append( $to_exclude, $this->get_connections( array(
176
  'from' => $item_id,
177
  'fields' => 'object_id'
202
  if ( !$this->self_connections && $from == $to )
203
  return new WP_Error( 'self_connection', 'Connection between an element and itself is not allowed.' );
204
 
205
+ if ( !$this->duplicate_connections && $this->get_p2p_id( $from, $to ) )
206
  return new WP_Error( 'duplicate_connection', 'Duplicate connections are not allowed.' );
207
 
208
  if ( 'one' == $this->get_opposite( 'cardinality' ) && $this->connection_exists( compact( 'from' ) ) )
211
  if ( 'one' == $this->get_current( 'cardinality' ) && $this->connection_exists( compact( 'to' ) ) )
212
  return new WP_Error( 'cardinality_current', 'Cardinality problem.' );
213
 
214
+ $p2p_id = $this->create_connection( array(
215
  'from' => $from,
216
  'to' => $to,
217
  'meta' => array_merge( $meta, $this->data )
218
  ) );
219
+
220
+ // Store additional default values
221
+ foreach ( $this->fields as $key => $args ) {
222
+ // (array) null == array()
223
+ foreach ( (array) $this->get_default( $args, $p2p_id ) as $default_value ) {
224
+ p2p_add_meta( $p2p_id, $key, $default_value );
225
+ }
226
+ }
227
+
228
+ return $p2p_id;
229
+ }
230
+
231
+ protected function get_default( $args, $p2p_id ) {
232
+ if ( isset( $args['default_cb'] ) )
233
+ return call_user_func( $args['default_cb'], p2p_get_connection( $p2p_id ) );
234
+
235
+ if ( !isset( $args['default'] ) )
236
+ return null;
237
+
238
+ return $args['default'];
239
  }
240
 
241
  /**
core/type-factory.php CHANGED
@@ -22,9 +22,10 @@ class P2P_Connection_Type_Factory {
22
  'to' => 'post',
23
  'from_query_vars' => array(),
24
  'to_query_vars' => array(),
 
25
  'data' => array(),
26
  'cardinality' => 'many-to-many',
27
- 'prevent_duplicates' => true,
28
  'self_connections' => false,
29
  'sortable' => false,
30
  'title' => array(),
22
  'to' => 'post',
23
  'from_query_vars' => array(),
24
  'to_query_vars' => array(),
25
+ 'fields' => array(),
26
  'data' => array(),
27
  'cardinality' => 'many-to-many',
28
+ 'duplicate_connections' => false,
29
  'self_connections' => false,
30
  'sortable' => false,
31
  'title' => array(),
core/type.php CHANGED
@@ -31,7 +31,7 @@ class P2P_Connection_Type {
31
  $this->indeterminate = true;
32
  }
33
  } else {
34
- $this->self_connections = true;
35
  }
36
 
37
  $this->set_cardinality( _p2p_pluck( $args, 'cardinality' ) );
@@ -40,11 +40,32 @@ class P2P_Connection_Type {
40
 
41
  $this->title = $this->expand_title( _p2p_pluck( $args, 'title' ) );
42
 
 
 
43
  foreach ( $args as $key => $value ) {
44
  $this->$key = $value;
45
  }
46
  }
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  private function set_cardinality( $cardinality ) {
49
  $parts = explode( '-', $cardinality );
50
 
31
  $this->indeterminate = true;
32
  }
33
  } else {
34
+ $args['self_connections'] = true;
35
  }
36
 
37
  $this->set_cardinality( _p2p_pluck( $args, 'cardinality' ) );
40
 
41
  $this->title = $this->expand_title( _p2p_pluck( $args, 'title' ) );
42
 
43
+ $this->fields = $this->expand_fields( _p2p_pluck( $args, 'fields' ) );
44
+
45
  foreach ( $args as $key => $value ) {
46
  $this->$key = $value;
47
  }
48
  }
49
 
50
+ private function expand_fields( $fields ) {
51
+ foreach ( $fields as &$field_args )
52
+ {
53
+ if ( !is_array( $field_args ) )
54
+ $field_args = array( 'title' => $field_args );
55
+
56
+ if ( !isset( $field_args['type'] ) )
57
+ {
58
+ $field_args['type'] = isset( $field_args['values'] ) ? 'select' : 'text';
59
+ }
60
+ elseif ( 'checkbox' == $field_args['type'] && !isset( $field_args['values'] ) )
61
+ {
62
+ $field_args['values'] = array( true => ' ' );
63
+ }
64
+ }
65
+
66
+ return $fields;
67
+ }
68
+
69
  private function set_cardinality( $cardinality ) {
70
  $parts = explode( '-', $cardinality );
71
 
core/util.php CHANGED
@@ -49,17 +49,6 @@ function _p2p_append( &$arr, $values ) {
49
  $arr = array_merge( $arr, $values );
50
  }
51
 
52
- /** @internal */
53
- function _p2p_get_field_type( $args ) {
54
- if ( isset( $args['type'] ) )
55
- return $args['type'];
56
-
57
- if ( isset( $args['values'] ) && is_array( $args['values'] ) )
58
- return 'select';
59
-
60
- return 'text';
61
- }
62
-
63
  /** @internal */
64
  function _p2p_first( $args ) {
65
  if ( empty( $args ) )
49
  $arr = array_merge( $arr, $values );
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
52
  /** @internal */
53
  function _p2p_first( $args ) {
54
  if ( empty( $args ) )
posts-to-posts.php CHANGED
@@ -2,31 +2,15 @@
2
  /*
3
  Plugin Name: Posts 2 Posts
4
  Description: Create many-to-many relationships between all types of posts.
5
- Version: 1.3
6
  Author: scribu
7
  Author URI: http://scribu.net/
8
  Plugin URI: http://scribu.net/wordpress/posts-to-posts
9
  Text Domain: posts-to-posts
10
  Domain Path: /lang
11
-
12
-
13
- Copyright (C) 2010-2012 Cristi Burcă (mail@scribu.net)
14
-
15
- This program is free software; you can redistribute it and/or modify
16
- it under the terms of the GNU General Public License as published by
17
- the Free Software Foundation; either version 3 of the License, or
18
- (at your option) any later version.
19
-
20
- This program is distributed in the hope that it will be useful,
21
- but WITHOUT ANY WARRANTY; without even the implied warranty of
22
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
- GNU General Public License for more details.
24
-
25
- You should have received a copy of the GNU General Public License
26
- along with this program. If not, see <http://www.gnu.org/licenses/>.
27
  */
28
 
29
- define( 'P2P_PLUGIN_VERSION', '1.3-beta' );
30
 
31
  define( 'P2P_TEXTDOMAIN', 'posts-to-posts' );
32
 
2
  /*
3
  Plugin Name: Posts 2 Posts
4
  Description: Create many-to-many relationships between all types of posts.
5
+ Version: 1.3.1
6
  Author: scribu
7
  Author URI: http://scribu.net/
8
  Plugin URI: http://scribu.net/wordpress/posts-to-posts
9
  Text Domain: posts-to-posts
10
  Domain Path: /lang
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  */
12
 
13
+ define( 'P2P_PLUGIN_VERSION', '1.3.1' );
14
 
15
  define( 'P2P_TEXTDOMAIN', 'posts-to-posts' );
16
 
readme.txt CHANGED
@@ -3,7 +3,9 @@ Contributors: scribu, ciobi
3
  Tags: connections, custom post types, relationships, many-to-many, users
4
  Requires at least: 3.2
5
  Tested up to: 3.4
6
- Stable tag: 1.3
 
 
7
 
8
  Efficient many-to-many connections between posts, pages, custom post types, users.
9
 
@@ -51,6 +53,13 @@ Make sure your host is running PHP 5. The only foolproof way to do this is to ad
51
 
52
  == Changelog ==
53
 
 
 
 
 
 
 
 
54
  = 1.3 =
55
  * allow passing entire objects to get_connected(), connect() etc.
56
  * made get_related() work with posts-to-users connections
3
  Tags: connections, custom post types, relationships, many-to-many, users
4
  Requires at least: 3.2
5
  Tested up to: 3.4
6
+ Stable tag: 1.3.1
7
+ License: GPLv2 or later
8
+ License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
10
  Efficient many-to-many connections between posts, pages, custom post types, users.
11
 
53
 
54
  == Changelog ==
55
 
56
+ = 1.3.1 =
57
+ * sanitize connection fields values on save, preventing security exploits
58
+ * improved connection field default value handling
59
+ * added 'default_cb' as an optional key when defining connection fields
60
+ * fixed parameter order for 'p2p_admin_box_show' filter
61
+ * pass the current post ID to the 'p2p_new_post_args' filter
62
+
63
  = 1.3 =
64
  * allow passing entire objects to get_connected(), connect() etc.
65
  * made get_related() work with posts-to-users connections
scb/Forms.php CHANGED
@@ -6,83 +6,20 @@ class scbForms {
6
 
7
  const TOKEN = '%input%';
8
 
9
- protected static $cur_name;
10
-
11
- function input_with_value( $args, $value ) {
12
- if ( is_null( $value ) && isset( $args['default'] ) )
13
- $value = $args['default'];
14
-
15
- if ( !is_null( $value ) ) {
16
- switch ( $args['type'] ) {
17
- case 'select':
18
- case 'radio':
19
- $args['selected'] = $value;
20
- break;
21
- case 'checkbox':
22
- if ( is_array( $value ) )
23
- $args['checked'] = $value;
24
- else
25
- $args['checked'] = ( $value || ( isset( $args['value'] ) && $value == $args['value'] ) );
26
- break;
27
- default:
28
- $args['value'] = $value;
29
- }
30
- }
31
 
32
- return self::input( $args );
33
  }
34
 
35
- static function input( $args, $formdata = false ) {
36
- if ( false !== $formdata ) {
37
- $form = new scbForm( $formdata );
38
- return $form->input( $args );
39
- }
40
-
41
- if ( empty( $args['name'] ) ) {
42
- return trigger_error( 'Empty name', E_USER_WARNING );
43
- }
44
-
45
- $args = wp_parse_args( $args, array(
46
- 'desc' => '',
47
- 'desc_pos' => 'after',
48
- 'wrap' => self::TOKEN,
49
- 'wrap_each' => self::TOKEN,
50
- ) );
51
 
52
- if ( isset( $args['value'] ) && is_array( $args['value'] ) ) {
53
- $args['values'] = $args['value'];
54
- unset( $args['value'] );
55
- }
56
-
57
- if ( isset( $args['extra'] ) && !is_array( $args['extra'] ) )
58
- $args['extra'] = shortcode_parse_atts( $args['extra'] );
59
-
60
- self::$cur_name = self::get_name( $args['name'] );
61
-
62
- switch ( $args['type'] ) {
63
- case 'select':
64
- case 'radio':
65
- $input = self::_single_choice( $args );
66
- break;
67
- case 'checkbox':
68
- if ( isset( $args['values'] ) )
69
- $input = self::_multiple_choice( $args );
70
- else
71
- $input = self::_checkbox( $args );
72
- break;
73
- default:
74
- $input = self::_input( $args );
75
- }
76
-
77
- return str_replace( self::TOKEN, $input, $args['wrap'] );
78
  }
79
 
80
-
81
- // ____________UTILITIES____________
82
-
83
-
84
  // Generates a table wrapped in a form
85
- static function form_table( $rows, $formdata = NULL ) {
86
  $output = '';
87
  foreach ( $rows as $row )
88
  $output .= self::table_row( $row, $formdata );
@@ -93,7 +30,7 @@ class scbForms {
93
  }
94
 
95
  // Generates a form
96
- static function form( $inputs, $formdata = NULL, $nonce ) {
97
  $output = '';
98
  foreach ( $inputs as $input )
99
  $output .= self::input( $input, $formdata );
@@ -104,7 +41,7 @@ class scbForms {
104
  }
105
 
106
  // Generates a table
107
- static function table( $rows, $formdata = NULL ) {
108
  $output = '';
109
  foreach ( $rows as $row )
110
  $output .= self::table_row( $row, $formdata );
@@ -115,7 +52,7 @@ class scbForms {
115
  }
116
 
117
  // Generates a table row
118
- static function table_row( $args, $formdata = NULL ) {
119
  return self::row_wrap( $args['title'], self::input( $args, $formdata ) );
120
  }
121
 
@@ -157,201 +94,6 @@ class scbForms {
157
  // ____________PRIVATE METHODS____________
158
 
159
 
160
- private static function _single_choice( $args ) {
161
- $args = wp_parse_args( $args, array(
162
- 'numeric' => false, // use numeric array instead of associative
163
- 'selected' => array( 'foo' ), // hack to make default blank
164
- ) );
165
-
166
- self::_expand_values( $args );
167
-
168
- if ( 'select' == $args['type'] )
169
- return self::_select( $args );
170
- else
171
- return self::_radio( $args );
172
- }
173
-
174
- private static function _multiple_choice( $args ) {
175
- $args = wp_parse_args( $args, array(
176
- 'numeric' => false, // use numeric array instead of associative
177
- 'checked' => null,
178
- ) );
179
-
180
- self::$cur_name .= '[]';
181
-
182
- self::_expand_values( $args );
183
-
184
- extract( $args );
185
-
186
- if ( !is_array( $checked ) )
187
- $checked = array();
188
-
189
- $opts = '';
190
- foreach ( $values as $value => $title ) {
191
- $single_input = self::_checkbox( array(
192
- 'type' => 'checkbox',
193
- 'value' => $value,
194
- 'checked' => in_array( $value, $checked ),
195
- 'desc' => $title,
196
- 'desc_pos' => 'after'
197
- ) );
198
-
199
- $opts .= str_replace( self::TOKEN, $single_input, $args['wrap_each'] );
200
- }
201
-
202
- return self::add_desc( $opts, $desc, $desc_pos );
203
- }
204
-
205
- private static function _expand_values( &$args ) {
206
- $values =& $args['values'];
207
-
208
- if ( !empty( $values ) && !self::is_associative( $values ) ) {
209
- if ( is_array( $args['desc'] ) ) {
210
- $values = array_combine( $values, $args['desc'] ); // back-compat
211
- $args['desc'] = false;
212
- } elseif ( !isset( $args['numeric'] ) || !$args['numeric'] ) {
213
- $values = array_combine( $values, $values );
214
- }
215
- }
216
- }
217
-
218
- private static function _radio( $args ) {
219
- extract( $args );
220
-
221
- if ( array( 'foo' ) == $selected ) {
222
- // radio buttons should always have one option selected
223
- $selected = key( $values );
224
- }
225
-
226
- $opts = '';
227
- foreach ( $values as $value => $title ) {
228
- $single_input = self::_checkbox( array(
229
- 'type' => 'radio',
230
- 'value' => $value,
231
- 'checked' => ( (string) $value == (string) $selected ),
232
- 'desc' => $title,
233
- 'desc_pos' => 'after'
234
- ) );
235
-
236
- $opts .= str_replace( self::TOKEN, $single_input, $args['wrap_each'] );
237
- }
238
-
239
- return self::add_desc( $opts, $desc, $desc_pos );
240
- }
241
-
242
- private static function _select( $args ) {
243
- extract( wp_parse_args( $args, array(
244
- 'text' => false,
245
- 'extra' => array()
246
- ) ) );
247
-
248
- $options = array();
249
-
250
- if ( false !== $text ) {
251
- $options[] = array(
252
- 'value' => '',
253
- 'selected' => ( $selected == array( 'foo' ) ),
254
- 'title' => $text
255
- );
256
- }
257
-
258
- foreach ( $values as $value => $title ) {
259
- $options[] = array(
260
- 'value' => $value,
261
- 'selected' => ( (string) $value == (string) $selected ),
262
- 'title' => $title
263
- );
264
- }
265
-
266
- $opts = '';
267
- foreach ( $options as $option ) {
268
- extract( $option );
269
-
270
- $opts .= html( 'option', compact( 'value', 'selected' ), $title );
271
- }
272
-
273
- $extra['name'] = self::$cur_name;
274
-
275
- $input = html( 'select', $extra, $opts );
276
-
277
- return self::add_label( $input, $desc, $desc_pos );
278
- }
279
-
280
- // Handle args for a single checkbox or radio input
281
- private static function _checkbox( $args ) {
282
- $args = wp_parse_args( $args, array(
283
- 'value' => true,
284
- 'desc' => NULL,
285
- 'checked' => false,
286
- 'extra' => array(),
287
- ) );
288
-
289
- foreach ( $args as $key => &$val )
290
- $$key = &$val;
291
- unset( $val );
292
-
293
- $extra['checked'] = $checked;
294
-
295
- if ( is_null( $desc ) && !is_bool( $value ) )
296
- $desc = str_replace( '[]', '', $value );
297
-
298
- return self::_input_gen( $args );
299
- }
300
-
301
- // Handle args for text inputs
302
- private static function _input( $args ) {
303
- $args = wp_parse_args( $args, array(
304
- 'value' => '',
305
- 'desc_pos' => 'after',
306
- 'extra' => array( 'class' => 'regular-text' ),
307
- ) );
308
-
309
- foreach ( $args as $key => &$val )
310
- $$key = &$val;
311
- unset( $val );
312
-
313
- if ( !isset( $extra['id'] ) && !is_array( $name ) && false === strpos( $name, '[' ) )
314
- $extra['id'] = $name;
315
-
316
- return self::_input_gen( $args );
317
- }
318
-
319
- // Generate html with the final args
320
- private static function _input_gen( $args ) {
321
- extract( wp_parse_args( $args, array(
322
- 'value' => NULL,
323
- 'desc' => NULL,
324
- 'extra' => array()
325
- ) ) );
326
-
327
- $extra['name'] = self::$cur_name;
328
-
329
- if ( 'textarea' == $type ) {
330
- $input = html( 'textarea', $extra, esc_textarea( $value ) );
331
- } else {
332
- $extra['value'] = $value;
333
- $extra['type'] = $type;
334
- $input = html( 'input', $extra );
335
- }
336
-
337
- return self::add_label( $input, $desc, $desc_pos );
338
- }
339
-
340
- private static function add_label( $input, $desc, $desc_pos ) {
341
- return html( 'label', self::add_desc( $input, $desc, $desc_pos ) ) . "\n";
342
- }
343
-
344
- private static function add_desc( $input, $desc, $desc_pos ) {
345
- if ( empty( $desc ) )
346
- return $input;
347
-
348
- if ( 'before' == $desc_pos )
349
- return $desc . ' ' . $input;
350
- else
351
- return $input . ' ' . $desc;
352
- }
353
-
354
-
355
  // Utilities
356
 
357
 
@@ -395,53 +137,52 @@ class scbForms {
395
  }
396
 
397
  /**
398
- * Given a list of fields, extract the appropriate POST data and return it.
399
  *
400
  * @param array $fields List of args that would be sent to scbForms::input()
401
- * @param array $to_update Existing data to update
402
  *
403
  * @return array
404
  */
405
- static function validate_post_data( $fields, $to_update = array() ) {
406
- foreach ( $fields as $field ) {
407
- $value = scbForms::get_value( $field['name'], $_POST );
 
408
 
409
- $value = stripslashes_deep( $value );
410
 
411
- switch ( $field['type'] ) {
412
- case 'checkbox':
413
- if ( isset( $field['values'] ) && is_array( $field['values'] ) )
414
- $value = array_intersect( $field['values'], (array) $value );
415
- else
416
- $value = (bool) $value;
417
 
418
- break;
419
- case 'radio':
420
- case 'select':
421
- self::_expand_values( $field );
422
 
423
- if ( !isset( $field['values'][ $value ] ) )
424
- continue 2;
425
- }
426
 
427
- self::set_value( $to_update, $field['name'], $value );
 
428
  }
429
 
430
  return $to_update;
431
  }
432
 
 
 
 
 
 
 
 
 
 
 
 
433
  static function input_from_meta( $args, $object_id, $meta_type = 'post' ) {
434
  $single = ( 'checkbox' != $args['type'] );
435
 
436
  $key = (array) $args['name'];
437
  $key = end( $key );
438
 
439
- $value = get_metadata( $meta_type, $object_id, $key );
440
-
441
- if ( empty( $value ) )
442
- $value = null;
443
- elseif( $single )
444
- $value = reset( $value );
445
 
446
  return self::input_with_value( $args, $value );
447
  }
@@ -487,11 +228,6 @@ class scbForms {
487
 
488
  $arr[ $final_key ] = $value;
489
  }
490
-
491
- private static function is_associative( $array ) {
492
- $keys = array_keys( $array );
493
- return array_keys( $keys ) !== $keys;
494
- }
495
  }
496
 
497
 
@@ -529,3 +265,372 @@ class scbForm {
529
  }
530
  }
531
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  const TOKEN = '%input%';
8
 
9
+ static function input_with_value( $args, $value ) {
10
+ $field = scbFormField::create( $args );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ return $field->render( $value );
13
  }
14
 
15
+ static function input( $args, $formdata = null ) {
16
+ $field = scbFormField::create( $args );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ return $field->render( scbForms::get_value( $args['name'], $formdata ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
 
 
 
 
21
  // Generates a table wrapped in a form
22
+ static function form_table( $rows, $formdata = null ) {
23
  $output = '';
24
  foreach ( $rows as $row )
25
  $output .= self::table_row( $row, $formdata );
30
  }
31
 
32
  // Generates a form
33
+ static function form( $inputs, $formdata = null, $nonce ) {
34
  $output = '';
35
  foreach ( $inputs as $input )
36
  $output .= self::input( $input, $formdata );
41
  }
42
 
43
  // Generates a table
44
+ static function table( $rows, $formdata = null ) {
45
  $output = '';
46
  foreach ( $rows as $row )
47
  $output .= self::table_row( $row, $formdata );
52
  }
53
 
54
  // Generates a table row
55
+ static function table_row( $args, $formdata = null ) {
56
  return self::row_wrap( $args['title'], self::input( $args, $formdata ) );
57
  }
58
 
94
  // ____________PRIVATE METHODS____________
95
 
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  // Utilities
98
 
99
 
137
  }
138
 
139
  /**
140
+ * Given a list of fields, validate some data.
141
  *
142
  * @param array $fields List of args that would be sent to scbForms::input()
143
+ * @param array $data The data to validate. Defaults to $_POST
144
  *
145
  * @return array
146
  */
147
+ static function validate_post_data( $fields, $data = null ) {
148
+ if ( null === $data ) {
149
+ $data = stripslashes_deep( $_POST );
150
+ }
151
 
152
+ $to_update = array();
153
 
154
+ foreach ( $fields as $field ) {
155
+ $value = scbForms::get_value( $field['name'], $data );
 
 
 
 
156
 
157
+ $fieldObj = scbFormField::create( $field );
 
 
 
158
 
159
+ $value = $fieldObj->validate( $value );
 
 
160
 
161
+ if ( null !== $value )
162
+ self::set_value( $to_update, $field['name'], $value );
163
  }
164
 
165
  return $to_update;
166
  }
167
 
168
+ /**
169
+ * For multiple-choice fields, we can never distinguish between "never been set" and "set to none".
170
+ * For single-choice fields, we can't distinguish either, because of how self::update_meta() works.
171
+ * Therefore, the 'default' parameter is always ignored.
172
+ *
173
+ * @param array $args Field arguments.
174
+ * @param int $object_id The object ID the metadata is attached to
175
+ * @param string $meta_type
176
+ *
177
+ * @return string
178
+ */
179
  static function input_from_meta( $args, $object_id, $meta_type = 'post' ) {
180
  $single = ( 'checkbox' != $args['type'] );
181
 
182
  $key = (array) $args['name'];
183
  $key = end( $key );
184
 
185
+ $value = get_metadata( $meta_type, $object_id, $key, $single );
 
 
 
 
 
186
 
187
  return self::input_with_value( $args, $value );
188
  }
228
 
229
  $arr[ $final_key ] = $value;
230
  }
 
 
 
 
 
231
  }
232
 
233
 
265
  }
266
  }
267
 
268
+
269
+ abstract class scbFormField {
270
+
271
+ protected $args;
272
+
273
+ public static function create( $args ) {
274
+ if ( is_a( $args, __CLASS__ ) )
275
+ return $args;
276
+
277
+ if ( empty( $args['name'] ) ) {
278
+ return trigger_error( 'Empty name', E_USER_WARNING );
279
+ }
280
+
281
+ if ( isset( $args['value'] ) && is_array( $args['value'] ) ) {
282
+ $args['values'] = $args['value'];
283
+ unset( $args['value'] );
284
+ }
285
+
286
+ if ( isset( $args['extra'] ) && !is_array( $args['extra'] ) )
287
+ $args['extra'] = shortcode_parse_atts( $args['extra'] );
288
+
289
+ $args = wp_parse_args( $args, array(
290
+ 'desc' => '',
291
+ 'desc_pos' => 'after',
292
+ 'wrap' => scbForms::TOKEN,
293
+ 'wrap_each' => scbForms::TOKEN,
294
+ ) );
295
+
296
+ if ( isset( $args['values'] ) )
297
+ self::_expand_values( $args );
298
+
299
+ switch ( $args['type'] ) {
300
+ case 'radio':
301
+ return new scbRadiosField( $args );
302
+ case 'select':
303
+ return new scbSelectField( $args );
304
+ case 'checkbox':
305
+ if ( isset( $args['values'] ) )
306
+ return new scbMultipleChoiceField( $args );
307
+ else
308
+ return new scbSingleCheckboxField( $args );
309
+ default:
310
+ return new scbTextField( $args );
311
+ }
312
+ }
313
+
314
+ protected function __construct( $args ) {
315
+ $this->args = $args;
316
+ }
317
+
318
+ public function __get( $key ) {
319
+ return $this->args[ $key ];
320
+ }
321
+
322
+ public function __isset( $key ) {
323
+ return isset( $this->args[ $key ] );
324
+ }
325
+
326
+ /**
327
+ * Generate the corresponding HTML for a field
328
+ *
329
+ * @param mixed $value The value to use
330
+ *
331
+ * @return string
332
+ */
333
+ public function render( $value = null ) {
334
+ if ( null === $value && isset( $this->default ) )
335
+ $value = $this->default;
336
+
337
+ $args = $this->args;
338
+
339
+ if ( null !== $value )
340
+ $this->_set_value( $args, $value );
341
+
342
+ $args['name'] = scbForms::get_name( $args['name'] );
343
+
344
+ return str_replace( scbForms::TOKEN, $this->_render( $args ), $this->wrap );
345
+ }
346
+
347
+ // Mutate the field arguments so that the value passed is rendered.
348
+ abstract protected function _set_value( &$args, $value );
349
+
350
+ // The actual rendering
351
+ abstract protected function _render( $args );
352
+
353
+ /**
354
+ * Validates a value against a field.
355
+ *
356
+ * @param mixed $value The value to check
357
+ *
358
+ * @return mixed null if the validation failed, sanitized value otherwise.
359
+ */
360
+ abstract public function validate( $value );
361
+
362
+ // Handle args for a single checkbox or radio input
363
+ protected static function _checkbox( $args ) {
364
+ $args = wp_parse_args( $args, array(
365
+ 'value' => true,
366
+ 'desc' => null,
367
+ 'checked' => false,
368
+ 'extra' => array(),
369
+ ) );
370
+
371
+ foreach ( $args as $key => &$val )
372
+ $$key = &$val;
373
+ unset( $val );
374
+
375
+ $extra['checked'] = $checked;
376
+
377
+ if ( is_null( $desc ) && !is_bool( $value ) )
378
+ $desc = str_replace( '[]', '', $value );
379
+
380
+ return self::_input_gen( $args );
381
+ }
382
+
383
+ // Generate html with the final args
384
+ protected static function _input_gen( $args ) {
385
+ extract( wp_parse_args( $args, array(
386
+ 'value' => null,
387
+ 'desc' => null,
388
+ 'extra' => array()
389
+ ) ) );
390
+
391
+ $extra['name'] = $name;
392
+
393
+ if ( 'textarea' == $type ) {
394
+ $input = html( 'textarea', $extra, esc_textarea( $value ) );
395
+ } else {
396
+ $extra['value'] = $value;
397
+ $extra['type'] = $type;
398
+ $input = html( 'input', $extra );
399
+ }
400
+
401
+ return self::add_label( $input, $desc, $desc_pos );
402
+ }
403
+
404
+ protected static function add_label( $input, $desc, $desc_pos ) {
405
+ return html( 'label', self::add_desc( $input, $desc, $desc_pos ) ) . "\n";
406
+ }
407
+
408
+ protected static function add_desc( $input, $desc, $desc_pos ) {
409
+ if ( empty( $desc ) )
410
+ return $input;
411
+
412
+ if ( 'before' == $desc_pos )
413
+ return $desc . ' ' . $input;
414
+ else
415
+ return $input . ' ' . $desc;
416
+ }
417
+
418
+ private static function _expand_values( &$args ) {
419
+ $values =& $args['values'];
420
+
421
+ if ( !empty( $values ) && !self::is_associative( $values ) ) {
422
+ if ( is_array( $args['desc'] ) ) {
423
+ $values = array_combine( $values, $args['desc'] ); // back-compat
424
+ $args['desc'] = false;
425
+ } elseif ( !isset( $args['numeric'] ) || !$args['numeric'] ) {
426
+ $values = array_combine( $values, $values );
427
+ }
428
+ }
429
+ }
430
+
431
+ private static function is_associative( $array ) {
432
+ $keys = array_keys( $array );
433
+ return array_keys( $keys ) !== $keys;
434
+ }
435
+ }
436
+
437
+
438
+ class scbTextField extends scbFormField {
439
+
440
+ public function validate( $value ) {
441
+ $sanitize = isset( $this->sanitize ) ? $this->sanitize : 'wp_filter_kses';
442
+
443
+ return call_user_func( $sanitize, $value, $this );
444
+ }
445
+
446
+ protected function _render( $args ) {
447
+ $args = wp_parse_args( $args, array(
448
+ 'value' => '',
449
+ 'desc_pos' => 'after',
450
+ 'extra' => array( 'class' => 'regular-text' ),
451
+ ) );
452
+
453
+ foreach ( $args as $key => &$val )
454
+ $$key = &$val;
455
+ unset( $val );
456
+
457
+ if ( !isset( $extra['id'] ) && !is_array( $name ) && false === strpos( $name, '[' ) )
458
+ $extra['id'] = $name;
459
+
460
+ return scbFormField::_input_gen( $args );
461
+ }
462
+
463
+ protected function _set_value( &$args, $value ) {
464
+ $args['value'] = $value;
465
+ }
466
+ }
467
+
468
+
469
+ abstract class scbSingleChoiceField extends scbFormField {
470
+
471
+ public function validate( $value ) {
472
+ if ( isset( $this->values[ $value ] ) )
473
+ return $value;
474
+
475
+ return null;
476
+ }
477
+
478
+ protected function _render( $args ) {
479
+ $args = wp_parse_args( $args, array(
480
+ 'numeric' => false, // use numeric array instead of associative
481
+ 'selected' => array( 'foo' ), // hack to make default blank
482
+ ) );
483
+
484
+ return $this->_render_specific( $args );
485
+ }
486
+
487
+ protected function _set_value( &$args, $value ) {
488
+ $args['selected'] = $value;
489
+ }
490
+
491
+ abstract protected function _render_specific( $args );
492
+ }
493
+
494
+
495
+ class scbSelectField extends scbSingleChoiceField {
496
+
497
+ protected function _render_specific( $args ) {
498
+ extract( wp_parse_args( $args, array(
499
+ 'text' => false,
500
+ 'extra' => array()
501
+ ) ) );
502
+
503
+ $options = array();
504
+
505
+ if ( false !== $text ) {
506
+ $options[] = array(
507
+ 'value' => '',
508
+ 'selected' => ( $selected == array( 'foo' ) ),
509
+ 'title' => $text
510
+ );
511
+ }
512
+
513
+ foreach ( $values as $value => $title ) {
514
+ $options[] = array(
515
+ 'value' => $value,
516
+ 'selected' => ( (string) $value == (string) $selected ),
517
+ 'title' => $title
518
+ );
519
+ }
520
+
521
+ $opts = '';
522
+ foreach ( $options as $option ) {
523
+ extract( $option );
524
+
525
+ $opts .= html( 'option', compact( 'value', 'selected' ), $title );
526
+ }
527
+
528
+ $extra['name'] = $name;
529
+
530
+ $input = html( 'select', $extra, $opts );
531
+
532
+ return scbFormField::add_label( $input, $desc, $desc_pos );
533
+ }
534
+ }
535
+
536
+
537
+ class scbRadiosField extends scbSelectField {
538
+
539
+ protected function _render_specific( $args ) {
540
+ extract( $args );
541
+
542
+ if ( array( 'foo' ) == $selected ) {
543
+ // radio buttons should always have one option selected
544
+ $selected = key( $values );
545
+ }
546
+
547
+ $opts = '';
548
+ foreach ( $values as $value => $title ) {
549
+ $single_input = scbFormField::_checkbox( array(
550
+ 'name' => $name,
551
+ 'type' => 'radio',
552
+ 'value' => $value,
553
+ 'checked' => ( (string) $value == (string) $selected ),
554
+ 'desc' => $title,
555
+ 'desc_pos' => 'after'
556
+ ) );
557
+
558
+ $opts .= str_replace( scbForms::TOKEN, $single_input, $wrap_each );
559
+ }
560
+
561
+ return scbFormField::add_desc( $opts, $desc, $desc_pos );
562
+ }
563
+ }
564
+
565
+
566
+ class scbMultipleChoiceField extends scbFormField {
567
+
568
+ public function validate( $value ) {
569
+ return array_intersect( array_keys( $this->values ), (array) $value );
570
+ }
571
+
572
+ protected function _render( $args ) {
573
+ $args = wp_parse_args( $args, array(
574
+ 'numeric' => false, // use numeric array instead of associative
575
+ 'checked' => null,
576
+ ) );
577
+
578
+ extract( $args );
579
+
580
+ if ( !is_array( $checked ) )
581
+ $checked = array();
582
+
583
+ $opts = '';
584
+ foreach ( $values as $value => $title ) {
585
+ $single_input = scbFormField::_checkbox( array(
586
+ 'name' => $name . '[]',
587
+ 'type' => 'checkbox',
588
+ 'value' => $value,
589
+ 'checked' => in_array( $value, $checked ),
590
+ 'desc' => $title,
591
+ 'desc_pos' => 'after'
592
+ ) );
593
+
594
+ $opts .= str_replace( scbForms::TOKEN, $single_input, $wrap_each );
595
+ }
596
+
597
+ return scbFormField::add_desc( $opts, $desc, $desc_pos );
598
+ }
599
+
600
+ protected function _set_value( &$args, $value ) {
601
+ $args['checked'] = (array) $value;
602
+ }
603
+ }
604
+
605
+
606
+ class scbSingleCheckboxField extends scbFormField {
607
+
608
+ public function validate( $value ) {
609
+ return (bool) $value;
610
+ }
611
+
612
+ protected function _render( $args ) {
613
+ $args = wp_parse_args( $args, array(
614
+ 'value' => true,
615
+ 'desc' => null,
616
+ 'checked' => false,
617
+ 'extra' => array(),
618
+ ) );
619
+
620
+ foreach ( $args as $key => &$val )
621
+ $$key = &$val;
622
+ unset( $val );
623
+
624
+ $extra['checked'] = $checked;
625
+
626
+ if ( is_null( $desc ) && !is_bool( $value ) )
627
+ $desc = str_replace( '[]', '', $value );
628
+
629
+ return scbFormField::_input_gen( $args );
630
+ }
631
+
632
+ protected function _set_value( &$args, $value ) {
633
+ $args['checked'] = ( $value || ( isset( $args['value'] ) && $value == $args['value'] ) );
634
+ }
635
+ }
636
+
scb/load.php CHANGED
@@ -1,6 +1,6 @@
1
  <?php
2
 
3
- $GLOBALS['_scb_data'] = array( 53, __FILE__, array(
4
  'scbUtil', 'scbOptions', 'scbForms', 'scbTable',
5
  'scbWidget', 'scbAdminPage', 'scbBoxesPage',
6
  'scbCron', 'scbHooks',
1
  <?php
2
 
3
+ $GLOBALS['_scb_data'] = array( 54, __FILE__, array(
4
  'scbUtil', 'scbOptions', 'scbForms', 'scbTable',
5
  'scbWidget', 'scbAdminPage', 'scbBoxesPage',
6
  'scbCron', 'scbHooks',