MDL-24081 completion Fixing sql injections and use of sql_ilike()
[moodle.git] / course / report / completion / index.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
19 /**
20  * Course completion progress report
21  *
22  * @package   moodlecore
23  * @copyright 2009 Catalyst IT Ltd
24  * @author    Aaron Barnes <aaronb@catalyst.net.nz>
25  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
27 require_once('../../../config.php');
28 require_once($CFG->libdir.'/completionlib.php');
30 /**
31  * Configuration
32  */
33 define('COMPLETION_REPORT_PAGE',        25);
34 define('COMPLETION_REPORT_COL_TITLES',  true);
36 /**
37  * Setup page, check permissions
38  */
40 // Get course
41 $courseid = required_param('course', PARAM_INT);
42 $format = optional_param('format','',PARAM_ALPHA);
43 $sort = optional_param('sort','',PARAM_ALPHA);
44 $edituser = optional_param('edituser', 0, PARAM_INT);
47 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
49 $url = new moodle_url('/course/report/completion/index.php', array('course'=>$course->id));
50 $PAGE->set_url($url);
51 $PAGE->set_pagelayout('standard');
53 $firstnamesort = ($sort == 'firstname');
54 $excel = ($format == 'excelcsv');
55 $csv = ($format == 'csv' || $excel);
57 // Paging
58 $start   = optional_param('start', 0, PARAM_INT);
59 $sifirst = optional_param('sifirst', 'all', PARAM_ALPHA);
60 $silast  = optional_param('silast', 'all', PARAM_ALPHA);
62 // Whether to show idnumber
63 $idnumbers = $CFG->grade_report_showuseridnumber;
65 // Function for quoting csv cell values
66 function csv_quote($value) {
67     global $excel;
68     if($excel) {
69         $tl=textlib_get_instance();
70         return $tl->convert('"'.str_replace('"',"'",$value).'"','UTF-8','UTF-16LE');
71     } else {
72         return '"'.str_replace('"',"'",$value).'"';
73     }
74 }
77 // Check permissions
78 require_login($course);
80 $context=get_context_instance(CONTEXT_COURSE, $course->id);
81 require_capability('coursereport/completion:view', $context);
83 // Get group mode
84 $group = groups_get_course_group($course, true); // Supposed to verify group
85 if($group === 0 && $course->groupmode == SEPARATEGROUPS) {
86     require_capability('moodle/site:accessallgroups',$context);
87 }
89 /**
90  * Load data
91  */
93 // Get criteria for course
94 $completion = new completion_info($course);
96 if (!$completion->has_criteria()) {
97     print_error('err_nocriteria', 'completion', $CFG->wwwroot.'/course/report.php?id='.$course->id);
98 }
100 // Get criteria and put in correct order
101 $criteria = array();
103 foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_COURSE) as $criterion) {
104     $criteria[] = $criterion;
107 foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY) as $criterion) {
108     $criteria[] = $criterion;
111 foreach ($completion->get_criteria() as $criterion) {
112     if (!in_array($criterion->criteriatype, array(
113             COMPLETION_CRITERIA_TYPE_COURSE, COMPLETION_CRITERIA_TYPE_ACTIVITY))) {
114         $criteria[] = $criterion;
115     }
118 // Can logged in user mark users as complete?
119 // (if the logged in user has a role defined in the role criteria)
120 $allow_marking = false;
121 $allow_marking_criteria = null;
123 if (!$csv) {
124     // Get role criteria
125     $rcriteria = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_ROLE);
127     if (!empty($rcriteria)) {
129         foreach ($rcriteria as $rcriterion) {
130             $users = get_role_users($rcriterion->role, $context, true);
132             // If logged in user has this role, allow marking complete
133             if ($users && in_array($USER->id, array_keys($users))) {
134                 $allow_marking = true;
135                 $allow_marking_criteria = $rcriterion->id;
136                 break;
137             }
138         }
139     }
142 /**
143  * Setup page header
144  */
145 if ($csv) {
146     header('Content-Disposition: attachment; filename=progress.'.
147         preg_replace('/[^a-z0-9-]/','_',strtolower($course->shortname)).'.csv');
148     // Unicode byte-order mark for Excel
149     if($excel) {
150         header('Content-Type: text/csv; charset=UTF-16LE');
151         print chr(0xFF).chr(0xFE);
152         $sep="\t".chr(0);
153         $line="\n".chr(0);
154     } else {
155         header('Content-Type: text/csv; charset=UTF-8');
156         $sep=",";
157         $line="\n";
158     }
160 } else {
161     // Navigation and header
162     $strcompletion = get_string('completionreport','completion');
164     $PAGE->set_title($strcompletion);
165     $PAGE->set_heading($course->fullname);
167     echo $OUTPUT->header();
169     $PAGE->requires->yui2_lib(
170         array(
171             'yahoo',
172             'dom',
173             'element',
174             'event',
175         )
176     );
178     $PAGE->requires->js('/course/report/completion/textrotate.js');
180     // Handle groups (if enabled)
181     groups_print_course_menu($course, $CFG->wwwroot.'/course/report/completion/?course='.$course->id);
185 // Generate where clause
186 $where = array();
187 $where_params = array();
189 if ($sifirst !== 'all') {
190     $where[] = $DB->sql_like('u.firstname', ':sifirst', false);
191     $where_params['sifirst'] = $sifirst.'%';
194 if ($silast !== 'all') {
195     $where[] = $DB->sql_like('u.lastname', ':silast', false);
196     $where_params['silast'] = $silast.'%';
199 // Get user match count
200 $total = $completion->get_num_tracked_users(implode(' AND ', $where), $where_params, $group);
202 // Total user count
203 $grandtotal = $completion->get_num_tracked_users('', array(), $group);
205 // If no users in this course what-so-ever
206 if (!$grandtotal) {
207     echo $OUTPUT->container(get_string('err_nousers', 'completion'), 'errorbox errorboxcontent');
208     echo $OUTPUT->footer();
209     exit;
212 // Get user data
213 $progress = array();
215 if ($total) {
216     $progress = $completion->get_progress_all(
217         implode(' AND ', $where),
218         $where_params,
219         $group,
220         $firstnamesort ? 'u.firstname ASC' : 'u.lastname ASC',
221         $csv ? 0 : COMPLETION_REPORT_PAGE,
222         $csv ? 0 : $start
223     );
227 // Build link for paging
228 $link = $CFG->wwwroot.'/course/report/completion/?course='.$course->id;
229 if (strlen($sort)) {
230     $link .= '&amp;sort='.$sort;
232 $link .= '&amp;start=';
234 // Build the the page by Initial bar
235 $initials = array('first', 'last');
236 $alphabet = explode(',', get_string('alphabet', 'langconfig'));
238 $pagingbar = '';
239 foreach ($initials as $initial) {
240     $var = 'si'.$initial;
242     $pagingbar .= ' <div class="initialbar '.$initial.'initial">';
243     $pagingbar .= get_string($initial.'name').':&nbsp;';
245     if ($$var == 'all') {
246         $pagingbar .= '<strong>'.get_string('all').'</strong> ';
247     }
248     else {
249         $pagingbar .= '<a href="'.$link.'">'.get_string('all').'</a> ';
250     }
252     foreach ($alphabet as $letter) {
253         if ($$var === $letter) {
254             $pagingbar .= '<strong>'.$letter.'</strong> ';
255         }
256         else {
257             $pagingbar .= '<a href="'.$link.'&amp;'.$var.'='.$letter.'">'.$letter.'</a> ';
258         }
259     }
261     $pagingbar .= '</div>';
264 // Do we need a paging bar?
265 if($total > COMPLETION_REPORT_PAGE) {
267     // Paging bar
268     $pagingbar .= '<div class="paging">';
269     $pagingbar .= get_string('page').': ';
271     // Display previous link
272     if ($start > 0) {
273         $pstart = max($start - COMPLETION_REPORT_PAGE, 0);
274         $pagingbar .= '(<a class="previous" href="'.$link.$pstart.'">'.get_string('previous').'</a>)&nbsp;';
275     }
277     // Create page links
278     $curstart = 0;
279     $curpage = 0;
280     while ($curstart < $total) {
281         $curpage++;
283         if ($curstart == $start) {
284             $pagingbar .= '&nbsp;'.$curpage.'&nbsp;';
285         }
286         else {
287             $pagingbar .= '&nbsp;<a href="'.$link.$curstart.'">'.$curpage.'</a>&nbsp;';
288         }
290         $curstart += COMPLETION_REPORT_PAGE;
291     }
293     // Display next link
294     $nstart = $start + COMPLETION_REPORT_PAGE;
295     if ($nstart < $total) {
296         $pagingbar .= '&nbsp;(<a class="next" href="'.$link.$nstart.'">'.get_string('next').'</a>)';
297     }
299     $pagingbar .= '</div>';
303 /**
304  * Draw table header
305  */
307 // Start of table
308 if(!$csv) {
309     print '<br class="clearer"/>'; // ugh
311     $total_header = ($total == $grandtotal) ? $total : "{$total}/{$grandtotal}";
312     echo $OUTPUT->heading(get_string('allparticipants').": {$total_header}", 3);
314     print $pagingbar;
316     if (!$total) {
317         echo $OUTPUT->heading(get_string('nothingtodisplay'), 2);
318         echo $OUTPUT->footer();
319         exit;
320     }
322     print '<table id="completion-progress" class="generaltable flexible boxaligncenter completionreport" style="text-align: left" cellpadding="5" border="1">';
324     // Print criteria group names
325     print PHP_EOL.'<tr style="vertical-align: top">';
326     print '<th scope="row" class="rowheader">'.get_string('criteriagroup', 'completion').'</th>';
328     $current_group = false;
329     $col_count = 0;
330     for ($i = 0; $i <= count($criteria); $i++) {
332         if (isset($criteria[$i])) {
333             $criterion = $criteria[$i];
335             if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
336                 ++$col_count;
337                 continue;
338             }
339         }
341         // Print header cell
342         if ($col_count) {
343             print '<th scope="col" colspan="'.$col_count.'" class="colheader criteriagroup">'.$current_group->get_type_title().'</th>';
344         }
346         if (isset($criteria[$i])) {
347             // Move to next criteria type
348             $current_group = $criterion;
349             $col_count = 1;
350         }
351     }
353     // Overall course completion status
354     print '<th style="text-align: center;">'.get_string('course').'</th>';
356     print '</tr>';
358     // Print aggregation methods
359     print PHP_EOL.'<tr style="vertical-align: top">';
360     print '<th scope="row" class="rowheader">'.get_string('aggregationmethod', 'completion').'</th>';
362     $current_group = false;
363     $col_count = 0;
364     for ($i = 0; $i <= count($criteria); $i++) {
366         if (isset($criteria[$i])) {
367             $criterion = $criteria[$i];
369             if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
370                 ++$col_count;
371                 continue;
372             }
373         }
375         // Print header cell
376         if ($col_count) {
377             $has_agg = array(
378                 COMPLETION_CRITERIA_TYPE_COURSE,
379                 COMPLETION_CRITERIA_TYPE_ACTIVITY,
380                 COMPLETION_CRITERIA_TYPE_ROLE,
381             );
383             if (in_array($current_group->criteriatype, $has_agg)) {
384                 // Try load a aggregation method
385                 $method = $completion->get_aggregation_method($current_group->criteriatype);
387                 $method = $method == 1 ? 'All' : 'Any';
389             } else {
390                 $method = '-';
391             }
393             print '<th scope="col" colspan="'.$col_count.'" class="colheader aggheader">'.$method.'</th>';
394         }
396         if (isset($criteria[$i])) {
397             // Move to next criteria type
398             $current_group = $criterion;
399             $col_count = 1;
400         }
401     }
403     // Overall course aggregation method
404     print '<th scope="col" class="colheader aggheader aggcriteriacourse">';
406     // Get course aggregation
407     $method = $completion->get_aggregation_method();
409     print $method == 1 ? 'All' : 'Any';
410     print '</th>';
412     print '</tr>';
415     // Print criteria titles
416     if (COMPLETION_REPORT_COL_TITLES) {
418         print PHP_EOL.'<tr>';
419         print '<th scope="row" class="rowheader">'.get_string('criteria', 'completion').'</th>';
421         foreach ($criteria as $criterion) {
422             // Get criteria details
423             $details = $criterion->get_title_detailed();
424             print '<th scope="col" class="colheader criterianame">';
425             print '<span class="completion-criterianame">'.$details.'</span>';
426             print '</th>';
427         }
429         // Overall course completion status
430         print '<th scope="col" class="colheader criterianame">';
432         print '<span class="completion-criterianame">'.get_string('coursecomplete', 'completion').'</span>';
434         print '</th></tr>';
435     }
437     // Print user heading and icons
438     print '<tr>';
440     // User heading / sort option
441     print '<th scope="col" class="completion-sortchoice" style="clear: both;">';
442     if($firstnamesort) {
443         print
444             get_string('firstname').' / <a href="./?course='.$course->id.'">'.
445             get_string('lastname').'</a>';
446     } else {
447         print '<a href="./?course='.$course->id.'&amp;sort=firstname">'.
448             get_string('firstname').'</a> / '.
449             get_string('lastname');
450     }
451     print '</th>';
454     // Print user id number column
455     if($idnumbers) {
456         print '<th>'.get_string('idnumber').'</th>';
457     }
459     ///
460     /// Print criteria icons
461     ///
462     foreach ($criteria as $criterion) {
464         // Generate icon details
465         $icon = '';
466         $iconlink = '';
467         $icontitle = ''; // Required if $iconlink set
468         $iconalt = ''; // Required
469         switch ($criterion->criteriatype) {
471             case COMPLETION_CRITERIA_TYPE_ACTIVITY:
472                 // Load activity
473                 $activity = $criterion->get_mod_instance();
475                 // Display icon
476                 $icon = $OUTPUT->pix_url('icon', $criterion->module);
477                 $iconlink = $CFG->wwwroot.'/mod/'.$criterion->module.'/view.php?id='.$activity->id;
478                 $icontitle = $activity->name;
479                 $iconalt = get_string('modulename', $criterion->module);
480                 break;
482             case COMPLETION_CRITERIA_TYPE_COURSE:
483                 // Load course
484                 $crs = $DB->get_record('course', array('id' => $criterion->courseinstance));
486                 // Display icon
487                 $iconlink = $CFG->wwwroot.'/course/view.php?id='.$criterion->courseinstance;
488                 $icontitle = $crs->fullname;
489                 $iconalt = $crs->shortname;
490                 break;
492             case COMPLETION_CRITERIA_TYPE_ROLE:
493                 // Load role
494                 $role = $DB->get_record('role', array('id' => $criterion->role));
496                 // Display icon
497                 $iconalt = $role->name;
498                 break;
499         }
501         // Print icon and cell
502         print '<th class="criteriaicon">';
504         // Create icon if not supplied
505         if (!$icon) {
506             $icon = $OUTPUT->pix_url('i/'.$COMPLETION_CRITERIA_TYPES[$criterion->criteriatype]);
507         }
509         print ($iconlink ? '<a href="'.$iconlink.'" title="'.$icontitle.'">' : '');
510         print '<img src="'.$icon.'" class="icon" alt="'.$iconalt.'" '.(!$iconlink ? 'title="'.$iconalt.'"' : '').' />';
511         print ($iconlink ? '</a>' : '');
513         print '</th>';
514     }
516     // Overall course completion status
517     print '<th class="criteriaicon">';
518     print '<img src="'.$OUTPUT->pix_url('i/course').'" class="icon" alt="Course" title="Course Complete" />'; //TODO: localize
519     print '</th>';
521     print '</tr>';
524 } else {
525     // TODO
526     if($idnumbers) {
527         print $sep;
528     }
532 ///
533 /// Display a row for each user
534 ///
535 foreach ($progress as $user) {
537     // User name
538     if($csv) {
539         print csv_quote(fullname($user));
540         if($idnumbers) {
541             print $sep.csv_quote($user->idnumber);
542         }
543     } else {
544         print PHP_EOL.'<tr id="user-'.$user->id.'">';
546         print '<th scope="row"><a href="'.$CFG->wwwroot.'/user/view.php?id='.
547             $user->id.'&amp;course='.$course->id.'">'.fullname($user).'</a></th>';
548         if($idnumbers) {
549             print '<td>'.htmlspecialchars($user->idnumber).'</td>';
550         }
551     }
553     // Progress for each course completion criteria
554     foreach ($criteria as $criterion) {
556         // Handle activity completion differently
557         if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
559             // Load activity
560             $mod = $criterion->get_mod_instance();
561             $activity = $DB->get_record('course_modules', array('id' => $criterion->moduleinstance));
562             $activity->name = $mod->name;
565             // Get progress information and state
566             if(array_key_exists($activity->id,$user->progress)) {
567                 $thisprogress=$user->progress[$activity->id];
568                 $state=$thisprogress->completionstate;
569                 $date=userdate($thisprogress->timemodified);
570             } else {
571                 $state=COMPLETION_INCOMPLETE;
572                 $date='';
573             }
575             $criteria_completion = $completion->get_user_completion($user->id, $criterion);
577             // Work out how it corresponds to an icon
578             switch($state) {
579                 case COMPLETION_INCOMPLETE : $completiontype='n'; break;
580                 case COMPLETION_COMPLETE : $completiontype='y'; break;
581                 case COMPLETION_COMPLETE_PASS : $completiontype='pass'; break;
582                 case COMPLETION_COMPLETE_FAIL : $completiontype='fail'; break;
583             }
585             $completionicon='completion-'.
586                 ($activity->completion==COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual').
587                 '-'.$completiontype;
589             $describe=get_string('completion-alt-auto-'.$completiontype,'completion');
590             $a=new StdClass;
591             $a->state=$describe;
592             $a->date=$date;
593             $a->user=fullname($user);
594             $a->activity=strip_tags($activity->name);
595             $fulldescribe=get_string('progress-title','completion',$a);
597             if($csv) {
598                 print $sep.csv_quote($describe).$sep.csv_quote($date);
599             } else {
600                 print '<td class="completion-progresscell">';
602                 print '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).
603                       '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" />';
605                 print '</td>';
606             }
608             continue;
609         }
611         // Handle all other criteria
612         $criteria_completion = $completion->get_user_completion($user->id, $criterion);
613         $is_complete = $criteria_completion->is_complete();
615         $completiontype = $is_complete ? 'y' : 'n';
616         $completionicon = 'completion-auto-'.$completiontype;
618         $describe = get_string('completion-alt-auto-'.$completiontype, 'completion');
620         $a = new Object();
621         $a->state    = $describe;
622         $a->date     = $is_complete ? userdate($criteria_completion->timecompleted) : '';
623         $a->user     = fullname($user);
624         $a->activity = strip_tags($criterion->get_title());
625         $fulldescribe = get_string('progress-title', 'completion', $a);
627         if ($csv) {
628             print $sep.csv_quote($describe);
629         } else {
631             if ($allow_marking_criteria === $criterion->id) {
632                 $describe = get_string('completion-alt-auto-'.$completiontype,'completion');
634                 print '<td class="completion-progresscell">'.
635                     '<a href="'.$CFG->wwwroot.'/course/togglecompletion.php?user='.$user->id.'&course='.$course->id.'&rolec='.$allow_marking_criteria.'">'.
636                     '<img src="'.$OUTPUT->pix_url('i/completion-manual-'.($is_complete ? 'y' : 'n')).
637                     '" alt="'.$describe.'" class="icon" title="Mark as complete" /></a></td>'; //TODO: localize
638             } else {
639                 print '<td class="completion-progresscell">'.
640                     '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).
641                     '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" /></td>';
642             }
643         }
644     }
646     // Handle overall course completion
648     // Load course completion
649     $params = array(
650         'userid'    => $user->id,
651         'course'    => $course->id
652     );
654     $ccompletion = new completion_completion($params);
655     $completiontype =  $ccompletion->is_complete() ? 'y' : 'n';
657     $describe = get_string('completion-alt-auto-'.$completiontype, 'completion');
659     $a = new StdClass;
660     $a->state    = $describe;
661     $a->date     = '';
662     $a->user     = fullname($user);
663     $a->activity = strip_tags(get_string('coursecomplete', 'completion'));
664     $fulldescribe = get_string('progress-title', 'completion', $a);
666     if ($csv) {
667         print $sep.csv_quote($describe);
668     } else {
670         print '<td class="completion-progresscell">';
672         // Display course completion status icon
673         print '<img src="'.$OUTPUT->pix_url('i/completion-auto-'.$completiontype).
674                '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" />';
676         print '</td>';
677     }
679     if($csv) {
680         print $line;
681     } else {
682         print '</tr>';
683     }
686 if($csv) {
687     exit;
689 print '</table>';
690 print $pagingbar;
692 print '<ul class="progress-actions"><li><a href="index.php?course='.$course->id.
693     '&amp;format=csv">'.get_string('csvdownload','completion').'</a></li>
694     <li><a href="index.php?course='.$course->id.'&amp;format=excelcsv">'.
695     get_string('excelcsvdownload','completion').'</a></li></ul>';
697 echo $OUTPUT->footer($course);