Completed
Push — develop ( 1b1461...9efb3c )
by David
03:28
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
	 * The Log service.
21
	 *
22
	 * @since 3.1.0
23
	 * @access private
24
	 * @var \Wordlift_Log_Service $log_service The Log service.
25
	 */
26
	private $log_service;
27
28
	/**
29
	 * Constructor. Recevies.... TODO write docs
30
	 */
31
	public function __construct( $args ) {
32
33
		$this->log_service = Wordlift_Log_Service::get_logger( 'WL_Metabox_Field' );
34
35
		if ( empty( $args ) ) {
36
			return;
37
		}
38
39
		// Save a copy of the custom field's params
40
		$this->raw_custom_field = reset( $args );
41
42
		// Extract meta name (post_meta key for the DB)
43
		$this->meta_name = key( $args );
44
45
		// Extract linked data predicate
46
		if ( isset( $this->raw_custom_field['predicate'] ) ) {
47
			$this->predicate = $this->raw_custom_field['predicate'];
48
		} else {
49
			return;
50
		}
51
52
		// Extract human readable label
53
		$exploded_predicate = explode( '/', $this->predicate );
54
		$this->label        = end( $exploded_predicate );
55
56
		// Extract field constraints (numerosity, expected type)
57
		// Default constaints: accept one string.
58
		if ( isset( $this->raw_custom_field['type'] ) ) {
59
			$this->expected_wl_type = $this->raw_custom_field['type'];
60
		} else {
61
			$this->expected_wl_type = Wordlift_Schema_Service::DATA_TYPE_STRING;
62
		}
63
64
		$this->cardinality = 1;
65
		if ( isset( $this->raw_custom_field['constraints'] ) ) {
66
67
			$constraints = $this->raw_custom_field['constraints'];
68
69
			// Extract cardinality
70
			if ( isset( $constraints['cardinality'] ) ) {
71
				$this->cardinality = $constraints['cardinality'];
72
			}
73
74
			// Which type of entity can we accept (e.g. Place, Event, ecc.)?
75
			if ( $this->expected_wl_type === Wordlift_Schema_Service::DATA_TYPE_URI && isset( $constraints['uri_type'] ) ) {
76
				$this->expected_uri_type = is_array( $constraints['uri_type'] ) ? $constraints['uri_type'] : array( $constraints['uri_type'] );
77
			}
78
79
		}
80
	}
81
82
	/**
83
	 * Return nonce HTML.
84
	 * Overwrite this method in a child class to obtain custom behaviour.
85
	 */
86
	public function html_nonce() {
87
88
		return wp_nonce_field( 'wordlift_' . $this->meta_name . '_entity_box', 'wordlift_' . $this->meta_name . '_entity_box_nonce', TRUE, FALSE );
89
	}
90
91
	/**
92
	 * Verify nonce.
93
	 * Overwrite this method in a child class to obtain custom behaviour.
94
	 *
95
	 * @return boolean Nonce verification
96
	 */
97
	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...
98
99
		$nonce_name   = 'wordlift_' . $this->meta_name . '_entity_box_nonce';
100
		$nonce_verify = 'wordlift_' . $this->meta_name . '_entity_box';
101
102
		if ( ! isset( $_POST[ $nonce_name ] ) ) {
103
			return FALSE;
104
		}
105
106
		// Verify that the nonce is valid.
107
		return wp_verify_nonce( $_POST[ $nonce_name ], $nonce_verify );
108
	}
109
110
	/**
111
	 * Load data from DB and store the resulting array in $this->data.
112
	 * Overwrite this method in a child class to obtain custom behaviour.
113
	 */
114
	public function get_data() {
115
116
		$data = get_post_meta( get_the_ID(), $this->meta_name );
117
118
		// Values are always contained in an array (makes it easier to manage cardinality)
119
		if ( ! is_array( $data ) ) {
120
			$data = array( $data );
121
		}
122
123
		$this->data = $data;
124
	}
125
126
	/**
127
	 * Sanitizes data before saving to DB. Default sanitization trashes empty values.
128
	 * Stores the sanitized values into $this->data so they can be later processed.
129
	 * Overwrite this method in a child class to obtain custom behaviour.
130
	 *
131
	 * @param array $values Array of values to be sanitized and then stored into $this->data
132
	 *
133
	 */
