PluginProbe
GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI / 8.0.5
GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI v8.0.5
8.0.5 8.0.4 8.0.3 8.0.1 8.0.2 8.0.0 7.9.9.7 7.9.9.6 7.9.9.5 7.9.9.4 7.9.9.3 7.9.9.2 7.9.9.1 7.9.9 7.9.8 7.9.7 7.9.6 7.9.5 7.9.4 7.9.3 7.9.2 7.9.1 7.9.0 7.8.9 7.8.8 All 47 releases
gamipress / includes / functions / achievements.php

achievements.php in GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI 8.0.5, at includes/functions/achievements.php

1,368 lines 43.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Achievements Functions
4 *
5 * @package GamiPress\Achievements_Functions
6 * @author GamiPress <[email protected]>, Ruben Garcia <[email protected]>
7 * @since 1.0.0
8 */
9 // Exit if accessed directly
10 if( !defined( 'ABSPATH' ) ) exit;
11
12 /**
13 * Check if post is a registered GamiPress achievement.
14 *
15 * @since 1.0.0
16 *
17 * @param object|int $post Post object or ID.
18 * @return bool True if post is an achievement, otherwise false.
19 */
20 function gamipress_is_achievement( $post = null ) {
21
22 $return = true;
23
24 if( gettype($post) === 'string' ) {
25 if( is_numeric($post) ) {
26 $post_type = gamipress_get_post_type( $post );
27 } else {
28 $post_type = $post;
29 }
30 } else {
31 $post_type = gamipress_get_post_type( $post );
32 }
33
34 // If post type is NOT a registered achievement type, it cannot be an achievement
35 if ( ! in_array( $post_type, gamipress_get_achievement_types_slugs() ) ) {
36 $return = false;
37 }
38
39 // If we pass both previous tests, this is a valid achievement (with filter to override)
40 return apply_filters( 'gamipress_is_achievement', $return, $post );
41
42 }
43
44 /**
45 * Get an array of achievements
46 *
47 * @since 1.0.0
48 * @param array $args An array of our relevant arguments
49 * @return array An array of the queried achievements
50 */
51 function gamipress_get_achievements( $args = array() ) {
52
53 // Setup our defaults
54 $defaults = array(
55 'post_type' => array_merge( gamipress_get_achievement_types_slugs(), gamipress_get_requirement_types_slugs() ),
56 'numberposts' => -1,
57 'suppress_filters' => false,
58 'achievement_relationship' => 'any',
59 );
60
61 $args = wp_parse_args( $args, $defaults );
62
63 // Since 1.5.1, requirements has their parent stored in the post_parent field, so it isn't required at all
64 if ( isset( $args['parent_of'] ) ) {
65
66 $post_parent = absint( gamipress_get_post_field( 'post_parent', $args['parent_of'] ) );
67
68 // Bail if achievement hasn't any parent
69 if( $post_parent === 0 )
70 return array();
71
72 $args['post__in'] = array( $post_parent );
73
74 }
75
76 // Since 1.5.1, requirements has their parent stored in the post_parent field, so it isn't required at all
77 if ( isset( $args['children_of'] ) ) {
78
79 $args['post_parent'] = $args['children_of'];
80
81 // When looking to get achievement steps, order is important to sequential steps
82 $args['orderby'] = 'menu_order';
83 $args['order'] = 'ASC';
84
85 }
86
87 // Get our achievement posts
88 $achievements = get_posts( $args );
89
90 return $achievements;
91
92 }
93
94 /**
95 * Get an achievement's parent posts
96 *
97 * @since 1.0.0
98 *
99 * @param integer $achievement_id The given achievement's post ID
100 *
101 * @return object|bool The post object of the achievement's parent, or false if none
102 */
103 function gamipress_get_parent_of_achievement( $achievement_id = 0 ) {
104
105 // Grab the current post ID if no achievement_id was specified
106 if ( ! $achievement_id ) {
107 global $post;
108 $achievement_id = $post->ID;
109 }
110
111 // Grab our achievement's parent
112 $parents = gamipress_get_achievements( array( 'parent_of' => $achievement_id ) );
113
114 // If it has a parent, return it, otherwise return false
115 if ( ! empty( $parents ) )
116 return $parents[0];
117 else
118 return false;
119
120 }
121
122 /**
123 * Get an achievement's children posts
124 *
125 * @since 1.0.0
126 *
127 * @param integer $achievement_id The given achievement's post ID
128 *
129 * @return array An array of our achievement's children (empty if none)
130 */
131 function gamipress_get_children_of_achievement( $achievement_id = 0 ) {
132
133 // Grab the current post ID if no achievement_id was specified
134 if ( ! $achievement_id ) {
135 global $post;
136 $achievement_id = $post->ID;
137 }
138
139 // Grab and return our achievement's children
140 return gamipress_get_achievements( array( 'children_of' => $achievement_id, 'achievement_relationship' => 'required' ) );
141
142 }
143
144 /**
145 * Get step's achievement
146 *
147 * @since 1.5.1
148 *
149 * @param integer $step_id The given step's post ID
150 *
151 * @return object|bool The post object of the achievement, or false if none
152 */
153 function gamipress_get_step_achievement( $step_id = 0 ) {
154
155 // Grab the current post ID if no step ID was specified
156 if ( ! $step_id ) {
157 global $post;
158 $step_id = $post->ID;
159 }
160
161 // The step's achievement is the post parent
162 $achievement_id = absint( gamipress_get_post_field( 'post_parent', $step_id ) );
163
164 if( $achievement_id !== 0 ) {
165 // If has parent, return his post object
166 return gamipress_get_post( $achievement_id );
167 } else {
168 return false;
169 }
170
171 }
172
173 /**
174 * Get achievement steps
175 *
176 * @since 1.5.1
177 *
178 * @param integer $achievement_id The given achievement requirement's post ID
179 * @param string $post_status The steps status (publish by default)
180 *
181 * @return array An array of post objects with the achievement's steps
182 */
183 function gamipress_get_achievement_steps( $achievement_id = 0, $post_status = 'publish' ) {
184
185 // Grab the current post ID if no achievement_id was specified
186 if ( ! $achievement_id ) {
187 global $post;
188 $achievement_id = $post->ID;
189 }
190
191 $cache = gamipress_get_cache( 'achievement_steps', array(), false );
192
193 // If result already cached, return it
194 if( isset( $cache[$achievement_id] ) && isset( $cache[$achievement_id][$post_status] ) ) {
195 return $cache[$achievement_id][$post_status];
196 }
197
198 $steps = get_posts( array(
199 'post_type' => 'step',
200 'post_parent' => $achievement_id,
201 'post_status' => $post_status,
202 'orderby' => 'menu_order',
203 'order' => 'ASC',
204 'posts_per_page' => -1,
205 'suppress_filters' => false,
206 ));
207
208 // Cache function result
209 if( ! isset( $cache[$achievement_id] ) ){
210 $cache[$achievement_id] = array();
211 }
212
213 $cache[$achievement_id][$post_status] = $steps;
214
215 gamipress_set_cache( 'achievement_steps', $cache );
216
217 // Return steps array
218 return $steps;
219
220 }
221
222 /**
223 * Check if the achievement's child achievements must be earned sequentially
224 *
225 * @since 1.0.0
226 *
227 * @param integer $achievement_id The given achievement's post ID
228 *
229 * @return bool True if steps are sequential, false otherwise
230 */
231 function gamipress_is_achievement_sequential( $achievement_id = 0 ) {
232
233 // Grab the current post ID if no achievement_id was specified
234 if ( ! $achievement_id ) {
235 global $post;
236 $achievement_id = $post->ID;
237 }
238
239 // If our achievement requires sequential steps, return true, otherwise false
240 if ( gamipress_get_post_meta( $achievement_id, '_gamipress_sequential' ) )
241 return true;
242 else
243 return false;
244 }
245
246 /**
247 * Check if user has already earned an achievement the maximum number of times
248 *
249 * @since 1.0.0
250 *
251 * @param integer $user_id The given user's ID
252 * @param integer $achievement_id The given achievement's post ID
253 *
254 * @return bool True if we've exceed the max possible earnings, false if we're still eligable
255 */
256 function gamipress_achievement_user_exceeded_max_earnings( $user_id = 0, $achievement_id = 0 ) {
257
258 // Sanitize vars
259 $user_id = absint( $user_id );
260 $achievement_id = absint( $achievement_id );
261
262 // Global max earnings
263 $global_max_earnings = gamipress_get_post_meta( $achievement_id, '_gamipress_global_maximum_earnings' );
264
265 // -1, 0 or empty means unlimited earnings
266 if( $global_max_earnings === '-1' || $global_max_earnings === '0' || empty( $global_max_earnings ) ) {
267 $global_max_earnings = 0;
268 }
269
270 $global_max_earnings = absint( $global_max_earnings );
271
272 // Only check global max earnings if isn't setup as unlimited
273 if( $global_max_earnings > 0 ) {
274
275 $earned_times = gamipress_get_earnings_count( array( 'post_id' => $achievement_id ) );
276
277 // Bail if achievement has exceeded its global max earnings
278 if( $earned_times >= $global_max_earnings ) {
279 return true;
280 }
281
282 }
283
284 // Per user max earnings
285 $max_earnings = gamipress_get_post_meta( $achievement_id, '_gamipress_maximum_earnings' );
286
287 // Unlimited maximum earnings per user check
288 if( $max_earnings === '-1' || $max_earnings === '0' || empty( $max_earnings ) ) {
289 return false;
290 }
291
292 // If the achievement has an earning limit per user, and we've earned it before...
293 if ( $max_earnings && $user_has_achievement = gamipress_get_earnings_count( array( 'user_id' => absint( $user_id ), 'post_id' => absint( $achievement_id ) ) ) ) {
294
295 // If we've earned it as many (or more) times than allowed, then we have exceeded maximum earnings, thus true
296 if ( $user_has_achievement >= $max_earnings ) {
297 return true;
298 }
299
300 }
301
302 // The post has no limit, or we're under it
303 return false;
304 }
305
306 /**
307 * Helper function for building an object for our achievement
308 *
309 * @since 1.0.0
310 * @param integer $achievement_id The given achievement's post ID
311 * @param string $context The context in which we're creating this object
312 *
313 * @return false|object Our object containing only the relevant bits of information we want
314 */
315 function gamipress_build_achievement_object( $achievement_id = 0, $context = 'earned' ) {
316
317 // Grab the new achievement's $post data, and bail if it doesn't exist
318 $achievement = gamipress_get_post( $achievement_id );
319
320 if ( ! $achievement ) {
321 return false;
322 }
323
324 // Setup a new object for the achievement
325 $achievement_object = new stdClass;
326 $achievement_object->ID = $achievement_id;
327 $achievement_object->post_type = $achievement->post_type;
328 $achievement_object->points = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_points' ) );
329 $achievement_object->points_type = gamipress_get_post_meta( $achievement_id, '_gamipress_points_type' );
330
331 // Store the current timestamp differently based on context
332 if ( 'earned' == $context ) {
333 $achievement_object->date_earned = current_time( 'timestamp' );
334 } elseif ( 'started' == $context ) {
335 $achievement_object->date_started = $achievement_object->last_activity_date = current_time( 'timestamp' );
336 }
337
338 // Return our achievement object, available filter so we can extend it elsewhere
339 return apply_filters( 'gamipress_achievement_object', $achievement_object, $achievement_id, $context );
340
341 }
342
343 /**
344 * Get an array of post IDs for achievements that are marked as "hidden"
345 *
346 * @since 1.0.0
347 *
348 * @param string|array $achievement_type Limit the array to a specific type of achievement
349 *
350 * @return array An array of hidden achievement post IDs
351 */
352 function gamipress_get_hidden_achievement_ids( $achievement_type = '' ) {
353
354 if( ! is_array( $achievement_type ) ) {
355 $achievement_type = array( $achievement_type );
356 }
357
358 $cache = gamipress_get_cache( 'hidden_achievements_ids', array(), false );
359 $hidden_ids = array();
360 $all_cached = true;
361
362 // loop al given types looking if all has been cached
363 foreach( $achievement_type as $type ) {
364
365 if( isset( $cache[$type] ) && is_array( $cache[$type] ) ) {
366 $hidden_ids = array_merge( $hidden_ids, $cache[$type] );
367 } else {
368 $all_cached = false;
369 }
370
371 }
372
373 // Return hidden achievements cached
374 if( $all_cached ) {
375 return $hidden_ids;
376 }
377
378 // Reset the hidden ids var
379 $hidden_ids = array();
380
381 // Grab our hidden achievements
382 $hidden_achievements = get_posts( array(
383 'post_type' => $achievement_type,
384 'post_status' => 'publish',
385 'posts_per_page' => -1,
386 'meta_key' => '_gamipress_hidden',
387 'meta_value' => 'hidden',
388 'suppress_filters' => false,
389 ) );
390
391 foreach ( $hidden_achievements as $achievement ) {
392
393 $hidden_ids[] = $achievement->ID;
394
395 // Initialize hidden achievement type cache
396 if( ! isset( $cache[$achievement->post_type] ) ) {
397 $cache[$achievement->post_type] = array();
398 }
399
400 $cache[$achievement->post_type][] = $achievement->ID;
401
402 }
403
404 // Cache hidden achievements
405 gamipress_set_cache( 'hidden_achievements_ids', $cache );
406
407 // Return our results
408 return $hidden_ids;
409 }
410
411 /**
412 * Check if achievement is marked as "hidden"
413 *
414 * @since 1.4.7
415 *
416 * @param int $achievement_id The achievement ID
417 *
418 * @return bool
419 */
420
421 function gamipress_is_achievement_hidden( $achievement_id ) {
422
423 $hidden = gamipress_get_post_meta( $achievement_id, '_gamipress_hidden' );
424
425 return ( $hidden === 'hidden' );
426
427 }
428
429 /**
430 * Get an array of post IDs for a user's earned achievements
431 *
432 * @since 1.0.0
433 * @param integer $user_id The given user's ID
434 * @param string $achievement_type Limit the array to a specific type of achievement
435 * @return array Our user's array of earned achievement post IDs
436 */
437 function gamipress_get_user_earned_achievement_ids( $user_id = 0, $achievement_type = '' ) {
438
439 // Assume we have no earned achievements
440 $earned_ids = array();
441
442 // Grab our earned achievements
443 $earned_achievements = gamipress_get_user_achievements( array(
444 'user_id' => $user_id,
445 'achievement_type' => $achievement_type,
446 'display' => true
447 ) );
448
449 foreach ( $earned_achievements as $achievement ) {
450 $earned_ids[] = $achievement->ID;
451 }
452
453 return $earned_ids;
454
455 }
456
457 /**
458 * Get an array of unique achievement types a user has earned
459 *
460 * @since 1.0.0
461 *
462 * @param int $user_id The ID of the user earning the achievement
463 * @return array The array of achievements the user has earned
464 */
465 function gamipress_get_user_earned_achievement_types( $user_id = 0 ){
466
467 $achievements = gamipress_get_user_achievements( array( 'user_id' => $user_id ) );
468
469 if( ! $achievements ) {
470 return array();
471 }
472
473 $achievement_types = wp_list_pluck( $achievements, 'post_type' );
474
475 return array_unique( $achievement_types );
476 }
477
478 /**
479 * Returns achievements that may be earned when the given achievement is earned.
480 *
481 * @since 1.0.0
482 *
483 * @param integer $achievement_id The given achievement's post ID
484 *
485 * @return array An array of achievements that are dependent on the given achievement
486 */
487 function gamipress_get_dependent_achievements( $achievement_id = 0 ) {
488
489 global $wpdb;
490
491 // Grab the current achievement ID if none specified
492 if ( ! $achievement_id ) {
493 global $post;
494 $achievement_id = $post->ID;
495 }
496
497 $posts = GamiPress()->db->posts;
498 $postmeta = GamiPress()->db->postmeta;
499
500 // Grab posts that can be earned by unlocking the given achievement
501 $achievements = $wpdb->get_results( $wpdb->prepare(
502 "SELECT *
503 FROM {$posts} as posts,
504 {$postmeta} as meta
505 WHERE posts.ID = meta.post_id
506 AND meta.meta_key = '_gamipress_achievement_post'
507 AND meta.meta_value = %d",
508 $achievement_id
509 ) );
510
511 // In addition, the post parent can be earned by unlocking the given achievement
512 $post_parent = absint( gamipress_get_post_field( 'post_parent', $achievement_id ) );
513
514 if( $post_parent !== 0 ) {
515 $achievements[] = gamipress_get_post( $post_parent );
516 }
517
518 // Note: Dependent achievements has been removed since it causes duplicated awards
519 // Also, on unlock an achievement of a specific type an event is triggered to handle its awards
520
521 // Available filter to modify an achievement's dependents
522 return apply_filters( 'gamipress_dependent_achievements', $achievements, $achievement_id );
523
524 }
525
526 /**
527 * Returns achievements that must be earned to earn given achievement.
528 *
529 * @since 1.0.0
530 * @updated 1.5.1 Added $post_status parameter
531 *
532 * @param integer $achievement_id The given achievement's post ID
533 * @param string $post_status The required achievements status (publish by default)
534 *
535 * @return array An array of achievements that are dependent on the given achievement
536 */
537 function gamipress_get_required_achievements_for_achievement( $achievement_id = 0, $post_status = 'publish' ) {
538
539 global $wpdb;
540
541 // Grab the current achievement ID if none specified
542 if ( ! $achievement_id ) {
543 global $post;
544 $achievement_id = $post->ID;
545 }
546
547 // Don't retrieve requirements if achievement is not earned by steps
548 if ( gamipress_get_post_meta( $achievement_id, '_gamipress_earned_by' ) !== 'triggers' )
549 return false;
550
551 $posts = GamiPress()->db->posts;
552
553 // Setup the post status var
554 if( ! is_array( $post_status ) ) {
555 $post_status = explode( ',', $post_status );
556 }
557
558 if( in_array( 'any', $post_status ) ) {
559 $post_status = array( 'publish', 'draft', 'pending', 'private' );
560 }
561
562 // Grab our requirements for this achievement
563 $requirements = $wpdb->get_results( $wpdb->prepare(
564 "SELECT *
565 FROM $posts as posts
566 WHERE posts.post_parent = %d
567 AND posts.post_status IN ( '" . implode( "', '", $post_status ) . "' )
568 ORDER BY CAST( posts.menu_order as SIGNED ) ASC",
569 $achievement_id
570 ) );
571
572 return $requirements;
573
574 }
575
576 /**
577 * Returns achievements that may be earned when the given achievement is earned.
578 *
579 * @since 1.0.0
580 * @updated 1.3.1 added steps with required points
581 * @updated 1.3.2 improved query
582 * @updated 1.3.9.4 added $points_type argument
583 *
584 * @return array An array of achievements that are dependent on the given achievement
585 */
586 function gamipress_get_points_based_achievements( $points_type = '' ) {
587
588 global $wpdb;
589
590 if( empty( $points_type ) ) {
591 return array();
592 }
593
594 $achievements = gamipress_get_transient( "gamipress_{$points_type}_based_achievements" );
595
596 if ( empty( $achievements ) ) {
597
598 $posts = GamiPress()->db->posts;
599 $postmeta = GamiPress()->db->postmeta;
600
601 // Grab posts that can be earned by unlocking the given achievement
602 $achievements = $wpdb->get_results( $wpdb->prepare(
603 "SELECT *
604 FROM {$posts} as p
605 LEFT JOIN {$postmeta} AS m1 ON ( p.ID = m1.post_id AND m1.meta_key = %s )
606 LEFT JOIN {$postmeta} AS m2 ON ( p.ID = m2.post_id AND m2.meta_key = %s )
607 LEFT JOIN {$postmeta} AS m3 ON ( p.ID = m3.post_id AND m3.meta_key = %s )
608 WHERE m1.meta_value = %s
609 AND ( m2.meta_value = %s OR m2.meta_value = %s OR m3.meta_value = %s )
610 GROUP BY p.ID",
611 '_gamipress_points_type_required', // (m1.meta_key) Of this post type
612 '_gamipress_trigger_type', // (m2.meta_key) Requirements based on earn points
613 '_gamipress_earned_by', // (m3.meta_key) Achievements earned by points
614 $points_type, // (m1.meta_value) Of this post type
615 'earn-points', // (m2.meta_value) Requirements based on earn points
616 'points-balance', // (m2.meta_value) Requirements based on points balance
617 'points' // (m3.meta_value) Achievements earned by points
618 ) );
619
620 $achievements = gamipress_sort_triggered_rank_requirements( $achievements );
621
622 // Store these posts to a transient for 1 day
623 gamipress_set_transient( "gamipress_{$points_type}_based_achievements", $achievements, 60*60*24 );
624 }
625
626 return (array) maybe_unserialize( $achievements );
627 }
628
629 /**
630 * Destroy the points-based achievements transient if we edit a points-based achievement
631 *
632 * @since 1.0.0
633 * @param integer $post_id The given post's ID
634 */
635 function gamipress_bust_points_based_achievements_cache( $post_id ) {
636
637 $post = get_post( $post_id );
638
639 if ( gamipress_is_achievement( $post )
640 && ( 'points' === gamipress_get_post_meta( $post_id, '_gamipress_earned_by' )
641 || ( isset( $_POST['_gamipress_earned_by'] ) && 'points' === $_POST['_gamipress_earned_by'] ) ) ) {
642
643 $points_type = gamipress_get_post_meta( $post_id, '_gamipress_points_type_required' );
644
645 // If the post is one of our achievement types and the achievement is awarded by minimum points, delete the transient
646 gamipress_delete_transient( "gamipress_{$points_type}_based_achievements" );
647
648 } else if( in_array( gamipress_get_post_type( $post_id ), gamipress_get_requirement_types_slugs() )
649 && in_array( gamipress_get_post_meta( $post_id, '_gamipress_trigger_type' ), array( 'earn-points', 'points-balance' ) ) ) {
650
651 $points_type = gamipress_get_post_meta( $post_id, '_gamipress_points_type_required' );
652
653 // If the post is one of our requirement types and the trigger type is a points based one, delete the transient
654 gamipress_delete_transient( "gamipress_{$points_type}_based_achievements" );
655
656 }
657
658 }
659 add_action( 'save_post', 'gamipress_bust_points_based_achievements_cache' );
660 add_action( 'trash_post', 'gamipress_bust_points_based_achievements_cache' );
661
662 /**
663 * Returns achievements that may be earned when the given achievement is earned.
664 *
665 * @since 1.3.1
666 * @updated 1.3.2 improved query
667 *
668 * @return array An array of achievements that are dependent on the given achievement
669 */
670 function gamipress_get_rank_based_achievements() {
671
672 global $wpdb;
673
674 $achievements = gamipress_get_transient( 'gamipress_rank_based_achievements' );
675
676 if ( empty( $achievements ) ) {
677
678 $posts = GamiPress()->db->posts;
679 $postmeta = GamiPress()->db->postmeta;
680
681 // Grab posts that can be earned by unlocking the given achievement
682 $achievements = $wpdb->get_results( $wpdb->prepare(
683 "SELECT *
684 FROM {$posts} as posts
685 LEFT JOIN {$postmeta} AS m1 ON ( posts.ID = m1.post_id AND m1.meta_key = %s )
686 LEFT JOIN {$postmeta} AS m2 ON ( posts.ID = m2.post_id AND m2.meta_key = %s )
687 WHERE m1.meta_value = %s
688 OR m2.meta_value = %s",
689 '_gamipress_trigger_type', // (m1.meta_key) Requirements based on earn rank
690 '_gamipress_earned_by', // (m2.meta_key) Achievements earned by rank
691 'earn-rank', // (m1.meta_value) Requirements based on earn rank
692 'rank' // (m2.meta_value) Achievements earned by rank
693 ) );
694
695 // Store these posts to a transient for 1 days
696 gamipress_set_transient( 'gamipress_rank_based_achievements', $achievements, 60*60*24 );
697 }
698
699 return (array) maybe_unserialize( $achievements );
700
701 }
702
703 /**
704 * Destroy the rank-based achievements transient if we edit a rank-based achievement
705 *
706 * @deprecated Removed the transient usage since 1.3.2
707 *
708 * @since 1.3.1
709 *
710 * @param integer $post_id The given post's ID
711 */
712 function gamipress_bust_rank_based_achievements_cache( $post_id ) {
713
714 $post = get_post( $post_id );
715
716 if (
717 gamipress_is_achievement( $post )
718 && (
719 'rank' === gamipress_get_post_meta( $post_id, '_gamipress_earned_by' )
720 || ( isset( $_POST['_gamipress_earned_by'] ) && 'rank' === $_POST['_gamipress_earned_by'] )
721 )
722 ) {
723
724 // If the post is one of our achievement types, and the achievement is awarded by a rank, delete the transient
725 gamipress_delete_transient( 'gamipress_rank_based_achievements' );
726
727 } else if(
728 in_array( gamipress_get_post_type( $post_id ), gamipress_get_requirement_types_slugs() )
729 && 'earn-rank' == gamipress_get_post_meta( $post_id, '_gamipress_trigger_type' )
730 ) {
731
732 // If the post is one of our requirement types and the trigger type is a rank based one, delete the transient
733 gamipress_delete_transient( 'gamipress_rank_based_achievements' );
734
735 }
736
737 }
738 add_action( 'save_post', 'gamipress_bust_rank_based_achievements_cache' );
739 add_action( 'trash_post', 'gamipress_bust_rank_based_achievements_cache' );
740
741 /**
742 * Helper function to retrieve an achievement post thumbnail
743 *
744 * Falls back to achievement type's thumbnail.
745 *
746 * @since 1.0.0
747 *
748 * @param integer $post_id The achievement's post ID
749 * @param string $image_size The name of a registered custom image size
750 * @param string $class A custom class to use for the image tag
751 *
752 * @return string Our formatted image tag
753 */
754 function gamipress_get_achievement_post_thumbnail( $post_id = 0, $image_size = 'gamipress-achievement', $class = 'gamipress-achievement-thumbnail' ) {
755
756 // Get our achievement thumbnail
757 $image = gamipress_get_the_post_thumbnail( $post_id, $image_size, array( 'class' => $class ) );
758
759 // If achievement don't have an image...
760 if ( ! $image ) {
761
762 // Grab our achievement type's post thumbnail
763 $achievement = get_page_by_path( gamipress_get_post_type( $post_id ), OBJECT, 'achievement-type' );
764 $image = is_object( $achievement ) ? gamipress_get_the_post_thumbnail( $achievement->ID, $image_size, array( 'class' => $class ) ) : false;
765
766 // If we still have no image
767 if ( ! $image ) {
768
769 // If we already have an array for image size
770 if ( is_array( $image_size ) ) {
771 // Write our sizes to an associative array
772 $image_sizes['width'] = $image_size[0];
773 $image_sizes['height'] = $image_size[1];
774
775 // Otherwise, attempt to grab the width/height from our specified image size
776 } else {
777 global $_wp_additional_image_sizes;
778 if ( isset( $_wp_additional_image_sizes[$image_size] ) )
779 $image_sizes = $_wp_additional_image_sizes[$image_size];
780 }
781
782 // If we can't get the defined width/height, set our own
783 if ( empty( $image_sizes ) ) {
784 $image_sizes = array(
785 'width' => 100,
786 'height' => 100
787 );
788 }
789
790 // Available filter: 'gamipress_default_achievement_post_thumbnail'
791 $default_thumbnail = apply_filters( 'gamipress_default_achievement_post_thumbnail', '', $achievement, $image_sizes );
792
793 if( ! empty( $default_thumbnail ) ) {
794 $image = '<img src="' . $default_thumbnail . '" width="' . $image_sizes['width'] . '" height="' . $image_sizes['height'] . '" class="' . $class . '">';
795 }
796
797 }
798 }
799
800 // Finally, return our image tag
801 return $image;
802 }
803
804 /**
805 * Get an array of all users who have earned a given achievement
806 *
807 * @since 1.0.0
808 * @updated 1.6.7 Added $args parameter and removed check to meet if user has earned the achievement, also removed 1.2.8 compatibility
809 *
810 * @param int $achievement_id The given achievement's post ID
811 * @param array $args Array of arguments that modifies the query result
812 *
813 * @return array Array of user objects
814 */
815 function gamipress_get_achievement_earners( $achievement_id = 0, $args = array() ) {
816
817 global $wpdb;
818
819 // Setup vars
820 $user_earnings = GamiPress()->db->user_earnings;
821 $from = "{$user_earnings} AS u ";
822 $where = "u.post_id = {$achievement_id} ";
823 $group_by = "u.user_id";
824 $order_by = '';
825 $limit = '';
826
827 // Setup default args
828 $defaults = array(
829 'limit' => -1,
830 'orderby' => 'u.date',
831 'order' => 'DESC',
832 );
833
834 // On multisite, get earners only of the current site
835 if( is_multisite() && ! gamipress_is_network_wide_active() ) {
836 $from .= "LEFT JOIN {$wpdb->usermeta} AS umcap ON ( umcap.user_id = u.user_id ) ";
837 $where .= "AND umcap.meta_key = '" . $wpdb->get_blog_prefix( gamipress_get_original_site_id() ) . "capabilities' ";
838 }
839
840 /**
841 * Filters the earners args
842 *
843 * @since 1.6.7
844 *
845 * @param array $args Array of given arguments
846 * @param int $achievement_id The given achievement's post ID
847 *
848 * @return array
849 */
850 $args = apply_filters( 'gamipress_get_achievement_earners_args', $args, $achievement_id );
851
852 $args = wp_parse_args( $args, $defaults );
853
854 // Sanitization
855 $args['limit'] = absint( $args['limit'] );
856 $args['order'] = gamipress_validate_from_array( strtoupper( $args['order'] ), array( 'ASC', 'DESC' ), 'DESC' );
857
858 // Setup FROM
859 if( ! empty( $args['from'] ) )
860 $from .= $args['from'];
861
862 // Setup WHERE
863 if( ! empty( $args['where'] ) )
864 $where .= $args['where'];
865
866 // Setup LIMIT
867 if( (int)( $args['limit'] ) > 0 )
868 $limit = '0, ' . $args['limit'];
869
870 // Setup ORDER BY
871 if( ! empty( $args['orderby'] ) )
872 $order_by = $args['orderby'] . ' ' . $args['order'];
873
874 $earners = $wpdb->get_col(
875 "SELECT u.user_id
876 FROM {$from}
877 WHERE {$where}
878 GROUP BY {$group_by} "
879 . ( ! empty( $order_by ) ? "ORDER BY {$order_by} " : '' )
880 . ( ! empty( $limit ) ? "LIMIT {$limit} " : '' )
881 );
882
883 $earned_users = array();
884
885 foreach( $earners as $earner_id ) {
886 $earned_users[] = new WP_User( $earner_id );
887 }
888
889 return $earned_users;
890
891 }
892
893 /**
894 * Build an unordered list of users who have earned a given achievement
895 *
896 * @since 1.0.0
897 * @updated 1.6.7 Added $args parameter
898 *
899 * @param int $achievement_id The given achievement's post ID
900 * @param array $args Array of arguments that modifies the earners list result
901 *
902 * @return string Concatenated markup
903 */
904 function gamipress_get_achievement_earners_list( $achievement_id = 0, $args = array() ) {
905
906 /**
907 * Filters the earners list args
908 *
909 * @since 1.6.7
910 *
911 * @param array $args Array of given arguments
912 * @param int $achievement_id The given achievement's post ID
913 *
914 * @return array
915 */
916 $args = apply_filters( 'gamipress_get_achievement_earners_list_args', $args, $achievement_id );
917
918 // Grab our users
919 $earners = gamipress_get_achievement_earners( $achievement_id, $args );
920 $output = '';
921
922 // Only generate output if we have earners
923 if ( ! empty( $earners ) ) {
924
925 /**
926 * Filters the achievement earners heading text
927 *
928 * @since 1.8.6
929 *
930 * @param string $heading_text The heading text
931 * @param int $achievement_id The given achievement's post ID
932 * @param array $args Array of given arguments
933 *
934 * @return string
935 */
936 $heading_text = apply_filters( 'gamipress_achievement_earners_heading', __( 'People who have earned this:', 'gamipress' ), $achievement_id, $args );
937
938 // Loop through each user and build our output
939 $output .= '<h4>' . $heading_text . '</h4>';
940
941 $output .= '<ul class="gamipress-achievement-earners-list achievement-' . $achievement_id . '-earners-list">';
942
943 foreach ( $earners as $user ) {
944
945 /**
946 * Filters the achievement earner url
947 *
948 * @since 1.8.6
949 *
950 * @param string $user_url The user URl, by default the get_author_posts_url()
951 * @param int $user_id The rendered user ID
952 * @param int $achievement_id The given achievement's post ID
953 * @param array $args Array of given arguments
954 *
955 * @return string
956 */
957 $user_url = apply_filters( 'gamipress_achievement_earner_user_url', get_author_posts_url( $user->ID ), $user->ID, $achievement_id, $args );
958
959 /**
960 * Filters the achievement earner display
961 *
962 * @since 1.8.6
963 *
964 * @param string $user_display The user display, by default the user display name
965 * @param int $user_id The rendered user ID
966 * @param int $achievement_id The given achievement's post ID
967 * @param array $args Array of given arguments
968 *
969 * @return string
970 */
971 $user_display = apply_filters( 'gamipress_achievement_earner_user_display', $user->display_name, $user->ID, $achievement_id, $args );
972
973 $user_content = '<li>'
974 . '<a href="' . $user_url . '">'
975 . get_avatar( $user->ID )
976 . '<span class="earner-display-name">' . $user_display . '</span>'
977 . '</a>'
978 . '</li>';
979
980 /**
981 * Filters the earners list user output
982 *
983 * @since 1.0.0
984 * @updated 1.6.7 Added $achievement_id and $args arguments
985 *
986 * @param string $user_content User output
987 * @param int $user_id The rendered user ID
988 * @param int $achievement_id The given achievement's post ID
989 * @param array $args Array of given arguments
990 *
991 * @return string
992 */
993 $output .= apply_filters( 'gamipress_get_achievement_earners_list_user', $user_content, $user->ID, $achievement_id, $args );
994
995 }
996
997 $output .= '</ul>';
998 }
999
1000 /**
1001 * Filters the achievement earners list output
1002 *
1003 * @since 1.0.0
1004 *
1005 * @param string $output Achievement earners list output
1006 * @param int $achievement_id The given achievement's post ID
1007 * @param array $args Array of given arguments
1008 * @param array $earners Array of achievement earners
1009 *
1010 * @return string
1011 */
1012 return apply_filters( 'gamipress_get_achievement_earners_list', $output, $achievement_id, $args, $earners );
1013 }
1014
1015 /**
1016 * Flush rewrite rules whenever an achievement type is published.
1017 *
1018 * @since 1.0.0
1019 *
1020 * @param string $new_status New status.
1021 * @param string $old_status Old status.
1022 * @param object $post Post object.
1023 */
1024 function gamipress_flush_rewrite_on_published_achievement( $new_status, $old_status, $post ) {
1025
1026 if ( 'achievement-type' === $post->post_type && 'publish' === $new_status && 'publish' !== $old_status ) {
1027 gamipress_flush_rewrite_rules();
1028 }
1029
1030 }
1031 add_action( 'transition_post_status', 'gamipress_flush_rewrite_on_published_achievement', 10, 3 );
1032
1033 /**
1034 * Update all dependent data if achievement type name has changed.
1035 *
1036 * @since 1.0.0
1037 *
1038 * @param array $data Post data.
1039 * @param array $post_args Post args.
1040 *
1041 * @return array Updated post data.
1042 */
1043 function gamipress_maybe_update_achievement_type( $data = array(), $post_args = array() ) {
1044
1045 // Bail if not is main site, on network wide installs achievements are just available on main site
1046 if( gamipress_is_network_wide_active() && ! is_main_site() ) {
1047 return $data;
1048 }
1049
1050 // Bail if not is a achievement type
1051 if( $post_args['post_type'] !== 'achievement-type') {
1052 return $data;
1053 }
1054
1055 // If user set an empty slug, then generate it
1056 if( empty( $post_args['post_name'] ) ) {
1057 $post_args['post_name'] = wp_unique_post_slug(
1058 sanitize_title( $post_args['post_title'] ),
1059 $post_args['ID'],
1060 $post_args['post_status'],
1061 $post_args['post_type'],
1062 $post_args['post_parent']
1063 );
1064 }
1065
1066 // Sanitize post_name
1067 $post_args['post_name'] = gamipress_sanitize_slug( $post_args['post_name'] );
1068
1069 if ( gamipress_achievement_type_changed( $post_args ) ) {
1070
1071 $original_type = get_post( $post_args['ID'] )->post_name;
1072 $new_type = $post_args['post_name'];
1073
1074 $data['post_name'] = gamipress_update_achievement_types( $original_type, $new_type );
1075
1076 /**
1077 * Action triggered when an achievement type has been changed
1078 *
1079 * @since 1.5.1
1080 *
1081 * @param array $data Post data.
1082 * @param array $post_args Post args.
1083 * @param string $original_type The original type slug.
1084 * @param string $new_type The new type slug.
1085 */
1086 do_action( 'gamipress_achievement_type_changed', $data, $post_args, $original_type, $new_type );
1087
1088 // Add a filter for redirect user to renamed achievement
1089 add_filter( 'redirect_post_location', 'gamipress_achievement_type_rename_redirect', 99 );
1090
1091 }
1092
1093 return $data;
1094 }
1095 add_filter( 'wp_insert_post_data' , 'gamipress_maybe_update_achievement_type' , 99, 2 );
1096
1097 /**
1098 * Check if an achievement type name has changed.
1099 *
1100 * @since 1.0.0
1101 *
1102 * @param array $post_args Post args.
1103 * @return bool True if name has changed, otherwise false.
1104 */
1105 function gamipress_achievement_type_changed( $post_args = array() ) {
1106
1107 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
1108 return false;
1109 }
1110
1111 $original_post = ( !empty( $post_args['ID'] ) && isset( $post_args['ID'] ) ) ? get_post( $post_args['ID'] ) : null;
1112 $status = false;
1113
1114 if ( is_object( $original_post ) ) {
1115 if (
1116 $post_args['post_type'] === 'achievement-type'
1117 && $original_post->post_status !== 'auto-draft'
1118 && ! empty( $original_post->post_name )
1119 && $original_post->post_name !== $post_args['post_name']
1120 ) {
1121 $status = true;
1122 }
1123 }
1124
1125 return $status;
1126 }
1127
1128 /**
1129 * Replace all instances of one achievement type with another.
1130 *
1131 * @since 1.0.0
1132 *
1133 * @param string $original_type Original achievement type.
1134 * @param string $new_type New achievement type.
1135 * @return string New achievement type.
1136 */
1137 function gamipress_update_achievement_types( $original_type = '', $new_type = '' ) {
1138
1139 // Sanity check to prevent altering core posts
1140 if ( empty( $original_type ) || in_array( $original_type, array( 'post', 'page', 'attachment', 'revision', 'nav_menu_item' ) ) ) {
1141 return $new_type;
1142 }
1143
1144 gamipress_update_achievements_achievement_types( $original_type, $new_type );
1145 gamipress_update_earned_meta_achievement_types( $original_type, $new_type );
1146
1147 /**
1148 * Action triggered when an achievement type gets updated (called before flush rewrite rules)
1149 *
1150 * @since 1.5.1
1151 *
1152 * @param string $original_type The original type slug.
1153 * @param string $new_type The new type slug.
1154 */
1155 do_action( 'gamipress_update_achievement_type', $original_type, $new_type );
1156
1157 gamipress_flush_rewrite_rules();
1158
1159 return $new_type;
1160
1161 }
1162
1163 /**
1164 * Change all achievements of one type to a new type.
1165 *
1166 * @since 1.0.0
1167 *
1168 * @param string $original_type Original achievement type.
1169 * @param string $new_type New achievement type.
1170 */
1171 function gamipress_update_achievements_achievement_types( $original_type = '', $new_type = '' ) {
1172
1173 $items = get_posts( array(
1174 'posts_per_page' => -1,
1175 'post_status' => 'any',
1176 'post_type' => $original_type,
1177 'fields' => 'id',
1178 'suppress_filters' => false,
1179 ) );
1180
1181 foreach ( $items as $item ) {
1182 set_post_type( $item->ID, $new_type );
1183 }
1184
1185 }
1186
1187 /**
1188 * Change all earned meta from one achievement type to another.
1189 *
1190 * @since 1.0.0
1191 *
1192 * @param string $original_type Original achievement type.
1193 * @param string $new_type New achievement type.
1194 */
1195 function gamipress_update_earned_meta_achievement_types( $original_type = '', $new_type = '' ) {
1196
1197 // Setup CT object
1198 $ct_table = ct_setup_table( 'gamipress_user_earnings' );
1199
1200 $ct_table->db->update(
1201 array( 'post_type' => $new_type ),
1202 array( 'post_type' => $original_type )
1203 );
1204
1205 ct_reset_setup_table();
1206
1207 }
1208
1209 /**
1210 * Get unserialized user achievement metas.
1211 *
1212 * @since 1.0.0
1213 *
1214 * @param string $meta_key Meta key.
1215 * @param string $original_type Achievement type.
1216 *
1217 * @return array User achievement metas.
1218 */
1219 function gamipress_get_unserialized_achievement_metas( $meta_key = '', $original_type = '' ) {
1220
1221 $metas = gamipress_get_achievement_metas( $meta_key, $original_type );
1222
1223 if ( ! empty( $metas ) ) {
1224 foreach ( $metas as $key => $meta ) {
1225 $metas[ $key ]->meta_value = maybe_unserialize( $meta->meta_value );
1226 }
1227 }
1228
1229 return $metas;
1230
1231 }
1232
1233 /**
1234 * Get serialized user achievement metas.
1235 *
1236 * @since 1.0.0
1237 *
1238 * @param string $meta_key Meta key.
1239 * @param string $original_type Achievement type.
1240 * @return array User achievement metas.
1241 */
1242 function gamipress_get_achievement_metas( $meta_key = '', $original_type = '' ) {
1243
1244 global $wpdb;
1245
1246 return $wpdb->get_results( $wpdb->prepare(
1247 "SELECT *
1248 FROM {$wpdb->usermeta}
1249 WHERE meta_key = %s
1250 AND meta_value LIKE '%%%s%%'",
1251 $meta_key,
1252 $original_type
1253 ) );
1254
1255 }
1256
1257 /**
1258 * Change user achievement meta from one achievement type to another.
1259 *
1260 * @since 1.0.0
1261 *
1262 * @param array $achievements Array of achievements.
1263 * @param string $original_type Original achievement type.
1264 * @param string $new_type New achievement type.
1265 *
1266 * @return array $achievements
1267 */
1268 function gamipress_update_meta_achievement_types( $achievements = array(), $original_type = '', $new_type = '' ) {
1269
1270 if ( is_array( $achievements ) && ! empty( $achievements ) ) {
1271
1272 foreach ( $achievements as $key => $achievement ) {
1273 if ( $achievement->post_type === $original_type ) {
1274 $achievements[ $key ]->post_type = $new_type;
1275 }
1276 }
1277
1278 }
1279
1280 return $achievements;
1281 }
1282
1283 /**
1284 * Redirect to include custom rename message.
1285 *
1286 * @since 1.0.0
1287 *
1288 * @param string $location Original URI.
1289 * @return string Updated URI.
1290 */
1291 function gamipress_achievement_type_rename_redirect( $location = '' ) {
1292
1293 remove_filter( 'redirect_post_location', __FUNCTION__, 99 );
1294
1295 return add_query_arg( 'message', 99, $location );
1296
1297 }
1298
1299 /**
1300 * Filter the "post updated" messages to include support for achievement types.
1301 *
1302 * @since 1.0.0
1303 *
1304 * @param array $messages Array of messages to display.
1305 *
1306 * @return array $messages Compiled list of messages.
1307 */
1308 function gamipress_achievement_type_update_messages( $messages ) {
1309
1310 $messages['achievement-type'] = array_fill( 1, 10, __( 'Achievement Type saved successfully.', 'gamipress' ) );
1311 $messages['achievement-type']['99'] = sprintf( __('Achievement Type renamed successfully. <p>All achievements of this type, and all active and earned user achievements, have been updated <strong>automatically</strong>.</p> All shortcodes, %s, and URIs that reference the old achievement type slug must be updated <strong>manually</strong>.', 'gamipress'), '<a href="' . esc_url( admin_url( 'widgets.php' ) ) . '">' . __( 'widgets', 'gamipress' ) . '</a>' );
1312
1313 return $messages;
1314
1315 }
1316 add_filter( 'post_updated_messages', 'gamipress_achievement_type_update_messages' );
1317
1318 /**
1319 * Log a user's achievements earns/awards
1320 *
1321 * @since 1.0.0
1322 * @updated 1.4.7 Added $trigger parameter
1323 *
1324 * @param int $user_id The user ID
1325 * @param int $achievement_id The associated achievement ID
1326 * @param int $admin_id An admin ID (if admin-awarded)
1327 * @param string $trigger The trigger that fires this function
1328 */
1329 function gamipress_log_user_achievement_award( $user_id, $achievement_id, $admin_id = 0, $trigger = '' ) {
1330
1331 $post_type = gamipress_get_post_type( $achievement_id );
1332
1333 $log_meta = array(
1334 'achievement_id' => $achievement_id,
1335 );
1336
1337 $access = 'public';
1338
1339 // Alter our log pattern if this was an admin action
1340 if ( $admin_id ) {
1341 $type = 'achievement_award';
1342 $access = 'private';
1343
1344 $log_meta['pattern'] = apply_filters( 'gamipress_achievement_awarded_log_pattern', __( '{admin} awarded {user} with the the {achievement} {achievement_type}', 'gamipress' ) );
1345 $log_meta['admin_id'] = $admin_id;
1346
1347 if( empty( $trigger ) ) {
1348 $trigger = 'gamipress_award_achievement';
1349 }
1350 } else {
1351 $type = 'achievement_earn';
1352
1353 if( in_array( $post_type, gamipress_get_requirement_types_slugs() ) ) {
1354 $log_meta['pattern'] = apply_filters( 'gamipress_requirement_complete_log_pattern', __( '{user} completed the {achievement_type} {achievement}', 'gamipress' ) );
1355 } else {
1356 $log_meta['pattern'] = apply_filters( 'gamipress_achievement_earned_log_pattern', __( '{user} unlocked the {achievement} {achievement_type}', 'gamipress' ) );
1357 }
1358
1359 if( empty( $trigger ) ) {
1360 $trigger = 'gamipress_unlock_achievement';
1361 }
1362 }
1363
1364 // Create the log entry
1365 gamipress_insert_log( $type, $user_id, $access, $trigger, $log_meta );
1366
1367 }
1368