diff --git a/blocks/comments/block_comments.php b/blocks/comments/block_comments.php index af88a4fbe68..60bca8d8dbe 100644 --- a/blocks/comments/block_comments.php +++ b/blocks/comments/block_comments.php @@ -1,5 +1,31 @@ . +/** + * The comments block + * + * @package block + * @subpackage comments + * @copyright 2009 Dongsheng Cai + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// Obviously required require_once($CFG->dirroot . '/comment/lib.php'); class block_comments extends block_base { @@ -38,13 +64,12 @@ class block_comments extends block_base { $this->content->footer = ''; $this->content->text = ''; list($context, $course, $cm) = get_context_info_array($PAGE->context->id); - $args = new stdClass(); + + $args = new stdClass; $args->context = $PAGE->context; $args->course = $course; $args->area = 'page_comments'; $args->itemid = 0; - // set 'env' to tell moodle tweak ui for this block - $args->env = 'block_comments'; $args->component = 'block_comments'; $args->linktext = get_string('showcomments'); $args->notoggle = true; diff --git a/blog/lib.php b/blog/lib.php index e4ac2774a9e..c7882484a9d 100644 --- a/blog/lib.php +++ b/blog/lib.php @@ -966,3 +966,65 @@ function blog_get_associated_count($courseid, $cmid=null) { } return $DB->count_records('blog_association', array('contextid' => $context->id)); } + +/** + * Running addtional permission check on plugin, for example, plugins + * may have switch to turn on/off comments option, this callback will + * affect UI display, not like pluginname_comment_validate only throw + * exceptions. + * Capability check has been done in comment->check_permissions(), we + * don't need to do it again here. + * + * @param stdClass $comment_param { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return array + */ +function blog_comment_permissions($comment_param) { + return array('post'=>true, 'view'=>true); +} + +/** + * Validate comment parameter before perform other comments actions + * + * @param stdClass $comment { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return boolean + */ +function blog_comment_validate($comment_param) { + global $DB; + // validate comment itemid + if (!$entry = $DB->get_record('post', array('id'=>$comment_param->itemid))) { + throw new comment_exception('invalidcommentitemid'); + } + // validate comment area + if ($comment_param->commentarea != 'format_blog') { + throw new comment_exception('invalidcommentarea'); + } + // validation for comment deletion + if (!empty($comment_param->commentid)) { + if ($record = $DB->get_record('comments', array('id'=>$comment_param->commentid))) { + if ($record->commentarea != 'format_blog') { + throw new comment_exception('invalidcommentarea'); + } + if ($record->contextid != $comment_param->context->id) { + throw new comment_exception('invalidcontext'); + } + if ($record->itemid != $comment_param->itemid) { + throw new comment_exception('invalidcommentitemid'); + } + } else { + throw new comment_exception('invalidcommentid'); + } + } + return true; +} diff --git a/comment/comment.js b/comment/comment.js index 2774ce6b9f1..87f0e9e7ce9 100644 --- a/comment/comment.js +++ b/comment/comment.js @@ -40,7 +40,6 @@ M.core_comment = { this.component = args.component; this.courseid = args.courseid; this.contextid = args.contextid; - this.env = args.env; this.autostart = (args.autostart); // expand comments? if (this.autostart) { @@ -116,7 +115,6 @@ bodyContent: '
'; - $manager->print_comments($page); - echo ''; + $return = $manager->print_comments($page); + // if no comments available, $return will be false + if ($return) { + echo ''; + } echo ''; } diff --git a/comment/lib.php b/comment/lib.php index 9db0f77d50a..2bf59795394 100644 --- a/comment/lib.php +++ b/comment/lib.php @@ -1,5 +1,4 @@ context context to use for the comment [required] + * component => string which plugin will comment being added to [required] + * itemid => int the id of the associated item (forum post, glossary item etc) [required] + * area => string comment area + * cm => stdClass course module + * course => course course object + * client_id => string an unique id to identify comment area + * autostart => boolean automatically expend comments + * showcount => boolean display the number of comments + * displaycancel => boolean display cancel button + * notoggle => boolean don't show/hide button + * linktext => string title of show/hide button + * } */ - public function __construct($options) { - global $CFG, $DB; - - if (empty($CFG->commentsperpage)) { - $CFG->commentsperpage = 15; - } - + public function __construct(stdClass $options) { $this->viewcap = false; $this->postcap = false; @@ -142,7 +191,11 @@ class comment { } if (!empty($options->component)) { + // set and validate component $this->set_component($options->component); + } else { + // component cannot be empty + throw new comment_exception('invalidcomponent'); } // setup course @@ -174,13 +227,6 @@ class comment { $this->itemid = 0; } - // setup env - if (!empty($options->env)) { - $this->env = $options->env; - } else { - $this->env = ''; - } - // setup customized linktext if (!empty($options->linktext)) { $this->linktext = $options->linktext; @@ -188,10 +234,24 @@ class comment { $this->linktext = get_string('comments'); } - if (!empty($options->ignore_permission)) { - $this->ignore_permission = true; - } else { - $this->ignore_permission = false; + // setup options for callback functions + $this->comment_param = new stdClass(); + $this->comment_param->context = $this->context; + $this->comment_param->courseid = $this->courseid; + $this->comment_param->cm = $this->cm; + $this->comment_param->commentarea = $this->commentarea; + $this->comment_param->itemid = $this->itemid; + + $this->allowanonymousaccess = false; + // By default everyone can view comments on the front page + if ($this->context->contextlevel == CONTEXT_COURSE && $this->context->instanceid == SITEID) { + $this->allowanonymousaccess = true; + } else if ($this->context->contextlevel == CONTEXT_MODULE && $this->courseid == SITEID) { + $this->allowanonymousaccess = true; + } + if (!empty($this->plugintype) && !empty($this->pluginname)) { + // Plugins can override this if they wish. + $this->allowanonymousaccess = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'allow_anonymous_access', array($this), $this->allowanonymousaccess); } // setup notoggle @@ -209,38 +269,23 @@ class comment { $this->set_displaycancel($options->displaycancel); } + // setup displaytotalcount if (!empty($options->showcount)) { - $count = $this->count(); - if (empty($count)) { - $this->count = ''; - } else { - $this->count = '('.$count.')'; - } - } else { - $this->count = ''; + $this->set_displaytotalcount($options->showcount); } - // setup options for callback functions - $this->args = new stdClass(); - $this->args->context = $this->context; - $this->args->courseid = $this->courseid; - $this->args->cm = $this->cm; - $this->args->commentarea = $this->commentarea; - $this->args->itemid = $this->itemid; - // setting post and view permissions $this->check_permissions(); // load template - $this->template = <<___picture___
-
- ___name___ - ___time___ -
___content___
-
-EOD; + $this->template = html_writer::tag('div', '___picture___', array('class' => 'comment-userpicture')); + $this->template .= html_writer::start_tag('div', array('class' => 'comment-content')); + $this->template .= '___name___ - '; + $this->template .= html_writer::tag('span', '___time___'); + $this->template .= html_writer::tag('div', '___content___'); + $this->template .= html_writer::end_tag('div'); // .comment-content if (!empty($this->plugintype)) { - $this->template = plugin_callback($this->plugintype, $this->pluginname, FEATURE_COMMENT, 'template', $this->args, $this->template); + $this->template = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'template', array($this->comment_param), $this->template); } unset($options); @@ -248,34 +293,63 @@ EOD; /** * Receive nonjs comment parameters + * + * @param moodle_page $page The page object to initialise comments within + * If not provided the global $PAGE is used */ - public static function init() { - global $PAGE, $CFG; + public static function init(moodle_page $page = null) { + global $PAGE; + + if (empty($page)) { + $page = $PAGE; + } // setup variables for non-js interface - self::$nonjs = optional_param('nonjscomment', '', PARAM_ALPHA); + self::$nonjs = optional_param('nonjscomment', '', PARAM_ALPHANUM); self::$comment_itemid = optional_param('comment_itemid', '', PARAM_INT); self::$comment_context = optional_param('comment_context', '', PARAM_INT); self::$comment_page = optional_param('comment_page', '', PARAM_INT); self::$comment_area = optional_param('comment_area', '', PARAM_ALPHAEXT); - $PAGE->requires->string_for_js('addcomment', 'moodle'); - $PAGE->requires->string_for_js('deletecomment', 'moodle'); - $PAGE->requires->string_for_js('comments', 'moodle'); - $PAGE->requires->string_for_js('commentsrequirelogin', 'moodle'); + $page->requires->string_for_js('addcomment', 'moodle'); + $page->requires->string_for_js('deletecomment', 'moodle'); + $page->requires->string_for_js('comments', 'moodle'); + $page->requires->string_for_js('commentsrequirelogin', 'moodle'); } + /** + * Sets the component. + * + * This method shouldn't be public, changing the component once it has been set potentially + * invalidates permission checks. + * A coding_error is now thrown if code attempts to change the component. + * + * @param string $component + * @return void + */ public function set_component($component) { + if (!empty($this->component) && $this->component !== $component) { + throw new coding_exception('You cannot change the component of a comment once it has been set'); + } $this->component = $component; list($this->plugintype, $this->pluginname) = normalize_component($component); - return null; } + /** + * Determines if the user can view the comment. + * + * @param bool $value + */ public function set_view_permission($value) { - $this->viewcap = $value; + $this->viewcap = (bool)$value; } + /** + * Determines if the user can post a comment + * + * @param bool $value + */ public function set_post_permission($value) { - $this->postcap = $value; + $this->postcap = (bool)$value; } /** @@ -285,12 +359,11 @@ EOD; * function named $pluginname_check_comment_post must be implemented */ private function check_permissions() { - global $CFG; $this->postcap = has_capability('moodle/comment:post', $this->context); $this->viewcap = has_capability('moodle/comment:view', $this->context); if (!empty($this->plugintype)) { - $permissions = plugin_callback($this->plugintype, $this->pluginname, FEATURE_COMMENT, 'permissions', array($this->args), array('post'=>true, 'view'=>true)); - if ($this->ignore_permission) { + $permissions = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'permissions', array($this->comment_param), array('post'=>false, 'view'=>false)); + if ($this->allowanonymousaccess) { $this->postcap = $permissions['post']; $this->viewcap = $permissions['view']; } else { @@ -319,7 +392,7 @@ EOD; 'comment_context' => $this->context->id, 'comment_area' => $this->commentarea, )); - $link->remove_params(array('nonjscomment', 'comment_page')); + $link->remove_params(array('comment_page')); return $link; } @@ -359,6 +432,18 @@ EOD; $this->displaycancel = (bool)$newvalue; } + /** + * Sets the displaytotalcount option + * + * If set to true then the total number of comments will be displayed + * when printing comments. + * + * @param bool $newvalue + */ + public function set_displaytotalcount($newvalue = true) { + $this->displaytotalcount = (bool)$newvalue; + } + /** * Initialises the JavaScript that enchances the comment API. * @@ -374,7 +459,6 @@ EOD; $options->page = 0; $options->courseid = $this->courseid; $options->contextid = $this->contextid; - $options->env = $this->env; $options->component = $this->component; $options->notoggle = $this->notoggle; $options->autostart = $this->autostart; @@ -404,12 +488,12 @@ EOD; // print html template // Javascript will use the template to render new comments - if (empty($template_printed) && !empty($this->viewcap)) { + if (empty($template_printed) && $this->can_view()) { $html .= html_writer::tag('div', $this->template, array('style' => 'display:none', 'id' => 'cmt-tmpl')); $template_printed = true; } - if (!empty($this->viewcap)) { + if ($this->can_view()) { // print commenting icon and tooltip $html .= html_writer::start_tag('div', array('class' => 'mdl-left')); $html .= html_writer::link($this->get_nojslink($PAGE), get_string('showcommentsnonjs'), array('class' => 'showcommentsnonjs')); @@ -417,27 +501,33 @@ EOD; if (!$this->notoggle) { // If toggling is enabled (notoggle=false) then print the controls to toggle // comments open and closed + $countstring = ''; + if ($this->displaytotalcount) { + $countstring = '('.$this->count().')'; + } $html .= html_writer::start_tag('a', array('class' => 'comment-link', 'id' => 'comment-link-'.$this->cid, 'href' => '#')); $html .= html_writer::empty_tag('img', array('id' => 'comment-img-'.$this->cid, 'src' => $OUTPUT->pix_url('t/collapsed'), 'alt' => $this->linktext, 'title' => $this->linktext)); - $html .= html_writer::tag('span', $this->linktext.' '.$this->count, array('id' => 'comment-link-text-'.$this->cid)); + $html .= html_writer::tag('span', $this->linktext.' '.$countstring, array('id' => 'comment-link-text-'.$this->cid)); $html .= html_writer::end_tag('a'); } $html .= html_writer::start_tag('div', array('id' => 'comment-ctrl-'.$this->cid, 'class' => 'comment-ctrl')); - $html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list')); - $html .= html_writer::tag('li', '', array('class' => 'first')); if ($this->autostart) { // If autostart has been enabled print the comments list immediatly + $html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list comments-loaded')); + $html .= html_writer::tag('li', '', array('class' => 'first')); $html .= $this->print_comments(0, true, false); $html .= html_writer::end_tag('ul'); // .comment-list $html .= $this->get_pagination(0); } else { + $html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list')); + $html .= html_writer::tag('li', '', array('class' => 'first')); $html .= html_writer::end_tag('ul'); // .comment-list $html .= html_writer::tag('div', '', array('id' => 'comment-pagination-'.$this->cid, 'class' => 'comment-pagination')); } - if (!empty($this->postcap)) { + if ($this->can_post()) { // print posting textarea $html .= html_writer::start_tag('div', array('class' => 'comment-area')); $html .= html_writer::start_tag('div', array('class' => 'db')); @@ -478,15 +568,15 @@ EOD; */ public function get_comments($page = '') { global $DB, $CFG, $USER, $OUTPUT; - if (empty($this->viewcap)) { + if (!$this->can_view()) { return false; } if (!is_numeric($page)) { $page = 0; } - $this->page = $page; $params = array(); - $start = $page * $CFG->commentsperpage; + $perpage = (!empty($CFG->commentsperpage))?$CFG->commentsperpage:15; + $start = $page * $perpage; $ufields = user_picture::fields('u'); $sql = "SELECT $ufields, c.id AS cid, c.content AS ccontent, c.format AS cformat, c.timecreated AS ctimecreated FROM {comments} c @@ -498,9 +588,8 @@ EOD; $params['itemid'] = $this->itemid; $comments = array(); - $candelete = has_capability('moodle/comment:delete', $this->context); $formatoptions = array('overflowdiv' => true); - $rs = $DB->get_recordset_sql($sql, $params, $start, $CFG->commentsperpage); + $rs = $DB->get_recordset_sql($sql, $params, $start, $perpage); foreach ($rs as $u) { $c = new stdClass(); $c->id = $u->cid; @@ -512,8 +601,9 @@ EOD; $c->fullname = fullname($u); $c->time = userdate($c->timecreated, get_string('strftimerecent', 'langconfig')); $c->content = format_text($c->content, $c->format, $formatoptions); - $c->avatar = $OUTPUT->user_picture($u, array('size'=>18)); + + $candelete = $this->can_delete($c->id); if (($USER->id == $u->id) || !empty($candelete)) { $c->delete = true; } @@ -523,31 +613,45 @@ EOD; if (!empty($this->plugintype)) { // moodle module will filter comments - $comments = plugin_callback($this->plugintype, $this->pluginname, FEATURE_COMMENT, 'display', array($comments, $this->args), $comments); + $comments = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'display', array($comments, $this->comment_param), $comments); } return $comments; } + /** + * Returns the number of comments associated with the details of this object + * + * @global moodle_database $DB + * @return int + */ public function count() { global $DB; - if ($count = $DB->count_records('comments', array('itemid'=>$this->itemid, 'commentarea'=>$this->commentarea, 'contextid'=>$this->context->id))) { - return $count; - } else { - return 0; + if ($this->totalcommentcount === null) { + $this->totalcommentcount = $DB->count_records('comments', array('itemid' => $this->itemid, 'commentarea' => $this->commentarea, 'contextid' => $this->context->id)); } + return $this->totalcommentcount; } + /** + * Returns HTML to display a pagination bar + * + * @global stdClass $CFG + * @global core_renderer $OUTPUT + * @param int $page + * @return string + */ public function get_pagination($page = 0) { - global $DB, $CFG, $OUTPUT; + global $CFG, $OUTPUT; $count = $this->count(); - $pages = (int)ceil($count/$CFG->commentsperpage); + $perpage = (!empty($CFG->commentsperpage))?$CFG->commentsperpage:15; + $pages = (int)ceil($count/$perpage); if ($pages == 1 || $pages == 0) { return html_writer::tag('div', '', array('id' => 'comment-pagination-'.$this->cid, 'class' => 'comment-pagination')); } if (!empty(self::$nonjs)) { // used in non-js interface - return $OUTPUT->paging_bar($count, $page, $CFG->commentsperpage, $this->get_nojslink(), 'comment_page'); + return $OUTPUT->paging_bar($count, $page, $perpage, $this->get_nojslink(), 'comment_page'); } else { // return ajax paging bar $str = ''; @@ -567,16 +671,18 @@ EOD; /** * Add a new comment + * + * @global moodle_database $DB * @param string $content * @return mixed */ public function add($content, $format = FORMAT_MOODLE) { global $CFG, $DB, $USER, $OUTPUT; - if (empty($this->postcap)) { + if (!$this->can_post()) { throw new comment_exception('nopermissiontocomment'); } $now = time(); - $newcmt = new stdClass(); + $newcmt = new stdClass; $newcmt->contextid = $this->contextid; $newcmt->commentarea = $this->commentarea; $newcmt->itemid = $this->itemid; @@ -585,20 +691,15 @@ EOD; $newcmt->userid = $USER->id; $newcmt->timecreated = $now; - if (!empty($this->plugintype)) { - // moodle module will check content - $ret = plugin_callback($this->plugintype, $this->pluginname, FEATURE_COMMENT, 'add', array(&$newcmt, $this->args), true); - if (!$ret) { - throw new comment_exception('modulererejectcomment'); - } - } + // This callback allow module to modify the content of comment, such as filter or replacement + plugin_callback($this->plugintype, $this->pluginname, 'comment', 'add', array(&$newcmt, $this->comment_param)); $cmt_id = $DB->insert_record('comments', $newcmt); if (!empty($cmt_id)) { $newcmt->id = $cmt_id; $newcmt->time = userdate($now, get_string('strftimerecent', 'langconfig')); $newcmt->fullname = fullname($USER); - $url = new moodle_url('/user/view.php', array('id'=>$USER->id, 'course'=>$this->courseid)); + $url = new moodle_url('/user/view.php', array('id' => $USER->id, 'course' => $this->courseid)); $newcmt->profileurl = $url->out(); $newcmt->content = format_text($newcmt->content, $format, array('overflowdiv'=>true)); $newcmt->avatar = $OUTPUT->user_picture($USER, array('size'=>16)); @@ -610,7 +711,7 @@ EOD; /** * delete by context, commentarea and itemid - * @param object $param { + * @param stdClass|array $param { * contextid => int the context in which the comments exist [required] * commentarea => string the comment area [optional] * itemid => int comment itemid [optional] @@ -629,7 +730,8 @@ EOD; /** * Delete page_comments in whole course, used by course reset - * @param object $context course context + * + * @param stdClass $context course context */ public function reset_course_page_comments($context) { global $DB; @@ -645,6 +747,7 @@ EOD; /** * Delete a comment + * * @param int $commentid * @return mixed */ @@ -663,6 +766,7 @@ EOD; /** * Print comments + * * @param int $page * @param boolean $return return comments list string or print it out * @param boolean $nonjs print nonjs comments list or not? @@ -670,6 +774,11 @@ EOD; */ public function print_comments($page = 0, $return = true, $nonjs = true) { global $DB, $CFG, $PAGE; + + if (!$this->can_view()) { + return ''; + } + $html = ''; if (!(self::$comment_itemid == $this->itemid && self::$comment_context == $this->context->id && @@ -680,38 +789,34 @@ EOD; $html = ''; if ($nonjs) { - $html .= '

'.get_string('comments').'

'; - $html .= "'; + $html .= html_writer::end_tag('ul'); $html .= $this->get_pagination($page); } - $sesskey = sesskey(); - $returnurl = $PAGE->url; - $strsubmit = get_string('submit'); - if ($nonjs) { - $html .= << - - - - - - - - - - - -EOD; + if ($nonjs && $this->can_post()) { + // Form to add comments + $html .= html_writer::start_tag('form', array('method' => 'post', 'action' => new moodle_url('/comment/comment_post.php'))); + // Comment parameters + $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'contextid', 'value' => $this->contextid)); + $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'add')); + $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'area', 'value' => $this->commentarea)); + $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'component', 'value' => $this->component)); + $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'itemid', 'value' => $this->itemid)); + $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'courseid', 'value' => $this->courseid)); + $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey())); + $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'returnurl', 'value' => $PAGE->url)); + // Textarea for the actual comment + $html .= html_writer::tag('textarea', '', array('name' => 'content', 'rows' => 2)); + // Submit button to add the comment + $html .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('submit'))); + $html .= html_writer::end_tag('form'); } if ($return) { return $html; @@ -720,14 +825,35 @@ EOD; } } + /** + * Returns an array containing comments in HTML format. + * + * @global core_renderer $OUTPUT + * @param stdClass $cmt { + * id => int comment id + * content => string comment content + * format => int comment text format + * timecreated => int comment's timecreated + * profileurl => string link to user profile + * fullname => comment author's full name + * avatar => string user's avatar + * delete => boolean does user have permission to delete comment? + * } + * @param bool $nonjs + * @return array + */ public function print_comment($cmt, $nonjs = true) { global $OUTPUT; $patterns = array(); $replacements = array(); if (!empty($cmt->delete) && empty($nonjs)) { - $cmt->content = '
'.get_string('delete').'
' . $cmt->content; - // add the button + $deletelink = html_writer::start_tag('div', array('class'=>'comment-delete')); + $deletelink .= html_writer::start_tag('a', array('href' => '#', 'id' => 'comment-delete-'.$this->cid.'-'.$cmt->id)); + $deletelink .= $OUTPUT->pix_icon('t/delete', get_string('delete')); + $deletelink .= html_writer::end_tag('a'); + $deletelink .= html_writer::end_tag('div'); + $cmt->content = $deletelink . $cmt->content; } $patterns[] = '___picture___'; $patterns[] = '___name___'; @@ -741,12 +867,101 @@ EOD; // use html template to format a single comment. return str_replace($patterns, $replacements, $this->template); } + + /** + * Revoke validate callbacks + * + * @param stdClass $params addtionall parameters need to add to callbacks + */ + protected function validate($params=array()) { + foreach ($params as $key=>$value) { + $this->comment_param->$key = $value; + } + $validation = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'validate', array($this->comment_param), false); + if (!$validation) { + throw new comment_exception('invalidcommentparam'); + } + } + + /** + * Returns true if the user is able to view comments + * @return bool + */ + public function can_view() { + $this->validate(); + return !empty($this->viewcap); + } + + /** + * Returns true if the user can add comments against this comment description + * @return bool + */ + public function can_post() { + $this->validate(); + return isloggedin() && !empty($this->postcap); + } + + /** + * Returns true if the user can delete this comment + * @param int $commentid + * @return bool + */ + public function can_delete($commentid) { + $this->validate(array('commentid'=>$commentid)); + return has_capability('moodle/comment:delete', $this->context); + } + + /** + * Returns the component associated with the comment + * @return string + */ + public function get_compontent() { + return $this->component; + } + + /** + * Returns the context associated with the comment + * @return stdClass + */ + public function get_context() { + return $this->context; + } + + /** + * Returns the course id associated with the comment + * @return int + */ + public function get_courseid() { + return $this->courseid; + } + + /** + * Returns the course module associated with the comment + * + * @return stdClass + */ + public function get_cm() { + return $this->cm; + } + + /** + * Returns the item id associated with the comment + * + * @return int + */ + public function get_itemid() { + return $this->itemid; + } + + /** + * Returns the comment area associated with the commentarea + * + * @return stdClass + */ + public function get_commentarea() { + return $this->commentarea; + } } class comment_exception extends moodle_exception { - public $message; - function __construct($errorcode) { - $this->errorcode = $errorcode; - $this->message = get_string($errorcode, 'error'); - } } diff --git a/comment/locallib.php b/comment/locallib.php index 41ab72872a7..fd62b619347 100644 --- a/comment/locallib.php +++ b/comment/locallib.php @@ -23,46 +23,70 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_manager { + + /** + * The number of comments to display per page + * @var int + */ private $perpage; - function __construct() { + + /** + * Constructs the comment_manage object + */ + public function __construct() { global $CFG; $this->perpage = $CFG->commentsperpage; } /** * Return comments by pages + * + * @global moodle_database $DB * @param int $page - * @return mixed + * @return array An array of comments */ function get_comments($page) { - global $DB, $CFG, $USER; - $params = array(); + global $DB; + if ($page == 0) { $start = 0; } else { - $start = $page*$this->perpage; + $start = $page * $this->perpage; } - $sql = "SELECT c.id, c.contextid, c.itemid, c.commentarea, c.userid, c.content, u.firstname, u.lastname, c.timecreated - FROM {comments} c, {user} u - WHERE u.id=c.userid ORDER BY c.timecreated ASC"; - $comments = array(); + + $sql = "SELECT c.id, c.contextid, c.itemid, c.commentarea, c.userid, c.content, u.firstname, u.lastname, c.timecreated + FROM {comments} c + JOIN {user} u + ON u.id=c.userid + ORDER BY c.timecreated ASC"; + $rs = $DB->get_recordset_sql($sql, null, $start, $this->perpage); $formatoptions = array('overflowdiv' => true); - if ($records = $DB->get_records_sql($sql, array(), $start, $this->perpage)) { - foreach ($records as $item) { - $item->fullname = fullname($item); - $item->time = userdate($item->timecreated); - $item->content = format_text($item->content, FORMAT_MOODLE, $formatoptions); - $comments[] = $item; - unset($item->firstname); - unset($item->lastname); - unset($item->timecreated); - } + foreach ($rs as $item) { + // Set calculated fields + $item->fullname = fullname($item); + $item->time = userdate($item->timecreated); + $item->content = format_text($item->content, FORMAT_MOODLE, $formatoptions); + // Unset fields not related to the comment + unset($item->firstname); + unset($item->lastname); + unset($item->timecreated); + // Record the comment + $comments[] = $item; } + $rs->close(); return $comments; } + /** + * Records the course object + * + * @global moodle_page $PAGE + * @global moodle_database $DB + * @param int $courseid + * @return void + */ private function setup_course($courseid) { global $PAGE, $DB; if (!empty($this->course)) { @@ -71,88 +95,116 @@ class comment_manager { } if ($courseid == $PAGE->course->id) { $this->course = $PAGE->course; - } else if (!$this->course = $DB->get_record('course', array('id'=>$courseid))) { + } else if (!$this->course = $DB->get_record('course', array('id' => $courseid))) { $this->course = null; } } + /** + * Sets up the module or block information for a comment + * + * @global moodle_database $DB + * @param stdClass $comment + * @return bool + */ private function setup_plugin($comment) { global $DB; $this->context = get_context_instance_by_id($comment->contextid); - if (!is_object($this->context)) { - return; + if (!$this->context) { + return false; } - if ($this->context->contextlevel == CONTEXT_BLOCK) { - if ($block = $DB->get_record('block_instances', array('id'=>$this->context->instanceid))) { - $this->plugintype = 'block'; - $this->pluginname = $block->blockname; - } - } - if ($this->context->contextlevel == CONTEXT_MODULE) { - $this->plugintype = 'mod'; - $this->cm = get_coursemodule_from_id('', $this->context->instanceid); - $this->setup_course($this->cm->course); - $this->modinfo = get_fast_modinfo($this->course); - $this->pluginname = $this->modinfo->cms[$this->cm->id]->modname; + switch ($this->context->contextlevel) { + case CONTEXT_BLOCK: + if ($block = $DB->get_record('block_instances', array('id' => $this->context->instanceid))) { + $this->plugintype = 'block'; + $this->pluginname = $block->blockname; + } else { + return false; + } + break; + case CONTEXT_MODULE: + $this->plugintype = 'mod'; + $this->cm = get_coursemodule_from_id('', $this->context->instanceid); + $this->setup_course($this->cm->course); + $this->modinfo = get_fast_modinfo($this->course); + $this->pluginname = $this->modinfo->cms[$this->cm->id]->modname; + break; } + return true; } /** * Print comments * @param int $page + * @return boolean return false if no comments available */ - function print_comments($page=0) { - global $CFG, $OUTPUT, $DB; - $count = $DB->count_records_sql('SELECT COUNT(*) FROM {comments} c'); + public function print_comments($page = 0) { + global $OUTPUT, $CFG, $OUTPUT, $DB; + + $count = $DB->count_records('comments'); $comments = $this->get_comments($page); + if (count($comments) == 0) { + echo $OUTPUT->notification(get_string('nocomments', 'moodle')); + return false; + } + $table = new html_table(); - $table->head = array (html_writer::checkbox('selectall', '', false, get_string('selectall'), array('id'=>'comment_select_all', 'class'=>'comment-report-selectall')), get_string('author', 'search'), get_string('content'), get_string('action')); + $table->head = array ( + html_writer::checkbox('selectall', '', false, get_string('selectall'), array('id'=>'comment_select_all', 'class'=>'comment-report-selectall')), + get_string('author', 'search'), + get_string('content'), + get_string('action') + ); $table->align = array ('left', 'left', 'left', 'left'); $table->attributes = array('class'=>'generaltable commentstable'); $table->data = array(); - $linkbase = $CFG->wwwroot.'/comment/index.php?action=delete&sesskey='.sesskey(); + + $link = new moodle_url('/comment/index.php', array('action' => 'delete', 'sesskey' => sesskey())); foreach ($comments as $c) { - $link = $linkbase . '&commentid='. $c->id; $this->setup_plugin($c); if (!empty($this->plugintype)) { - $context_url = plugin_callback($this->plugintype, $this->pluginname, FEATURE_COMMENT, 'url', array($c)); + $context_url = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'url', array($c)); } $checkbox = html_writer::checkbox('comments', $c->id, false); - $action = html_writer::link($link, get_string('delete')); + $action = html_writer::link(new moodle_url($link, array('commentid' => $c->id)), get_string('delete')); if (!empty($context_url)) { - $action .= html_writer::tag('br', null); + $action .= html_writer::empty_tag('br'); $action .= html_writer::link($context_url, get_string('commentincontext'), array('target'=>'_blank')); } $table->data[] = array($checkbox, $c->fullname, $c->content, $action); } echo html_writer::table($table); echo $OUTPUT->paging_bar($count, $page, $this->perpage, $CFG->wwwroot.'/comment/index.php'); + return true; } /** - * delete a comment + * Delete a comment + * * @param int $commentid + * @return bool */ public function delete_comment($commentid) { global $DB; - if ($comment = $DB->get_record('comments', array('id'=>$commentid))) { - $DB->delete_records('comments', array('id'=>$commentid)); + if ($DB->record_exists('comments', array('id' => $commentid))) { + $DB->delete_records('comments', array('id' => $commentid)); return true; } return false; } /** - * delete comments - * @param int $commentid + * Delete comments + * + * @param string $list A list of comment ids separated by hyphens + * @return bool */ public function delete_comments($list) { global $DB; $ids = explode('-', $list); foreach ($ids as $id) { - if (is_int((int)$id)) { - if ($comment = $DB->get_record('comments', array('id'=>$id))) { - $DB->delete_records('comments', array('id'=>$comment->id)); - } + $id = (int)$id; + if ($DB->record_exists('comments', array('id' => $id))) { + $DB->delete_records('comments', array('id' => $id)); } } return true; diff --git a/lang/en/error.php b/lang/en/error.php index 8c43d35a647..d7199dc652a 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -32,6 +32,7 @@ $string['blockcannotread'] = 'Could not read data for blockid= {$a}'; $string['blockdoesnotexist'] = 'This block does not exist'; $string['blockdoesnotexistonpage'] = 'This block (id={$a->instanceid}) does not exist on this page ({$a->url}).'; $string['blocknameconflict'] = 'Naming conflict: block {$a->name} has the same title with an existing block: {$a->conflict}!'; +$string['callbackrejectcomment'] = 'Comment callback rejected this comment.'; $string['cannotaddcoursemodule'] = 'Could not add a new course module'; $string['cannotaddcoursemoduletosection'] = 'Could not add the new course module to that section'; $string['cannotaddmodule'] = '{$a} module could not be added to the module list!'; @@ -262,6 +263,11 @@ $string['invalidbulkenrolop'] = 'Invalid bulk enrolment operation requested.'; $string['invalidcategory'] = 'Incorrect category!'; $string['invalidcategoryid'] = 'Incorrect category id!'; $string['invalidcomment'] = 'Comment is incorrect'; +$string['invalidcommentid'] = 'Invalid comment id'; +$string['invalidcommentitemid'] = 'Invalid comment itemid'; +$string['invalidcommentarea'] = 'Invalid comment area'; +$string['invalidcommentparam'] = 'Invalid comment parameters'; +$string['invalidcomponent'] = 'Invalid component name'; $string['invalidconfirmdata'] = 'Invalid confirmation data'; $string['invalidcontext'] = 'Invalid context'; $string['invalidcourse'] = 'Invalid course'; @@ -344,7 +350,6 @@ $string['moduledisable'] = 'This module ({$a}) has been disabled for this partic $string['moduledoesnotexist'] = 'This module does not exist'; $string['moduleinstancedoesnotexist'] = 'The instance of this module does not exist'; $string['modulemissingcode'] = 'Module {$a} is missing the code needed to perform this function'; -$string['modulerejectcomment'] = 'Module rejects to add this comment'; $string['multiplerecordsfound'] = 'Multiple records found, only one record expected.'; $string['multiplerestorenotallow'] = 'Multiple restore execution not allowed!'; $string['mustbeloggedin'] = 'You must be logged in to do this'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index d4d24252870..0d6215b3841 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -242,6 +242,7 @@ $string['closewindow'] = 'Close this window'; $string['collapseall'] = 'Collapse all'; $string['commentincontext'] = 'Find this comment in context'; $string['comments'] = 'Comments'; +$string['commentsnotenabled'] = 'Comments feature is not enabled'; $string['commentsrequirelogin'] = 'You need to login to view the comments'; $string['comparelanguage'] = 'Compare and edit current language'; $string['complete'] = 'Complete'; @@ -1137,6 +1138,7 @@ $string['nobody'] = 'Nobody'; $string['nocourses'] = 'No courses'; $string['nocoursesfound'] = 'No courses were found with the words \'{$a}\''; $string['nocoursesyet'] = 'No courses in this category'; +$string['nocomments'] = 'No comments'; $string['nodstpresets'] = 'The administrator has not enabled Daylight Savings Time support.'; $string['nofilesselected'] = 'No files have been selected to restore'; $string['nofilesyet'] = 'No files have been uploaded to your course yet'; diff --git a/lib/moodlelib.php b/lib/moodlelib.php index bad7c450897..9e84a877233 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -7284,7 +7284,7 @@ function plugin_callback($type, $name, $feature, $action, $options = null, $defa $name = clean_param($name, PARAM_SAFEDIR); $function = $name.'_'.$feature.'_'.$action; - $file = get_plugin_directory($type, $name) . '/lib.php'; + $file = get_component_directory($type . '_' . $name) . '/lib.php'; // Load library and look for function if (file_exists($file)) { diff --git a/mod/data/lang/en/data.php b/mod/data/lang/en/data.php index eb49b32bdee..0a3f1df4c3a 100644 --- a/mod/data/lang/en/data.php +++ b/mod/data/lang/en/data.php @@ -62,6 +62,7 @@ $string['commentempty'] = 'Comment was empty'; $string['comments'] = 'Comments'; $string['commentsaved'] = 'Comment saved'; $string['commentsn'] = '{$a} comment(s)'; +$string['commentsoff'] = 'Comments feature is not enabled'; $string['configenablerssfeeds'] = 'This switch will enable the possibility of RSS feeds for all databases. You will still need to turn feeds on manually in the settings for each database.'; $string['confirmdeletefield'] = 'You are about to delete this field, are you sure?'; $string['confirmdeleterecord'] = 'Are you sure you want to delete this entry?'; diff --git a/mod/data/lib.php b/mod/data/lib.php index ec767b76945..4001436a303 100644 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -3098,3 +3098,108 @@ function data_presets_export($course, $cm, $data, $tostorage=false) { // Return the full path to the exported preset file: return $exportfile; } + +/** + * Running addtional permission check on plugin, for example, plugins + * may have switch to turn on/off comments option, this callback will + * affect UI display, not like pluginname_comment_validate only throw + * exceptions. + * Capability check has been done in comment->check_permissions(), we + * don't need to do it again here. + * + * @param stdClass $comment_param { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return array + */ +function data_comment_permissions($comment_param) { + global $CFG, $DB; + if (!$record = $DB->get_record('data_records', array('id'=>$comment_param->itemid))) { + throw new comment_exception('invalidcommentitemid'); + } + if (!$data = $DB->get_record('data', array('id'=>$record->dataid))) { + throw new comment_exception('invalidid', 'data'); + } + if ($data->comments) { + return array('post'=>true, 'view'=>true); + } else { + return array('post'=>false, 'view'=>false); + } +} + +/** + * Validate comment parameter before perform other comments actions + * + * @param stdClass $comment_param { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return boolean + */ +function data_comment_validate($comment_param) { + global $DB; + // validate comment area + if ($comment_param->commentarea != 'database_entry') { + throw new comment_exception('invalidcommentarea'); + } + // validate itemid + if (!$record = $DB->get_record('data_records', array('id'=>$comment_param->itemid))) { + throw new comment_exception('invalidcommentitemid'); + } + if (!$data = $DB->get_record('data', array('id'=>$record->dataid))) { + throw new comment_exception('invalidid', 'data'); + } + if (!$course = $DB->get_record('course', array('id'=>$data->course))) { + throw new comment_exception('coursemisconf'); + } + if (!$cm = get_coursemodule_from_instance('data', $data->id, $course->id)) { + throw new comment_exception('invalidcoursemodule'); + } + if (!$data->comments) { + throw new comment_exception('commentsoff', 'data'); + } + $context = get_context_instance(CONTEXT_MODULE, $cm->id); + + //check if approved + if ($data->approval and !$record->approved and !data_isowner($record) and !has_capability('mod/data:approve', $context)) { + throw new comment_exception('notapproved', 'data'); + } + + // group access + if ($record->groupid) { + $groupmode = groups_get_activity_groupmode($cm, $course); + if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { + if (!groups_is_member($record->groupid)) { + throw new comment_exception('notmemberofgroup'); + } + } + } + // validate context id + if ($context->id != $comment_param->context->id) { + throw new comment_exception('invalidcontext'); + } + // validation for comment deletion + if (!empty($comment_param->commentid)) { + if ($comment = $DB->get_record('comments', array('id'=>$comment_param->commentid))) { + if ($comment->commentarea != 'database_entry') { + throw new comment_exception('invalidcommentarea'); + } + if ($comment->contextid != $comment_param->context->id) { + throw new comment_exception('invalidcontext'); + } + if ($comment->itemid != $comment_param->itemid) { + throw new comment_exception('invalidcommentitemid'); + } + } else { + throw new comment_exception('invalidcommentid'); + } + } + return true; +} diff --git a/mod/glossary/lang/en/glossary.php b/mod/glossary/lang/en/glossary.php index e81f1968e36..0cb92660d09 100644 --- a/mod/glossary/lang/en/glossary.php +++ b/mod/glossary/lang/en/glossary.php @@ -216,6 +216,7 @@ $string['nopermissiontodelcomment'] = 'You can\'t delete other people\'s comment $string['nopermissiontodelinglossary'] = 'You can\'t comments in this glossary!'; $string['nopermissiontoviewresult'] = 'You can only look at results for your own entries'; $string['notcategorised'] = 'Not categorised'; +$string['notapproved'] = 'glossary entry is not approved yet.'; $string['numberofentries'] = 'Number of entries'; $string['onebyline'] = '(one per line)'; $string['pluginadministration'] = 'Glossary administration'; diff --git a/mod/glossary/lib.php b/mod/glossary/lib.php index b79d4a63666..6815c736da5 100644 --- a/mod/glossary/lib.php +++ b/mod/glossary/lib.php @@ -2654,3 +2654,82 @@ function glossary_extend_settings_navigation(settings_navigation $settings, navi $glossarynode->add($string, $url, settings_navigation::TYPE_SETTING, null, null, new pix_icon('i/rss', '')); } } + +/** + * Running addtional permission check on plugin, for example, plugins + * may have switch to turn on/off comments option, this callback will + * affect UI display, not like pluginname_comment_validate only throw + * exceptions. + * Capability check has been done in comment->check_permissions(), we + * don't need to do it again here. + * + * @param stdClass $comment_param { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return array + */ +function glossary_comment_permissions($comment_param) { + return array('post'=>true, 'view'=>true); +} + +/** + * Validate comment parameter before perform other comments actions + * + * @param stdClass $comment_param { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return boolean + */ +function glossary_comment_validate($comment_param) { + global $DB; + // validate comment area + if ($comment_param->commentarea != 'glossary_entry') { + throw new comment_exception('invalidcommentarea'); + } + if (!$record = $DB->get_record('glossary_entries', array('id'=>$comment_param->itemid))) { + throw new comment_exception('invalidcommentitemid'); + } + if (!$glossary = $DB->get_record('glossary', array('id'=>$record->glossaryid))) { + throw new comment_exception('invalidid', 'data'); + } + if (!$course = $DB->get_record('course', array('id'=>$glossary->course))) { + throw new comment_exception('coursemisconf'); + } + if (!$cm = get_coursemodule_from_instance('glossary', $glossary->id, $course->id)) { + throw new comment_exception('invalidcoursemodule'); + } + $context = get_context_instance(CONTEXT_MODULE, $cm->id); + + if ($glossary->defaultapproval and !$record->approved and !has_capability('mod/glossary:approve', $context)) { + throw new comment_exception('notapproved', 'glossary'); + } + // validate context id + if ($context->id != $comment_param->context->id) { + throw new comment_exception('invalidcontext'); + } + // validation for comment deletion + if (!empty($comment_param->commentid)) { + if ($comment = $DB->get_record('comments', array('id'=>$comment_param->commentid))) { + if ($comment->commentarea != 'glossary_entry') { + throw new comment_exception('invalidcommentarea'); + } + if ($comment->contextid != $comment_param->context->id) { + throw new comment_exception('invalidcontext'); + } + if ($comment->itemid != $comment_param->itemid) { + throw new comment_exception('invalidcommentitemid'); + } + } else { + throw new comment_exception('invalidcommentid'); + } + } + return true; +} diff --git a/mod/wiki/lib.php b/mod/wiki/lib.php index b0204652963..024f2bef6d5 100644 --- a/mod/wiki/lib.php +++ b/mod/wiki/lib.php @@ -548,3 +548,92 @@ function wiki_extend_navigation(navigation_node $navref, $course, $module, $cm) function wiki_get_extra_capabilities() { return array('moodle/comment:view', 'moodle/comment:post', 'moodle/comment:delete'); } + +/** + * Running addtional permission check on plugin, for example, plugins + * may have switch to turn on/off comments option, this callback will + * affect UI display, not like pluginname_comment_validate only throw + * exceptions. + * Capability check has been done in comment->check_permissions(), we + * don't need to do it again here. + * + * @param stdClass $comment_param { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return array + */ +function wiki_comment_permissions($comment_param) { + return array('post'=>true, 'view'=>true); +} + +/** + * Validate comment parameter before perform other comments actions + * + * @param stdClass $comment_param { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return boolean + */ +function wiki_comment_validate($comment_param) { + global $DB, $CFG; + require_once($CFG->dirroot . '/mod/wiki/locallib.php'); + // validate comment area + if ($comment_param->commentarea != 'wiki_page') { + throw new comment_exception('invalidcommentarea'); + } + // validate itemid + if (!$record = $DB->get_record('wiki_pages', array('id'=>$comment_param->itemid))) { + throw new comment_exception('invalidcommentitemid'); + } + if (!$subwiki = wiki_get_subwiki($record->subwikiid)) { + throw new comment_exception('invalidsubwikiid'); + } + if (!$wiki = wiki_get_wiki_from_pageid($comment_param->itemid)) { + throw new comment_exception('invalidid', 'data'); + } + if (!$course = $DB->get_record('course', array('id'=>$wiki->course))) { + throw new comment_exception('coursemisconf'); + } + if (!$cm = get_coursemodule_from_instance('wiki', $wiki->id, $course->id)) { + throw new comment_exception('invalidcoursemodule'); + } + $context = get_context_instance(CONTEXT_MODULE, $cm->id); + // group access + if ($subwiki->groupid) { + $groupmode = groups_get_activity_groupmode($cm, $course); + if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { + if (!groups_is_member($subwiki->groupid)) { + throw new comment_exception('notmemberofgroup'); + } + } + } + // validate context id + if ($context->id != $comment_param->context->id) { + throw new comment_exception('invalidcontext'); + } + // validation for comment deletion + if (!empty($comment_param->commentid)) { + if ($comment = $DB->get_record('comments', array('id'=>$comment_param->commentid))) { + if ($comment->commentarea != 'wiki_page') { + throw new comment_exception('invalidcommentarea'); + } + if ($comment->contextid != $context->id) { + throw new comment_exception('invalidcontext'); + } + if ($comment->itemid != $comment_param->itemid) { + throw new comment_exception('invalidcommentitemid'); + } + } else { + throw new comment_exception('invalidcommentid'); + } + } + return true; +}