Completed
Push — master ( 201c1b...d51ce1 )
by Dwain
04:57
created

Sensei_WC::is_woocommerce_present()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 15
rs 9.2
cc 4
eloc 6
nc 8
nop 0
1
<?php
2
if ( ! defined( 'ABSPATH' ) ) exit; // security check, don't load file outside WP
3
4
/**
5
 * Sensei WooCommerce class
6
 *
7
 * All functions needed to integrate Sensei and WooCommerce
8
 *
9
 * @package Sensei
10
 * @category WooCommerce
11
 * @since 1.9.0
12
 */
13
14
Class Sensei_WC{
15
    /**
16
     * Load the files needed for the woocommerce integration.
17
     *
18
     * @since 1.9.0
19
     */
20
    public static function load_woocommerce_integration_hooks(){
21
22
        require_once( __DIR__ . '/hooks/woocommerce.php' );
23
24
    }
25
    /**
26
     * check if WooCommerce plugin is loaded and allowed by Sensei
27
     *
28
     * @since 1.9.0
29
     * @return bool
30
     */
31
    public static function is_woocommerce_active(){
32
33
        $is_woocommerce_enabled_in_settings = isset( Sensei()->settings->settings['woocommerce_enabled'] ) && Sensei()->settings->settings['woocommerce_enabled'];
34
        return self::is_woocommerce_present() && $is_woocommerce_enabled_in_settings;
35
36
    } // end is_woocommerce_active
37
38
    /**
39
     * Checks if the WooCommerce plugin is installed and activation.
40
     *
41
     * If you need to check if WooCommerce is activated use Sensei_Utils::is_woocommerce_active().
42
     * This function does nott check to see if the Sensei setting for WooCommerce is enabled.
43
     *
44
     * @since 1.9.0
45
     *
46
     * @return bool
47
     */
48
    public static function is_woocommerce_present(){
49
50
        $active_plugins = (array) get_option( 'active_plugins', array() );
51
52
        if ( is_multisite() ){
53
54
            $active_plugins = array_merge( $active_plugins, get_site_option( 'active_sitewide_plugins', array() ) );
55
56
        }
57
58
        $is_woocommerce_plugin_present_and_activated = in_array( 'woocommerce/woocommerce.php', $active_plugins ) || array_key_exists( 'woocommerce/woocommerce.php', $active_plugins );
59
60
        return class_exists( 'Woocommerce' ) || $is_woocommerce_plugin_present_and_activated;
61
62
    }// end is_woocommerce_present
63
64
    /**
65
     * Find the order active number (completed or processing ) for a given user on a course. It will return the latest order.
66
     *
67
     * If multiple exist we will return the latest order.
68
     *
69
     * @param $user_id
70
     * @param $course_id
71
     * @return array $user_course_orders
72
     */
73
    public static function get_learner_course_active_order_id( $user_id, $course_id ){
74
75
        $course_product_id = get_post_meta( $course_id, '_course_woocommerce_product', true );
76
77
        $orders_query = new WP_Query( array(
78
            'post_type'   => 'shop_order',
79
            'posts_per_page' => -1,
80
            'post_status' => array( 'wc-processing', 'wc-completed' ),
81
            'meta_key'=> '_customer_user',
82
            'meta_value'=> $user_id,
83
        ) );
84
85
        if( $orders_query->post_count == 0 ){
86
87
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Sensei_WC::get_learner_course_active_order_id of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
88
89
        }
90
91
        foreach( $orders_query->get_posts() as $order ){
92
93
            $order = new WC_Order( $order->ID );
94
            $items = $order->get_items();
95
96
            $user_orders =  array();
0 ignored issues
show
Unused Code introduced by
$user_orders is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
97
98
            foreach( $items as $item ){
99
100
                // if the product id on the order and the one given to this function
101
                // this order has been placed by the given user on the given course.
102
                $product = wc_get_product( $item['product_id'] );
103
104
                if ( $product->is_type( 'variable' )) {
105
106
                    $item_product_id = $item['variation_id'];
107
108
                } else {
109
110
                    $item_product_id =  $item['product_id'];
111
112
                }
113
114
                if( $course_product_id == $item_product_id ){
115
116
                    return $order->id;
117
118
                }
119
120
121
            }//end for each order item
122
123
        } // end for each order
124
125
        // if we reach this place we found no order
126
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Sensei_WC::get_learner_course_active_order_id of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
127
128
    } // end get_learner_course_active_order_ids
129
130
    /**
131
     * Output WooCommerce specific course filters
132
     * Removing the paged argument
133
     *
134
     * @since 1.9.0
135
     * @param $filter_links
136
     * @return mixed
137
     */
138
    public static function add_course_archive_wc_filter_links( $filter_links ){
139
140
        $course_url = remove_query_arg('paged', Sensei_Utils::get_current_url() );
141
142
        $free_courses = self::get_free_courses();
143
        $paid_courses = self::get_paid_courses();
144
145
        if ( empty( $free_courses ) || empty( $paid_courses )  ){
146
            // do not show any WooCommerce filters if all courses are
147
            // free or if all courses are paid
148
            return $filter_links;
149
150
        }
151
152
        $filter_links[] = array(    'id'=>'paid' ,
153
                                    'url'=> add_query_arg('course_filter', 'paid', $course_url),
154
                                    'title'=>__( 'Paid', 'woothemes-sensei' )
155
        );
156
157
        $filter_links[] = array(    'id'=>'free',
158
                                    'url'=>add_query_arg('course_filter', 'free', $course_url),
159
                                    'title'=>__( 'Free', 'woothemes-sensei' )
160
        );
161
162
        return $filter_links;
163
164
    }// end add_course_archive_wc_filter_links
165
166
    /**
167
     * Apply the free filter the the course query
168
     * getting all course with no products or products with zero price
169
     *
170
     * hooked into pre_get_posts
171
     *
172
     * @since 1.9.0
173
     * @param WP_Query $query
174
     * @return WP_Query $query
175
     */
176
    public static function course_archive_wc_filter_free( $query ){
177
178
        if( isset( $_GET['course_filter'] ) && 'free' == $_GET['course_filter']
179
            && 'course' == $query->get( 'post_type') && $query->is_main_query()  ){
180
181
            // setup the course meta query
182
            $meta_query = self::get_free_courses_meta_query_args();
183
184
            // manipulate the query to return free courses
185
            $query->set('meta_query', $meta_query );
186
187
            // don't show any paid courses
188
            $courses = self::get_paid_courses();
189
            $ids = array();
190
            foreach( $courses as $course ){
191
                $ids[] = $course->ID;
192
            }
193
            $query->set( 'post__not_in', $ids );
194
195
        }// end if course_filter
196
197
        return $query;
198
199
    }// course_archive_wc_filter_free
200
201
    /**
202
     * Apply the paid filter to the course query on the courses page
203
     * will include all course with a product attached with a price
204
     * more than 0
205
     *
206
     * hooked into pre_get_posts
207
     *
208
     * @since 1.9.0
209
     * @param WP_Query $query
210
     * @return WP_Query $query
211
     */
212
    public static function course_archive_wc_filter_paid( $query ){
213
214
        if( isset( $_GET['course_filter'] ) && 'paid' == $_GET['course_filter']
215
            && 'course' == $query->get( 'post_type') && $query->is_main_query() ){
216
217
            // setup the course meta query
218
            $meta_query = self::get_paid_courses_meta_query_args();
219
220
            // manipulate the query to return free courses
221
            $query->set('meta_query', $meta_query );
222
223
        }
224
225
        return $query;
226
227
    }
228
229
    /**
230
     * Load the WooCommerce single product actions above
231
     * single courses if woocommerce is active allowing purchase
232
     * information and actions to be hooked from WooCommerce.
233
     */
234
    public static function do_single_course_wc_single_product_action(){
235
236
        /**
237
         * this hooks is documented within the WooCommerce plugin.
238
         */
239
        if ( Sensei_WC::is_woocommerce_active() ) {
240
241
            do_action( 'woocommerce_before_single_product' );
242
243
        } // End If Statement
244
245
    }// end do_single_course_wc_single_product_action
246
247
    /**
248
     * Hooking into the single lesson page to alter the
249
     * user access permissions based on if they have purchased the
250
     * course the lesson belongs to.
251
     *
252
     * This function will only return false or the passed in user_access value.
253
     * It doesn't return true in order to avoid altering other options.
254
     *
255
     * @since 1.9.0
256
     *
257
     * @param $can_user_view_lesson
258
     * @param $lesson_id
259
     * @param $user_id
260
     * @return bool
261
     */
262
    public static function alter_can_user_view_lesson ( $can_user_view_lesson, $lesson_id, $user_id  ){
263
264
        // check if the course has a valid product attached to it
265
        // which the user should have purchased if they want to access
266
        // the current lesson
267
        $course_id = get_post_meta( $lesson_id , '_lesson_course', true);
268
        $wc_post_id = get_post_meta( $course_id, '_course_woocommerce_product', true );
269
        $product = Sensei()->sensei_get_woocommerce_product_object($wc_post_id);
270
        if( isset ($product) && is_object($product) ){
271
272
            // valid product found
273
            $order_id = self::get_learner_course_active_order_id( $user_id, $course_id );
274
275
            // product has a successful order so this user may access the content
276
            // this function may only return false or the default
277
            // returning true may override other negatives which we don't want
278
            if( ! $order_id ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $order_id of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
279
280
                return false;
281
282
            }
283
284
        }
285
286
        // return the passed in value
287
        return $can_user_view_lesson;
288
289
    }
290
291
    /**
292
     * Add course link to order thank you and details pages.
293
     *
294
     * @since  1.4.5
295
     * @access public
296
     *
297
     * @return void
298
     */
299
    public static function course_link_from_order( ) {
300
301
        if( ! is_order_received_page() ){
302
            return;
303
        }
304
305
        $order_id = get_query_var( 'order-received' );
306
		$order = new WC_Order( $order_id );
307
308
		// exit early if not wc-completed or wc-processing
309
		if( 'wc-completed' != $order->post_status
310
            && 'wc-processing' != $order->post_status  ) {
311
            return;
312
        }
313
314
        $course_links = array(); // store the for links for courses purchased
315
		foreach ( $order->get_items() as $item ) {
316
317
            if ( isset( $item['variation_id'] ) && ( 0 < $item['variation_id'] ) ) {
318
319
                // If item has variation_id then its a variation of the product
320
                $item_id = $item['variation_id'];
321
322
            } else {
323
324
                //If not its real product set its id to item_id
325
                $item_id = $item['product_id'];
326
327
            } // End If Statement
328
329
            $user_id = get_post_meta( $order->id, '_customer_user', true );
330
331
            if( $user_id ) {
332
333
                // Get all courses for product
334
                $args = Sensei_Course::get_default_query_args();
335
                $args['meta_query'] = array( array(
336
                            'key' => '_course_woocommerce_product',
337
                            'value' => $item_id
338
                        ) );
339
                $args['orderby'] = 'menu_order date';
340
                $args['order'] = 'ASC';
341
342
                // loop through courses
343
                $courses = get_posts( $args );
344
                if( $courses && count( $courses ) > 0 ) {
345
346
                    foreach( $courses as $course ) {
347
348
                        $title = $course->post_title;
349
                        $permalink = get_permalink( $course->ID );
350
                        $course_links[] .= '<a href="' . esc_url( $permalink ) . '" >' . $title . '</a> ';
351
352
                    } // end for each
353
354
                    // close the message div
355
356
                }// end if $courses check
357
            }
358
        }// end loop through orders
359
360
        // add the courses to the WooCommerce notice
361
        if( ! empty( $course_links) ){
362
363
            $courses_html = _nx(
364
                'You have purchased the following course:',
365
                'You have purchased the following courses:',
366
                count( $course_links ),
367
                'Purchase thank you note on Checkout page. The course link(s) will be show', 'woothemes-sensei'
368
            );
369
370
            foreach( $course_links as $link ){
371
372
                $courses_html .= '<li>' . $link . '</li>';
373
374
            }
375
376
            $courses_html .= ' </ul>';
377
378
            wc_add_notice( $courses_html, 'success' );
379
        }
380
381
	} // end course_link_order_form
382
383
    /**
384
     * Show the message that a user should complete
385
     * their purchase if the course is in the cart
386
     *
387
     * This should be used within the course loop or single course page
388
     *
389
     * @since 1.9.0
390
     */
391
    public static function course_in_cart_message(){
392
393
        global $post;
394
395
        if( self::is_course_in_cart( $post->ID ) ){ ?>
396
397
            <div class="sensei-message info">'
398
                <?php
399
400
                $cart_link =  '<a class="cart-complete" href="' . WC()->cart->get_checkout_url()
401
                              . '" title="' . __('complete purchase', 'woothemes-sensei') . '">'
402
                              . __('complete the purchase', 'woothemes-sensei') . '</a>';
403
404
                sprintf(  __('You have already added this Course to your cart. Please %1$s to access the course.', 'woothemes-sensei'), $cart_link );
405
406
                ?>
407
            </div>
408
        <?php }
409
410
    } // End sensei_woocommerce_in_cart_message()
411
412
    /**
413
     * Checks the cart to see if a course is in the cart.
414
     *
415
     * @param $course_id
416
     * @return bool
417
     */
418
    public static function is_course_in_cart( $course_id ){
419
420
        $wc_post_id = absint( get_post_meta( $course_id, '_course_woocommerce_product', true ) );
421
        $user_course_status_id = Sensei_Utils::user_started_course( $course_id , get_current_user_id() );
422
423
        if ( 0 < intval( $wc_post_id ) && ! $user_course_status_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user_course_status_id of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
424
425
            if ( self::is_product_in_cart( $wc_post_id ) ) {
426
427
                return true;
428
429
            }
430
431
        }
432
433
        return false;
434
435
    }// is_course_in_cart
436
437
    /**
438
     * Check the cart to see if the product is in the cart
439
     *
440
     * @param $product_id
441
     * @return bool
442
     */
443
    public static function is_product_in_cart( $product_id ){
444
445
        if ( 0 < $product_id ) {
446
447
            $product = wc_get_product( $product_id );
448
449
            $parent_id = '';
0 ignored issues
show
Unused Code introduced by
$parent_id is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
450
            if( isset( $product->variation_id ) && 0 < intval( $product->variation_id ) ) {
451
                $wc_product_id = $product->parent->id;
0 ignored issues
show
Unused Code introduced by
$wc_product_id is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
452
            }
453
            foreach( WC()->cart->get_cart() as $cart_item_key => $values ) {
454
455
                $cart_product = $values['data'];
456
                if( $product_id == $cart_product->id ) {
457
458
                    return true;
459
460
                }
461
462
            }
463
        } // End If Statement
464
465
        return false;
466
467
    } // end is_product_in_car
468
469
    /**
470
     * Get all free WooCommerce products
471
     *
472
     * @since 1.9.0
473
     *
474
     * @return array $free_products{
475
     *  @type int $wp_post_id
476
     * }
477
     */
478
    public static function get_free_product_ids(){
479
480
        return  get_posts( array(
481
            'post_type' => 'product',
482
            'posts_per_page' => '1000',
483
            'fields' => 'ids',
484
            'meta_query'=> array(
485
                'relation' => 'OR',
486
                array(
487
                    'key'=> '_regular_price',
488
                    'value' => 0,
489
                ),
490
                array(
491
                    'key'=> '_sale_price',
492
                    'value' => 0,
493
                ),
494
            ),
495
        ));
496
497
    }// end get free product query
498
499
    /**
500
     * The metat query for courses that are free
501
     *
502
     * @since 1.9.0
503
     * @return array $wp_meta_query_param
504
     */
505
    public static function get_free_courses_meta_query_args(){
506
507
        return array(
508
            'relation' => 'OR',
509
            array(
510
                'key'     => '_course_woocommerce_product',
511
                'value' => '-',
512
                'compare' => '=',
513
            ),
514
            array(
515
                'key'     => '_course_woocommerce_product',
516
                'value' => self::get_free_product_ids(),
517
                'compare' => 'IN',
518
            ),
519
        );
520
521
    }// get_free_courses_meta_query
522
523
    /**
524
     * The metat query for courses that are free
525
     *
526
     * @since 1.9.0
527
     * @return array $wp_query_meta_query_args_param
528
     */
529
    public static function get_paid_courses_meta_query_args(){
530
531
        $paid_product_ids = self::get_paid_product_ids();
532
533
        return array(
534
            array(
535
                'key'     => '_course_woocommerce_product',
536
                // when empty we give a false post_id to ensure the caller doesn't get any courses for their
537
                // query
538
                'value' => empty( $paid_product_ids  )? '-1000' : $paid_product_ids,
539
                'compare' => 'IN',
540
            ),
541
        );
542
543
    }// get_free_courses_meta_query
544
545
    /**
546
     * The WordPress Query args
547
     * for paid products on sale
548
     *
549
     * @since 1.9.0
550
     * @return array $product_query_args
551
     */
552 View Code Duplication
    public static function get_paid_products_on_sale_query_args(){
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
553
554
        $args = array(
555
                   'post_type' 		=> 'product',
556
                   'posts_per_page' 		=> 1000,
557
                   'orderby'         	=> 'date',
558
                   'order'           	=> 'DESC',
559
                   'suppress_filters' 	=> 0
560
        );
561
562
        $args[ 'fields' ]     = 'ids';
563
564
        $args[ 'meta_query' ] = array(
565
            'relation' => 'AND',
566
            array(
567
                'key'=> '_regular_price',
568
                'compare' => '>',
569
                'value' => 0,
570
            ),
571
            array(
572
                'key'=> '_sale_price',
573
                'compare' => '>',
574
                'value' => 0,
575
            ),
576
        );
577
578
        return $args;
579
580
    } // get_paid_products_on_sale_query_args
581
582
583
    /**
584
     * Return the WordPress query args for
585
     * products not on sale but that is not a free
586
     *
587
     * @since 1.9.0
588
     *
589
     * @return array
590
     */
591 View Code Duplication
    public static function get_paid_products_not_on_sale_query_args(){
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
592
593
        $args = array(
594
            'post_type' 		=> 'product',
595
            'posts_per_page' 		=> 1000,
596
            'orderby'         	=> 'date',
597
            'order'           	=> 'DESC',
598
            'suppress_filters' 	=> 0
599
        );
600
601
        $args[ 'fields' ]     = 'ids';
602
        $args[ 'meta_query' ] = array(
603
            'relation' => 'AND',
604
            array(
605
                'key'=> '_regular_price',
606
                'compare' => '>',
607
                'value' => 0,
608
            ),
609
            array(
610
                'key'=> '_sale_price',
611
                'compare' => '=',
612
                'value' => '',
613
            ),
614
        );
615
616
        return $args;
617
618
619
    } // get_paid_courses_meta_query
620
621
    /**
622
     * Get all WooCommerce non-free product id's
623
     *
624
     * @since 1.9.0
625
     *
626
     * @return array $woocommerce_paid_product_ids
627
     */
628
    public static function get_paid_product_ids(){
629
630
        // get all the paid WooCommerce products that has regular
631
        // and sale price greater than 0
632
        // will be used later to check for course with the id as meta
633
        $paid_product_ids_with_sale =  get_posts( self::get_paid_products_on_sale_query_args() );
634
635
        // get all the paid WooCommerce products that has regular price
636
        // greater than 0 without a sale price
637
        // will be used later to check for course with the id as meta
638
        $paid_product_ids_without_sale = get_posts( self::get_paid_products_not_on_sale_query_args() );
639
640
        // combine products ID's with regular and sale price grater than zero and those without
641
        // sale but regular price greater than zero
642
        $woocommerce_paid_product_ids = array_merge( $paid_product_ids_with_sale, $paid_product_ids_without_sale );
643
644
        // if
645
        if( empty($woocommerce_paid_product_ids) ){
646
            return array( );
647
        }
648
        return $woocommerce_paid_product_ids;
649
650
    }
651
652
    /**
653
     * Get all free courses.
654
     *
655
     * This course that have a WC product attached
656
     * that has a price or sale price of zero and
657
     * other courses with no WooCommerce products
658
     * attached.
659
     *
660
     * @since 1.9.0
661
     *
662
     * @return array
663
     */
664
    public static function get_free_courses(){
665
666
        $free_course_query_args = Sensei_Course::get_default_query_args();
667
        $free_course_query_args[ 'meta_query' ] = self::get_free_courses_meta_query_args();
668
669
        // don't show any paid courses
670
        $courses = self::get_paid_courses();
671
        $ids = array();
672
        foreach( $courses as $course ){
673
            $ids[] = $course->ID;
674
        }
675
        $free_course_query_args[ 'post__not_in' ] =  $ids;
676
677
        return get_posts( $free_course_query_args );
678
679
    }
680
681
    /**
682
     * Return all products that are not free
683
     *
684
     * @since 1.9.0
685
     * @return array
686
     */
687
    public static function get_paid_courses(){
688
689
        $paid_course_query_args = Sensei_Course::get_default_query_args();
690
691
        $paid_course_query_args[ 'meta_query' ] = self::get_paid_courses_meta_query_args();
692
693
        return get_posts(  $paid_course_query_args );
694
    }
695
696
    /**
697
     * Show the WooCommerce add to cart button for the  current course
698
     *
699
     * The function will only show the button if
700
     * 1- the user can buy the course
701
     * 2- if they have completed their pre-requisite
702
     * 3- if the course has a valid product attached
703
     *
704
     * @since 1.9.0
705
     * @param int $course_id
706
     * @return string $html markup for the button or nothing if user not allowed to buy
707
     */
708
    public static function the_add_to_cart_button_html( $course_id ){
709
710
        if ( ! Sensei_Course::is_prerequisite_complete( $course_id )) {
711
            return '';
712
        }
713
714
        $wc_post_id = self::get_course_product_id( $course_id );
715
716
        // Check if customer purchased the product
717
        if ( self::has_customer_bought_product(  get_current_user_id(), $wc_post_id )
718
            || empty( $wc_post_id ) ) {
719
720
            return '';
721
722
        }
723
724
        // based on simple.php in WC templates/single-product/add-to-cart/
725
        // Get the product
726
        $product = Sensei()->sensei_get_woocommerce_product_object( $wc_post_id );
727
728
        // do not show the button for invalid products, non purchasable products, out
729
        // of stock product or if course is already in cart
730
        if ( ! isset ( $product )
731
            || ! is_object( $product )
732
            || ! $product->is_purchasable()
733
            || ! $product->is_in_stock()
734
            || self::is_course_in_cart( $wc_post_id ) ) {
735
736
            return '';
737
738
        }
739
740
        //
741
        // button  output:
742
        //
743
        ?>
744
745
        <form action="<?php echo esc_url( $product->add_to_cart_url() ); ?>"
746
              class="cart"
747
              method="post"
748
              enctype="multipart/form-data">
749
750
            <input type="hidden" name="product_id" value="<?php echo esc_attr( $product->id ); ?>" />
751
752
            <input type="hidden" name="quantity" value="1" />
753
754
            <?php if ( isset( $product->variation_id ) && 0 < intval( $product->variation_id ) ) { ?>
755
756
                <input type="hidden" name="variation_id" value="<?php echo $product->variation_id; ?>" />
757
                <?php if( isset( $product->variation_data ) && is_array( $product->variation_data ) && count( $product->variation_data ) > 0 ) { ?>
758
759
                    <?php foreach( $product->variation_data as $att => $val ) { ?>
760
761
                        <input type="hidden" name="<?php echo esc_attr( $att ); ?>" id="<?php echo esc_attr( str_replace( 'attribute_', '', $att ) ); ?>" value="<?php echo esc_attr( $val ); ?>" />
762
763
                    <?php } ?>
764
765
                <?php } ?>
766
767
            <?php } ?>
768
769
            <button type="submit" class="single_add_to_cart_button button alt">
770
                <?php echo $product->get_price_html(); ?> - <?php  _e('Purchase this Course', 'woothemes-sensei'); ?>
771
            </button>
772
773
        </form>
774
775
        <?php
776
    } // end the_add_to_cart_button_html
777
778
    /**
779
     * Alter the no permissions message on the single course page
780
     * Changes the message to a WooCommerce specific message.
781
     *
782
     * @since 1.9.0
783
     *
784
     * @param $message
785
     * @param $post_id
786
     *
787
     * @return string $message
788
     */
789
    public static function alter_no_permissions_message( $message, $post_id ){
790
791
        if( empty( $post_id ) || 'course'!=get_post_type( $post_id ) ){
792
            return  $message;
793
        }
794
795
        $product_id = self::get_course_product_id( $post_id );
796
797
        if( ! $product_id
798
            || self::has_customer_bought_product( get_current_user_id(),$product_id ) ){
799
800
            return $message;
801
802
        }
803
804
        ob_start();
805
        self::the_course_no_permissions_message( $post_id );
806
        $woocommerce_course_no_permissions_message = ob_get_clean();
807
808
        return $woocommerce_course_no_permissions_message ;
809
810
    }
811
    /**
812
     * Show the no permissions message when a user is logged in
813
     * and have not yet purchased the current course
814
     *
815
     * @since 1.9.0
816
     */
817
    public static function the_course_no_permissions_message( $course_id ){
0 ignored issues
show
Unused Code introduced by
The parameter $course_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
818
819
        // login link
820
        $my_courses_page_id = intval( Sensei()->settings->settings[ 'my_course_page' ] );
821
        $login_link =  '<a href="' . esc_url( get_permalink( $my_courses_page_id ) ) . '">' . __( 'log in', 'woothemes-sensei' ) . '</a>';
822
823
        ?>
824
825
        <span class="add-to-cart-login">
826
            <?php echo sprintf( __( 'Or %1$s to access your purchased courses', 'woothemes-sensei' ), $login_link ); ?>
827
        </span>
828
829
    <?php }
830
831
    /**
832
     * Checks if a user has bought a product item.
833
     *
834
     * @since  1.9.0
835
     *
836
     * @param  int $user_id
837
     * @param  int $product_id
838
     *
839
     * @return bool
840
     */
841
    public static function has_customer_bought_product ( $user_id, $product_id ){
842
843
        $orders = get_posts( array(
844
            'posts_per_page' => -1,
845
            'meta_key'    => '_customer_user',
846
            'meta_value'  => intval( $user_id ),
847
            'post_type'   => 'shop_order',
848
            'post_status' =>  array( 'wc-processing', 'wc-completed' ),
849
        ) );
850
851
        foreach ( $orders as $order_id ) {
852
853
            $order = new WC_Order( $order_id->ID );
854
855
            if ( $order->post_status != 'wc-completed' && $order->post_status != 'wc-processing' ) {
856
857
                continue;
858
            }
859
860
            if ( ! ( 0 < sizeof( $order->get_items() ) ) ) {
861
862
                continue;
863
864
            }
865
866
            foreach( $order->get_items() as $item ) {
867
868
                // Check if user has bought product
869
                if ( $item['product_id'] == $product_id || $item['variation_id'] == $product_id ) {
870
871
                    // Check if user has an active subscription for product
872
                    if( class_exists( 'WC_Subscriptions_Manager' ) ) {
873
                        $sub_key = WC_Subscriptions_Manager::get_subscription_key( $order_id->ID, $product_id );
874
                        if( $sub_key ) {
875
                            $sub = WC_Subscriptions_Manager::get_subscription( $sub_key );
876
                            if( $sub && isset( $sub['status'] ) ) {
877
                                if( 'active' == $sub['status'] ) {
878
                                    return true;
879
                                } else {
880
                                    return false;
881
                                }
882
                            }
883
                        }
884
                    }
885
886
                    // Customer has bought product
887
                    return true;
888
                } // End If Statement
889
890
            } // End For each item
891
892
        } // End For each order
893
894
    } // end has customer bought product
895
896
    /**
897
     * Return the product id for the given course
898
     *
899
     * @since 1.9.0
900
     *
901
     * @param int $course_id
902
     *
903
     * @return string $woocommerce_product_id or false if none exist
904
     *
905
     */
906
    public static function get_course_product_id( $course_id ){
907
908
        $product_id =  get_post_meta( $course_id, '_course_woocommerce_product', true );
909
910
        if( empty( $product_id ) || 'product' != get_post_type( $product_id ) ){
911
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Sensei_WC::get_course_product_id of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
912
        }
913
914
        return $product_id;
915
916
    }
917
918
    /**
919
     * Alter the body classes adding WooCommerce to the body
920
     *
921
     * @param array $classes
922
     * @return array
923
     */
924
    public static function add_woocommerce_body_class( $classes ){
925
926
        if( ! in_array( 'woocommerce', $classes ) ){
927
928
            $classes[] ='woocommerce';
929
930
        }
931
932
933
        return $classes;
934
935
    }
936
937
}// end Sensei_WC
938