Completed
Push — master ( 829da2...8585d5 )
by David
03:05
created

WL_Metabox_Field::html()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 38
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 8
nop 0
dl 0
loc 38
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * All custom WL_Metabox_Filed(s) must extend this class.
5
 * This class deals with saving the most basic data type, strings.
6
 * Use the methods that are useful or overwrite them if you need custom behaviour.
7
 */
8
class WL_Metabox_Field {
9
10
	public $meta_name;
11
	public $raw_custom_field;
12
	public $predicate;
13
	public $label;
14
	public $expected_wl_type;
15
	public $expected_uri_type;
16
	public $cardinality;
17
	public $data;
18
19
	/**
20
	 * @param array $args
21
	 */
22
	public function __construct( $args ) {
23
24
		if ( empty( $args ) ) {
25
			return;
26
		}
27
28
		// Save a copy of the custom field's params
29
		$this->raw_custom_field = reset( $args );
30
31
		// Extract meta name (post_meta key for the DB)
32
		$this->meta_name = key( $args );
33
34
		// Extract linked data predicate
35
		if ( isset( $this->raw_custom_field['predicate'] ) ) {
36
			$this->predicate = $this->raw_custom_field['predicate'];
37
		} else {
38
			return;
39
		}
40
41
		// Extract human readable label
42
		$exploded_predicate = explode( '/', $this->predicate );
43
44
		// Use the label defined for the property if set, otherwise the last part of the schema.org/xyz predicate.
45
		$this->label = isset( $this->raw_custom_field['metabox']['label'] ) ? __( $this->raw_custom_field['metabox']['label'] ) : end( $exploded_predicate );
46
47
		// Extract field constraints (numerosity, expected type)
48
		// Default constaints: accept one string.
49
		if ( isset( $this->raw_custom_field['type'] ) ) {
50
			$this->expected_wl_type = $this->raw_custom_field['type'];
51
		} else {
52
			$this->expected_wl_type = Wordlift_Schema_Service::DATA_TYPE_STRING;
53
		}
54
55
		$this->cardinality = 1;
56
		if ( isset( $this->raw_custom_field['constraints'] ) ) {
57
58
			$constraints = $this->raw_custom_field['constraints'];
59
60
			// Extract cardinality
61
			if ( isset( $constraints['cardinality'] ) ) {
62
				$this->cardinality = $constraints['cardinality'];
63
			}
64
65
			// Which type of entity can we accept (e.g. Place, Event, ecc.)?
66
			if ( $this->expected_wl_type === Wordlift_Schema_Service::DATA_TYPE_URI && isset( $constraints['uri_type'] ) ) {
67
				$this->expected_uri_type = is_array( $constraints['uri_type'] ) ? $constraints['uri_type'] : array( $constraints['uri_type'] );
68
			}
69
70
		}
71
	}
72
73
	/**
74
	 * Return nonce HTML.
75
	 * Overwrite this method in a child class to obtain custom behaviour.
76
	 */
77
	public function html_nonce() {
78
79
		return wp_nonce_field( 'wordlift_' . $this->meta_name . '_entity_box', 'wordlift_' . $this->meta_name . '_entity_box_nonce', TRUE, FALSE );
80
	}
81
82
	/**
83
	 * Verify nonce.
84
	 * Overwrite this method in a child class to obtain custom behaviour.
85
	 *
86
	 * @return boolean Nonce verification
87
	 */
88
	public function verify_nonce() {
0 ignored issues
show
Coding Style introduced by
verify_nonce uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
89
90
		$nonce_name   = 'wordlift_' . $this->meta_name . '_entity_box_nonce';
91
		$nonce_verify = 'wordlift_' . $this->meta_name . '_entity_box';
92
93
		if ( ! isset( $_POST[ $nonce_name ] ) ) {
94
			return FALSE;
95
		}
96
97
		// Verify that the nonce is valid.
98
		return wp_verify_nonce( $_POST[ $nonce_name ], $nonce_verify );
99
	}
100
101
	/**
102
	 * Load data from DB and store the resulting array in $this->data.
103
	 * Overwrite this method in a child class to obtain custom behaviour.
104
	 */
105
	public function get_data() {
106
107
		$data = get_post_meta( get_the_ID(), $this->meta_name );
108
109
		// Values are always contained in an array (makes it easier to manage cardinality)
110
		if ( ! is_array( $data ) ) {
111
			$data = array( $data );
112
		}
113
114
		$this->data = $data;
115
	}
116
117
	/**
118
	 * Sanitizes data before saving to DB. Default sanitization trashes empty values.
119
	 * Stores the sanitized values into $this->data so they can be later processed.
120
	 * Overwrite this method in a child class to obtain custom behaviour.
121
	 *
122
	 * @param array $values Array of values to be sanitized and then stored into $this->data
123
	 *
124
	 */
125
	public function sanitize_data( $values ) {
126
127
		$sanitized_data = array();
128
129
		if ( ! is_array( $values ) ) {
130
			$values = array( $values );
131
		}
132
133
		foreach ( $values as $value ) {
134
			$sanitized_value = $this->sanitize_data_filter( $value );
135
			if ( ! is_null( $sanitized_value ) ) {
136
				$sanitized_data[] = $sanitized_value;
137
			}
138
		}
139
140
		$this->data = $sanitized_data;
141
	}
142
143
	/**
144
	 * Sanitize a single value. Called from $this->sanitize_data. Default sanitization excludes empty values.
145
	 * Overwrite this method in a child class to obtain custom behaviour.
146
	 *
147
	 * @return mixed Returns sanitized value, or null.
148
	 */
149
	public function sanitize_data_filter( $value ) {
150
151
		// TODO: all fields should provide their own sanitize which shouldn't be part of a UI class.
152
		// If the field provides its own validation, use it.
153
		if ( isset( $this->raw_custom_field['sanitize'] ) ) {
154
			return call_user_func( $this->raw_custom_field['sanitize'], $value );
155
		}
156
157
		if ( ! is_null( $value ) && $value !== '' ) {         // do not use 'empty()' -> https://www.virendrachandak.com/techtalk/php-isset-vs-empty-vs-is_null/
158
			return $value;
159
		}
160
161
		return NULL;
162
	}
163
164
	/**
165
	 * Save data to DB.
166
	 * Overwrite this method in a child class to obtain custom behaviour.
167
	 */
168
	public function save_data( $values ) {
169
170
		// Will sanitize data and store them in $field->data
171
		$this->sanitize_data( $values );
172
173
		$entity_id = get_the_ID();
174
175
		// Take away old values
176
		delete_post_meta( $entity_id, $this->meta_name );
177
178
		// insert new values, respecting cardinality
179
		$single = ( $this->cardinality == 1 );
180
		foreach ( $this->data as $value ) {
0 ignored issues
show
Bug introduced by
The expression $this->data of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
181
			add_post_meta( $entity_id, $this->meta_name, $value, $single );
182
		}
183
	}
184
185
	/**
186
	 * Returns the HTML tag that will contain the Field. By default the we return a <div> with data- attributes on cardinality and expected types.
187
	 * It is useful to provide data- attributes for the JS scripts.
188
	 * Overwrite this method in a child class to obtain custom behaviour.
189
	 */
190
	public function html_wrapper_open() {
191
192
		return "<div class='wl-field' data-cardinality='$this->cardinality'>";
193
	}
194
195
	/**
196
	 * Returns Field HTML (nonce included).
197
	 * Overwrite this method (or methods called from this method) in a child class to obtain custom behaviour.
198
	 */
199
	public function html() {
200
201
		// Open main <div> for the Field
202
		$html = $this->html_wrapper_open();
203
204
		// Label
205
		$html .= "<h3>$this->label</h3>";
206
207
		// print nonce
208
		$html .= $this->html_nonce();
209
210
		// print data loaded from DB
211
		$count = 0;
212
		if ( $this->data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
213
			foreach ( $this->data as $value ) {
214
				if ( $count < $this->cardinality ) {
215
					$html .= $this->html_input( $value );
216
				}
217
				$count ++;
218
			}
219
		}
220
221
		// Print the empty <input> to add new values
222
		if ( $count === 0 ) { // } || $count < $this->cardinality ) { DO NOT print empty inputs unless requested by the editor since fields might support empty strings.
223
			$html .= $this->html_input( '' );    // Will print an empty <input>
224
			$count ++;
225
		}
226
227
		// If cardiality allows it, print button to add new values.
228
		if ( $count < $this->cardinality ) {
229
			$html .= '<button class="button wl-add-input wl-button" type="button">Add</button>';
230
		}
231
232
		// Close the HTML wrapper
233
		$html .= $this->html_wrapper_close();
234
235
		return $html;
236
	}
237
238
	/**
239
	 * Return a single <input> tag for the Field.
240
	 *
241
	 * @param mixed $value Input value
242
	 */
243
	public function html_input( $value ) {
244
		$html = <<<EOF
245
			<div class="wl-input-wrapper">
246
				<input type="text" id="$this->meta_name" name="wl_metaboxes[$this->meta_name][]" value="$value" style="width:88%" />
247
				<button class="button wl-remove-input wl-button" type="button" style="width:10 % ">Remove</button>
248
			</div>
249
EOF;
250
251
		return $html;
252
	}
253
254
	/**
255
	 * Returns closing for the wrapper HTML tag.
256
	 */
257
	public function html_wrapper_close() {
258
259
		return '</div><hr>';
260
	}
261
}
262
263