PluginProbe
GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI / 7.9.8
GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI v7.9.8
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 7.8.7 All 46 releases
gamipress / includes / functions / achievements.php

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

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