Issues (896)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/class-sensei-updates.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 13 and the first side effect is on line 2.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
3
4
/**
5
 * Sensei Updates Class
6
 *
7
 * Class that contains the updates for Sensei data and structures.
8
 *
9
 * @package Core
10
 * @author Automattic
11
 * @since 1.1.0
12
 */
13
class Sensei_Updates
14
{
15
    public $token = 'woothemes-sensei';
16
    public $version;
17
    public $updates_run;
18
    public $updates;
19
    private $parent;
20
21
    /**
22
     * Constructor.
23
     *
24
     * @access  public
25
     * @since   1.1.0
26
     * @param   string $parent The main Sensei object by Ref.
27
     * @return  void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
28
     */
29
    public function __construct($parent)
30
    {
31
32
		// Setup object data
33
		$this->parent = $parent;
34
		$this->updates_run = get_option( 'woothemes-sensei-upgrades', array() );
35
36
        // The list of upgrades to run
37
        $this->updates = array('1.1.0' => array('auto' => array('assign_role_caps' => array('title' => __('Assign role capabilities', 'woothemes-sensei'), 'desc' => __('Assigns Sensei capabilites to the relevant user roles.', 'woothemes-sensei'), 'product' => 'Sensei')),
38
            'manual' => array()
39
        ),
40
            '1.3.0' => array('auto' => array('set_default_quiz_grade_type' => array('title' => __('Set default quiz grade type', 'woothemes-sensei'), 'desc' => __('Sets all quizzes to the default \'auto\' grade type.', 'woothemes-sensei')),
41
                'set_default_question_type' => array('title' => __('Set default question type', 'woothemes-sensei'), 'desc' => __('Sets all questions to the default \'multiple choice\' type.', 'woothemes-sensei'))
42
            ),
43
                'manual' => array('update_question_answer_data' => array('title' => __('Update question answer data', 'woothemes-sensei'), 'desc' => __('Updates questions to use the new question types structure.', 'woothemes-sensei')))
44
            ),
45
            '1.4.0' => array('auto' => array('update_question_grade_points' => array('title' => __('Update question grade points', 'woothemes-sensei'), 'desc' => __('Sets all question grade points to the default value of \'1\'.', 'woothemes-sensei'))),
46
                'manual' => array()
47
            ),
48
            '1.5.0' => array('auto' => array('convert_essay_paste_questions' => array('title' => __('Convert essay paste questions into multi-line questions', 'woothemes-sensei'), 'desc' => __('Converts all essay paste questions into multi-line questions as the essay paste question type was removed in v1.5.0.', 'woothemes-sensei'))),
49
                'manual' => array('set_random_question_order' => array('title' => __('Set all quizzes to have a random question order', 'woothemes-sensei'), 'desc' => __('Sets the order all of questions in all quizzes to a random order, which can be switched off per quiz.', 'woothemes-sensei')),
50
                    'set_default_show_question_count' => array('title' => __('Set all quizzes to show all questions', 'woothemes-sensei'), 'desc' => __('Sets all quizzes to show all questions - this can be changed per quiz.', 'woothemes-sensei')),
51
                    'remove_deleted_user_activity' => array('title' => __('Remove Sensei activity for deleted users', 'woothemes-sensei'), 'desc' => __('Removes all course, lesson &amp; quiz activity for users that have already been deleted from the database. This will fix incorrect learner counts in the Analysis section.', 'woothemes-sensei')))
52
            ),
53
            '1.6.0' => array('auto' => array('add_teacher_role' => array('title' => __('Add \'Teacher\' role', 'woothemes-sensei'), 'desc' => __('Adds a \'Teacher\' role to your WordPress site that will allow users to mange the Grading and Analysis pages.', 'woothemes-sensei')),
54
                'add_sensei_caps' => array('title' => __('Add administrator capabilities', 'woothemes-sensei'), 'desc' => __('Adds the \'manage_sensei\' and \'manage_sensei_grades\' capabilities to the Administrator role.', 'woothemes-sensei')),
55
                'restructure_question_meta' => array('title' => __('Restructure question meta data', 'woothemes-sensei'), 'desc' => __('Restructures the question meta data as it relates to quizzes - this accounts for changes in the data structure in v1.6+.', 'woothemes-sensei')),
56
                'update_quiz_settings' => array('title' => __('Add new quiz settings', 'woothemes-sensei'), 'desc' => __('Adds new settings to quizzes that were previously registered as global settings.', 'woothemes-sensei')),
57
                'reset_lesson_order_meta' => array('title' => __('Set default order of lessons', 'woothemes-sensei'), 'desc' => __('Adds data to lessons to ensure that they show up on the \'Order Lessons\' screen - if this update has been run once before then it will reset all lessons to the default order.', 'woothemes-sensei')),),
58
                'manual' => array()
59
            ),
60
            '1.7.0' => array('auto' => array('add_editor_caps' => array('title' => __('Add Editor capabilities', 'woothemes-sensei'), 'desc' => __('Adds the \'manage_sensei_grades\' capability to the Editor role.', 'woothemes-sensei')),),
61
                'forced' => array('update_question_gap_fill_separators' => array('title' => __('Update Gap Fill questions', 'woothemes-sensei'), 'desc' => __('Updates the format of gap fill questions to allow auto grading and greater flexibility in matching.', 'woothemes-sensei')),
62
                    'update_quiz_lesson_relationship' => array('title' => __('Restructure quiz lesson relationship', 'woothemes-sensei'), 'desc' => __('Adds data to quizzes and lessons to ensure that they maintain their 1 to 1 relationship.', 'woothemes-sensei')),
63
                    'status_changes_fix_lessons' => array('title' => __('Update lesson statuses', 'woothemes-sensei'), 'desc' => __('Update existing lesson statuses.', 'woothemes-sensei')),
64
                    'status_changes_convert_lessons' => array('title' => __('Convert lesson statuses', 'woothemes-sensei'), 'desc' => __('Convert to new lesson statuses.', 'woothemes-sensei')),
65
                    'status_changes_convert_courses' => array('title' => __('Convert course statuses', 'woothemes-sensei'), 'desc' => __('Convert to new course statuses.', 'woothemes-sensei')),
66
                    'status_changes_convert_questions' => array('title' => __('Convert question statuses', 'woothemes-sensei'), 'desc' => __('Convert to new question statuses.', 'woothemes-sensei')),
67
                    'update_legacy_sensei_comments_status' => array('title' => __('Convert legacy Sensei activity types', 'woothemes-sensei'), 'desc' => __('Convert all legacy Sensei activity types such as \'sensei_lesson_start\' and \'sensei_user_answer\' to new status format.', 'woothemes-sensei')),
68
                    'update_comment_course_lesson_comment_counts' => array('title' => __('Update comment counts', 'woothemes-sensei'), 'desc' => __('Update comment counts on Courses and Lessons due to status changes.', 'woothemes-sensei')),),
69
            ),
70
            '1.7.2' => array('auto' => array('index_comment_status_field' => array('title' => __('Add database index to comment statuses', 'woothemes-sensei'), 'desc' => __('This indexes the comment statuses in the database, which will speed up all Sensei activity queries.', 'woothemes-sensei')),),
71
                /*'manual' 		=> array( 'remove_legacy_comments' => array( 'title' => __( 'Remove legacy Sensei activity types', 'woothemes-sensei' ), 'desc' => __( 'This removes all legacy (pre-1.7) Sensei activity types - only run this update once the update to v1.7 is complete and everything is stable.', 'woothemes-sensei' ) ) )*/
72
            ),
73
            '1.8.0' => array('auto' => array('enhance_teacher_role' => array('title' => 'Enhance the \'Teacher\' role', 'desc' => 'Adds the ability for a \'Teacher\' to create courses, lessons , quizes and manage their learners.'),),
74
                'manual' => array()
75
            ),
76
        );
77
78
		$this->updates = apply_filters( 'sensei_upgrade_functions', $this->updates, $this->updates );
79
		$this->version = get_option( 'woothemes-sensei-version' );
80
81
        // Manual Update Screen
82
        add_action('admin_menu', array($this, 'add_update_admin_screen'), 50);
83
84
    } // End __construct()
85
86
    /**
87
     * add_update_admin_screen Adds admin screen to run manual udpates
88
     *
89
     * @access public
90
     * @since  1.3.7
91
     * @return void
92
     */
93
    public function add_update_admin_screen()
94
    {
95
        if (current_user_can('manage_options')) {
96
            add_submenu_page('sensei', __('Sensei Updates', 'woothemes-sensei'), __('Data Updates', 'woothemes-sensei'), 'manage_options', 'sensei_updates', array($this, 'sensei_updates_page'));
97
        }
98
    } // End add_update_admin_screen()
99
100
    /**
101
     * sensei_updates_page HTML output for manual update screen
102
     *
103
     * @access public
104
     * @since  1.3.7
105
     * @return void
106
     */
107
    public function sensei_updates_page() {
108
109
        // Only allow admins to load this page and run the update functions
110
        if ( ! current_user_can('manage_options')) {
111
112
            return;
113
114
        }
115
        ?>
116
117
        <div class="wrap">
118
119
        <div id="icon-woothemes-sensei" class="icon32"><br></div>
120
        <h2><?php _e('Sensei Updates', 'woothemes-sensei'); ?></h2>
121
122
        <?php
123
        $function_name= '';
124
        if ( isset($_GET['action']) && $_GET['action'] == 'update'
125
            && isset($_GET['n']) && intval($_GET['n']) >= 0
126
            && ( (isset($_POST['checked'][0]) && '' != $_POST['checked'][0]) || (isset($_GET['functions']) && '' != $_GET['functions']))) {
127
128
            // Setup the data variables
129
            $n = intval($_GET['n']);
130
            $functions_list = '';
131
            $done_processing = false;
132
133
            // Check for updates to run
134
            if (isset($_POST['checked'][0]) && '' != $_POST['checked'][0]) {
135
136 View Code Duplication
                foreach ($_POST['checked'] as $key => $function_name) {
137
138
                    if( ! isset(  $_POST[ $function_name.'_nonce_field' ] ) 
139
                        || ! wp_verify_nonce( $_POST[ $function_name.'_nonce_field' ] , 'run_'.$function_name ) ){
140
141
                        wp_die(
142
                            '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
143
                            '<p>' . __( 'The nonce supplied in order to run this update function is invalid','woothemes-sensei') . '</p>',
144
                            403
145
                        );
146
147
                    }
148
149
                    // Dynamic function call
150
                    if (method_exists($this, $function_name)) {
151
152
                        $done_processing = call_user_func_array(array($this, $function_name), array(50, $n));
153
154
                    } elseif ($this->function_in_whitelist($function_name)) {
155
156
                        $done_processing = call_user_func_array($function_name, array(50, $n));
157
158
                    } else {
159
160
                        _doing_it_wrong( esc_html( $function_name) , 'Is not a valid Sensei updater function', 'Sensei 1.9.0');
161
                        return;
162
163
                    }// End If Statement
164
165
                // Add to functions list get args
166
                if ('' == $functions_list) {
167
                    $functions_list .= $function_name;
168
                } else {
169
                    $functions_list .= '+' . $function_name;
170
                } // End If Statement
171
172
                // Mark update has having been run
173
                $this->set_update_run($function_name);
174
175
            } // End For Loop
176
177
        } // End If Statement
178
179
        // Check for updates to run
180
        if (isset($_GET['functions']) && '' != $_GET['functions']) {
181
182
            // Existing functions from GET variables instead of POST
183
            $functions_array = $_GET['functions'];
184
185 View Code Duplication
            foreach ($functions_array as $key => $function_name) {
186
187
                if( ! isset( $_GET[ $function_name.'_nonce' ] )
188
                    || ! wp_verify_nonce( $_GET[ $function_name.'_nonce' ] , 'run_'.$function_name ) ){
189
190
                    wp_die(
191
                        '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
192
                        '<p>' . __( 'The nonce supplied in order to run this update function is invalid','woothemes-sensei') . '</p>',
193
                        403
194
                    );
195
196
                }
197
198
                // Dynamic function call
199
                if (method_exists($this, $function_name)) {
200
201
                    $done_processing = call_user_func_array(array($this, $function_name), array(50, $n));
202
203
                } elseif ($this->function_in_whitelist($function_name)) {
204
205
                    $done_processing = call_user_func_array($function_name, array(50, $n));
206
207
                } else {
208
209
                    _doing_it_wrong( esc_html( $function_name) , 'Is not a valid Sensei updater function', 'Sensei 1.9.0');
210
                    return;
211
212
                } // End If Statement
213
214
                // Add to functions list get args
215
                if ('' == $functions_list) {
216
                    $functions_list .= $function_name;
217
                } else {
218
                    $functions_list .= '+' . $function_name;
219
                } // End If Statement
220
221
                $this->set_update_run($function_name);
222
223
            } // End For Loop
224
225
        } // End If Statement
226
227
        if (!$done_processing) { ?>
228
229
            <h3><?php _e('Processing Updates...', 'woothemes-sensei'); ?></h3>
230
231
            <p>
232
233
                <?php _e( "If your browser doesn't start loading the next page automatically, click this button:", 'woothemes-sensei' ); ?>
234
235
                <?php
236
                $next_action_url = add_query_arg( array(
237
                    'page' => 'sensei_updates',
238
                    'action' => 'update',
239
                    'n' => $n + 50,
240
                    'functions' => array( $functions_list ),
241
                    $function_name.'_nonce' => wp_create_nonce( 'run_'. $function_name ),
242
                ), admin_url( 'admin.php' ) );
243
                ?>
244
245
                <a class="button"  href="<?php echo esc_url( $next_action_url ); ?>">
246
247
                    <?php _e( 'Next', 'woothemes-sensei' ); ?>
248
249
                </a>
250
251
            </p>
252
            <script type='text/javascript'>
253
                <!--
254
                function js_sensei_nextpage() {
255
                    location.href = "<?php echo esc_url_raw(  $next_action_url );?>";
256
                }
257
                setTimeout( "js_sensei_nextpage()", 250 );
258
                //-->
259
            </script>
260
261
        <?php  } else { ?>
262
263
            <p><strong><?php _e('Update completed successfully!', 'woothemes-sensei'); ?></strong></p>
264
            <p>
265
                <a href="<?php echo admin_url('edit.php?post_type=lesson'); ?>"><?php _e('Create a new lesson', 'woothemes-sensei'); ?></a>
266
                or <a
267
                    href="<?php echo admin_url('admin.php?page=sensei_updates'); ?>"><?php _e('run some more updates', 'woothemes-sensei'); ?></a>.
268
            </p>
269
270
        <?php } // End If Statement
271
272
        } else { ?>
273
274
            <h3><?php _e('Updates', 'woothemes-sensei'); ?></h3>
275
            <p><?php printf(__('These are updates that have been made available as new Sensei versions have been released. Updates of type %1$sAuto%2$s will run as you update Sensei to the relevant version - other updates need to be run manually and you can do that here.', 'woothemes-sensei'), '<code>', '</code>'); ?></p>
276
277
            <div class="updated"><p>
278
                    <strong><?php _e('Only run these updates if you have been instructed to do so by WooThemes support staff.', 'woothemes-sensei'); ?></strong>
279
                </p></div>
280
281
            <table class="widefat" cellspacing="0" id="update-plugins-table">
282
283
                <thead>
284
                <tr>
285
                    <th scope="col" class="manage-column"><?php _e('Update', 'woothemes-sensei'); ?></th>
286
                    <th scope="col" class="manage-column"><?php _e('Type', 'woothemes-sensei'); ?></th>
287
                    <th scope="col" class="manage-column"><?php _e('Action', 'woothemes-sensei'); ?></th>
288
                </tr>
289
                </thead>
290
291
                <tfoot>
292
                <tr>
293
                    <th scope="col" class="manage-column"><?php _e('Update', 'woothemes-sensei'); ?></th>
294
                    <th scope="col" class="manage-column"><?php _e('Type', 'woothemes-sensei'); ?></th>
295
                    <th scope="col" class="manage-column"><?php _e('Action', 'woothemes-sensei'); ?></th>
296
                </tr>
297
                </tfoot>
298
299
                <tbody class="updates">
300
                <?php
301
                // Sort updates with the latest at the top
302
                uksort($this->updates, array($this, 'sort_updates'));
303
                $this->updates = array_reverse($this->updates, true);
304
                $class = 'alternate';
305
                foreach ($this->updates as $version => $version_updates) {
306
                    foreach ($version_updates as $type => $updates) {
307
                        foreach ($updates as $update => $data) {
308
                            $update_run = $this->has_update_run($update);
309
                            $product = 'Sensei';
310
                            if (isset($data['product']) && '' != $data['product']) {
311
                                $product = $data['product'];
312
                            } // End If Statement
313
                            ?>
314
                            <form method="post" action="admin.php?page=sensei_updates&action=update&n=0"
315
                                  name="update-sensei" class="upgrade">
316
                                <tr class="<?php echo $class; ?>">
317
                                    <td>
318
                                        <p>
319
                                            <input type="hidden" name="checked[]" value="<?php echo $update; ?>">
320
                                            <strong><?php echo $data['title']; ?></strong><br><?php echo $data['desc']; ?>
321
                                            <br>
322
                                            <em><?php printf(__('Originally included in %s v%s', 'woothemes-sensei'), $product, $version); ?></em>
323
                                        </p>
324
                                    </td>
325
                                    <?php
326
                                    $type_label = __('Auto', 'woothemes-sensei');
327
                                    if ($type != 'auto') {
328
                                        $type_label = __('Manual', 'woothemes-sensei');
329
                                    }
330
                                    ?>
331
                                    <td><p><?php echo $type_label; ?></p></td>
332
                                    <td>
333
                                        <p>
334
                                            <input onclick="javascript:return confirm('<?php echo addslashes( sprintf( __( 'Are you sure you want to run the \'%s\' update?', 'woothemes-sensei' ), $data['title'] ) ); ?>');"
335
                                                   id="update-sensei"
336
                                                   class="button<?php if( ! $update_run ) { echo ' button-primary'; } ?>"
337
                                                   type="submit"
338
                                                   value="<?php if( $update_run ) { _e( 'Re-run Update', 'woothemes-sensei' ); } else { _e( 'Run Update', 'woothemes-sensei' ); } ?>"
339
                                                   name="update">
340
341
                                            <?php
342
                                            $nonce_action = 'run_'.$update;
343
                                            $nonce_field_name = $update.'_nonce_field';
344
                                            wp_nonce_field( $nonce_action, $nonce_field_name, false, true );
345
                                            ?>
346
                                        </p>
347
                                    </td>
348
                                </tr>
349
                            </form>
350
                            <?php
351
                            if ('alternate' == $class) {
352
                                $class = '';
353
                            } else {
354
                                $class = 'alternate';
355
                            }
356
                        }
357
                    }
358
                }
359
                ?>
360
                </tbody>
361
362
            </table>
363
364
            </div>
365
366
            <?php
367
        } // End If Statement
368
    } // End sensei_updates_page()
369
370
    /**
371
     * Since 1.9.0
372
     *
373
     * A list of safe to execute functions withing the
374
     * updater context.
375
     *
376
     * @param string $function_name
377
     */
378
    public function function_in_whitelist( $function_name ){
379
380
        $function_whitelist = array(
381
            'status_changes_convert_questions',
382
            'status_changes_fix_lessons',
383
            'status_changes_convert_courses',
384
            'status_changes_convert_lessons',
385
            'status_changes_repair_course_statuses',
386
387
        );
388
389
        return in_array($function_name, $function_whitelist );
390
391
    }// end function_in_whitelist
392
393
	/**
394
	 * Sort updates list by version number
395
	 *
396
	 * @param  string $a First key
397
	 * @param  string $b Second key
398
	 * @return integer
399
	 */
400
	private function sort_updates( $a, $b ) {
401
		return strcmp( $a, $b );
402
	}
403
404
	/**
405
	 * update Calls the functions for updating
406
	 *
407
	 * @param  string $type specifies if the update is 'auto' or 'manual'
408
	 * @since  1.1.0
409
	 * @access public
410
	 * @return boolean
411
	 */
412
	public function update ( $type = 'auto' ) {
413
414
		// Only allow admins to run update functions
415
		if( ! current_user_can( 'manage_options' ) ) {
416
            return false;
417
        }
418
419
        $this->force_updates();
420
421
        // Run through all functions
422
        foreach ( $this->updates as $version => $value ) {
423
            foreach ( $this->updates[$version] as $upgrade_type => $function_to_run ) {
424
                if ( $upgrade_type == $type ) {
425
                    $updated = false;
426
                    // Run the update function
427
                    foreach ( $function_to_run as $function_name => $update_data ) {
428
                        if ( isset( $function_name ) && '' != $function_name ) {
429
                            if ( ! in_array( $function_name, $this->updates_run ) ) {
430
                                $updated = false;
431
                                if ( method_exists( $this, $function_name ) ) {
432
433
            $this->updates_run = array_unique( $this->updates_run ); // we only need one reference per update
434
			update_option( Sensei()->token . '-upgrades', $this->updates_run );
435
			return true;
436
437
                                } elseif( $this->function_in_whitelist( $function_name ) ) {
438
439
                                    $updated = call_user_func( $function_name );
440
441
                                }  // End If Statement
442
443
                                if ( $updated ) {
444
                                    array_push( $this->updates_run, $function_name );
445
                                } // End If Statement
446
                            }
447
                        } // End If Statement
448
                    } // End For Loop
449
                } // End If Statement
450
            } // End For Loop
451
        } // End For Loop
452
453
        $this->updates_run = array_unique( $this->updates_run ); // we only need one reference per update
454
        update_option( $this->token . '-upgrades', $this->updates_run );
455
456
        return true;
457
458
	} // End update()
459
460
	private function force_updates() {
461
462
		if( ! isset( $_GET['page'] ) || 'sensei_updates' != $_GET['page'] ) {
463
464
			// $skip_forced_updates = false;
465
			// if( ! get_option( 'woothemes-sensei-force-updates', false ) ) {
466
			// 	$skip_forced_updates = true;
467
			// }
468
469
			// Force critical updates if only if lessons already exist
470
			$skip_forced_updates = false;
471
			$lesson_posts = wp_count_posts( 'lesson' );
472
			if( ! isset( $lesson_posts->publish ) || ! $lesson_posts->publish ) {
473
				$skip_forced_updates = true;
474
			}
475
476
			$use_the_force = false;
477
478
			$updates_to_run = array();
479
480
			foreach ( $this->updates as $version => $value ) {
481
				foreach ( $this->updates[$version] as $upgrade_type => $function_to_run ) {
482
					if ( $upgrade_type == 'forced' ) {
483
						foreach ( $function_to_run as $function_name => $update_data ) {
484
485
							if( $skip_forced_updates ) {
486
								$this->set_update_run( $function_name );
487
								continue;
488
							}
489
490
							$update_run = $this->has_update_run( $function_name );
491
492
							if( ! $update_run ) {
493
								$use_the_force = true;
494
								$updates_to_run[ $function_name ] = $update_data;
495
							}
496
						}
497
					}
498
				}
499
			}
500
501
			if( $skip_forced_updates ) {
502
				return;
503
			}
504
505
			if( $use_the_force && 0 < count( $updates_to_run ) ) {
506
507
				$update_title = __( 'Important Sensei updates required', 'woothemes-sensei' );
508
509
				$update_message = '<h1>' . __( 'Important Sensei upgrades required!', 'woothemes-sensei' ) . '</h1>' . "\n";
510
511
				// $update_message .= '<h4>' . sprintf( __( 'These updates are only required if you are updating from a previous version of Sensei. If you are installing Sensei for the first time, %1$syou can dismiss this page by clicking here%2$s.', 'woothemes-sensei' ), '<a href="' . add_query_arg( array( 'sensei_skip_forced_updates' => 'true' ) ) . '">', '</a>' ) . '</h4>' ."\n";
512
513
				$update_message .= '<p>' . __( 'The latest version of Sensei requires some important database upgrades. In order to run these upgrades you will need to follow the step by step guide below. Your site will not function correctly unless you run these critical updates.', 'woothemes-sensei' ) . '</p>' . "\n";
514
515
				$update_message .= '<p><b>' . __( 'To run the upgrades click on each of the links below in the order that they appear.', 'woothemes-sensei' ) . '</b></p>' . "\n";
516
517
				$update_message .= '<p>' . __( 'Clicking each link will open up a new window/tab - do not close that window/tab until you see the message \'Update completed successfully\'. Once you see that message you can close the window/tab and start the next upgrade by clicking on the next link in the list.', 'woothemes-sensei' ) . '</p>' . "\n";
518
519
				$update_message .= '<p><b>' . __( 'Once all the upgrades have been completed you will be able to use your WordPress site again.', 'woothemes-sensei' ) . '</b></p>' . "\n";
520
521
				$update_message .= '<ol>' . "\n";
522
523
					foreach( $updates_to_run as $function => $data ) {
524
525
						if( ! isset( $data['title'] ) ) {
526
							break;
527
						}
528
529
						$update_message .= '<li style="margin:5px 0;"><a href="' . admin_url( 'admin.php?page=sensei_updates&action=update&n=0&functions[]=' . $function ) . '" target="_blank">' . $data['title'] . '</a></li>';
530
					}
531
532
				$update_message .= '</ol>' . "\n";
533
534
				switch( $version ) {
535
536
					case '1.7.0':
537
						$update_message .= '<p><em>' . sprintf( __( 'Want to know what these upgrades are all about? %1$sFind out more here%2$s.', 'woothemes-sensei' ), '<a href="http://develop.woothemes.com/sensei/2014/12/03/important-information-about-sensei-1-7" target="_blank">', '</a>' ) . '</em></p>' . "\n";
538
					break;
539
540
				}
541
542
				wp_die( $update_message, $update_title );
543
			}
544
		}
545
	}
546
547
	/**
548
	 * Check if specified update has already been run
549
	 *
550
	 * @param  string  $update Update to check
551
	 * @since  1.4.0
552
	 * @return boolean
553
	 */
554
	private function has_update_run( $update ) {
555
		if ( in_array( $update, $this->updates_run ) ) {
556
			return true;
557
		}
558
		return false;
559
	}
560
561
	/**
562
	 * Mark update as having been run
563
	 *
564
	 * @param string $update Update to process
565
	 * @since  1.4.0
566
	 */
567
	private function set_update_run( $update ) {
568
		array_push( $this->updates_run, $update );
569
        $this->updates_run = array_unique( $this->updates_run ); // we only need one reference per update
570
		update_option( Sensei()->token . '-upgrades', $this->updates_run );
571
	}
572
573
	/**
574
	 * Sets the role capabilities for WordPress users.
575
	 *
576
	 * @since  1.1.0
577
	 * @access public
578
	 * @return void
579
	 */
580
	public function assign_role_caps() {
581
		foreach ( $this->parent->post_types->role_caps as $role_cap_set  ) {
582
			foreach ( $role_cap_set as $role_key => $capabilities_array ) {
583
				/* Get the role. */
584
				$role = get_role( $role_key );
585
				foreach ( $capabilities_array as $cap_name  ) {
586
					/* If the role exists, add required capabilities for the plugin. */
587
					if ( !empty( $role ) ) {
588
						if ( !$role->has_cap( $cap_name ) ) {
589
							$role->add_cap( $cap_name );
590
						} // End If Statement
591
					} // End If Statement
592
				} // End For Loop
593
			} // End For Loop
594
		} // End For Loop
595
	} // End assign_role_caps
596
597
	/**
598
	 * Set default quiz grade type
599
	 *
600
	 * @since 1.3.0
601
	 * @access public
602
	 * @return void
603
	 */
604 View Code Duplication
	public function set_default_quiz_grade_type() {
605
		$args = array(	'post_type' 		=> 'quiz',
606
						'posts_per_page' 		=> -1,
607
						'post_status'		=> 'publish',
608
						'suppress_filters' 	=> 0
609
						);
610
		$quizzes = get_posts( $args );
611
612
		foreach( $quizzes as $quiz ) {
613
			update_post_meta( $quiz->ID, '_quiz_grade_type', 'auto' );
614
			update_post_meta( $quiz->ID, '_quiz_grade_type_disabled', '' );
615
		}
616
	} // End set_default_quiz_grade_type
617
618
	/**
619
	 * Set default question type
620
	 *
621
	 * @since 1.3.0
622
	 * @access public
623
	 * @return void
624
	 */
625
	public function set_default_question_type() {
626
		$args = array(	'post_type' 		=> 'question',
627
						'posts_per_page' 		=> -1,
628
						'post_status'		=> 'publish',
629
						'suppress_filters' 	=> 0
630
						);
631
		$questions = get_posts( $args );
632
633
		$already_run = true;
634
		foreach( $questions as $question ) {
635
			if( $already_run ) {
636
				$terms = wp_get_post_terms( $question->ID, 'question-type' );
637
				if( is_array( $terms ) && count( $terms ) > 0 ) {
638
					break;
639
				}
640
			}
641
			$already_run = false;
642
			wp_set_post_terms( $question->ID, array( 'multiple-choice' ), 'question-type' );
643
		}
644
645
	} // End set_default_question_type
646
647
	/**
648
	 * Update question answers to use new data structure
649
	 *
650
	 * @since 1.3.0
651
	 * @access public
652
	 * @return boolean
653
	 */
654
	public function update_question_answer_data( $n = 50, $offset = 0 ) {
655
656
		// Get Total Number of Updates to run
657
		$quiz_count_object = wp_count_posts( 'quiz' );
658
		$quiz_count_published = $quiz_count_object->publish;
659
660
		// Calculate if this is the last page
661
		if ( 0 == $offset ) {
662
			$current_page = 1;
663
		} else {
664
			$current_page = intval( $offset / $n );
665
		} // End If Statement
666
		$total_pages = intval( $quiz_count_published / $n );
667
668
669
		$args = array(	'post_type' 		=> 'quiz',
670
						'posts_per_page' 		=> $n,
671
						'offset'			=> $offset,
672
						'post_status'		=> 'publish',
673
						'suppress_filters' 	=> 0
674
						);
675
		$quizzes = get_posts( $args );
676
677
		$old_answers = array();
678
		$right_answers = array();
679
		$old_user_answers = array();
680
681
		if( is_array( $quizzes ) ) {
682
			foreach( $quizzes as $quiz ) {
683
				$quiz_id = $quiz->ID;
684
685
				// Get current user answers
686
				$comments = Sensei_Utils::sensei_check_for_activity( array( 'post_id' => $quiz_id, 'type' => 'sensei_quiz_answers' ), true  );
687
				// Need to always return an array, even with only 1 item
688
				if ( !is_array($comments) ) {
689
					$comments = array( $comments );
690
				}
691
				foreach ( $comments as $comment ) {
692
					$user_id = $comment->user_id;
693
					$content = maybe_unserialize( base64_decode( $comment->comment_content ) );
694
					$old_user_answers[ $quiz_id ][ $user_id ] = $content;
695
				}
696
697
				// Get correct answers
698
				$questions = Sensei_Utils::sensei_get_quiz_questions( $quiz_id );
699
				if( is_array( $questions ) ) {
700
					foreach( $questions as $question ) {
701
						$right_answer = get_post_meta( $question->ID, '_question_right_answer', true );
702
						$right_answers[ $quiz_id ][ $question->ID ] = $right_answer;
703
					}
704
				}
705
			}
706
		}
707
708
		if( is_array( $right_answers ) ) {
709
			foreach( $right_answers as $quiz_id => $question ) {
710
				$count = 0;
711
				if( is_array( $question ) ) {
712
					foreach( $question as $question_id => $answer ) {
713
						++$count;
714
						if( isset( $old_user_answers[ $quiz_id ] ) ) {
715
							$answers_linkup[ $quiz_id ][ $count ] = $question_id;
716
						}
717
					}
718
				}
719
			}
720
		}
721
722
		if( is_array( $old_user_answers ) ) {
723
			foreach( $old_user_answers as $quiz_id => $user_answers ) {
724
				foreach( $user_answers as $user_id => $answers ) {
725
					foreach( $answers as $answer_id => $user_answer ) {
726
						$question_id = $answers_linkup[ $quiz_id ][ $answer_id ];
727
						$new_user_answers[ $question_id ] = $user_answer;
728
						Sensei_Utils::sensei_grade_question_auto( $question_id, '', $user_answer, $user_id );
729
					}
730
					$lesson_id = get_post_meta( $quiz_id, '_quiz_lesson', true );
731
					Sensei_Utils::sensei_start_lesson( $lesson_id, $user_id );
732
					Sensei_Utils::sensei_save_quiz_answers( $new_user_answers, $user_id );
733
				}
734
			}
735
		}
736
737
		if ( $current_page == $total_pages ) {
738
			return true;
739
		} else {
740
			return false;
741
		} // End If Statement
742
743
	} // End update_question_answer_data
744
745
	/**
746
	 * Add default question grade points for v1.4.0
747
	 *
748
	 * @since  1.4.0
749
	 * @return boolean
750
	 */
751 View Code Duplication
	public function update_question_grade_points() {
752
		$args = array(	'post_type' 		=> 'question',
753
						'posts_per_page' 		=> -1,
754
						'post_status'		=> 'publish',
755
						'suppress_filters' 	=> 0
756
						);
757
		$questions = get_posts( $args );
758
759
		foreach( $questions as $question ) {
760
			update_post_meta( $question->ID, '_question_grade', '1' );
761
		}
762
		return true;
763
	} // End update_question_grade_points
764
765
	/**
766
	 * Convert all essay paste questions into multi-line for v1.5.0
767
	 *
768
	 * @since  1.5.0
769
	 * @return boolean
770
	 */
771
	public function convert_essay_paste_questions() {
772
		$args = array(	'post_type' 		=> 'question',
773
						'posts_per_page' 		=> -1,
774
						'post_status'		=> 'publish',
775
						'tax_query'			=> array(
776
							array(
777
								'taxonomy'		=> 'question-type',
778
								'terms'			=> 'essay-paste',
779
								'field'			=> 'slug'
780
							)
781
						),
782
						'suppress_filters' 	=> 0
783
						);
784
		$questions = get_posts( $args );
785
786
		foreach( $questions as $question ) {
787
			wp_set_object_terms( $question->ID, 'multi-line', 'question-type', false );
788
789
			$quiz_id = get_post_meta( $question->ID, '_quiz_id', true );
790
			if( 0 < intval( $quiz_id ) ) {
791
				add_post_meta( $question->ID, '_quiz_question_order' . $quiz_id, $quiz_id . '0000', true );
792
			}
793
		}
794
		return true;
795
	} // End convert_essay_paste_questions
796
797
	/**
798
	 * Set all quizzes to have a random question order
799
	 *
800
	 * @since  1.5.0
801
	 * @return boolean
802
	 */
803
	public function set_random_question_order( $n = 50, $offset = 0 ) {
804
805
		// Get Total Number of Updates to run
806
		$quiz_count_object = wp_count_posts( 'quiz' );
807
		$quiz_count_published = $quiz_count_object->publish;
808
809
		// Calculate if this is the last page
810
		if ( 0 == $offset ) {
811
			$current_page = 1;
812
		} else {
813
			$current_page = intval( $offset / $n );
814
		} // End If Statement
815
		$total_pages = intval( $quiz_count_published / $n );
816
817
		$args = array(	'post_type' 		=> 'quiz',
818
						'post_status'		=> 'any',
819
						'posts_per_page' 		=> $n,
820
						'offset'			=> $offset,
821
						'suppress_filters' 	=> 0
822
						);
823
		$quizzes = get_posts( $args );
824
825
		foreach( $quizzes as $quiz ) {
826
			update_post_meta( $quiz->ID, '_random_question_order', 'yes' );
827
		}
828
829
		if ( $current_page == $total_pages ) {
830
			return true;
831
		} else {
832
			return false;
833
		} // End If Statement
834
835
	} // End set_random_question_order()
836
837
	/**
838
	 * Set all quizzes to display all questions
839
	 *
840
	 * @since  1.5.0
841
	 * @return boolean
842
	 */
843
	public function set_default_show_question_count( $n = 50, $offset = 0 ) {
844
845
		$args = array(	'post_type' 		=> 'quiz',
846
						'post_status'		=> 'any',
847
						'posts_per_page' 		=> $n,
848
						'offset'			=> $offset,
849
						'meta_key'			=> '_show_questions',
850
						'suppress_filters' 	=> 0
851
						);
852
		$quizzes = get_posts( $args );
853
854
		$total_quizzes = count( $quizzes );
855
856
		if( 0 == intval( $total_quizzes ) ) {
857
			return true;
858
		}
859
860
		foreach( $quizzes as $quiz ) {
861
			delete_post_meta( $quiz->ID, '_show_questions' );
862
		}
863
864
		$total_pages = intval( $total_quizzes / $n );
865
866
		// Calculate if this is the last page
867
		if ( 0 == $offset ) {
868
			$current_page = 1;
869
		} else {
870
			$current_page = intval( $offset / $n );
871
		} // End If Statement
872
873
		if ( $current_page == $total_pages ) {
874
			return true;
875
		} else {
876
			return false;
877
		} // End If Statement
878
879
	}
880
881
	public function remove_deleted_user_activity( $n = 50, $offset = 0 ) {
882
883
		$all_activity = get_comments( array( 'status' => 'approve' ) );
884
		$activity_count = array();
885
		foreach( $all_activity as $activity ) {
886
			if( '' == $activity->comment_type ) continue;
887
			if( strpos( 'sensei_', $activity->comment_type ) != 0 ) continue;
888
			if( 0 == $activity->user_id ) continue;
889
			$activity_count[] = $activity->comment_ID;
890
		}
891
892
		$args = array(
893
			'number' => $n,
894
			'offset' => $offset,
895
			'status' => 'approve'
896
		);
897
898
		$activities = get_comments( $args );
899
900
		foreach( $activities as $activity ) {
901
			if( '' == $activity->comment_type ) continue;
902
			if( strpos( 'sensei_', $activity->comment_type ) != 0 ) continue;
903
			if( 0 == $activity->user_id ) continue;
904
905
			$user_exists = get_userdata( $activity->user_id );
906
907
			if( ! $user_exists ) {
908
				wp_delete_comment( intval( $activity->comment_ID ), true );
909
				wp_cache_flush();
910
			}
911
		}
912
913
		$total_activities = count( $activity_count );
914
915
		$total_pages = intval( $total_activities / $n );
916
917
		// Calculate if this is the last page
918
		if ( 0 == $offset ) {
919
			$current_page = 1;
920
		} else {
921
			$current_page = intval( $offset / $n );
922
		} // End If Statement
923
924
		if ( $current_page >= $total_pages ) {
925
			return true;
926
		} else {
927
			return false;
928
		} // End If Statement
929
930
	}
931
932
	public function add_teacher_role() {
933
		add_role( 'teacher', __( 'Teacher', 'woothemes-sensei' ), array( 'read' => true, 'manage_sensei_grades' => true ) );
934
		return true;
935
	}
936
937
	public function add_sensei_caps() {
938
		$role = get_role( 'administrator' );
939
940
		if( ! is_null( $role ) ) {
941
			$role->add_cap( 'manage_sensei' );
942
			$role->add_cap( 'manage_sensei_grades' );
943
		}
944
945
		return true;
946
	}
947
948
	public function restructure_question_meta() {
949
		$args = array(
950
			'post_type' 		=> 'question',
951
			'posts_per_page' 	=> -1,
952
			'post_status'		=> 'any',
953
			'suppress_filters' 	=> 0
954
		);
955
956
		$questions = get_posts( $args );
957
958
		foreach( $questions as $question ) {
959
960
			if( ! isset( $question->ID ) ) continue;
961
962
			$quiz_id = get_post_meta( $question->ID, '_quiz_id', true );
963
964
			$question_order = get_post_meta( $question->ID, '_quiz_question_order', true );
965
			update_post_meta( $question->ID, '_quiz_question_order' . $quiz_id, $question_order );
966
967
		}
968
969
		return true;
970
	}
971
972
	public function update_quiz_settings() {
973
974
		$settings = get_option( 'woothemes-sensei-settings', array() );
975
976
		$lesson_completion = false;
977
		if( isset( $settings['lesson_completion'] ) ) {
978
			$lesson_completion = $settings['lesson_completion'];
979
		}
980
981
		$reset_quiz_allowed = false;
982
		if( isset( $settings['quiz_reset_allowed'] ) ) {
983
			$reset_quiz_allowed = $settings['quiz_reset_allowed'];
984
		}
985
986
		$args = array(
987
			'post_type' 		=> 'quiz',
988
			'posts_per_page' 	=> -1,
989
			'post_status'		=> 'any',
990
			'suppress_filters' 	=> 0
991
		);
992
993
		$quizzes = get_posts( $args );
994
995
		foreach( $quizzes as $quiz ) {
996
997
			if( ! isset( $quiz->ID ) ) continue;
998
999
			if( isset( $lesson_completion ) && 'passed' == $lesson_completion ) {
1000
				update_post_meta( $quiz->ID, '_pass_required', 'on' );
1001
			} else {
1002
				update_post_meta( $quiz->ID, '_quiz_passmark', 0 );
1003
			}
1004
1005
			if( isset( $reset_quiz_allowed ) && $reset_quiz_allowed ) {
1006
				update_post_meta( $quiz->ID, '_enable_quiz_reset', 'on' );
1007
			}
1008
		}
1009
1010
		return true;
1011
	}
1012
1013
	public function reset_lesson_order_meta() {
1014
		$args = array(
1015
			'post_type' 		=> 'lesson',
1016
			'posts_per_page' 	=> -1,
1017
			'post_status'		=> 'any',
1018
			'suppress_filters' 	=> 0
1019
		);
1020
1021
		$lessons = get_posts( $args );
1022
1023
		foreach( $lessons as $lesson ) {
1024
1025
			if( ! isset( $lesson->ID ) ) continue;
1026
1027
			$course_id = get_post_meta( $lesson->ID, '_lesson_course', true);
1028
1029
			if( $course_id ) {
1030
				update_post_meta( $lesson->ID, '_order_' . $course_id, 0 );
1031
			}
1032
1033
            $module = Sensei()->modules->get_lesson_module( $lesson->ID );
1034
1035
            if( $module ) {
1036
                update_post_meta( $lesson->ID, '_order_module_' . $module->term_id, 0 );
1037
            }
1038
1039
		}
1040
1041
		return true;
1042
	}
1043
1044
	public function add_editor_caps() {
1045
		$role = get_role( 'editor' );
1046
1047
		if( ! is_null( $role ) ) {
1048
			$role->add_cap( 'manage_sensei_grades' );
1049
		}
1050
1051
		return true;
1052
	}
1053
1054
	/**
1055
	 * Updates all gap fill questions, converting the | separator to || matching the changes in code. Using || allows the use of | within the pre, gap or post field.
1056
	 *
1057
	 * @global type $wpdb
1058
	 * @return boolean
1059
	 */
1060
	public function update_question_gap_fill_separators() {
1061
		global $wpdb;
1062
1063
		$sql = "UPDATE $wpdb->postmeta AS m, $wpdb->term_relationships AS tr, $wpdb->term_taxonomy AS tt, $wpdb->terms AS t SET m.meta_value = replace(m.meta_value, '|', '||')
1064
					WHERE m.meta_key = '_question_right_answer' AND m.meta_value LIKE '%|%' AND m.meta_value NOT LIKE '%||%'
1065
						AND m.post_id = tr.object_id AND tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.term_id = t.term_id
1066
						AND tt.taxonomy = 'question-type' AND t.slug = 'gap-fill'";
1067
		$wpdb->query( $sql );
1068
1069
		return true;
1070
	}
1071
1072
	public function update_quiz_lesson_relationship( $n = 50, $offset = 0 ) {
1073
		$count_object = wp_count_posts( 'quiz' );
1074
1075
		$count_published = 0;
1076
		foreach ( $count_object AS $status => $count ) {
1077
			$count_published += $count;
1078
		}
1079
1080
		// Calculate if this is the last page
1081
		if ( 0 == $offset ) {
1082
			$current_page = 1;
1083
		} else {
1084
			$current_page = intval( $offset / $n );
1085
		}
1086
		$total_pages = ceil( $count_published / $n );
1087
1088
		$args = array(
1089
			'post_type' => 'quiz',
1090
			'posts_per_page' => $n,
1091
			'offset' => $offset,
1092
			'post_status' => 'any'
1093
		);
1094
1095
		$quizzes = get_posts( $args );
1096
1097
		foreach( $quizzes as $quiz ) {
1098
1099
			if( ! isset( $quiz->ID ) || 0 != $quiz->post_parent ) continue;
1100
1101
			$lesson_id = get_post_meta( $quiz->ID, '_quiz_lesson', true );
1102
1103
			if( empty( $lesson_id ) ) continue;
1104
1105
			$data = array(
1106
				'ID' => $quiz->ID,
1107
				'post_parent' => $lesson_id,
1108
			);
1109
			wp_update_post( $data );
1110
1111
			update_post_meta( $lesson_id, '_lesson_quiz', $quiz->ID );
1112
		}
1113
1114
		if ( $current_page == $total_pages || 0 == $total_pages ) {
1115
			return true;
1116
		} else {
1117
			return false;
1118
		}
1119
	}
1120
1121
	function status_changes_fix_lessons( $n = 50, $offset = 0 ) {
1122
		global $wpdb;
1123
1124
		$count_object = wp_count_posts( 'lesson' );
1125
		$count_published = 0;
1126
		foreach ( $count_object AS $status => $count ) {
1127
			$count_published += $count;
1128
		}
1129
1130
		if ( 0 == $count_published ) {
1131
			return true;
1132
		}
1133
1134
		// Calculate if this is the last page
1135
		if ( 0 == $offset ) {
1136
			$current_page = 1;
1137
		} else {
1138
			$current_page = intval( $offset / $n );
1139
		}
1140
		$total_pages = ceil( $count_published / $n );
1141
1142
		// Get all Lessons with (and without) Quizzes...
1143
		$args = array(
1144
			'post_type' => 'lesson',
1145
			'post_status' => 'any',
1146
			'posts_per_page' => $n,
1147
			'offset' => $offset,
1148
			'fields' => 'ids'
1149
		);
1150
		$lesson_ids = get_posts( $args );
1151
1152
		// ...get all Quiz IDs for the above Lessons
1153
		$id_list = join( ',', $lesson_ids );
1154
		$meta_list = $wpdb->get_results( "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = '_quiz_lesson' AND meta_value IN ($id_list)", ARRAY_A );
1155
		$lesson_quiz_ids = array();
1156 View Code Duplication
		if ( !empty($meta_list) ) {
1157
			foreach ( $meta_list as $metarow ) {
1158
				$lesson_id = $metarow['meta_value'];
1159
				$quiz_id = $metarow['post_id'];
1160
				$lesson_quiz_ids[ $lesson_id ] = $quiz_id;
1161
			}
1162
		}
1163
1164
		// ...check all Quiz IDs for questions
1165
		$id_list = join( ',', array_values($lesson_quiz_ids) );
1166
		$meta_list = $wpdb->get_results( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_quiz_id' AND meta_value IN ($id_list)", ARRAY_A );
1167
		$lesson_quiz_ids_with_questions = array();
1168
		if ( !empty($meta_list) ) {
1169
			foreach ( $meta_list as $metarow ) {
1170
				$quiz_id = $metarow['meta_value'];
1171
				$lesson_quiz_ids_with_questions[] = $quiz_id;
1172
			}
1173
		}
1174
1175
		// For each quiz check there are questions, if not remove the corresponding meta keys from Quizzes and Lessons
1176
		// if there are questions on the quiz add the corresponding meta keys to Quizzes and Lessons
1177
		$d_count = $a_count =0;
1178
		foreach ( $lesson_quiz_ids AS $lesson_id => $quiz_id ) {
1179
			if ( !in_array( $quiz_id, $lesson_quiz_ids_with_questions ) ) {
1180
1181
				// Quiz has no questions, drop the corresponding data
1182
				delete_post_meta( $quiz_id, '_pass_required' );
1183
				delete_post_meta( $quiz_id, '_quiz_passmark' );
1184
				delete_post_meta( $lesson_id, '_quiz_has_questions' );
1185
				$d_count++;
1186
			}
1187
			else if ( in_array( $quiz_id, $lesson_quiz_ids_with_questions ) ) {
1188
1189
				// Quiz has no questions, drop the corresponding data
1190
				update_post_meta( $lesson_id, '_quiz_has_questions', true );
1191
				$a_count++;
1192
			}
1193
		}
1194
1195
		if ( $current_page == $total_pages ) {
1196
			return true;
1197
		} else {
1198
			return false;
1199
		}
1200
	}
1201
1202
	function status_changes_convert_lessons( $n = 50, $offset = 0 ) {
1203
		global $wpdb;
1204
1205
		wp_defer_comment_counting( true );
1206
1207
		$user_count_result = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->users " );
1208
1209
		if ( 0 == $user_count_result ) {
1210
			return true;
1211
		}
1212
1213
		if ( 0 == $offset ) {
1214
			$current_page = 1;
1215
		} else {
1216
			$current_page = intval( $offset / $n );
1217
		}
1218
1219
		$total_pages = ceil( $user_count_result / $n );
1220
1221
		// Get all Lessons with Quizzes...
1222
		$args = array(
1223
			'post_type' => 'lesson',
1224
			'post_status' => 'any',
1225
			'posts_per_page' => -1,
1226
			'meta_query' => array(
1227
				array(
1228
					'key' => '_quiz_has_questions',
1229
					'value' => 1,
1230
				),
1231
			),
1232
			'fields' => 'ids'
1233
		);
1234
		$lesson_ids_with_quizzes = get_posts( $args );
1235
		// ...get all Quiz IDs for the above Lessons
1236
		$id_list = join( ',', $lesson_ids_with_quizzes );
1237
		$meta_list = $wpdb->get_results( "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = '_quiz_lesson' AND meta_value IN ($id_list)", ARRAY_A );
1238
		$lesson_quiz_ids = array();
1239 View Code Duplication
		if ( !empty($meta_list) ) {
1240
			foreach ( $meta_list as $metarow ) {
1241
				$lesson_id = $metarow['meta_value'];
1242
				$quiz_id = $metarow['post_id'];
1243
				$lesson_quiz_ids[ $lesson_id ] = $quiz_id;
1244
			}
1245
		}
1246
1247
		// ...get all Pass Required & Passmarks for the above Lesson/Quizzes
1248
		$id_list = join( ',', array_values($lesson_quiz_ids) );
1249
		$meta_list = $wpdb->get_results( "SELECT post_id, meta_key, meta_value FROM $wpdb->postmeta WHERE ( meta_key = '_pass_required' OR meta_key = '_quiz_passmark' ) AND post_id IN ($id_list)", ARRAY_A );
1250
		$quizzes_pass_required = $quizzes_passmarks = array();
1251
		if ( !empty($meta_list) ) {
1252
			foreach ( $meta_list as $metarow ) {
1253
				if ( !empty($metarow['meta_value']) ) {
1254
					$quiz_id = $metarow['post_id'];
1255
					$key = $metarow['meta_key'];
1256
					$value = $metarow['meta_value'];
1257
					if ( '_pass_required' == $key ) {
1258
						$quizzes_pass_required[ $quiz_id ] = $value;
1259
					}
1260
					if ( '_quiz_passmark' == $key ) {
1261
						$quizzes_passmarks[ $quiz_id ] = $value;
1262
					}
1263
				}
1264
			}
1265
		}
1266
1267
		$users_sql = "SELECT ID FROM $wpdb->users ORDER BY ID ASC LIMIT %d OFFSET %d";
1268
		$start_sql = "SELECT comment_post_ID, comment_date FROM $wpdb->comments WHERE comment_type = 'sensei_lesson_start' AND user_id = %d GROUP BY comment_post_ID ";
1269
		$end_sql = "SELECT comment_post_ID, comment_date FROM $wpdb->comments WHERE comment_type = 'sensei_lesson_end' AND user_id = %d GROUP BY comment_post_ID ";
1270
		$grade_sql = "SELECT comment_post_ID, comment_content FROM $wpdb->comments WHERE comment_type = 'sensei_quiz_grade' AND user_id = %d GROUP BY comment_post_ID ORDER BY comment_content DESC ";
1271
		$answers_sql = "SELECT comment_post_ID, comment_content FROM $wpdb->comments WHERE comment_type = 'sensei_quiz_asked' AND user_id = %d GROUP BY comment_post_ID ORDER BY comment_date_gmt DESC ";
1272
		$check_existing_sql = "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d AND user_id = %d AND comment_type = 'sensei_lesson_status' ";
1273
1274
		// $per_page users at a time, could be batch run via an admin ajax command, 1 user at a time?
1275
		$user_ids = $wpdb->get_col( $wpdb->prepare($users_sql, $n, $offset) );
1276
1277
		foreach ( $user_ids AS $user_id ) {
1278
1279
			$lesson_ends = $lesson_grades = $lesson_answers = array();
1280
1281
			// Pre-process the lesson ends
1282
			$_lesson_ends = $wpdb->get_results( $wpdb->prepare($end_sql, $user_id), ARRAY_A );
1283
			foreach ( $_lesson_ends as $lesson_end ) {
1284
				// This will overwrite existing entries with the newer ones
1285
				$lesson_ends[ $lesson_end['comment_post_ID'] ] = $lesson_end['comment_date'];
1286
			}
1287
			unset( $_lesson_ends );
1288
1289
			// Pre-process the lesson grades
1290
			$_lesson_grades = $wpdb->get_results( $wpdb->prepare($grade_sql, $user_id), ARRAY_A );
1291
			foreach ( $_lesson_grades as $lesson_grade ) {
1292
				// This will overwrite existing entries with the newer ones (assuming the grade is higher)
1293
				if ( empty($lesson_grades[ $lesson_grade['comment_post_ID'] ]) || $lesson_grades[ $lesson_grade['comment_post_ID'] ] < $lesson_grade['comment_content'] ) {
1294
					$lesson_grades[ $lesson_grade['comment_post_ID'] ] = $lesson_grade['comment_content'];
1295
				}
1296
			}
1297
			unset( $_lesson_grades );
1298
1299
			// Pre-process the lesson answers
1300
			$_lesson_answers = $wpdb->get_results( $wpdb->prepare($answers_sql, $user_id), ARRAY_A );
1301
			foreach ( $_lesson_answers as $lesson_answer ) {
1302
				// This will overwrite existing entries with the newer ones
1303
				$lesson_answers[ $lesson_answer['comment_post_ID'] ] = $lesson_answer['comment_content'];
1304
			}
1305
			unset( $_lesson_answers );
1306
1307
			// Grab all the lesson starts for the user
1308
			$lesson_starts = $wpdb->get_results( $wpdb->prepare($start_sql, $user_id), ARRAY_A );
1309
			foreach ( $lesson_starts as $lesson_log ) {
1310
1311
				$lesson_id = $lesson_log['comment_post_ID'];
1312
1313
				// Default status
1314
				$status = 'in-progress';
1315
1316
				$status_date = $lesson_log['comment_date'];
1317
				// Additional data for the lesson
1318
				$meta_data = array(
1319
					'start' => $status_date,
1320
				);
1321
				// Check if there is a lesson end
1322
				if ( !empty($lesson_ends[$lesson_id]) ) {
1323
					$status_date = $lesson_ends[$lesson_id];
1324
					// Check lesson has quiz
1325
					if ( !empty( $lesson_quiz_ids[$lesson_id] ) ) {
1326
						// Check for the quiz answers
1327
						if ( !empty($lesson_answers[$quiz_id]) ) {
1328
							$meta_data['questions_asked'] = $lesson_answers[$quiz_id];
1329
						}
1330
						// Check if there is a quiz grade
1331
						$quiz_id = $lesson_quiz_ids[$lesson_id];
1332
						if ( !empty($lesson_grades[$quiz_id]) ) {
1333
							$meta_data['grade'] = $quiz_grade = $lesson_grades[$quiz_id];
1334
							// Check if the user has to get the passmark and has or not
1335
							if ( !empty( $quizzes_pass_required[$quiz_id] ) && $quizzes_passmarks[$quiz_id] <= $quiz_grade ) {
1336
								$status = 'passed';
1337
							}
1338
							elseif ( !empty( $quizzes_pass_required[$quiz_id] ) && $quizzes_passmarks[$quiz_id] > $quiz_grade ) {
1339
								$status = 'failed';
1340
							}
1341
							else {
1342
								$status = 'graded';
1343
							}
1344
						}
1345
						else {
1346
							// If the lesson has a quiz, but the user doesn't have a grade, it's not yet been graded
1347
							$status = 'ungraded';
1348
						}
1349
					}
1350
					else {
1351
						// Lesson has no quiz, so it can only ever be this status
1352
						$status = 'complete';
1353
					}
1354
				}
1355
				$data = array(
1356
					// This is the minimum data needed, the db defaults handle the rest
1357
						'comment_post_ID' => $lesson_id,
1358
						'comment_approved' => $status,
1359
						'comment_type' => 'sensei_lesson_status',
1360
						'comment_date' => $status_date,
1361
						'user_id' => $user_id,
1362
						'comment_date_gmt' => get_gmt_from_date($status_date),
1363
						'comment_author' => '',
1364
					);
1365
				// Check it doesn't already exist
1366
				$sql = $wpdb->prepare( $check_existing_sql, $lesson_id, $user_id );
1367
				$comment_ID = $wpdb->get_var( $sql );
1368 View Code Duplication
				if ( !$comment_ID ) {
1369
					// Bypassing WP wp_insert_comment( $data ), so no actions/filters are run
1370
					$wpdb->insert($wpdb->comments, $data);
1371
					$comment_ID = (int) $wpdb->insert_id;
1372
1373
					if ( $comment_ID && !empty($meta_data) ) {
1374
						foreach ( $meta_data as $key => $value ) {
1375
							// Bypassing WP add_comment_meta(() so no actions/filters are run
1376
							if ( $wpdb->get_var( $wpdb->prepare(
1377
									"SELECT COUNT(*) FROM $wpdb->commentmeta WHERE comment_id = %d AND meta_key = %s ",
1378
									$comment_ID, $key ) ) ) {
1379
									continue; // Found the meta data already
1380
							}
1381
							$result = $wpdb->insert( $wpdb->commentmeta, array(
1382
								'comment_id' => $comment_ID,
1383
								'meta_key' => $key,
1384
								'meta_value' => $value
1385
							) );
1386
						}
1387
					}
1388
				}
1389
			}
1390
		}
1391
		$wpdb->flush();
1392
1393
		if ( $current_page == $total_pages ) {
1394
			return true;
1395
		} else {
1396
			return false;
1397
		}
1398
	}
1399
1400
	function status_changes_convert_courses( $n = 50, $offset = 0 ) {
1401
		global $wpdb;
1402
1403
		wp_defer_comment_counting( true );
1404
1405
		$user_count_result = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->users " );
1406
1407
		if ( 0 == $user_count_result ) {
1408
			return true;
1409
		}
1410
1411
		if ( 0 == $offset ) {
1412
			$current_page = 1;
1413
		} else {
1414
			$current_page = intval( $offset / $n );
1415
		}
1416
1417
		$total_pages = ceil( $user_count_result / $n );
1418
1419
		// Get all Lesson => Course relationships
1420
		$meta_list = $wpdb->get_results( "SELECT $wpdb->postmeta.post_id, $wpdb->postmeta.meta_value FROM $wpdb->postmeta INNER JOIN $wpdb->posts ON ($wpdb->posts.ID = $wpdb->postmeta.post_id) WHERE $wpdb->posts.post_type = 'lesson' AND $wpdb->postmeta.meta_key = '_lesson_course'", ARRAY_A );
1421
		$course_lesson_ids = array();
1422 View Code Duplication
		if ( !empty($meta_list) ) {
1423
			foreach ( $meta_list as $metarow ) {
1424
				$lesson_id = $metarow['post_id'];
1425
				$course_id = $metarow['meta_value'];
1426
				$course_lesson_ids[ $course_id ][] = $lesson_id;
1427
			}
1428
		}
1429
1430
		$users_sql = "SELECT ID FROM $wpdb->users ORDER BY ID ASC LIMIT %d OFFSET %d";
1431
		$start_sql = "SELECT comment_post_ID, comment_date FROM $wpdb->comments WHERE comment_type = 'sensei_course_start' AND user_id = %d GROUP BY comment_post_ID ";
1432
		$lessons_sql = "SELECT comment_approved AS status, comment_date FROM $wpdb->comments WHERE comment_type = 'sensei_lesson_status' AND user_id = %d AND comment_post_ID IN ( %s ) GROUP BY comment_post_ID ORDER BY comment_date_gmt DESC ";
1433
		$check_existing_sql = "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d AND user_id = %d AND comment_type = 'sensei_course_status' ";
1434
1435
		// $per_page users at a time, could be batch run via an admin ajax command, 1 user at a time?
1436
		$user_ids = $wpdb->get_col( $wpdb->prepare($users_sql, $n, $offset) );
1437
1438
		foreach ( $user_ids AS $user_id ) {
1439
1440
			// Grab all the course starts for the user
1441
			$course_starts = $wpdb->get_results( $wpdb->prepare($start_sql, $user_id), ARRAY_A );
1442
			foreach ( $course_starts as $course_log ) {
1443
1444
				$course_id = $course_log['comment_post_ID'];
1445
1446
				// Default status
1447
				$status = 'complete';
1448
1449
				$status_date = $course_log['comment_date'];
1450
				// Additional data for the course
1451
				$meta_data = array(
1452
					'start' => $status_date,
1453
					'complete' => 0,
1454
					'percent' => 0,
1455
				);
1456
				// Check if the course has lessons
1457
				if ( !empty( $course_lesson_ids[$course_id] ) ) {
1458
1459
					$lessons_completed = 0;
1460
					$total_lessons = count( $course_lesson_ids[ $course_id ] );
1461
1462
					// Don't use prepare as we need to provide the id join
1463
					$sql = sprintf($lessons_sql, $user_id, join(', ', $course_lesson_ids[ $course_id ]) );
1464
					// Get all lesson statuses for this Courses' lessons
1465
					$lesson_statuses = $wpdb->get_results( $sql, ARRAY_A );
1466
					// Not enough lesson statuses, thus cannot be complete
1467
					if ( $total_lessons > count($lesson_statuses) ) {
1468
						$status = 'in-progress';
1469
					}
1470
					// Count each lesson to work out the overall percentage
1471
					foreach ( $lesson_statuses as $lesson_status ) {
1472
						$status_date = $lesson_status['comment_date'];
1473
						switch ( $lesson_status['status'] ) {
1474
							case 'complete': // Lesson has no quiz/questions
1475
							case 'graded': // Lesson has quiz, but it's not important what the grade was
1476
							case 'passed':
1477
								$lessons_completed++;
1478
								break;
1479
1480
							case 'in-progress':
1481
							case 'ungraded': // Lesson has quiz, but it hasn't been graded
1482
							case 'failed': // User failed the passmark on the lesson/quiz
1483
								$status = 'in-progress';
1484
								break;
1485
						}
1486
					}
1487
					$meta_data['complete'] = $lessons_completed;
1488
					$meta_data['percent'] = abs( round( ( doubleval( $lessons_completed ) * 100 ) / ( $total_lessons ), 0 ) );
1489
				}
1490
				else {
1491
					// Course has no lessons, therefore cannot be 'complete'
1492
					$status = 'in-progress';
1493
				}
1494
				$data = array(
1495
					// This is the minimum data needed, the db defaults handle the rest
1496
						'comment_post_ID' => $course_id,
1497
						'comment_approved' => $status,
1498
						'comment_type' => 'sensei_course_status',
1499
						'comment_date' => $status_date,
1500
						'user_id' => $user_id,
1501
						'comment_date_gmt' => get_gmt_from_date($status_date),
1502
						'comment_author' => '',
1503
					);
1504
				// Check it doesn't already exist
1505
				$sql = $wpdb->prepare( $check_existing_sql, $course_id, $user_id );
1506
				$comment_ID = $wpdb->get_var( $sql );
1507 View Code Duplication
				if ( !$comment_ID ) {
1508
					// Bypassing WP wp_insert_comment( $data ), so no actions/filters are run
1509
					$wpdb->insert($wpdb->comments, $data);
1510
					$comment_ID = (int) $wpdb->insert_id;
1511
1512
					if ( $comment_ID && !empty($meta_data) ) {
1513
						foreach ( $meta_data as $key => $value ) {
1514
							// Bypassing WP wp_insert_comment( $data ), so no actions/filters are run
1515
							if ( $wpdb->get_var( $wpdb->prepare(
1516
									"SELECT COUNT(*) FROM $wpdb->commentmeta WHERE comment_id = %d AND meta_key = %s ",
1517
									$comment_ID, $key ) ) ) {
1518
									continue; // Found the meta data already
1519
							}
1520
							$result = $wpdb->insert( $wpdb->commentmeta, array(
1521
								'comment_id' => $comment_ID,
1522
								'meta_key' => $key,
1523
								'meta_value' => $value
1524
							) );
1525
						}
1526
					}
1527
				}
1528
			}
1529
		}
1530
		$wpdb->flush();
1531
1532
		if ( $current_page == $total_pages ) {
1533
			return true;
1534
		} else {
1535
			return false;
1536
		}
1537
	}
1538
1539
	/**
1540
	 * Force the re-calculation of all Course statuses working from all Lesson statuses
1541
	 *
1542
	 * @global type $woothemes_sensei
1543
	 * @global type $wpdb
1544
	 * @param type $n
1545
	 * @param type $offset
1546
	 * @return boolean
1547
	 */
1548
	function status_changes_repair_course_statuses( $n = 50, $offset = 0 ) {
1549
		global $wpdb;
1550
1551
		$count_object = wp_count_posts( 'lesson' );
1552
		$count_published = $count_object->publish;
1553
1554
		if ( 0 == $count_published ) {
1555
			return true;
1556
		}
1557
1558
		// Calculate if this is the last page
1559
		if ( 0 == $offset ) {
1560
			$current_page = 1;
1561
		} else {
1562
			$current_page = intval( $offset / $n );
1563
		}
1564
		$total_pages = ceil( $count_published / $n );
1565
1566
		$course_lesson_ids = $lesson_user_statuses = array();
1567
1568
		// Get all Lesson => Course relationships
1569
		$meta_list = $wpdb->get_results( "SELECT $wpdb->postmeta.post_id, $wpdb->postmeta.meta_value FROM $wpdb->postmeta INNER JOIN $wpdb->posts ON ($wpdb->posts.ID = $wpdb->postmeta.post_id) WHERE $wpdb->posts.post_type = 'lesson' AND $wpdb->postmeta.meta_key = '_lesson_course' LIMIT $n OFFSET $offset ", ARRAY_A );
1570 View Code Duplication
		if ( !empty($meta_list) ) {
1571
			foreach ( $meta_list as $metarow ) {
1572
				$lesson_id = $metarow['post_id'];
1573
				$course_id = $metarow['meta_value'];
1574
				$course_lesson_ids[ $course_id ][] = $lesson_id;
1575
			}
1576
		}
1577
1578
		// Get all Lesson => Course relationships
1579
		$status_list = $wpdb->get_results( "SELECT user_id, comment_post_ID, comment_approved FROM $wpdb->comments WHERE comment_type = 'sensei_lesson_status' GROUP BY user_id, comment_post_ID ", ARRAY_A );
1580
		if ( !empty($status_list) ) {
1581
			foreach ( $status_list as $status ) {
1582
				$lesson_user_statuses[ $status['comment_post_ID'] ][ $status['user_id'] ] = $status['comment_approved'];
1583
			}
1584
		}
1585
1586
		$course_completion = Sensei()->settings->settings[ 'course_completion' ];
1587
1588
		$per_page = 40;
1589
		$comment_id_offset = $count = 0;
1590
1591
		$course_sql = "SELECT * FROM $wpdb->comments WHERE comment_type = 'sensei_course_status' AND comment_ID > %d LIMIT $per_page";
1592
		// $per_page users at a time
1593
		while ( $course_statuses = $wpdb->get_results( $wpdb->prepare($course_sql, $comment_id_offset) ) ) {
1594
1595
			foreach ( $course_statuses AS $course_status ) {
1596
				$user_id = $course_status->user_id;
1597
				$course_id = $course_status->comment_post_ID;
1598
				$total_lessons = count( $course_lesson_ids[ $course_id ] );
1599
				if ( $total_lessons <= 0 ) {
1600
					$total_lessons = 1; // Fix division of zero error, some courses have no lessons
1601
				}
1602
				$lessons_completed = 0;
1603
				$status = 'in-progress';
1604
1605
				// Some Courses have no lessons... (can they ever be complete?)
1606
				if ( !empty($course_lesson_ids[ $course_id ]) ) {
1607
					foreach( $course_lesson_ids[ $course_id ] AS $lesson_id ) {
1608
						$lesson_status = $lesson_user_statuses[ $lesson_id ][ $user_id ];
1609
						// If lessons are complete without needing quizzes to be passed
1610 View Code Duplication
						if ( 'passed' != $course_completion ) {
1611
							switch ( $lesson_status ) {
1612
								// A user cannot 'complete' a course if a lesson...
1613
								case 'in-progress': // ...is still in progress
1614
								case 'ungraded': // ...hasn't yet been graded
1615
									break;
1616
1617
								default:
1618
									$lessons_completed++;
1619
									break;
1620
							}
1621
						}
1622
						else {
1623
							switch ( $lesson_status ) {
1624
								case 'complete': // Lesson has no quiz/questions
1625
								case 'graded': // Lesson has quiz, but it's not important what the grade was
1626
								case 'passed': // Lesson has quiz and the user passed
1627
									$lessons_completed++;
1628
									break;
1629
1630
								// A user cannot 'complete' a course if on a lesson...
1631
								case 'failed': // ...a user failed the passmark on a quiz
1632
								default:
1633
									break;
1634
							}
1635
						}
1636
					} // Each lesson
1637
				} // Check for lessons
1638
				if ( $lessons_completed == $total_lessons ) {
1639
					$status = 'complete';
1640
				}
1641
				// update the overall percentage of the course lessons complete (or graded) compared to 'in-progress' regardless of the above
1642
				$metadata = array(
1643
					'complete' => $lessons_completed,
1644
					'percent' => abs( round( ( doubleval( $lessons_completed ) * 100 ) / ( $total_lessons ), 0 ) ),
1645
				);
1646
				Sensei_Utils::update_course_status( $user_id, $course_id, $status, $metadata );
1647
				$count++;
1648
1649
			} // per course status
1650
			$comment_id_offset = $course_status->comment_ID;
1651
		} // all course statuses
1652
1653
		if ( $current_page == $total_pages ) {
1654
			return true;
1655
		} else {
1656
			return false;
1657
		}
1658
	}
1659
1660
	function status_changes_convert_questions( $n = 50, $offset = 0 ) {
1661
		global $wpdb;
1662
1663
		wp_defer_comment_counting( true );
1664
1665
		$user_count_result = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->users " );
1666
1667
		if ( 0 == $user_count_result ) {
1668
			return true;
1669
		}
1670
1671
		// Calculate if this is the last page
1672
		if ( 0 == $offset ) {
1673
			$current_page = 1;
1674
		} else {
1675
			$current_page = intval( $offset / $n );
1676
		}
1677
1678
		$total_pages = ceil( $user_count_result / $n );
1679
1680
		$users_sql = "SELECT ID FROM $wpdb->users ORDER BY ID ASC LIMIT %d OFFSET %d";
1681
		$answers_sql = "SELECT * FROM $wpdb->comments WHERE comment_type = 'sensei_user_answer' AND user_id = %d GROUP BY comment_post_ID ";
1682
		$grades_sql = "SELECT comment_post_ID, comment_content FROM $wpdb->comments WHERE comment_type = 'sensei_user_grade' AND user_id = %d GROUP BY comment_post_ID ";
1683
		$notes_sql = "SELECT comment_post_ID, comment_content FROM $wpdb->comments WHERE comment_type = 'sensei_answer_notes' AND user_id = %d GROUP BY comment_post_ID ";
1684
1685
		$user_ids = $wpdb->get_col( $wpdb->prepare($users_sql, $n, $offset) );
1686
1687
		foreach ( $user_ids AS $user_id ) {
1688
1689
			$answer_grades = $answer_notes = array();
1690
1691
			// Pre-process the answer grades
1692
			$_answer_grades = $wpdb->get_results( $wpdb->prepare($grades_sql, $user_id), ARRAY_A );
1693
			foreach ( $_answer_grades as $answer_grade ) {
1694
				// This will overwrite existing entries with the newer ones
1695
				$answer_grades[ $answer_grade['comment_post_ID'] ] = $answer_grade['comment_content'];
1696
			}
1697
			unset( $_answer_grades );
1698
1699
			// Pre-process the answer notes
1700
			$_answer_notes = $wpdb->get_results( $wpdb->prepare($notes_sql, $user_id), ARRAY_A );
1701
			foreach ( $_answer_notes as $answer_note ) {
1702
				// This will overwrite existing entries with the newer ones
1703
				$answer_notes[ $answer_note['comment_post_ID'] ] = $answer_note['comment_content'];
1704
			}
1705
			unset( $_answer_notes );
1706
1707
			// Grab all the questions for the user
1708
			$sql = $wpdb->prepare($answers_sql, $user_id);
1709
			$answers = $wpdb->get_results( $sql, ARRAY_A );
1710
			foreach ( $answers as $answer ) {
1711
1712
				// Excape data
1713
				$answer = wp_slash($answer);
1714
1715
				$comment_ID = $answer['comment_ID'];
1716
1717
				$meta_data = array();
1718
1719
				// Check if the question has been graded, add as meta
1720
				if ( !empty($answer_grades[ $answer['comment_post_ID'] ]) ) {
1721
					$meta_data['user_grade'] = $answer_grades[ $answer['comment_post_ID'] ];
1722
				}
1723
				// Check if there is an answer note, add as meta
1724
				if ( !empty($answer_notes[ $answer['comment_post_ID'] ]) ) {
1725
					$meta_data['answer_note'] = $answer_notes[ $answer['comment_post_ID'] ];
1726
				}
1727
1728
				// Wipe the unnessary data from the main comment
1729
				$data = array(
1730
						'comment_author' => '',
1731
						'comment_author_email' => '',
1732
						'comment_author_url' => '',
1733
						'comment_author_IP' => '',
1734
						'comment_agent' => '',
1735
					);
1736
				$data = array_merge($answer, $data);
1737
1738
				$rval = $wpdb->update( $wpdb->comments, $data, compact( 'comment_ID' ) );
1739
				if ( $rval ) {
1740
					if ( !empty($meta_data) ) {
1741
						foreach ( $meta_data as $key => $value ) {
1742
							// Bypassing WP wp_insert_comment( $data ), so no actions/filters are run
1743
							if ( $wpdb->get_var( $wpdb->prepare(
1744
									"SELECT COUNT(*) FROM $wpdb->commentmeta WHERE comment_id = %d AND meta_key = %s ",
1745
									$comment_ID, $key ) ) ) {
1746
									continue; // Found the meta data already
1747
							}
1748
							$result = $wpdb->insert( $wpdb->commentmeta, array(
1749
								'comment_id' => $comment_ID,
1750
								'meta_key' => $key,
1751
								'meta_value' => $value
1752
							) );
1753
						}
1754
					}
1755
				}
1756
			}
1757
		}
1758
		$wpdb->flush();
1759
1760
		if ( $current_page == $total_pages ) {
1761
			return true;
1762
		} else {
1763
			return false;
1764
		}
1765
	}
1766
1767
	/**
1768
	 * Updates all pre-existing Sensei activity types with a new status value
1769
	 *
1770
	 * @global type $wpdb
1771
	 * @return boolean
1772
	 */
1773
	public function update_legacy_sensei_comments_status() {
1774
		global $wpdb;
1775
1776
		// Update 'sensei_user_answer' entries to use comment_approved = 'log' so they don't appear in counts
1777
		$wpdb->query( "UPDATE $wpdb->comments SET comment_approved = 'log' WHERE comment_type = 'sensei_user_answer' " );
1778
1779
		// Mark all old Sensei comment types with comment_approved = 'legacy' so they no longer appear in counts, but can be restored if required
1780
		$wpdb->query( "UPDATE $wpdb->comments SET comment_approved = 'legacy' WHERE comment_type IN ('sensei_course_start', 'sensei_course_end', 'sensei_lesson_start', 'sensei_lesson_end', 'sensei_quiz_asked', 'sensei_user_grade', 'sensei_answer_notes', 'sensei_quiz_grade') " );
1781
1782
		return true;
1783
	}
1784
1785
	/**
1786
	 * Update the comment counts for all Courses and Lessons now that sensei comments will no longer be counted.
1787
	 *
1788
	 * @global type $wpdb
1789
	 * @param type $n
1790
	 * @param type $offset
1791
	 * @return boolean
1792
	 */
1793
	public function update_comment_course_lesson_comment_counts( $n = 50, $offset = 0 ) {
1794
		global $wpdb;
1795
1796
		$item_count_result = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type IN ('course', 'lesson') " );
1797
1798
		if ( 0 == $item_count_result ) {
1799
			return true;
1800
		}
1801
1802
		// Calculate if this is the last page
1803
		if ( 0 == $offset ) {
1804
			$current_page = 1;
1805
		} else {
1806
			$current_page = intval( $offset / $n );
1807
		}
1808
1809
		$total_pages = ceil( $item_count_result / $n );
1810
1811
		// Recalculate all counts
1812
		$items = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type IN ('course', 'lesson') LIMIT %d OFFSET %d", $n, $offset ) );
1813
		foreach ( (array) $items as $post ) {
1814
			// Code copied from wp_update_comment_count_now()
1815
			$new = (int) $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1'", $post->ID) );
1816
			$wpdb->update( $wpdb->posts, array('comment_count' => $new), array('ID' => $post->ID) );
1817
1818
			clean_post_cache( $post->ID );
1819
		}
1820
1821
		if ( $current_page == $total_pages ) {
1822
			return true;
1823
		} else {
1824
			return false;
1825
		}
1826
	}
1827
1828
	public function remove_legacy_comments () {
1829
		global $wpdb;
1830
1831
		$result = $wpdb->delete( $wpdb->comments, array( 'comment_approved' => 'legacy' ) );
1832
1833
		return true;
1834
	}
1835
1836
	public function index_comment_status_field () {
1837
		global $wpdb;
1838
1839
		$wpdb->query("ALTER TABLE `$wpdb->comments` ADD INDEX `comment_type` ( `comment_type` )");
1840
		$wpdb->query("ALTER TABLE `$wpdb->comments` ADD INDEX `comment_type_user_id` ( `comment_type`, `user_id` )");
1841
1842
		return true;
1843
1844
1845
	}
1846
1847
     /**
1848
     * WooThemes_Sensei_Updates::enhance_teacher_role
1849
     *
1850
     * This runs the update to create the teacher role
1851
     * @access public
1852
     * @since 1.8.0
1853
     * @return bool;
1854
     */
1855
    public  function enhance_teacher_role ( ) {
1856
1857
        require_once('class-sensei-teacher.php');
1858
        $teacher = new Sensei_Teacher();
1859
        $teacher->create_role();
1860
        return true;
1861
1862
    }// end enhance_teacher_role
1863
1864
} // End Class
1865
1866
/**
1867
 * Class WooThemes_Sensei_Updates
1868
 * @ignore only for backward compatibility
1869
 * @since 1.9.0
1870
 */
1871
class WooThemes_Sensei_Updates extends Sensei_Updates {}
1872