134
	public function sanitize_data( $values ) {
135
136
		$sanitized_data = array();
137
138
		if ( ! is_array( $values ) ) {
139
			$values = array( $values );
140
		}
141
142
		foreach ( $values as $value ) {
143
			$sanitized_value = $this->sanitize_data_filter( $value );
144
			if ( ! is_null( $sanitized_value ) ) {
145
				$sanitized_data[] = $sanitized_value;
146
			}
147
		}
148
149
		$this->data = $sanitized_data;
150
	}
151
152
	/**
153
	 * Sanitize a single value. Called from $this->sanitize_data. Default sanitization excludes empty values.
154
	 * Overwrite this method in a child class to obtain custom behaviour.
155
	 *
156
	 * @return mixed Returns sanitized value, or null.
157
	 */
158
	public function sanitize_data_filter( $value ) {
159
160
		// TODO: all fields should provide their own sanitize which shouldn't be part of a UI class.
161
		// If the field provides its own validation, use it.
162
		if ( isset( $this->raw_custom_field['sanitize'] ) ) {
163
			return call_user_func( $this->raw_custom_field['sanitize'], $value );
164
		}
165
166
		if ( ! is_null( $value ) && $value !== '' ) {         // do not use 'empty()' -> https://www.virendrachandak.com/techtalk/php-isset-vs-empty-vs-is_null/
167
			return $value;
168
		}
169
170
		return NULL;
171
	}
172
173
	/**
174
	 * Save data to DB.
175
	 * Overwrite this method in a child class to obtain custom behaviour.
176
	 */
177
	public function save_data( $values ) {
178
179
		// Will sanitize data and store them in $field->data
180
		$this->sanitize_data( $values );
181
182
		$entity_id = get_the_ID();
183
184
		// Take away old values
185
		delete_post_meta( $entity_id, $this->meta_name );
186
187
		// insert new values, respecting cardinality
188
		$single = ( $this->cardinality == 1 );
189
		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...
190
			add_post_meta( $entity_id, $this->meta_name, $value, $single );
191
		}
192
	}
193
194
	/**
195
	 * Returns the HTML tag that will contain the Field. By default the we return a <div> with data- attributes on cardinality and expected types.
196
	 * It is useful to provide data- attributes for the JS scripts.
197
	 * Overwrite this method in a child class to obtain custom behaviour.
198
	 */
199
	public function html_wrapper_open() {
200
201
		return "<div class='wl-field' data-cardinality='$this->cardinality'>";
202
	}
203
204
	/**
205
	 * Returns Field HTML (nonce included).
206
	 * Overwrite this method (or methods called from this method) in a child class to obtain custom behaviour.
207
	 */
208
	public function html() {
209
210
		// Open main <div> for the Field
211
		$html = $this->html_wrapper_open();
212
213
		// Label
214
		$html .= "<h3>$this->label</h3>";
215
216
		// print nonce
217
		$html .= $this->html_nonce();
218
219
		// print data loaded from DB
220
		$count = 0;
221
		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...
222
			foreach ( $this->data as $value ) {
223
				if ( $count < $this->cardinality ) {
224
					$html .= $this->html_input( $value );
225
				}
226
				$count ++;
227
			}
228
		}
229
230
		// Print the empty <input> to add new values
231
		if ( $count === 0 ) { // } || $count < $this->cardinality ) { DO NOT print empty inputs unless requested by the editor since fields might support empty strings.
232
			$html .= $this->html_input( '' );    // Will print an empty <input>
233
			$count ++;
234
		}
235
236
		// If cardiality allows it, print button to add new values.
237
		if ( $count < $this->cardinality ) {
238
			$html .= '<button class="button wl-add-input wl-button" type="button">Add</button>';
239
		}
240
241
		// Close the HTML wrapper
242
		$html .= $this->html_wrapper_close();
243
244
		return $html;
245
	}
246
247
	/**
248
	 * Return a single <input> tag for the Field.
249
	 *
250
	 * @param mixed $value Input value
251
	 */
252
	public function html_input( $value ) {
253
		$html = <<<EOF
254
			<div class="wl-input-wrapper">
255
				<input type="text" id="$this->meta_name" name="wl_metaboxes[$this->meta_name][]" value="$value" style="width:88%" />
256
				<button class="button wl-remove-input wl-button" type="button" style="width:10 % ">Remove</button>
257
			</div>
258
EOF;
259
260
		return $html;
261
	}
262
263
	/**
264
	 * Returns closing for the wrapper HTML tag.
265
	 */
266
	public function html_wrapper_close() {
267
268
		return '</div><hr>';
269
	}
270
}
271
272