PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.28
Booktics – Appointment Booking Calendar for Service Businesses v1.0.28
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.2 1.0.3 All 31 releases
← All changes | base/abstracts/post-model.php +138 -174 1.0.3 → 1.0.28 View file →
@@ -17,16 +17,16 @@
17 17 * List of accepted post statuses
18 18 *
19 19 * @var array
20 20 */
21 - public static $accepted_statuses = [
21 + public static $accepted_statuses = array(
22 22 'active',
23 23 'deactive',
24 24 'draft',
25 25 'publish',
26 26 'cancelled',
27 - 'pending'
28 - ];
27 + 'pending',
28 + );
29 29
30 30 /**
31 31 * Store post
32 32 *
@@ -38,9 +38,9 @@
38 38 * Store meta property
39 39 *
40 40 * @var array
41 41 */
42 - protected $fillable = [];
42 + protected $fillable = array();
43 43
44 44 /**
45 45 * Store post attributes
46 46 *
@@ -45,9 +45,9 @@
45 45 * Store post attributes
46 46 *
47 47 * @var array
48 48 */
49 - protected $attributes = [];
49 + protected $attributes = array();
50 50
51 51 /**
52 52 * Store origianl data
53 53 *
@@ -52,9 +52,9 @@
52 52 * Store origianl data
53 53 *
54 54 * @var array
55 55 */
56 - protected $original = [];
56 + protected $original = array();
57 57
58 58 /**
59 59 * Store post type
60 60 *
@@ -82,85 +82,8 @@
82 82
83 83 if ( $this->post ) {
84 84 $this->load_attributes();
85 85 }
86 -
87 - register_post_status( 'active', [
88 - 'label' => __( 'Active', 'booktics' ),
89 - 'public' => true,
90 - 'publicly_queryable' => true,
91 - 'exclude_from_search' => false,
92 - 'show_in_admin_all_list' => true,
93 - 'show_in_admin_status_list' => true,
94 - // translators: %s: number of items active
95 - 'label_count' => _n_noop( 'Active <span class="count">(%s)</span>', 'Active <span class="count">(%s)</span>', 'booktics' ),
96 - ] );
97 -
98 - register_post_status( 'deactive', [
99 - 'label' => __( 'Deactive', 'booktics' ),
100 - 'public' => true,
101 - 'publicly_queryable' => true,
102 - 'exclude_from_search' => false,
103 - 'show_in_admin_all_list' => true,
104 - 'show_in_admin_status_list' => true,
105 - // translators: %s: number of items deactive
106 - 'label_count' => _n_noop( 'Deactive <span class="count">(%s)</span>', 'Deactive <span class="count">(%s)</span>', 'booktics' ),
107 - ] );
108 -
109 - register_post_status( 'pending', [
110 - 'label' => __( 'Pending', 'booktics' ),
111 - 'public' => true,
112 - 'publicly_queryable' => true,
113 - 'exclude_from_search' => false,
114 - 'show_in_admin_all_list' => true,
115 - 'show_in_admin_status_list' => true,
116 - // translators: %s: number of items pending
117 - 'label_count' => _n_noop( 'Pending <span class="count">(%s)</span>', 'Pending <span class="count">(%s)</span>', 'booktics' ),
118 - ] );
119 -
120 - register_post_status( 'cancelled', [
121 - 'label' => __( 'Cancelled', 'booktics' ),
122 - 'public' => true,
123 - 'publicly_queryable' => true,
124 - 'exclude_from_search' => false,
125 - 'show_in_admin_all_list' => true,
126 - 'show_in_admin_status_list' => true,
127 - // translators: %s: number of items cancelled
128 - 'label_count' => _n_noop( 'Cancelled <span class="count">(%s)</span>', 'Cancelled <span class="count">(%s)</span>', 'booktics' ),
129 - ] );
130 -
131 - register_post_status( 'approved', [
132 - 'label' => __( 'Approved', 'booktics' ),
133 - 'public' => true,
134 - 'publicly_queryable' => true,
135 - 'exclude_from_search' => false,
136 - 'show_in_admin_all_list' => true,
137 - 'show_in_admin_status_list' => true,
138 - // translators: %s: number of items approved
139 - 'label_count' => _n_noop( 'Approved <span class="count">(%s)</span>', 'Approved <span class="count">(%s)</span>', 'booktics' ),
140 - ] );
141 -
142 - register_post_status( 'completed', [
143 - 'label' => __( 'Completed', 'booktics' ),
144 - 'public' => true,
145 - 'publicly_queryable' => true,
146 - 'exclude_from_search' => false,
147 - 'show_in_admin_all_list' => true,
148 - 'show_in_admin_status_list' => true,
149 - // translators: %s: number of items Completed
150 - 'label_count' => _n_noop( 'Completed <span class="count">(%s)</span>', 'Completed <span class="count">(%s)</span>', 'booktics' ),
151 - ] );
152 -
153 - register_post_status( 'not_show', [
154 - 'label' => __( 'Not Show', 'booktics' ),
155 - 'public' => true,
156 - 'publicly_queryable' => true,
157 - 'exclude_from_search' => false,
158 - 'show_in_admin_all_list' => true,
159 - 'show_in_admin_status_list' => true,
160 - // translators: %s: number of items not_show
161 - 'label_count' => _n_noop( 'Not Show <span class="count">(%s)</span>', 'Not Show <span class="count">(%s)</span>', 'booktics' ),
162 - ] );
163 86 }
164 87
165 88 /**
166 89 * Get post type
@@ -174,15 +97,15 @@
174 97 *
175 98 * @return void Set all necessary attributes
176 99 */
177 100 protected function load_attributes() {
178 - $this->attributes = [
101 + $this->attributes = array(
179 102 'id' => $this->post->ID,
180 103 'title' => $this->post->post_title,
181 104 'content' => $this->post->post_content,
182 105 'status' => $this->post->post_status,
183 106 'link' => $this->get_link( $this->post ),
184 - ];
107 + );
185 108
186 109 $custom_meta = get_post_meta( $this->post->ID );
187 110
188 111 foreach ( $this->get_fillable() as $key => $value ) {
@@ -220,9 +143,8 @@
220 143 }
221 144
222 145 // translators: %s is the name of the undefined property.
223 146 throw new Exception( sprintf( esc_html__( 'Undefined property %s', 'booktics' ), esc_html( $key ) ) );
224 -
225 147 }
226 148
227 149 /**
228 150 * Set attributes
@@ -234,9 +156,9 @@
234 156 * @throws Exception
235 157 */
236 158 public function __set( $key, $value ) {
237 159
238 - if ( ! isset( $this->get_fillable()[ $key ] ) && ! in_array( $key, [ 'link' ] ) ) {
160 + if ( ! isset( $this->get_fillable()[ $key ] ) && ! in_array( $key, array( 'link', 'customer' ) ) ) {
239 161
240 162 // translators: %s is the name of the undefined property.
241 163 throw new Exception( sprintf( esc_html__( 'Undefined property %s', 'booktics' ), esc_html( $key ) ) );
242 164 }
@@ -279,14 +201,16 @@
279 201 * @param array $args Post arguments
280 202 *
281 203 * @return array collection of PostModel
282 204 */
283 - public static function all( $args = [] ) {
284 - $args = array_merge( [
285 - 'post_type' => ( new static )->get_post_type(),
286 - 'post_status' => static::$accepted_statuses,
287 - 'numberposts' => - 1
288 - ], $args );
205 + public static function all( $args = array() ) {
206 + $args = array_merge(
207 + array(
208 + 'post_type' => ( new static() )->get_post_type(),
209 + 'post_status' => static::$accepted_statuses,
210 + 'numberposts' => - 1,
211 + ), $args
212 + );
289 213
290 214 $posts = get_posts( $args );
291 215
292 216 return array_map( fn( $p ) => new static( $p ), $posts );
@@ -300,27 +224,31 @@
300 224 * @param array $args other args
301 225 *
302 226 * @return array collection of records
303 227 */
304 - public static function paginate( $per_page = 10, $page = 1, $args = [] ) {
305 - $args = array_merge( [
306 - 'post_type' => ( new static )->get_post_type(),
307 - 'post_status' => static::$accepted_statuses,
308 - 'posts_per_page' => $per_page,
309 - 'paged' => $page,
310 - ], $args );
228 + public static function paginate( $per_page = 10, $page = 1, $args = array() ) {
229 + $args = array_merge(
230 + array(
231 + 'post_type' => ( new static() )->get_post_type(),
232 + 'post_status' => static::$accepted_statuses,
233 + 'posts_per_page' => $per_page,
234 + 'paged' => $page,
235 + ), $args
236 + );
311 237
312 238 $query = new WP_Query( $args );
313 239
314 - return [
315 - 'items' => array_map( function ( $post ) {
316 - return new static( $post );
317 - }, $query->posts ),
240 + return array(
241 + 'items' => array_map(
242 + function ( $post ) {
243 + return new static( $post );
244 + }, $query->posts
245 + ),
318 246 'total' => $query->found_posts,
319 247 'per_page' => $per_page,
320 248 'current_page' => $page,
321 249 'last_page' => ceil( $query->found_posts / $per_page ),
322 - ];
250 + );
323 251 }
324 252
325 253
326 254 /**
@@ -331,24 +259,27 @@
331 259 *
332 260 * @return array collection of post model
333 261 */
334 262 public static function where( $meta_key, $meta_value ) {
335 - $args = [
336 - 'post_type' => ( new static )->get_post_type(),
263 + $args = array(
264 + 'post_type' => ( new static() )->get_post_type(),
337 265 'post_status' => static::$accepted_statuses,
338 - 'meta_query' => [
339 - [
266 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering posts by meta fields
267 + 'meta_query' => array(
268 + array(
340 269 'key' => $meta_key,
341 270 'value' => $meta_value,
342 - ]
343 - ]
344 - ];
271 + ),
272 + ),
273 + );
345 274
346 275 $posts = get_posts( $args );
347 276
348 - return array_map( function ( $p ) {
349 - return new static( $p );
350 - }, $posts );
277 + return array_map(
278 + function ( $p ) {
279 + return new static( $p );
280 + }, $posts
281 + );
351 282 }
352 283
353 284 /**
354 285 * Create a single row
@@ -362,14 +293,14 @@
362 293 if ( is_wp_error( $validation ) ) {
363 294 return $validation;
364 295 }
365 296
366 - $post_data = [
367 - 'post_type' => ( new static )->get_post_type(),
297 + $post_data = array(
298 + 'post_type' => ( new static() )->get_post_type(),
368 299 'post_status' => $attributes['status'] ?? 'deactive',
369 300 'post_title' => $attributes['title'] ?? '',
370 301 'post_content' => $attributes['content'] ?? $attributes['description'] ?? '',
371 - ];
302 + );
372 303
373 304 $post_id = wp_insert_post( $post_data );
374 305
375 306 if ( is_wp_error( $post_id ) ) {
@@ -382,10 +313,9 @@
382 313 return $instance;
383 314 }
384 315
385 316 foreach ( $attributes as $key => $value ) {
386 - if ( ! in_array( $key, [ 'title', 'content', 'status' ] ) ) {
387 -
317 + if ( ! in_array( $key, array( 'title', 'content', 'status' ) ) ) {
388 318 update_post_meta( $post_id, $key, $value );
389 319
390 320 $instance->$key = $value;
391 321 }
@@ -399,9 +329,9 @@
399 329 * Update a certain record
400 330 *
401 331 * @param array $attributes Collection of attributes
402 332 *
403 - * @return WP_Error
333 + * @return WP_Error|Post_Model
404 334 */
405 335 public function update( array $attributes ) {
406 336 if ( ! $this->post ) {
407 337 return new WP_Error( 'post_not_found', esc_html__( 'Cannot update a non-existent post.', 'booktics' ) );
@@ -411,9 +341,9 @@
411 341 if ( is_wp_error( $validation ) ) {
412 342 return $validation;
413 343 }
414 344
415 - $post_data = [ 'ID' => $this->post->ID ];
345 + $post_data = array( 'ID' => $this->post->ID );
416 346
417 347 if ( isset( $attributes['title'] ) ) {
418 348 $this->title = $attributes['title'];
419 349 $post_data['post_title'] = $attributes['title'];
@@ -428,22 +358,58 @@
428 358 $this->status = $attributes['status'];
429 359 $post_data['post_status'] = $attributes['status'];
430 360 }
431 361
362 + // Handle permalink update when 'link' attribute is provided
363 + if ( isset( $attributes['link'] ) ) {
364 + $new_slug = $this->extract_slug_from_link( $attributes['link'] );
365 + if ( $new_slug ) {
366 + $post_data['post_name'] = sanitize_title( $new_slug );
367 + }
368 + }
369 +
432 370 wp_update_post( $post_data );
433 371
434 372 // Update custom meta fields
435 373 foreach ( $attributes as $key => $value ) {
436 - if ( ! in_array( $key, [ 'title', 'content', 'status' ] ) ) {
374 + if ( ! in_array( $key, array( 'title', 'content', 'status', 'link' ) ) ) {
437 375 update_post_meta( $this->post->ID, $key, $value );
438 376 $this->attributes[ $key ] = $value;
439 377 }
440 378 }
441 379
380 + // Refresh the post object and regenerate the link if permalink was updated
381 + if ( isset( $attributes['link'] ) ) {
382 + $this->post = get_post( $this->post->ID );
383 + $this->attributes['link'] = $this->get_link( $this->post );
384 + }
385 +
442 386 return $this;
443 387 }
444 388
445 389 /**
390 + * Extract slug from a full permalink URL
391 + *
392 + * @param string $link Full permalink URL
393 + * @return string|null Extracted slug or null if invalid
394 + */
395 + private function extract_slug_from_link( $link ) {
396 + // Remove trailing slash
397 + $link = rtrim( $link, '/' );
398 +
399 + // Extract the last part of the URL as the slug
400 + $slug = basename( $link );
401 +
402 + // Basic validation - ensure it's not empty and contains valid characters
403 + if ( empty( $slug ) || $slug === '.' || $slug === '..' ) {
404 + return null;
405 + }
406 +
407 + return $slug;
408 + }
409 +
410 +
411 + /**
446 412 * Save a single record
447 413 *
448 414 * @return Post_Model instance of the PostModel
449 415 * @throws Exception
@@ -453,22 +419,24 @@
453 419 if ( ! $this->post ) {
454 420 return static::create( $this->attributes );
455 421 }
456 422
457 - $post_data = [
423 + $post_data = array(
458 424 'ID' => $this->post->ID,
459 425 'post_title' => $this->title,
460 426 'post_content' => $this->content,
461 - ];
427 + );
462 428
463 429 wp_update_post( $post_data );
464 430
465 431 foreach ( $this->attributes as $key => $value ) {
466 - if ( ! in_array( $key, [
467 - 'ID',
468 - 'title',
469 - 'content'
470 - ] ) && $value !== ( $this->original[ $key ] ?? null ) ) {
432 + if ( ! in_array(
433 + $key, array(
434 + 'ID',
435 + 'title',
436 + 'content',
437 + )
438 + ) && $value !== ( $this->original[ $key ] ?? null ) ) {
471 439 update_post_meta( $this->post->ID, $key, $value );
472 440 }
473 441 }
474 442
@@ -493,9 +461,9 @@
493 461 *
494 462 * @return Object
495 463 */
496 464 public static function query() {
497 - return new Query_Builder( new static );
465 + return new Query_Builder( new static() );
498 466 }
499 467
500 468 /**
501 469 * Validate attributes
@@ -503,22 +471,27 @@
503 471 * @param array $attributes Post data
504 472 *
505 473 * @return WP_Error|true
506 474 */
507 - protected static function validate_attributes( $attributes = [] ) {
475 + protected static function validate_attributes( $attributes = array() ) {
508 476 if ( ! is_array( $attributes ) ) {
509 477 return new WP_Error( 'invalid_attributes', esc_html__( 'Attributes must be an array.', 'booktics' ) );
510 478 }
511 479
512 480 foreach ( $attributes as $key => $value ) {
513 - if ( ! isset( ( new static )->get_fillable()[ $key ] ) && ! in_array( $key, [
514 - 'ID',
515 - 'team_member_details',
516 - 'category_details',
517 - 'shortcode',
518 - 'service_details',
519 - 'duration_unit',
520 - ] ) ) {
481 + if ( ! isset( ( new static() )->get_fillable()[ $key ] ) && ! in_array(
482 + $key, array(
483 + 'ID',
484 + 'team_member_details',
485 + 'category_details',
486 + 'shortcode',
487 + 'service_details',
488 + 'duration_unit',
489 + 'customer',
490 + 'service',
491 + 'team_member',
492 + )
493 + ) ) {
521 494 // translators: %s: Property name
522 495 return new WP_Error( 'invalid_property', sprintf( esc_html__( 'Invalid property: %s', 'booktics' ), $key ) );
523 496 }
524 497 }
@@ -531,9 +504,9 @@
531 504 *
532 505 * @return array
533 506 */
534 507 public function to_array(): array {
535 - $data = [];
508 + $data = array();
536 509
537 510 foreach ( $this->get_fillable() as $key => $default ) {
538 511 if ( $key === 'user_email' ) {
539 512 $data['email'] = $this->$key;
@@ -541,17 +514,17 @@
541 514 }
542 515
543 516 $value = $this->$key;
544 517
545 - // Unserialize if necessary
518 + // Unserialize if necessary (object instantiation disallowed — see helper).
546 519 if ( is_string( $value ) && $this->is_serialized( $value ) ) {
547 - $value = maybe_unserialize( $value );
520 + $value = \booktics_safe_maybe_unserialize( $value );
548 521 }
549 522
550 523 // Normalize value to default type
551 524 if ( gettype( $value ) !== gettype( $default ) ) {
552 525 if ( gettype( $default ) === 'array' && is_string( $value ) && $this->is_serialized( $value ) ) {
553 - $value = maybe_unserialize( $value );
526 + $value = \booktics_safe_maybe_unserialize( $value );
554 527 } elseif ( gettype( $default ) === 'array' ) {
555 528 $value = $default; // Set to default empty array if the value doesn't match
556 529 }
557 530 }
@@ -556,26 +529,30 @@
556 529 }
557 530 }
558 531
559 532 // Cast specific keys to objects or arrays if needed
560 - if ( in_array( $key, [
561 - 'team_member',
562 - 'holiday',
563 - 'categories',
564 - 'additional_durations'
565 - ] ) ) {
566 - $value = is_array( $value ) ? $value : [];
533 + if ( in_array(
534 + $key, array(
535 + 'team_member',
536 + 'holiday',
537 + 'categories',
538 + 'additional_durations',
539 + )
540 + ) ) {
541 + $value = is_array( $value ) ? $value : array();
567 542 }
568 543
569 - if ( in_array( $key, [ 'schedule', 'custom_field' ] ) ) {
570 - $value = empty( $value ) ? (object) [] : $value;
544 + if ( in_array( $key, array( 'schedule', 'custom_field' ) ) ) {
545 + $value = empty( $value ) ? (object) array() : $value;
571 546 }
572 547
573 548 // Cast specific keys to boolean
574 - if ( in_array( $key, [
575 - 'enable_custom_schedule',
576 - 'enable_custom_holiday',
577 - ] ) ) {
549 + if ( in_array(
550 + $key, array(
551 + 'enable_custom_schedule',
552 + 'enable_custom_holiday',
553 + )
554 + ) ) {
578 555 $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
579 556 }
580 557
581 558 $data[ $key ] = $value ?? $default;
@@ -601,23 +578,10 @@
601 578 *
602 579 * @return bool True if the data is serialized, false otherwise.
603 580 */
604 581 protected function is_serialized( $data ) {
605 - // If it isn't a string, return false
606 - if ( ! is_string( $data ) ) {
607 - return false;
608 - }
609 -
610 - // Check if it's a serialized string (Array or Object)
611 - $data = trim( $data );
612 -
613 - if ( 'N;' === $data || ! preg_match( '/^([adObis]):/', $data ) ) {
614 - return false;
615 - }
616 -
617 - $result = @unserialize( $data );
618 -
619 - return $result !== false || $data === 'b:0;'; // Return false for failed unserialization and true for successful
582 + // Use WordPress core's purely structural check. Unlike the previous
583 + // implementation, \is_serialized() never calls unserialize(), so testing
584 + // attacker-controlled meta can no longer trigger PHP Object Injection.
585 + return is_string( $data ) && \is_serialized( $data );
620 586 }
621 -
622 587 }
623 -