File: D:/HostingSpaces/PvdBoogaard/indoorski.nl/backup/oude-site/cms/modules/comments/module.comments.php
<?php
/**
* This implements the 'Feedback' functionality.
*
* @version $Id$
* @author Gwilym Evans <gwilym.evans@interspire.com>
*
* @package IWP_Modules
* @subpackage Comments_Module
*/
/**
* Defintions for use by the comments system
*/
define('IWP_MODULE_COMMENTS_STATUS_APPROVED', 'approved');
define('IWP_MODULE_COMMENTS_STATUS_DISAPPROVED', 'disapproved');
define('IWP_MODULE_COMMENTS_STATUS_PENDING', 'pending');
define('IWP_MODULE_COMMENTS_MESSAGE_MINIMUMLENGTH', 10);
define('IWP_MODULE_COMMENTS_MESSAGE_MAXIMUMLENGTH', 60000);
define('IWP_MODULE_COMMENTS_OTHERFIELDS_MAXIMUMLENGTH', 255);
// taken from php manual comments - this will convert offsets from bytes to characters and is necessary after 5.2.7 fixed/changed how DOMText->splitText works
function mb_preg_match_all($ps_pattern, $ps_subject, &$pa_matches, $pn_flags = PREG_PATTERN_ORDER, $pn_offset = 0, $ps_encoding = NULL) {
// WARNING! - All this function does is to correct offsets, nothing else:
if (is_null($ps_encoding)) {
$ps_encoding = mb_internal_encoding();
}
$pn_offset = strlen(mb_substr($ps_subject, 0, $pn_offset, $ps_encoding));
$ret = preg_match_all($ps_pattern, $ps_subject, $pa_matches, $pn_flags, $pn_offset);
if ($ret && ($pn_flags & PREG_OFFSET_CAPTURE)) {
foreach($pa_matches as &$ha_match) {
foreach($ha_match as &$ha_match) {
$ha_match[1] = mb_strlen(substr($ps_subject, 0, $ha_match[1]), $ps_encoding);
}
}
}
//
// (code is independent of PREG_PATTER_ORDER / PREG_SET_ORDER)
return $ret;
}
/**
* Data binding class to support comment data
*
*/
class iwp_module_comments_commentdata extends iwp_engine
{
/**
* The column listing for the commentdata
*
* @var Array
**/
protected $tableFields = array(
'id',
'comment',
'fromname',
'fromemail',
'website',
'ip',
'contentid',
'status',
'parentid',
'userid',
'fromadmin',
'sortingnewest',
'sortingoldest',
'date',
'notifyreply',
'notifymoderate',
'uniqueid',
'counter',
);
/**
* Definition of the column list for comment data
*
* @var array
*/
protected $_columns = array(
'id' => '',
'comment' => '',
'fromname' => '',
'fromemail' => '',
'website' => '',
'ip' => '',
'contentid' => '',
'status' => '',
'parentid' => '',
'userid' => '',
'fromadmin' => '',
'sortingnewest' => '',
'sortingoldest' => '',
'date' => '',
'notifyreply' => '',
'notifymoderate' => '',
'uniqueid' => '',
'counter' => '',
);
/**
* A set of default values
*
* @var array
*/
protected $data = array(
'comment' => '',
'fromname' => '',
'fromemail' => '',
'website' => '',
'ip' => '',
'contentid' => '',
'status' => '',
'parentid' => '',
'userid' => '',
'fromadmin' => '',
'sortingnewest' => '',
'sortingoldest' => '',
'date' => '',
'notifyreply' => '',
'notifymoderate' => '',
'uniqueid' => '',
'counter' => '',
);
/**
* The primary key for the commentdata table
**/
protected $primaryKey = 'id';
/**
* The table name for the comment data
**/
protected $baseTableName = 'module_commentdata';
/**
* Holds any validation necessary for this commentdata
*
* @var Array
**/
protected $validation = array();
/**
* This static variable holds the current instance of this object being loaded.
* So using the getInstance function anywhere will return the very same instance.
*
* @var iwp_module_comments_commentdata Instance
**/
public static $Instance;
/**
* This variable holds an instance of the iwp_content this comment is related to. Use the GetContent method to load-once and access it.
*
* @var iwp_content Instance
*/
protected $Content = null;
/**
* This class extends the iwp_engine and uses the Save/Load functions which call this function, so we make a wrapper to our main module class.
*
* @return string
*/
public function GetTableName() {
return iwp_module_comments::getInstance()->getTable('commentdata');
}
/**
* Overriding the iwp_engine::Load method to automatically process siteURL placeholders.
*
* @see iwp_engine::Load
*/
public function Load ($id=null) {
$success = parent::Load($id);
if ($success) {
$comment = $this->Get('comment');
$comment = iwp_content::getInstance()->DecodeSiteURLs($comment);
$this->Set('comment', $comment);
}
return $success;
}
/**
* Overriding the iwp_engine::Save method to automatically process siteURL placeholders.
*
* @see iwp_engine::Save
*/
public function Save () {
$comment = $this->Get('comment');
$comment = iwp_content::getInstance()->EncodeSiteURLs($comment);
$this->Set('comment', $comment);
$success = parent::Save();
// because some comment code does a Save() and then continues to work on the comment, we should decode it immediately
$comment = iwp_content::getInstance()->DecodeSiteURLs($comment);
$this->Set('comment', $comment);
return $success;
}
/**
* Overriding the iwp_engine::Delete method to make use of activity log
*
* @see iwp_engine::Delete
*/
public function Delete () {
$text = $this->GetCommentAsActivityLogText();
$id = $this->GetId();
if (parent::Delete()) {
iwp_activity::AddEntry('module', 'comments', 'delete', $id, $text);
}
}
/**
* Returns an instance of iwp_content representing the content this comment is attached to.
*
* @return iwp_content Instance, or null if no matching content was found.
*/
public function GetContent ()
{
if (is_null($this->Content)) {
$content = new iwp_content();
$content->Load($this->Get('contentid'));
if ($content->GetId() == $this->Get('contentid')) {
$this->Content = $content;
}
}
return $this->Content;
}
/**
* Recursive function used internally to convert a comment body DOMElement into a text representation
*
*/
private function CommentTextFetcher (DOMNode $element) {
$return = '';
$isTextNode = is_a($element, 'DOMText');
if ($isTextNode) {
$return .= $element->textContent;
} else {
if ($element->nodeName == 'li') {
$return .= '* ';
}
}
if ($element->hasChildNodes()) {
for ($i = 0; $i < $element->childNodes->length; $i++) {
$return .= $this->CommentTextFetcher($element->childNodes->item($i));
}
}
if (!$isTextNode) {
if ($element->nodeName != 'body' && !in_array($element->nodeName, InterspireHTMLCleaner::$inlineTags)) {
$return .= "\n";
if ($element->nodeName == 'p') {
// again!
$return .= "\n";
}
}
if ($element->nodeName == 'a') {
$return .= ' <'. $element->getAttribute('href') .'>';
}
}
return $return;
}
/**
* Returns this comment as a text string useful for sending by plain-text emails.
*
* @return string
*/
public function GetCommentAsText () {
$cleaner = new InterspireHTMLCleaner();
$cleaner->LoadHTMLAsDocument($this->Get('comment'));
$body = $cleaner->GetBodyElement($cleaner->GetDocument());
return $this->CommentTextFetcher($body);
}
/**
* Returns this comment as a text string useful for logging in the activity log (one line)
*
* @return string
*/
public function GetCommentAsActivityLogText () {
$text = $this->GetCommentAsText();
$text = trim(str_replace(array("\n", "\r"), '', $text));
return $text;
}
/**
* getInstance
* This is a static function that sets up the class instance and stores it to the static variable. It will then return that instantiation in the future.
*
* @return iwp_module_comments_commentdata Returns the instantiated object
*/
public static function getInstance(){
if(!isset(self::$Instance)){
self::$Instance = new self();
}
return self::$Instance;
}
/**
* Returns a URL for linking to the preferences page to manage a person's reply notification emails.
*
* @param boolean $full Optional. If set to true, this will return a full url including the domain. Otherwise, only an absolute url for the current domain will be returned.
* @return string
*/
public function GetReplyNotificationPreferencesUrl ($full = false)
{
$module = iwp_module_comments::getInstance();
$url = $module->GetFullUri('CommentsReplyNotificationPreferences', $this->GetId());
$url = str_replace('{uniqueid}', $this->Get('uniqueid'), $url);
return $url;
}
/**
* Returns a permalink-style URL to this comment.
*
* @param boolean $full Optional. If set to true, this will return a full url including the domain. Otherwise, only an absolute url for the current domain will be returned.
* @return string
*/
public function GetUrl ($full = false)
{
// base url of the content
$baseURL = $this->GetContent()->GetUrl($full);
$id = $this->GetId();
$anchor = sprintf('comment_%d', $id);
return sprintf('%s?comment_id=%d#%s', $baseURL, $id, $anchor);
}
/**
* Sends an email to the poster of the parent of this comment about their comment being replied to.
*
* @return boolean Returns true if the email was sent successfully or if the comment does not have visitor-notify enabled, otherwise false.
*
*/
public function SendEmailVisitorNew ()
{
$parent = new iwp_module_comments_commentdata();
$parent->Load($this->Get('parentid'));
if ($parent->GetId() != $this->Get('parentid') || $parent->Get('notifyreply') != 1) {
// invalid parent record, or does not have reply notification enabled
return true;
}
if ($parent->Get('fromemail') == $this->Get('fromemail')) {
// do not send notificatin if the new comment has the same email as the parent comment
return true;
}
$module = iwp_module_comments::getInstance();
$this->template->Assign('parent', $parent->GetData());
$subject = $module->lang->Get('EmailVisitorNewCommentSubject');
$content = $this->GetContent();
$subject = sprintf($subject, $content->Get('title'));
$template = 'comments.email.visitor.new';
$toEmail = $parent->Get('fromemail');
$toName = $parent->Get('fromname');
$this->template->Assign('preferenceurl', $parent->GetReplyNotificationPreferencesUrl(true));
return $this->SendEmail($subject, $template, $toEmail, $toName);
}
/**
* Sends an email to the poster of this comment about their comment being approved.
*
* @return boolean Returns true if the email was sent successfully or if the comment does not have visitor-notify enabled, otherwise false.
*/
public function SendEmailVisitorApproved ()
{
if ($this->Get('notifymoderate') != 1) {
return true;
}
$module = iwp_module_comments::getInstance();
$subject = $module->lang->Get('EmailVisitorApprovedSubject');
$content = $this->GetContent();
$subject = sprintf($subject, $content->GetContentTypeInstance()->Get('name_singular'), $content->Get('title'));
$template = 'comments.email.visitor.approved';
$toEmail = $this->Get('fromemail');
$toName = $this->Get('fromname');
$this->template->Assign('preferenceurl', $this->GetReplyNotificationPreferencesUrl(true));
return $this->SendEmail($subject, $template, $toEmail, $toName);
}
/**
* Sends an email to the poster of this comment about their comment being disapproved.
*
* @return boolean Returns true if the email was sent successfully or if the comment does not have visitor-notify enabled, otherwise false.
*/
public function SendEmailVisitorDisapproved ()
{
if ($this->Get('notifymoderate') != 1) {
return true;
}
$module = iwp_module_comments::getInstance();
$subject = $module->lang->Get('EmailVisitorDisapprovedSubject');
$content = $this->GetContent();
$subject = sprintf($subject, $content->GetContentTypeInstance()->Get('name_singular'), $content->Get('title'));
$template = 'comments.email.visitor.disapproved';
$toEmail = $this->Get('fromemail');
$toName = $this->Get('fromname');
$this->template->Assign('preferenceurl', $this->GetReplyNotificationPreferencesUrl(true));
return $this->SendEmail($subject, $template, $toEmail, $toName);
}
/**
* Sends an email (or emails) to the content author(s) about their content being commented on.
*
* @return boolean|array Returns true if all emails were sent successfully, otherwise returns an array of email addresses which failed.
*/
public function SendEmailAuthorNew ()
{
$module = iwp_module_comments::getInstance();
$subject = $module->lang->Get('EmailAuthorNewCommentSubject');
$content = $this->GetContent();
$subject = sprintf($subject, $content->GetContentTypeInstance()->Get('name_singular'), $content->Get('title'));
$template = 'comments.email.author.new';
$authors = $content->GetAuthorList();
$fail = array();
foreach ($authors as $author) {
$toEmail = $author['email'];
if ($this->Get('fromemail') == $toEmail) {
// do not send emails to authors that match the new comment's email address
continue;
}
$toName = sprintf('%s %s', $author['firstname'], $author['lastname']);
$this->template->Assign('author', $author, false);
if (!$this->SendEmail($subject, $template, $toEmail, $toName)) {
$fail[] = $toEmail;
}
}
if (count($fail)) {
return $fail;
}
return true;
}
/**
* Sends an administrative email about a new comment being posted with auto-approval enabled.
*
* @return boolean Returns true if the email was sent successfully, otherwise false.
*/
public function SendEmailAdminNew ()
{
$this->template->Assign('manageurl', iwp_config::Get('siteURL') . '/admin/index.php?section=module&action=custom&module=comments&moduleaction=view', false);
$module = iwp_module_comments::getInstance();
$subject = $module->lang->Get('EmailAdminNewCommentSubject');
$template = 'comments.email.admin.new';
$toEmail = iwp_config::Get('AdminEmail');
if ($this->Get('fromemail') == $toEmail) {
// do not send an admin email if the comment poster email is the admin email
return true;
}
$toName = iwp_language::getInstance()->Get('AdminName');
return $this->SendEmail($subject, $template, $toEmail, $toName);
}
/**
* Sends an administrative email about a new comment being posted and pending moderation.
*
* @return boolean Returns true if the email was sent successfully, otherwise false.
*/
public function SendEmailAdminNewPending ()
{
$this->template->Assign('manageurl', iwp_config::Get('siteURL') . '/admin/index.php?section=module&action=custom&module=comments&moduleaction=viewpending', false);
$module = iwp_module_comments::getInstance();
$subject = $module->lang->Get('EmailAdminNewPendingCommentSubject');
$template = 'comments.email.admin.newpending';
// was going to check for admin's email in comment but decided to let the email proceed to remind them that it needs to pass through the moderation queue
$toEmail = iwp_config::Get('AdminEmail');
$toName = iwp_language::getInstance()->Get('AdminName');
return $this->SendEmail($subject, $template, $toEmail, $toName);
}
/**
* Finally fires an email after all the template variables have been assigned by the other more specific SendEmail* functions in this class.
*
* @param string $subject Subject line of email to send.
* @param string $template Template in this module's directory to parse.
* @param string $to Recipient email address as string.
* @param string $toname Recipient name (optional)
*
* @return boolean Returns true if the email was sent successfully, otherwise false.
*/
public function SendEmail ($subject, $template, $toEmail, $toName = null)
{
if (is_null($toName)) {
$toName = '';
}
$module = iwp_module_comments::getInstance();
$content = $this->GetContent();
$this->template->Assign('toemail', $toEmail, false);
$this->template->Assign('toname', $toName, false);
$this->template->Assign('adminemail', iwp_config::Get('AdminEmail'), false);
$this->template->Assign('adminname', iwp_language::getInstance()->Get('AdminName'), false);
$this->template->Assign('contenturl', $content->GetUrl(true), false);
$this->template->Assign('commenturl', $this->GetUrl(true));
$this->template->Assign('contenttype', $content->GetContentTypeInstance()->GetData());
$this->template->Assign('content', $content->GetData(), false);
$this->template->Assign('comment', $this->GetData(), false);
$this->template->Assign(array('comment', 'comment_text'), $this->GetCommentAsText(), false);
$email = new iwp_email();
$email->Set('Subject', $subject);
$this->template->SetTemplatePath(IWP_MODULES_PATH . '/' . $module->moduleName . '/templates/', 'tpl');
$cleanWhiteSpace = $this->template->GetCleanWhiteSpace();
$this->template->SetCleanWhiteSpace(false);
$emailContent = $this->template->ParseTemplate($template, true);
$this->template->SetCleanWhiteSpace($cleanWhiteSpace);
$email->Set('CharSet', 'UTF-8');
$email->Set('ContentEncoding', '8bit');
$email->From(iwp_config::Get('AdminEmail'), iwp_language::getInstance()->Get('AdminName'));
$email->AddRecipient($toEmail, $toName, 't');
if (!IsUtf8($emailContent)) {
utf8_encode_all($emailContent);
}
$email->AddBody('text', $emailContent);
if (!iwp_event::trigger(new iwp_event_module_comments_beforeemailsend($template, $this, $email))) {
return false;
}
$result = $email->Send();
iwp_event::trigger(new iwp_event_module_comments_afteremailsend($template, $this, $email, $result));
if (count($result['fail'])) {
return false;
}
return true;
}
/**
* Finds out if the current control panel login can perform a given action on this comment
*
* @param string $action Action name according to iwp_module_comments::ContentPermissionOptions
* @param integer $typeid Optional. Default null. Content type id of content the comment is attached to, to check against permissions. If null is provided, the type id will be taken from the currently loaded comment.
* @return boolean Returns true if the user can perform the given action
*/
public function CanUserAction ($action, $typeid = null)
{
$auth = iwp_admin_auth::getInstance();
if (!$auth->HasPerm('sitemodules', 'comments', $action)) {
// no sitewide permission, check contenttype
if (is_null($typeid)) {
$content = $this->GetContent();
$typeid = $content->Get('typeid');
}
if (!$auth->HasPerm('content', $typeid, 'comments_' . $action)) {
// neither permission, abort
return false;
}
}
return true;
}
/**
* Changes this comment's status to approved.
*
* @param boolean $sendEmail Optional. Default true. Set to true to activate sending of email notification based on the user's preference.
* @param boolean $save Optional. Default true. Set to true to call the Save() method to commit this to the database.
*
* @return boolean Returns false if mail sending was active and failed, otherwise returns true. Will also return false if the status didn't change, or if the action was cancelled by an event listener.
*/
public function Approve ($sendEmail = true, $save = true)
{
if (!$this->CanUserAction('approve')) {
return false;
}
if ($this->Get('status') == IWP_MODULE_COMMENTS_STATUS_APPROVED) {
return false;
}
if (!iwp_event::trigger(new iwp_event_module_comments_beforecommentapprove($this, $sendEmail, $save))) {
return false;
}
$this->Set('status', IWP_MODULE_COMMENTS_STATUS_APPROVED);
if ($save) {
$this->Save();
}
if ($sendEmail) {
$success = $this->SendEmailVisitorApproved();
} else {
$success = true;
}
iwp_activity::AddEntry('module', 'comments', 'approve', $this->GetId(), $this->GetCommentAsActivityLogText());
iwp_event::trigger(new iwp_event_module_comments_aftercommentapprove($this, $sendEmail, $save));
return $success;
}
/**
* Changes this comment's status to disapproved.
*
* @param boolean $sendEmail Optional. Default true. Set to true to activate sending of email notification based on the user's preference.
* @param boolean $save Optional. Default true. Set to true to call the Save() method to commit this to the database.
*
* @return boolean Returns false if mail sending was active and failed, otherwise returns true. Will also return false if the status didn't change.
*/
public function Disapprove ($sendEmail = true, $save = true)
{
if (!$this->CanUserAction('disapprove')) {
return false;
}
if ($this->Get('status') == IWP_MODULE_COMMENTS_STATUS_DISAPPROVED) {
return false;
}
if (!iwp_event::trigger(new iwp_event_module_comments_beforecommentdisapprove($this, $sendEmail, $save))) {
return false;
}
$this->Set('status', IWP_MODULE_COMMENTS_STATUS_DISAPPROVED);
if ($save) {
$this->Save();
}
if ($sendEmail) {
$success = $this->SendEmailVisitorDisapproved();
} else {
$success = true;
}
iwp_activity::AddEntry('module', 'comments', 'disapprove', $this->GetId(), $this->GetCommentAsActivityLogText());
iwp_event::trigger(new iwp_event_module_comments_aftercommentdisapprove($this, $sendEmail, $save));
return $success;
}
/**
* Adds the loaded comment's ip to the blocked ip list.
*
* @return boolean Returns true if the ip was added, otherwise false.
*/
public function BlockIP ()
{
if (!$this->GetId()) {
return false;
}
$module = iwp_module_comments::getInstance();
return $module->BlockIP($this->Get('ip'));
}
/**
* Removes the loaded comment's ip from the blocked ip list.
*
* @return boolean Returns true if the ip was removed, otherwise false.
*/
public function UnblockIP ()
{
if (!$this->GetId()) {
return false;
}
$module = iwp_module_comments::getInstance();
return $module->UnblockIP($this->Get('ip'));
}
/**
* Renders the comments.view.row template for sending back in an AJAX request for partial page refresh functionality
*
* @return string
*/
public function RenderAdminViewRow ($renderChecked = false)
{
$module = iwp_module_comments::getInstance();
// Data that will be formatted for the data that is returned by CommentDataViewList for the row that is rendered
// in the xml that then replaces the row in the HTML. Whew! There is no id in $this->data and there needs to be
// an id for the view for links/actions to work properly without refreshing the page.
$formatData = $this->data;
$formatData['id'] = $this->GetId();
$data = array();
$data['lang'] = $module->lang->GetLangVars();
$this->template->Assign($module->moduleName, $data, false);
$this->template->Assign('row', $module->CommentDataViewList($formatData), false);
$this->template->Assign('DisplayFields', $module->GetDisplayFields());
if ($renderChecked) {
$this->template->Assign('all_checked', ' checked="checked"', false);
}
return $this->template->ParseTemplate('comments.view.row', true, 'comments');
}
}
/**
* Class iwp_module_comments
*
*/
class iwp_module_comments extends iwp_module
{
/**
* @var string
*/
public $moduleName = 'comments';
/**
* For comments, this is used only for the existing table check
*
* @var Array
*/
protected $_columns = array(
'id' => '',
'newcomments' => '',
'existingcomments' => '',
'notifyauthor' => '',
);
protected $contentIdColumn = 'id';
/**
* Instance
* This static variable holds the current instance of this object being loaded.
* So using the getInstance function anywhere will return the very same instance.
*
* @var object Instance
*/
public static $Instance;
/**
* getInstance
* This is a static function that sets up the class instance and stores it to the static variable. It will then return that instantiation in the future.
*
* @return iwp_module_comments Returns the instantiated object
**/
public static function getInstance(){
if(!isset(self::$Instance)){
self::$Instance = new self();
}
return self::$Instance;
}
/**
* A list of uri tokens which this module implements
*
* @var array
*/
public static $UriMatches = array(
'From' => array('{inreplyto}', '{uniqueid}'),
'To' => array('(?P<inreplyto>[0-9]*)', '(?P<uniqueid>[a-zA-Z0-9]*)')
);
/**
* Returns a list of database tables for this module.
*
* @param boolean $withoutPrefix A boolean for whether or not to include the prefix for the table.
*
* @return string
*/
public function getTables($withoutPrefix=false) {
$tables = array(
'comments',
'commentdata',
);
if($withoutPrefix) {
return $tables;
}
foreach($tables as $k=>$table ){
$tables[$k] = IWP_MODULE_DB_PREFIX . $table;
}
return $tables;
}
/**
* Returns the list of uri tokens which this module implements
*
* @return array
*/
public static function getUriMatches(){
if(is_array(self::$UriMatches)){
return self::$UriMatches;
}
return array();
}
/**
* Deletes all data associated with this module.
* This is used when cleaning out the data during the importer process
*
* @return void
*/
public function TruncateAllData () {
$this->db->Query('TRUNCATE TABLE ' . $this->getTable('comments'));
$this->db->Query('TRUNCATE TABLE ' . $this->getTable('commentdata'));
}
/**
* Constructor
*
*/
public function __construct(){
parent::__construct();
}
/**
* Returns whether or not this module implements content functionality.
*
* @return boolean
*/
public function IsContentModule () { return true; }
/**
* Returns whether or not this module implements a configuration screen
*
* @return boolean
*/
public function HasConfigurationScreen () { return true; }
/*
public function AdminGetContentTypeArray (){
$arrFields = array(
'title' => $this->lang->Get('CommentOptions'),
'fields' => array( 'allowdisable' => array( 'title' => $this->lang->Get('AllowDisabled'),
'type' => 'checkbox',
'default' => 'checked',
'label' => $this->lang->Get('YesAllowDisabled')
),
)
);
return $arrFields;
}
*/
/**
* Returns an array describing the fields which can be configured on a per-content-item basis
*
* @param integer $id Content id
* @return array
*/
public function AdminGetContentArray ($id = null) {
$id = (int)$id;
$fields = array(
'newcomments' => array(
'type' => 'select',
'default' => '',
'value' => null,
'enum' => array(
'default' => $this->lang->Get('ContentNewCommentsDefault'),
'enabled' => $this->lang->Get('ContentNewCommentsEnabled'),
'disabled' => $this->lang->Get('ContentNewCommentsDisabled'),
),
),
'existingcomments' => array(
'type' => 'select',
'default' => '',
'value' => null,
'enum' => array(
'default' => $this->lang->Get('ContentExistingCommentsDefault'),
'visible' => $this->lang->Get('ContentExistingCommentsVisible'),
'hidden' => $this->lang->Get('ContentExistingCommentsHidden'),
),
),
'notifyauthor' => array(
'type' => 'checkbox',
'default' => '',
'value' => null,
),
);
if ($id) {
$data = $this->GetContentModuleData($id);
if ($data) {
$fields['newcomments']['value'] = $data['newcomments'];
$fields['existingcomments']['value'] = $data['existingcomments'];
$fields['notifyauthor']['value'] = $data['notifyauthor'] == 1 ? 'checked' : '';
}
}
return $fields;
}
/**
* This function takes care of saving the data posted by the user. It is assumed that when this function is run the ValidateSave() function has
* already been run without error. This function can not be run without a valid content ID number.
*
* @param array $post The array with the posted data
* @param integer $contentid The current content id number
* @return mixed Array if there is an error, true otherwise
*/
function SaveField ($post, $contentid)
{
$Insert = array();
$Insert['id'] = $contentid;
$Insert['newcomments'] = $post['newcomments'];
$Insert['existingcomments'] = $post['existingcomments'];
$Insert['notifyauthor'] = (isset($post['notifyauthor']) ? '1' : '0');
$this->db->InsertOnDuplicateKeyUpdateQuery($this->getTable('comments'), $Insert);
return true;
}
public static $ContentPermissionOptions;
/**
* Returns this module's content-type-specific permission options
*
* @return array
*/
public static function GetContentPermissionOptions ()
{
return iwp_module_comments::$ContentPermissionOptions;
}
public static $SitePermissionOptions;
/**
* Returns this module's site-wide permission options
*
* @return array
*/
public static function GetSitePermissionOptions ()
{
return iwp_module_comments::$SitePermissionOptions;
}
/**
* A list of event listeners used for registering and unregistering events during the module's activation and deactivation.
*
* @var array
*/
protected $eventListeners = array(
'iwp_event_admin_navigation_dropdownmenucreated' => array(
'iwp_module_comments', 'OnDropdownMenuCreated'
),
'iwp_event_content_afterdelete' => array(
'iwp_module_comments', 'OnAfterContentDelete'
),
'iwp_event_admin_home_pendinglistcreated' => array(
'iwp_module_comments', 'OnPendingListCreated'
),
'iwp_event_admin_home_ataglancelistcreated' => array(
'iwp_module_comments', 'OnAtAGlanceListCreated'
),
);
/**
* A list of events used for creating and removing events during the module's activation and deactivation.
*
* @var array
*/
protected $events = array(
'iwp_event_module_comments_beforenewcommentsave',
'iwp_event_module_comments_afternewcommentsave',
'iwp_event_module_comments_beforeemailsend',
'iwp_event_module_comments_afteremailsend',
'iwp_event_module_comments_beforecommentapprove',
'iwp_event_module_comments_aftercommentapprove',
'iwp_event_module_comments_beforecommentdisapprove',
'iwp_event_module_comments_aftercommentdisapprove',
'iwp_event_module_comments_beforeipblock',
'iwp_event_module_comments_afteripblock',
'iwp_event_module_comments_beforeipunblock',
'iwp_event_module_comments_afteripunblock',
);
/**
* Configures the default settings for this module
*
* @param bool $force Optional. Default false. If true, this function will overwrite all settings with defaults.
* @return void
*/
public function SetDefaultSettings ($force = false)
{
$defaults = array(
'sitewidenewcomments' => 'enabled',
'sitewideexistingcomments' => 'visible',
'captcha' => 'checked',
'allowhtml' => 'some',
'parselinks' => 'checked',
'nofollow' => 'checked',
'autoapprove' => '',
'wysiwyg' => 'checked',
'notifyadminnewcomment' => 'checked',
'notifyvisitormoderated' => 'checked',
'notifyvisitorreply' => 'checked',
'notifyauthorreply' => 'checked',
'perpage' => '50',
'perpageuser' => 'checked',
'threaddisplay' => 'threaded',
'threaddisplayuser' => 'checked',
'datesort' => 'oldest',
'datesortuser' => 'checked',
'blocked' => '',
'hideform' => '',
);
foreach ($defaults as $key => $value) {
if ($force || !$this->VarExists($key)) {
$this->SetVar($key, $value);
}
}
}
/**
* @see iwp_module::AdminGetVariablesArray
*/
public function AdminGetVariablesArray () {
return array(
'sitewidenewcomments',
'sitewideexistingcomments',
'captcha',
'allowhtml',
'parselinks',
'nofollow',
'autoapprove',
'wysiwyg',
'notifyadminnewcomment',
'notifyvisitormoderated',
'notifyvisitorreply',
'notifyauthorreply',
'perpage',
'perpageuser',
'threaddisplay',
'threaddisplayuser',
'datesort',
'datesortuser',
'blocked',
'hideform',
);
}
/**
* Apply module-specific filtering to configure screen data before it is sent to the database
*
* @param array $data
*/
public function AdminConfigureSave (&$data)
{
preg_match_all('/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/s', $data['blocked'], $matches);
asort($matches[0]);
$data['blocked'] = implode("\n", $matches[0]);
}
/**
* Called by the module framework when the configuration screen is requested.
*
* @return void
*/
public function AdminConfigure ()
{
$this->form->EnableTabs = true;
$this->form->AddTab($this->lang->Get('NewCommentsTab'));
$this->form->AddGroup($this->lang->Get('NewCommentsSettings'));
$field = $this->form->AddField('select', 'sitewidenewcomments', null, $this->lang);
$field->AddFieldOption('enabled', $this->lang->Get('SiteWideNewCommentsEnabled'));
$field->AddFieldOption('disabled', $this->lang->Get('SiteWideNewCommentsDisabled'));
$field->AddFieldOption('enabledall', $this->lang->Get('SiteWideNewCommentsEnabledAll'));
$field->AddFieldOption('disabledall', $this->lang->Get('SiteWideNewCommentsDisabledAll'));
$field = $this->form->AddField('checkbox', 'autoapprove', null, $this->lang);
$this->form->AddField('checkbox', 'captcha', null, $this->lang);
$this->form->AddField('checkbox', 'wysiwyg', null, $this->lang);
$this->form->AddField('checkbox', 'hideform', null, $this->lang);
$this->form->AddTab($this->lang->Get('CommentDisplayTab'));
$this->form->AddGroup($this->lang->Get('CommentDisplaySettings'));
$field = $this->form->AddField('select', 'sitewideexistingcomments', null, $this->lang);
$field->AddFieldOption('visible', $this->lang->Get('SiteWideExistingCommentsVisible'));
$field->AddFieldOption('hidden', $this->lang->Get('SiteWideExistingCommentsHidden'));
$field->AddFieldOption('visibleall', $this->lang->Get('SiteWideExistingCommentsVisibleAll'));
$field->AddFieldOption('hiddenall', $this->lang->Get('SiteWideExistingCommentsHiddenAll'));
$field = $this->form->AddField('select', 'allowhtml', null, $this->lang);
$field->AddFieldOption('none', $this->lang->Get('AllowHTMLNone'));
$field->AddFieldOption('some', $this->lang->Get('AllowHTMLSome'));
$field->AddFieldOption('all', $this->lang->Get('AllowHTMLAll'));
$this->form->AddField('checkbox', 'parselinks', null, $this->lang);
$this->form->AddField('checkbox', 'nofollow', null, $this->lang);
$field = $this->form->AddField('select', 'perpage', null, $this->lang);
$options = $this->GetPerPageOptions(false);
while (list($key, $val) = each($options)) {
$field->AddFieldOption($key, $val);
}
$field->AddFieldOption('0', $this->lang->Get('field_perpage_All'));
$field = $this->form->AddField('select', 'threaddisplay', null, $this->lang);
$field->AddFieldOption('flat', $this->lang->Get('threaddisplay_flat'));
$field->AddFieldOption('threaded', $this->lang->Get('threaddisplay_threaded'));
$field = $this->form->AddField('select', 'datesort', null, $this->lang);
$field->AddFieldOption('oldest', $this->lang->Get('datesort_oldest'));
$field->AddFieldOption('newest', $this->lang->Get('datesort_newest'));
$this->form->AddField('checkbox', 'perpageuser', null, $this->lang);
$this->form->AddField('checkbox', 'threaddisplayuser', null, $this->lang)->DisableName();
$this->form->AddField('checkbox', 'datesortuser', null, $this->lang)->DisableName();
$this->form->AddTab($this->lang->Get('EmailNotificationTab'));
$this->form->AddGroup($this->lang->Get('EmailNotificationSettings'));
$this->form->AddField('checkbox', 'notifyadminnewcomment', null, $this->lang);
$this->form->AddField('checkbox', 'notifyvisitormoderated', null, $this->lang);
$this->form->AddField('checkbox', 'notifyvisitorreply', null, $this->lang);
$this->form->AddField('checkbox', 'notifyauthorreply', null, $this->lang);
$this->form->AddTab($this->lang->Get('BlockTab'));
$this->form->AddGroup($this->lang->Get('BlockSettings'));
$this->form->AddField('textarea', 'blocked', null, $this->lang);
$this->form->JsOnLoad[] = "$('.AdminEmailAddressContainer').text(". iwp_FilterJavascriptString(iwp_config::Get('AdminEmail')) .")";
}
/**
* ArrayInsert
* Inserts one array into another at a given position.
*
* @param array $array The recipient array to be inserted into, passed by reference and will have it's pointer reset
* @param mixed $position The index (numeric) position at which to insert
* @param mixed $insert_array The array to be inserted
*/
private static function ArrayInsert (&$array, $position, $insert_array) {
$first_array = array_splice ($array, 0, $position);
$array = array_merge ($first_array, $insert_array, $array);
}
/**
* Listener for the iwp_event_content_afterdelete event
* Prunes any comments for a content item after it's been deleted
*
* @param iwp_event_content_afterdelete $data
*/
public static function OnAfterContentDelete (iwp_event_content_afterdelete $data)
{
$module = iwp_module_comments::getInstance();
$query = sprintf("DELETE FROM `%s` WHERE contentid = %d", $module->getTable('commentdata'), $data->contentId);
$module->db->Query($query);
}
public static function OnPendingListCreated (iwp_event_admin_home_pendinglistcreated $data)
{
$module = iwp_module_comments::getInstance();
$sql = sprintf("SELECT COUNT(*) FROM `%s` WHERE status = 'pending'", $module->getTable('commentdata'));
$count = $module->db->FetchOne($sql);
if ($count) {
$data->pendingList[] = array(
'link' => 'index.php?section=module&action=custom&module=comments&moduleaction=view&searchField=status&searchQuery=pending',
'text' => sprintf(GetLang('PendingLinkTemplate'), ($count == 1 ? GetLang('is') : GetLang('are')), $count, ($count == 1 ? $module->lang->Get('CommentCountSingular') : $module->lang->Get('CommentCountPlural'))),
'className' => 'Comment',
);
}
}
public static function SortAtAGlanceList ($a, $b) {
return $b['count'] - $a['count'];
}
public static function OnAtAGlanceListCreated (iwp_event_admin_home_ataglancelistcreated $data)
{
$module = iwp_module_comments::getInstance();
$sql = sprintf("SELECT COUNT(*) FROM `%s`", $module->getTable('commentdata'));
$count = $module->db->FetchOne($sql);
if ($count) {
$data->atAGlanceList[] = array(
'link' => 'index.php?section=module&action=custom&module=comments&moduleaction=view',
'text' => ucwords($count == 1 ? $module->lang->Get('CommentCountSingular') : $module->lang->Get('CommentCountPlural')),
'count' => $count,
);
}
usort($data->atAGlanceList, array('iwp_module_comments', 'SortAtAGlanceList'));
}
/**
* Listener for the iwp_event_admin_navigation_dropdownmenucreated event
*
* @param iwp_event_admin_navigation_dropdownmenucreated $data
*/
public static function OnDropdownMenuCreated (iwp_event_admin_navigation_dropdownmenucreated $data)
{
$auth = iwp_admin_auth::getInstance();
if ($auth->HasPerm('sitemodules', 'comments', '*') || $auth->HasMultiPerm(IWP_MULTIPERM_MATCHTYPE_ANY, 'content', '*', array('comments_edit', 'comments_delete', 'comments_approve', 'comments_disapprove'))) {
// note to any module writers attempting to emulate this menu-'plugin' functionality:
// images need to be present in the admin/images/ folder to support the menu we slot in, currently there is no other, cleaner way of doing this
$module = iwp_module_comments::getInstance();
$menu = array(
array(
'text' => $module->lang->Get('menuManageComments'),
'link' => 'index.php?section=module&action=custom&module=comments&moduleaction=view',
'show' => true,
'help' => $module->lang->Get('menuManageCommentsHelp'),
'icon' => '../../modules/comments/images/comment_dropdown_menu.gif',
),
);
$contentMenuIndex = array_search('mnuContent', array_keys($data->menuItems));
if ($contentMenuIndex === false || $contentMenuIndex == count($data->menuItems) - 1) {
// content menu not found or content menu is at end of tab list, insert at end of tab list
$data->menuItems['module_comments'] = $menu;
} else {
// otherwise insert after the content tab
iwp_module_comments::ArrayInsert($data->menuItems, $contentMenuIndex + 1, array('module_comments' => $menu));
}
}
if (@$_GET['section'] == 'module' && @$_GET['action'] == 'custom' && @$_GET['module'] == 'comments') {
$data->currentMenu = 'module_comments';
}
// adds pending comment count to dropdown
$module = iwp_module_comments::getInstance();
$pendingCount = $module->db->FetchOne(sprintf("SELECT COUNT(*) FROM `%s` comments WHERE comments.status = '%s'", $module->getTable('commentdata'), IWP_MODULE_COMMENTS_STATUS_PENDING));
iwp_template::getInstance()->Append('bodyTagEnd', '<'.'script type="text/javascript">
jQuery(function($){
$(".menu_module_comments .dropdown-tab-label")
.append($("<span style=\"text-decoration:none; margin-left:8px; background:#FFA500; color:#FFF; padding:0 4px; -moz-border-radius:6px; -webkit-border-radius:6px; font-size:90%;\">' . $pendingCount . '</span>"));
});
</script>');
}
/**
* Gets a full uri from this module's lists of uris with the site url appended, intended for use in emails and permanent links.
*
* @param string $which URI entry code
* @param integer $id Content id
* @return string
*/
public function GetFullUri ($which, $id)
{
$this->LoadUris();
$uri = $this->UriList[$which];
if (!is_null($id)) {
$uri = str_replace('{idnumber}', $id, $uri);
}
$uri = $this->urls->ForceSlash($uri, true);
return iwp_config::Get('siteURL') . $uri;
}
/**
* Gets a uri from this module's lists of uris with the site app path appended
*
* @param string $which URI entry code
* @param integer $id Content id
* @return string
*/
public function GetUri ($which, $id = null)
{
$this->LoadUris();
$uri = $this->UriList[$which];
if (!is_null($id)) {
$uri = str_replace('{idnumber}', $id, $uri);
}
$uri = $this->urls->ForceSlash($uri);
return $this->urls->GetURLPrepend() . $uri;
}
/**
* Validates the ajax form submission for the new comment form. Will die() with an xml error on failure.
*
* @return array An array of field values
*/
public function ValidateFormSubmission () {
$useCaptcha = ($this->GetVar('captcha') == 'checked');
if ($useCaptcha) {
$captcha = new captcha();
$captcha->sessionKey = 'commentsCaptcha';
}
$fail = false;
$fields = array(
'fromname' => '',
'fromemail' => '',
'website' => '',
'message' => '',
);
foreach ($fields as $fieldName=>$value) {
if (isset($_POST[$fieldName])) {
$fields[$fieldName] = $value = trim(@$_POST[$fieldName]);
}
switch ($fieldName) {
case 'message':
$minlength = IWP_MODULE_COMMENTS_MESSAGE_MINIMUMLENGTH;
$maxlength = IWP_MODULE_COMMENTS_MESSAGE_MAXIMUMLENGTH;
break;
default:
$minlength = 0;
$maxlength = IWP_MODULE_COMMENTS_OTHERFIELDS_MAXIMUMLENGTH;
break;
}
$strlenfield = strlen($fields[$fieldName]);
if ($strlenfield > $maxlength) {
$fail = true;
$this->xml->startElement('field');
$this->xml->writeAttribute('name', $fieldName);
$this->xml->writeAttribute('highlight', 'true');
$this->xml->writeAttribute('message', sprintf($this->lang->Get('fieldTooLong'), $maxlength));
$this->xml->endElement();
continue;
}
if ($strlenfield < $minlength) {
$fail = true;
$this->xml->startElement('field');
$this->xml->writeAttribute('name', $fieldName);
$this->xml->writeAttribute('highlight', 'true');
$this->xml->writeAttribute('message', sprintf($this->lang->Get('fieldTooShort'), $minlength));
$this->xml->endElement();
continue;
}
if ($fieldName == 'website') {
// not required
continue;
}
if (!isset($_POST[$fieldName]) || $this->valid->IsBlank($value)) {
$this->xml->startElement('field');
$this->xml->writeAttribute('name', $fieldName);
$this->xml->writeAttribute('highlight', 'true');
$this->xml->writeAttribute('message', $this->lang->Get('missing' . $fieldName));
$this->xml->endElement();
$fail = true;
}
}
if ($useCaptcha) {
if(!(isset($_POST['captcha']) && isset($_SESSION['commentsCaptcha']) && iwp_strtolower($_POST['captcha']) == iwp_strtolower($captcha->LoadSecret()))) {
$this->xml->startElement('field');
$this->xml->writeAttribute('name', 'captcha');
$this->xml->writeAttribute('highlight', 'true');
$this->xml->writeAttribute('message', $this->lang->Get('invalidCaptcha'));
$this->xml->endElement();
// send a url back to the client to refresh the current image just incase the session expired
$captcha = new captcha();
$captcha->sessionKey = 'commentsCaptcha';
$captcha->CaptchaURL = $this->urls->GetStaticUrl('captchaimg');
$this->xml->writeCDataElement('captchaimg', $captcha->ShowCaptcha());
$fail = true;
}
}
if (!$this->valid->IsBlank($fields['fromemail']) && !$this->valid->ValidEmail($fields['fromemail'])) {
$this->xml->startElement('field');
$this->xml->writeAttribute('name', 'fromemail');
$this->xml->writeAttribute('highlight', 'true');
$this->xml->writeAttribute('message', $this->lang->Get('invalidEmail'));
$this->xml->endElement();
$fail = true;
}
if ($fail) {
$this->xml->writeElement('status', 0);
$this->xml->writeElement('error', $this->lang->Get('fieldsIncomplete'));
$this->xml->outputXML();
die();
}
// special case for checkboxes
if ($this->GetVar('notifyvisitorreply') == 'checked') {
// only allow browser to define this setting if it is globally allowed
$fields['notifyreply'] = (isset($_POST['notifyreply']) ? 1 : 0);
} else {
$fields['notifyreply'] = 0;
}
if ($this->GetVar('notifyvisitormoderated') == 'checked') {
// only allow browser to define this setting if it is globally allowed
$fields['notifymoderate'] = (isset($_POST['notifymoderate']) ? 1 : 0);
} else {
$fields['notifymoderate'] = 0;
}
return $fields;
}
/**
* Handles a post containing (client-side) settings data
*
* @param integer $id
*/
public function SettingsAction ($id)
{
$content = iwp_content::getInstance();
if ($content->GetId() != $id) {
$content->Load($id);
}
if (!$content->GetId()) {
$this->controller->ShowPage('NotFound');
}
// load whether or not the visitor is allowed to set their own settings
$threaddisplayuser = ($this->GetVar('threaddisplayuser') == 'checked');
$datesortuser = ($this->GetVar('datesortuser') == 'checked');
$perpageuser = ($this->GetVar('perpageuser') == 'checked');
if ($threaddisplayuser && isset($_POST['threaddisplay']) && ($_POST['threaddisplay'] == 'default' || array_key_exists($_POST['threaddisplay'], $this->GetThreadDisplayOptions()))) {
$this->SetCookie('threaddisplay', $_POST['threaddisplay'], time() + 3600 * 24 * 365);
}
if ($datesortuser && isset($_POST['datesort']) && ($_POST['datesort'] == 'default' || array_key_exists($_POST['datesort'], $this->GetSortOptions()))) {
$this->SetCookie('datesort', $_POST['datesort'], time() + 3600 * 24 * 365);
}
if ($perpageuser && isset($_POST['perpage']) && ($_POST['perpage'] == 'default' || array_key_exists($_POST['perpage'], $this->GetPerPageOptions()))) {
$this->SetCookie('perpage', $_POST['perpage'], time() + 3600 * 24 * 365);
}
$this->xml->writeElement('status', 1);
$this->xml->writeElement('message', $this->lang->Get('saved'));
$this->xml->writeElement('redirect', $content->GetUrl());
$this->xml->outputXML();
die();
}
/**
* Pragmatically add a new comment to the database. Used internally by PostAction, can also be used to feed in comments from an importer or content generator.
*
* Does NOT perform any checking of settings, field validation or email notifications, but will build the correct threading strings.
*
* @param integer $contentId
* @param integer $parentId
* @param string $status
* @param array $fields message, fromname, fromemail, website, notifyreply, notifymoderate, and optionally date and ip
* @return iwp_module_comments_commendata
*/
public function NewComment ($contentId, $parentId, $status, $fields)
{
$comment = new iwp_module_comments_commentdata();
if ($parentId) {
$parent = new iwp_module_comments_commentdata();
$parent->Load($parentId);
$parentId = $parent->GetId();
}
// build the new thread code
if ($parentId) {
// replying to an existing comment
// trim the sorting code of the parent comment
$sortingParentTrimmed = rtrim($parent->Get('sortingnewest'), '/');
// find the sorting code for the most recent reply (direct child) to this comment
$sql = sprintf("SELECT MAX(c.sortingnewest) AS sorting FROM `%s` c WHERE c.contentid = %d AND c.sortingnewest LIKE '%s.%%%%'", $this->getTable('commentdata'), $contentId, $sortingParentTrimmed);
$sortingPrevious = $this->db->FetchQuery($sql);
if (is_null($sortingPrevious['sorting'])) {
// none found, this is the first reply to this parent id, add a new depth part to the sorting code
$sortingoldest = $sortingParentTrimmed . '.' . iwp_admin_categories::Int2Sorting(0);
} else {
// this is a reply to comment which has existing replies
// calculate a new sorting code based on an increment of the previous reply
$sortingPreviousTrimmed = rtrim($sortingPrevious['sorting'], '/');
$sortingPreviousArray = explode('.', $sortingPreviousTrimmed);
$sortingPrevious = $sortingPreviousArray[count(explode('.', $sortingParentTrimmed))];
$sortingoldest = $sortingParentTrimmed . '.' . iwp_admin_categories::Int2Sorting(iwp_admin_categories::Sorting2Int($sortingPrevious) + 1);
}
} else {
// new reply on content, not in reply to another comment
// find the maximum sorting code of all replies which, due to text sorting, will be the most recent root reply
$sql = sprintf("SELECT MAX(c.sortingnewest) AS sorting FROM `%s` c WHERE c.contentid = %d", $this->getTable('commentdata'), $contentId);
$sortingPrevious = $this->db->FetchQuery($sql);
if (is_null($sortingPrevious['sorting'])) {
// this is the first reply to this content, start at 00
$sortingoldest = iwp_admin_categories::Int2Sorting(0);
} else {
// increment the previous value
$sortingPreviousTrimmed = rtrim($sortingPrevious['sorting'], '/');
$sortingoldest = iwp_admin_categories::Int2Sorting(iwp_admin_categories::Sorting2Int($sortingPreviousTrimmed) + 1);
}
}
$sortingnewest = $sortingoldest .'/';
$comment_html = $this->CommentBodyToHTML($fields['message']);
$sql = sprintf("SELECT MAX(c.counter) AS counter FROM `%s` c WHERE c.contentid = %d", $this->getTable('commentdata'), $contentId);
$counter = $this->db->FetchQuery($sql);
$counter = (int)$counter['counter'];
if (!$counter) {
$counter = 0;
}
if(!isset($fields['ip']) || empty($fields['ip']) || is_null($fields['ip'])) {
$fields['ip'] = GetIP();
}
if(!isset($fields['date']) || empty($fields['date']) || is_null($fields['date'])) {
$fields['date'] = GetMysqlDateTime();
}
$comment->Set(array(
'comment' => $comment_html,
'fromname' => $fields['fromname'],
'fromemail' => $fields['fromemail'],
'website' => $fields['website'],
'contentid' => $contentId,
'ip' => $fields['ip'],
'status' => $status,
'parentid' => $parentId,
'userid' => 0,
'fromadmin' => 0,
'sortingnewest' => $sortingnewest,
'sortingoldest' => $sortingoldest,
'date' => $fields['date'],
'notifyreply' => $fields['notifyreply'],
'notifymoderate' => $fields['notifymoderate'],
'uniqueid' => iwp_module_comments::GenerateUniqueId(16),
'counter' => $counter + 1,
));
if (iwp_event::trigger(new iwp_event_module_comments_beforenewcommentsave($comment))) {
// only proceed to save if the event was not cancelled
if ($comment->Save()) {
iwp_event::trigger(new iwp_event_module_comments_afternewcommentsave($comment));
}
}
return $comment;
}
/**
* Handle a post containing a NEW comment
*
* @param integer $contentId Content id
*/
public function PostAction ($contentId)
{
$content = iwp_content::getInstance();
if ($content->GetId() != $contentId) {
$content->Load($contentId);
}
if (!$content->GetId()) {
// replying to an invalid content id
$this->controller->ShowPage('NotFound');
}
$contentSettings = $this->GetContentModuleData($content->GetId());
$newcomments = $this->GetNewCommentsSetting($content->GetId());
if (!$newcomments) {
$this->controller->ShowPage('NotFound');
}
$fields = $this->ValidateFormSubmission(); // ValidateFormSubmission will output xml and die on validation failure
if (isset($_POST['replyto'])) {
$parentId = (int)$_POST['replyto'];
} else {
$parentId = 0;
}
$autoapprove = ($this->GetVar('autoapprove') == 'checked');
$commentStatus = $autoapprove ? IWP_MODULE_COMMENTS_STATUS_APPROVED : IWP_MODULE_COMMENTS_STATUS_PENDING;
$comment = $this->NewComment($contentId, $parentId, $commentStatus, $fields);
$newReplyId = $comment->GetId();
if (!$newReplyId) {
$this->xml->writeElement('status', 0);
$this->xml->writeElement('error', $this->lang->Get('databaseError'));
$this->xml->outputXML();
die();
}
iwp_activity::AddEntry('module', 'comments', 'post', $newReplyId, $comment->GetCommentAsActivityLogText(), $fields['fromemail'], $fields['fromname']);
// set cookie values for their name/email/website
$cookieExpiry = time() + 3600 * 24 * 365;
$this->SetCookie('fromname', $fields['fromname'], $cookieExpiry);
$this->SetCookie('fromemail', $fields['fromemail'], $cookieExpiry);
$this->SetCookie('website', $fields['website'], $cookieExpiry);
$this->SetCookie('notifyreply', $fields['notifyreply'], $cookieExpiry);
$this->SetCookie('notifymoderate', $fields['notifymoderate'], $cookieExpiry);
// send emails
$notifyadminnewcomment = ($this->GetVar('notifyadminnewcomment') == 'checked');
if ($notifyadminnewcomment) {
if ($commentStatus == IWP_MODULE_COMMENTS_STATUS_PENDING) {
$comment->SendEmailAdminNewPending();
} else if ($commentStatus == IWP_MODULE_COMMENTS_STATUS_APPROVED) {
$comment->SendEmailAdminNew();
$comment->SendEmailAuthorNew();
$comment->SendEmailVisitorNew();
}
}
$useCaptcha = ($this->GetVar('captcha') == 'checked');
if ($useCaptcha) {
// reset the captcha to foil any session-based spam attacks
$captcha = new captcha();
$captcha->sessionKey = 'commentsCaptcha';
$captcha->CreateSecret();
$captcha->CaptchaURL = $this->urls->GetStaticUrl('captchaimg');
// send it back to the client now that we are using an inline form
$this->xml->writeCDataElement('captchaimg', $captcha->ShowCaptcha());
}
$this->GetPageNumberForComment($newReplyId);
$this->xml->writeElement('status', 1);
$this->xml->writeElement('id', $newReplyId);
$baseURL = $content->GetUrl();
$replyAnchor = 'comment_' . $newReplyId;
switch ($comment->Get('status')) {
case IWP_MODULE_COMMENTS_STATUS_PENDING:
$this->xml->writeElement('customMessage', $this->lang->Get('postSuccessPending'));
break;
default:
$this->xml->writeElement('message', $this->lang->Get('postSuccess'));
$this->xml->writeElement('redirect', sprintf('%s?comment_id=%d#%s', $baseURL, $newReplyId, $replyAnchor));
break;
}
$this->xml->outputXML();
die();
}
/**
* Shows a page for this module a particular URI
*
* @param string $url The visited URL
* @return mixed
*/
public function ShowPageByIniUri ($url)
{
$uriList = $this->urls->GetModuleUrls($this->moduleName);
$matchedUri = $this->urls->GetCurrentModulePage();
$uriBits = $this->urls->GetCurrentMatches();
if (!isset($uriBits['idnumber']) || !iwp_IsId($uriBits['idnumber'])) {
$this->controller->ShowPage('NotFound');
}
if (isset($uriBits['inreplyto']) && $uriBits['inreplyto'] != 0 && !iwp_IsId($uriBits['inreplyto'])) {
$this->controller->ShowPage('NotFound');
}
switch ($matchedUri) {
case 'CommentsImageAction':
return $this->ImageAction($uriBits['idnumber']);
break;
case 'CommentsPostAction':
return $this->PostAction($uriBits['idnumber']);
break;
case 'CommentsReplyDialogAction':
return $this->ReplyDialogAction($uriBits['idnumber'], $uriBits['uniqueid'], $uriBits['inreplyto']);
break;
case 'CommentsSettingsDialogAction':
return $this->SettingsDialogAction($uriBits['idnumber'], $uriBits['uniqueid']);
break;
case 'CommentsSettingsAction':
return $this->SettingsAction($uriBits['idnumber']);
break;
case 'CommentsReplyNotificationPreferences':
return $this->ReplyNotificationPreferencesAction($uriBits['idnumber'], $uriBits['uniqueid']);
break;
case 'CommentsReplyNotificationPreferencesSubmit':
return $this->ReplyNotificationPreferencesSubmitAction($uriBits['idnumber'], $uriBits['uniqueid']);
break;
}
}
/**
* Processes a request for changing the reply notification preferences of a comment and outputs an XML response.
*
* @param integer $commentId Comment id in database
* @param string $uniqueId Comment random unique string in database
*/
public function ReplyNotificationPreferencesSubmitAction ($commentId, $uniqueId)
{
// select comment from database based on id and unique string
$comment = new iwp_module_comments_commentdata();
$comment->Load($commentId);
if ($comment->GetId() != $commentId || $comment->Get('uniqueid') !== $uniqueId) {
$this->xml->writeElement('status', 0);
$this->xml->writeElement('error', $this->lang->Get('ReplyNotificationPreferencesCommentNotFound'));
$this->xml->outputXML();
die();
}
$allow = ($this->GetVar('notifyvisitorreply') == 'checked');
if ($allow) {
$enabled = isset($_POST['notifyreply']) ? 1 : 0;
} else {
$enabled = false;
}
$comment->Set('notifyreply', $enabled);
$allow = ($this->GetVar('notifyvisitormoderated') == 'checked');
if ($allow) {
$enabled = isset($_POST['notifymoderate']) ? 1 : 0;
} else {
$enabled = false;
}
$comment->Set('notifymoderate', $enabled);
if (!$comment->Save()) {
$this->xml->writeElement('status', 1);
$this->xml->writeElement('message', $this->lang->Get('databaseError'));
$this->xml->outputXML();
die();
}
$this->xml->writeElement('status', 1);
$this->xml->writeElement('message', $this->lang->Get('ReplyNotificationPreferencesSaved'));
$this->xml->outputXML();
die();
}
/**
* Renders a front-end page displaying reply-notification-preferences for the given comment.
*
* @param integer $commentId Comment id in database
* @param string $uniqueId Comment random unique string in database
*/
public function ReplyNotificationPreferencesAction ($commentId, $uniqueId)
{
// select comment from database based on id and unique string
$comment = new iwp_module_comments_commentdata();
$comment->LoadByField('uniqueid', $uniqueId);
if ($comment->GetId() != $commentId) {
//$this->controller->ShowPage('NotFound');
//
$error = sprintf('<h2>%s</h2><p>%s</p>', iwp_language::getInstance()->Get('Error'), $this->lang->Get('ReplyNotificationPreferencesCommentNotFound'));
$this->controller->SetContent($error);
$this->controller->ShowTemplate('error');
}
// present interface for enabling/disabling reply notification for this comment
$this->template->AddRequiredJS(IWP_BASE_URI . '/javascript/jquery.form.js');
$this->template->AddRequiredJS(IWP_BASE_URI . '/javascript/jquery/plugins/jquery.interspireAjaxForm.js');
$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.js');
$data = array();
$data['lang'] = $this->lang->GetLangVars();
$data['formAction'] = str_replace('{uniqueid}', $comment->Get('uniqueid'), $this->GetUri('CommentsReplyNotificationPreferencesSubmit', $comment->GetId()));
$data['commenturl'] = $comment->GetUrl();
$data['allownotifyreply'] = ($this->GetVar('notifyvisitorreply') == 'checked');
$data['allownotifymoderate'] = ($this->GetVar('notifyvisitormoderated') == 'checked');
$data['notifyreply_checked'] = ($comment->Get('notifyreply') == 1 ? 'checked="checked"' : '');
$data['notifymoderate_checked'] = ($comment->Get('notifymoderate') == 1 ? 'checked="checked"' : '');
$this->template->Assign($this->moduleName, $data, false);
$this->controller->SetContent($this->template->ParseTemplate('comments.replynotifications', true, $this->moduleName));
$this->controller->ShowTemplate('module_comments');
}
public function GetThreadDisplayOptions ()
{
return array(
'threaded' => $this->lang->Get('threaddisplay_threaded'),
'flat' => $this->lang->Get('threaddisplay_flat'),
);
}
public function GetSortOptions () {
return array(
'oldest' => $this->lang->Get('datesort_oldest'),
'newest' => $this->lang->Get('datesort_newest'),
);
}
/**
* Returns a list of options which are available for 'per page' settings.
*
* @return array
*/
public function GetPerPageOptions () {
return array(
'5' => '5',
'10' => '10',
'20' => '20',
'50' => '50',
'100' => '100',
'200' => '200',
);
}
/**
* Displays the settings form HTML for embedding into ajax imodal
*
* @param integer $id Content id we are commenting on
* @param string $uniqueId Unique id of module instance passed in by client
* @return void
*/
public function SettingsDialogAction ($id, $uniqueId)
{
$data = $this->LoadFrontEndData($id, false);
$data['uniqueId'] = $uniqueId;
$data['lang'] = $this->lang->GetLangVars();
$data['urlToModule'] = IWP_MODULES_URI . '/' . $this->moduleName;
if ($data['threaddisplayuser']) {
$threadDisplayOptions = array('default' => $this->lang->Get('useDefaultSetting')) + $this->GetThreadDisplayOptions();
$data['threadDisplayOptions'] = '';
while (list($key, $val) = each($threadDisplayOptions)) {
if ($this->GetCookie('threaddisplay') == $key) {
$selected = ' selected="selected"';
} else {
$selected = '';
}
$data['threadDisplayOptions'] .= sprintf('<option value="%s"%s>%s</option>', $key, $selected, $val);
}
}
if ($data['datesortuser']) {
$sortDisplayOptions = array('default' => $this->lang->Get('useDefaultSetting')) + $this->GetSortOptions();
$data['sortDisplayOptions'] = '';
while (list($key, $val) = each($sortDisplayOptions)) {
if ($this->GetCookie('datesort') == $key) {
$selected = ' selected="selected"';
} else {
$selected = '';
}
$data['sortDisplayOptions'] .= sprintf('<option value="%s"%s>%s</option>', $key, $selected, $val);
}
}
if ($data['perpageuser']) {
$perPageOptions = array('default' => $this->lang->Get('useDefaultSetting')) + $this->GetPerPageOptions();
$data['perPageOptions'] = '';
while (list($key, $val) = each($perPageOptions)) {
if ($this->GetCookie('perpage') == $key) {
$selected = ' selected="selected"';
} else {
$selected = '';
}
$data['perPageOptions'] .= sprintf('<option value="%s"%s>%s</option>', $key, $selected, $val);
}
}
$this->template->Assign($this->moduleName, $data, false);
$this->template->ParseTemplate('comments.settingsform', false, $this->moduleName);
}
/**
* Displays the comment form HTML for embedding into ajax modal dialog
*
* @param integer $id Content id we are commenting on
* @param string $uniqueId Unique id of module instance passed in by client
* @param integer $reply Parent comment id to reply to, or 0 if not replying to another comment
* @return void
*/
public function ReplyDialogAction ($id, $uniqueId, $reply)
{
$data = $this->LoadFrontEndData($id, false);
$data['uniqueId'] = $uniqueId;
$data['lang'] = $this->lang->GetLangVars();
$data['urlToModule'] = IWP_MODULES_URI . '/' . $this->moduleName;
$content = iwp_content::getInstance();
if ($content->GetId() != $id) {
$content->Load($id);
}
if (iwp_IsId($reply)) {
$replyingToComment = $this->GetIndividualComment($reply);
if ($replyingToComment) {
$data['parent'] = $replyingToComment['id'];
}
}
$this->template->Assign($this->moduleName, $data, false);
$this->template->ParseTemplate('comments.replyform', false, $this->moduleName);
}
/**
* Retreives a single comment from the database and returns it as an associative array of columns
*
* @param integer $commentId Comment id, as per module_commentdata.id column
* @param integer $contentId Content id which comment is attached to. Optional. if provided it will be included in the query.
* @return array|boolean Comment data from database, or false if comment not found
*/
public function GetIndividualComment ($commentId, $contentId = false)
{
$sql = sprintf("SELECT c.id, c.comment, c.fromname, c.fromemail, c.website, c.date, c.parentid, c.sortingoldest FROM `%s` c WHERE c.id = %d", $this->getTable('commentdata'), $commentId);
if ($contentId) {
$sql .= sprintf(" AND c.contentid = %d", $contentId);
}
$result = $this->db->Query($sql);
if ($result && $row = $this->db->Fetch($result)) {
return $row;
}
return false;
}
/**
* Generates a session captcha code and returns data for refreshing the clientside image
*
* @param integer $id
*/
public function ImageAction ($id)
{
$useCaptcha = ($this->GetVar('captcha') == 'checked');
if ($useCaptcha) {
$captcha = new captcha();
$captcha->sessionKey = 'commentsCaptcha';
$captcha->CreateSecret();
$captcha->CaptchaURL = $this->urls->GetStaticUrl('captchaimg');
$this->xml->writeElement('status', 1);
$this->xml->writeCDataElement('captchaimg', $captcha->ShowCaptcha());
$this->xml->outputXML();
} else {
$this->xml->writeElement('status', 0);
}
die();
}
/**
* TruncateLink()
*
* Truncates a link URL if it is too long. Used from within hyperlinkUrls().
*
* @name truncateLink()
* @author Scott Reilly
* @author Jordie Bodlay <jordie@interspire.com>
* @author Gwilym Evans <gwilym.evans@interspire.com>
* @copyright Copyright (c) 2004 by Scott Reilly (aka coffee2code)
* Used with Permission
* @version 2.02
*
* @param string $url the url to link to
* @param integer $mode 0 = display full url, 11+ = number of characters to truncate after
* @param string $trunc_before what to display before truncation
* @param string $trunc_after what to display after truncation
*
*/
public static function TruncateLink ($url, $mode = '0', $trunc_before = '', $trunc_after = '...') {
if (1 == $mode) {
$url = preg_replace("/(([a-z]+?):\\/\\/[A-Za-z0-9\-\.]+).*/i", "$1", $url);
$url = $trunc_before . preg_replace("/([A-Za-z0-9\-\.]+\.(com|org|net|gov|edu|us|info|biz|ws|name|tv|ee|co|aero|arpa|asia|cat|coop|int|jobs|mil|mobi|museum|pro|tel|travel|ac|ltd)).*/i", "$1", $url) . $trunc_after;
} elseif (($mode > 10) && (strlen($url) > $mode)) {
$url = $trunc_before . substr($url, 0, $mode) . $trunc_after;
}
return $url;
}
/**
* Parses the given DOMElement's child nodes for text links in text nodes and changes them to A elements.
*
* @param DOMXPath $xpath An instance of DOMXPath to use for querying.
* @param DOMElement $element The context DOMElement to use when querying.
*/
public static function ParseTextLinks ($xpath, $element)
{
// PHP 5.2.7 fixed/changed the behaviour of splitText to now use character offsets, see http://bugs.php.net/bug.php?id=46335
$useCharacterOffsets = version_compare(PHP_VERSION, '5.2.6', '>');
if ($useCharacterOffsets) {
$pregFunctionName = 'mb_preg_match_all';
$strlenFunctionName = 'mb_strlen';
} else {
$pregFunctionName = 'preg_match_all';
$strlenFunctionName = 'strlen';
}
// select all text nodes
$blocks = $xpath->query("//text()", $element);
foreach ($blocks as $block) {
$parent = $block->parentNode;
if ($parent->nodeName == 'a') {
// skip text nodes which are already inside links
continue;
}
$text = ' ' . $block->textContent . ' '; // pad the text for easier regex, this doesn't affect the final output
// we only want to run the dom manipulation routine once, so build a list of splits we want to make in the text
// note that the lengths below are reduced by 1 because we're matching a space before links which we don't want included in the split
// ...but the offset is not increased by one because we're going to be splitting the original text, which does not have a space added to the front of it (see padding above)
$links = array();
// hyperlink class b domains *.(com|org|net|gov|edu|us etc)(/*)
$pattern = "#([\s{}\(\)\[\]])([A-Za-z0-9\-\.]+)\.(com|org|net|gov|edu|us|info|biz|ws|name|tv|ee|co|aero|arpa|asia|cat|coop|int|jobs|mil|mobi|museum|pro|tel|travel|ac|ltd)(\.([A-Za-z]{2}))?((?:/[^\s{}\(\)\[\]]*[^\.,\s{}\(\)\[\]]?)?)#ie";
$pregFunctionName($pattern, $text, $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $match) {
$offset = $match[1];
$length = $strlenFunctionName($match[0]) - 1;
$uri = 'http://' . trim($match[0]);
$title = $uri;
$links[$offset] = array($length, $uri, $title);
}
// protocol link e.g. 'http://' pattern
$pattern = "#([\s{}\(\)\[\]])(([a-z]+?)://([A-Za-z_0-9\-]+\.([^\s{}\(\)\[\]]+[^\s,\.\;{}\(\)\[\]])))#ie";
$pregFunctionName($pattern, $text, $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $match) {
$offset = $match[1];
$length = $strlenFunctionName($match[0]) - 1;
$uri = trim($match[0]);
$title = $uri;
$links[$offset] = array($length, $uri, $title);
}
// email address pattern
$pattern = "#([\s{}\(\)\[\]])([A-Za-z0-9\-_\.]+?)@([^\s,{}\(\)\[\]]+\.[^\s.,{}\(\)\[\]]+)#ie";
$pregFunctionName($pattern, $text, $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $match) {
$offset = $match[1];
$length = $strlenFunctionName($match[0]) - 1;
$uri = 'mailto:' . trim($match[0]);
$title = trim($match[0]);
$links[$offset] = array($length, $uri, $title);
}
if (count($links) < 1) {
// no links found, skip
continue;
}
// sort the array backwards so we can split text starting off from the end
krsort($links);
foreach ($links as $offset => $link) {
$length = $link[0];
$uri = $link[1];
$title = $link[2];
if(($offset + $length) < 0) {
continue;
}
// first, split off any text after the link and keep a reference to it so we can insert before it
$trailing = $block->splitText($offset + $length);
if($trailing === false) {
continue;
}
// next, split at the start of the link text
$linktext = $block->splitText($offset);
// adjust the text to account for link truncating
$linktext->data = iwp_module_comments::TruncateLink($linktext->data, '50');
// now, $block contains everything before the link
// create a link element to hold our link text
$linkelement = $block->ownerDocument->createElement('a');
$linkelement->setAttribute('href', $uri);
$linkelement->setAttribute('title', $title);
// move the linktext (DOMText) inside the new element
$linkelement->appendChild($linktext);
// and add the new element to the dom before the trailing text
$block->parentNode->insertBefore($linkelement, $trailing);
}
}
}
/**
* Defines and returns the tag/attribute whitelist used for HTML cleaning
*
* @return array
*/
public function GetTagAttributeWhitelist ()
{
$whitelist = InterspireHTMLCleaner::$DefaultTagAttributeWhitelist;
$whitelist['ul'] = false;
$whitelist['ol'] = false;
$whitelist['li'] = false;
return $whitelist;
}
/**
* Converts a comment to it's HTML representation based on the settings configured in the module.
*
* @param string $body
* @param boolean $newlines If true, newline characters will be translated into <br /> tags
* @return string
*/
public function CommentBodyToHTML ($body, $newlines = true)
{
$allowhtml = $this->GetVar('allowhtml');
$nofollow = ($this->GetVar('nofollow') == 'checked');
$parselinks = ($this->GetVar('parselinks') == 'checked');
$cleaner = new InterspireHTMLCleaner();
// filter the provided text based on html allowance
switch ($allowhtml) {
case 'all':
// we don't filter the provided HTML but we have to load it into DOMDocument anyway so that we can parse text links
// has the bonus of un-breaking some broken html, like unclosed tags or tags closed in the wrong order
$cleaner->LoadHTMLAsDocument($body);
$document = $cleaner->GetDocument();
$xpath = $cleaner->LoadXPath($document);
break;
case 'some':
// clean up the html using the full process
$cleaner->SetTagAttributeWhitelist($this->GetTagAttributeWhitelist());
$cleaner->SetRelNoFollow(false); // don't add nofollow yet, we'll hit this manually later because we have to parse text links yet
$cleaner->CleanHTML($body);
$document = $cleaner->GetDocument();
$xpath = $cleaner->GetXPath();
break;
case 'none':
// change all provided html to text and pass it into the cleaner as a text node
$body = iwp_htmlentities($body);
$cleaner->LoadHTMLAsDocument($body);
$document = $cleaner->GetDocument();
$xpath = $cleaner->LoadXPath($document);
break;
}
// grab the element for the <body> tag so we can work on it's content instead of the headers
$bodyElement = $cleaner->GetBodyElement($document);
if(!($bodyElement instanceof DOMElement) || !($xpath instanceof DOMXPath)) {
return $body;
}
if ($parselinks) {
// if text link parsing is enabled, run this now before we add rel=nofollow target=_blank
iwp_module_comments::ParseTextLinks($xpath, $bodyElement);
}
if ($nofollow) {
// add rel=nofollow to all links in the document
$cleaner->AddRelNoFollow($xpath, $bodyElement);
}
// add target=_blank to all links in the document
$cleaner->SetAttributeOnMatches($xpath, $bodyElement, '//a', 'target', '_blank');;
// we're done, get the resulting xhtml string
$body = $cleaner->GetBodyContent($bodyElement);
if ($newlines) {
// stripping newlines completely instead of using nl2br which leaves them as "<br />\n"
$body = str_replace("\n", "<br />", $body);
}
return $body;
}
/**
* Given a comment id and a page size, this function will discover which page the comment is on based on the current page size, threading and sorting preferences.
*
* @param integer $commentId Id of the comment to find
* @param integer $pageSize Size of pages to calculate (optional)
* @return integer|boolean Page number the comment resides on, where pages begin at number 1. Otherwise, if the comment is not found, false will be returned.
*/
public function GetPageNumberForComment ($commentId, $pageSize = null)
{
if (is_null($pageSize)) {
$pageSize = $this->GetPerPagePreference();
}
$threading = $this->GetThreadDisplayPreference();
$sorting = $this->GetSortingPreference();
if ($threading == 'threaded') {
// threaded display
if ($sorting == 'oldest') {
// oldest first
$operator = '<';
$subcolumn = 'sortingoldest';
} else {
// newest first
$operator = '>';
$subcolumn = 'sortingnewest';
}
} else {
// flat display
if ($sorting == 'oldest') {
// oldest first
$operator = '<';
} else {
// newest first
$operator = '>';
}
$subcolumn = 'date';
}
// find where in the comments list this comment is by counting the amount of comments with a lower sorting value
$subsql = sprintf("SELECT c_sub.%s FROM `%s` c_sub WHERE c_sub.id = %d", $subcolumn, $this->getTable('commentdata'), $commentId);
$sql = sprintf("SELECT COUNT(*) FROM `%s` c WHERE c.%s %s (%s) AND c.contentid = (SELECT c_sub.contentid FROM `%s` c_sub WHERE c_sub.id = %d) AND c.status = '%s'", $this->getTable('commentdata'), $subcolumn, $operator, $subsql, $this->getTable('commentdata'), $commentId, IWP_MODULE_COMMENTS_STATUS_APPROVED);
$index = (int)$this->db->FetchOne($sql) + 1;
$page = (int)ceil($index / $pageSize);
return $page;
}
/**
* Generates a random string of characters usable as a unique id
*
* @param integer $len Length of string to return. Default is 8. Maximum length is 32.
* @return string
*/
public static function GenerateUniqueId ($len = 8)
{
return substr(md5(uniqid(rand())), 0 , $len);
}
/**
* Returns the preference for comments-per-page in the following priority (first to last): client cookie, server module config, default 20
*
* @return integer The sorting preference
*/
public function GetPerPagePreference ()
{
$allowclient = ($this->GetVar('perpageuser') == 'checked');
if ($allowclient) {
$client = $this->GetCookie('perpage');
if ($client !== false && array_key_exists($client, $this->GetPerPageOptions()) && $client != 'default') {
// if the client specifies a valid sorting option, return that
return (int)$client;
}
}
$server = $this->GetVar('perpage');
if ($server !== false) {
// if a var on the server has been set, return that
return (int)$server;
}
// return the module-default value
return 20;
}
/**
* Returns the preference for comment sorting in the following priority (first to last): client cookie, server module config, default 'oldest'
*
* @return string The sorting preference
*/
public function GetSortingPreference ()
{
$allowclient = ($this->GetVar('datesortuser') == 'checked');
if ($allowclient) {
$client = $this->GetCookie('datesort');
if ($client !== false && array_key_exists($client, $this->GetSortOptions()) && $client != 'default') {
// if the client specifies a valid sorting option, return that
return $client;
}
}
$server = $this->GetVar('datesort');
if ($server !== false) {
// if a var on the server has been set, return that
return $server;
}
// return the module-default value
return 'oldest';
}
/**
* Returns the preference for threaded display in the following priority (first to last): client cookie, server module config, default 'threaded'
*
* @return string The display preference
*/
public function GetThreadDisplayPreference ()
{
$allowclient = ($this->GetVar('threaddisplayuser') == 'checked');
if ($allowclient) {
$client = $this->GetCookie('threaddisplay');
if ($client !== false && array_key_exists($client, $this->GetThreadDisplayOptions()) && $client != 'default') {
// if the client specifies a valid sorting option, return that
return $client;
}
}
$server = $this->GetVar('threaddisplay');
if ($server !== false) {
// if a var on the server has been set, return that
return $server;
}
// return the module-default value
return 'threaded';
}
/**
* Returns whether or not new comments are enabled for the given content based on the site-wide and per-content settings. Will also return a negative result if existing comments are hidden, as this implies posting of new comments are disabled.
*
* @param integer $id Content id of the content to check.
* @return boolean Returns true if new comments are enabled, otherwise false.
*/
public function GetNewCommentsSetting ($id)
{
$visible = $this->GetExistingCommentsSetting($id);
if (!$visible) {
// posting is disabled if existing comments are not visible
return false;
}
$setting = $this->GetVar('sitewidenewcomments');
if ($setting == 'enabledall') {
return true;
} else if ($setting == 'disabledall') {
return false;
}
$content = $this->GetContentModuleData($id);
if ($content) {
if ($content['newcomments'] != 'default') {
return ($content['newcomments'] == 'enabled');
}
}
return ($setting == 'enabled');
}
/**
* Returns whether or not existing comments are visible for the given content based on the side-wide and per-content settings.
*
* @param integer $id Content id of the content to check.
* @return boolean Returns true if existing comments are visible, otherwise false.
*/
public function GetExistingCommentsSetting ($id)
{
$setting = $this->GetVar('sitewideexistingcomments');
if ($setting == 'visibleall') {
return true;
} else if ($setting == 'hiddenall') {
return false;
}
$content = $this->GetContentModuleData($id);
if ($content) {
if ($content['existingcomments'] != 'default') {
return ($content['existingcomments'] == 'visible');
}
}
return ($setting == 'visible');
}
/**
* Returns a count of comments, optionally filtered by content id and status.
*
* @return integer
*/
public function GetCommentCount ($contentId = null, $status = IWP_MODULE_COMMENTS_STATUS_APPROVED)
{
$sql = sprintf("SELECT COUNT(*) FROM `%s` c", $this->getTable('commentdata'));
$where = array();
if (!is_null($contentId)) {
$where[] = sprintf("c.contentid = %d", $contentId);
}
if ($status) {
$where[] = sprintf("c.status = '%s'", $status);
}
if (count($where)) {
$where = 'WHERE ('. implode(') AND (', $where) . ')';
} else {
$where = '';
}
$sql = sprintf("SELECT COUNT(*) FROM `%s` c %s", $this->getTable('commentdata'), $where);
return intval($this->db->FetchOne($sql));
}
/**
* Returns an array containing list of associative arrays representing a page of comments for a given content and status.
*
* @param string $threading The threading type to use when calculating a page.
* @param string $sorting The sorting type to use when calculating a page.
* @param iwp_paging $paging An instance of iwp_paging representing generic paging configuration.
* @param integer $contentId The content id to select comments from. If none is provided, or this parameter is null, then all comments will be returned.
* @param string $status The comment status to filter by. If none is provided, or this parameter is null, all comments will be returned.
* @return array
*/
public function GetCommentList ($threading, $sorting, iwp_paging &$paging, $contentId = null, $status = null)
{
$pageSize = $paging->ItemsPerPage;
$firstCommentOnPage = ($paging->CurrentPage - 1) * $pageSize;
$where = array();
if (!is_null($contentId)) {
$where[] = sprintf("c.contentid = %d", $contentId);
}
if (!is_null($status)) {
$where[] = sprintf("c.status = '%s'", $status);
}
if (count($where)) {
$where = 'WHERE ('. implode(') AND (', $where) . ')';
} else {
$where = '';
}
$columns = "c.id, c.comment, c.fromname, c.fromemail, c.website, c.date, c.parentid, c.counter, parent.counter AS parent_counter";
$join = sprintf("LEFT JOIN `%s` parent ON parent.id = c.parentid", $this->getTable('commentdata'));
if ($threading == 'threaded') {
// threaded display
if ($sorting == 'oldest') {
// oldest first
$sql = sprintf("SELECT ". $columns .", c.sortingoldest as sorting FROM `%s` c %s %s ORDER BY c.sortingoldest LIMIT %d, %d", $this->getTable('commentdata'), $join, $where, $firstCommentOnPage, $pageSize);
} else {
// newest first
$sql = sprintf("SELECT ". $columns .", c.sortingoldest as sorting FROM `%s` c %s %s ORDER BY c.sortingnewest DESC LIMIT %d, %d", $this->getTable('commentdata'), $join, $where, $firstCommentOnPage, $pageSize);
}
} else {
// flat display
if ($sorting == 'oldest') {
// oldest first
$dateSort = 'ASC';
} else {
// newest first
$dateSort = 'DESC';
}
$sql = sprintf("SELECT ". $columns ." FROM `%s` c %s %s ORDER BY c.date %s, c.id %s LIMIT %d, %d", $this->getTable('commentdata'), $join, $where, $dateSort, $dateSort, $firstCommentOnPage, $pageSize);
}
$result = $this->db->Query($sql);
$rows = array();
while ($row = $this->db->Fetch($result)) {
$row['comment'] = iwp_content::getInstance()->DecodeSiteURLs($row['comment']);
$rows[] = $row;
}
return $rows;
}
/**
* Loads data for displaying comments on the front end when viewing a single content item
*
* @param integer $id Content id from content table
* @param boolean $loadComments Optional. Default true. When true, this function will load the front-end comment data.
* @return array
*/
public function LoadFrontEndData ($id, $loadComments = true)
{
$data = array();
if (!iwp_IsId($id)) {
return false;
}
$content = iwp_content::getInstance();
if ($content->GetId() != $id) {
$content->Load($id);
}
if (!$content->GetId()) {
return false;
}
$data['newcomments'] = $this->GetNewCommentsSetting($content->GetId());
$data['existingcomments'] = $this->GetExistingCommentsSetting($content->GetId());
$data['ipblocked'] = $this->IsBlockedIP(GetIP());
$wysiwyg = $data['wysiwyg'] = ($this->GetVar('wysiwyg') == 'checked');
if ($wysiwyg) {
// gzip check taken from tiny_mce_gzip.php
$encodings = array();
$supportsGzip = false;
// Check if it supports gzip
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']))
$encodings = explode(',', iwp_strtolower(preg_replace("/\s+/", "", $_SERVER['HTTP_ACCEPT_ENCODING'])));
if ((in_array('gzip', $encodings) || in_array('x-gzip', $encodings) || isset($_SERVER['---------------'])) && function_exists('ob_gzhandler') && !ini_get('zlib.output_compression')) {
$enc = in_array('x-gzip', $encodings) ? "x-gzip" : "gzip";
$supportsGzip = true;
}
$data['wysiwyggzip'] = $supportsGzip;
}
$data['hideform'] = ($this->GetVar('hideform') == 'checked');
$this->template->AddRequiredJS(IWP_BASE_URI . '/javascript/jquery.form.js');
$this->template->AddRequiredJS(IWP_BASE_URI . '/javascript/jquery/plugins/jquery.interspireAjaxForm.js');
$this->template->AddRequiredJS(IWP_BASE_URI . '/javascript/jquery/plugins/jquery.scrollTo.js');
$this->template->AddRequiredCSS(IWP_BASE_URI . '/lib/imodal/imodal.css');
$this->template->AddRequiredJS(IWP_BASE_URI . '/lib/imodal/imodal.js');
$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.js');
$useCaptcha = $data['useCaptcha'] = ($this->GetVar('captcha') == 'checked');
if ($useCaptcha) {
$captcha = new captcha();
$captcha->sessionKey = 'commentsCaptcha';
$captcha->CreateSecret();
$captcha->CaptchaURL = $this->urls->GetStaticUrl('captchaimg');
$data['SecurityImage'] = $captcha->ShowCaptcha();
}
$data['uniqueId'] = iwp_module_comments::GenerateUniqueId();
$data['formAction'] = $this->GetUri('CommentsPostAction', iwp_content::getInstance()->GetId());
$data['formHeadingJs'] = iwp_validation::FilterJavascriptString($this->lang->Get('formHeading'));
$data['formHeadingReplyJs'] = iwp_validation::FilterJavascriptString($this->lang->Get('formHeadingReply'));
$data['captchaImageAction'] = iwp_validation::FilterJavascriptString($this->GetUri('CommentsImageAction', iwp_content::getInstance()->GetId()));
$data['replyDialogAction'] = iwp_validation::FilterJavascriptString(str_replace('{uniqueid}', $data['uniqueId'], $this->GetUri('CommentsReplyDialogAction', iwp_content::getInstance()->GetId())));
$data['settingsFormAction'] = $this->GetUri('CommentsSettingsAction', iwp_content::getInstance()->GetId());
$data['settingsDialogAction'] = iwp_validation::FilterJavascriptString(str_replace('{uniqueid}', $data['uniqueId'], $this->GetUri('CommentsSettingsDialogAction', iwp_content::getInstance()->GetId())));
$data['settingsDialogTitleJs'] = iwp_validation::FilterJavascriptString($this->lang->Get('settingsDialogTitle'));
$allowhtml = $data['allowhtml'] = $this->GetVar('allowhtml');
$data['htmlAllowed'] = $this->lang->Get('allowhtml_'. $allowhtml);
if ($allowhtml == 'some') {
$tags = array_keys($this->GetTagAttributeWhitelist());
sort($tags);
$data['htmlAllowedTags'] = '<'. implode('>, <', $tags) .'>';
}
$data['threaddisplayuser'] = ($this->GetVar('threaddisplayuser') == 'checked');
$data['datesortuser'] = ($this->GetVar('datesortuser') == 'checked');
$data['perpageuser'] = ($this->GetVar('perpageuser') == 'checked');
$data['showSettingsLink'] = ($data['threaddisplayuser'] || $data['datesortuser'] || $data['perpageuser']);
$data['comments'] = array();
$data['commentCount'] = false;
$data['commentCountTitle'] = '';
// use previously replied values if present
$data['fromname'] = iwp_htmlspecialchars($this->GetCookie('fromname'));
$data['fromemail'] = iwp_htmlspecialchars($this->GetCookie('fromemail'));
$data['website'] = iwp_htmlspecialchars($this->GetCookie('website'));
$data['allownotifyreply'] = ($this->GetVar('notifyvisitorreply') == 'checked');
if ($data['allownotifyreply']) {
$data['notifyreply_checked'] = ($this->GetCookie('notifyreply') == 1 ? 'checked="checked"' : '');
}
$data['allownotifymoderate'] = ($this->GetVar('notifyvisitormoderated') == 'checked');
if ($data['allownotifymoderate']) {
$data['notifymoderate_checked'] = ($this->GetCookie('notifymoderate') == 1 ? 'checked="checked"' : '');
}
if ($data['existingcomments'] && $loadComments) {
$data['commentCount'] = $commentCount = $this->GetCommentCount($content->GetId());
$data['commentCountTitle'] = sprintf(' (%s)', number_format($data['commentCount']));
$pageSize = $this->GetPerPagePreference();
$requestedCommentId = (int)@$_GET['comment_id'];
if ($requestedCommentId) {
// the browser has requested a particular comment id
// based on the paging parameters, we need to discover which page this comment is on
$requestedCommentPage = $this->GetPageNumberForComment($requestedCommentId, $pageSize);
} else {
$requestedCommentPage = (int)@$_GET['comments_page'];
}
if ($commentCount) {
$paging = iwp_paging::getInstance();
$paging->SetPaging($commentCount, $pageSize, $requestedCommentPage, '?comments_page=%d#contentcomments');
$firstCommentOnPage = ($paging->CurrentPage - 1) * $pageSize;
$baseURL = $content->GetUrl();
$pagingVars = array();
$pagingVars['PageCount'] = $paging->PageCount;
$pagingVars['currentPage'] = $paging->CurrentPage;
$pagingVars['prevPageLink'] = $_SERVER['REQUEST_URI'] . $paging->PreviousURL;
$pagingVars['nextPageLink'] = $_SERVER['REQUEST_URI'] . $paging->NextURL;
$pagingVars['links'] = $paging->PageList;
$pagingVars['firstPageLink']= $_SERVER['REQUEST_URI'] . $paging->FirstURL;
$pagingVars['lastPageLink'] = $_SERVER['REQUEST_URI'] . $paging->LastURL;
// to avoid any conflicts with the paging of content pages
$previousPaging = $this->template->Get('paging');
$this->template->Assign('paging', $pagingVars);
$data['paging'] = $this->template->ParseSection('paginglinks_standard');
$this->template->Assign('paging', $previousPaging, false);
$threading = $this->GetThreadDisplayPreference();
$sorting = $this->GetSortingPreference();
$dateFormat = iwp_getShortDateFormat();
$timeFormat = 'h:i a';
$commentsOnThisPage = array();
$rows = $this->GetCommentList($threading, $sorting, $paging, $content->GetId(), IWP_MODULE_COMMENTS_STATUS_APPROVED);
foreach ($rows as $row) {
$commentsOnThisPage[] = $row['id'];
$row['extraClasses'] = '';
if ($requestedCommentId && $requestedCommentId == $row['id']) {
$row['extraClasses'] .= ' CommentItemOutsideHighlight';
}
$row['anchor'] = 'comment_' . $row['id'];
$row['endanchor'] = 'commentend_' . $row['id'];
$row['permalinkURL'] = sprintf('%s?comment_id=%d#%s', $baseURL, $row['id'], $row['anchor']);
$timestamp = strtotime($row['date']);
$row['dateString'] = date($dateFormat, $timestamp);
$row['timeString'] = date($timeFormat, $timestamp);
if ($threading == 'threaded') {
$lineage = explode('.', $row['sorting']);
$row['depth'] = count($lineage) - 1;
} else {
$row['depth'] = 0;
}
$row['depthOpenTags'] = str_repeat('<div class="CommentItemDepth">', $row['depth']);
$row['depthCloseTags'] = str_repeat('</div>', $row['depth']);
if ($row['parentid']) {
if (in_array($row['parentid'], $commentsOnThisPage)) {
$row['inReplyToLink'] = sprintf('#comment_%d', $row['parentid']);
} else {
$row['inReplyToLink'] = sprintf('%s?comment_id=%d#comment_%d', $baseURL, $row['parentid'], $row['parentid']);
}
//$row['inReplyToText'] = sprintf($this->lang->Get('inReplyTo'), $row['parentid']);
}
$row['fromname_html'] = iwp_htmlspecialchars($row['fromname']);
$row['website_html'] = iwp_htmlspecialchars($this->GetWebsiteURL($row['website']));
$data['comments'][] = $row;
}
}
}
$contentType = $content->GetContentTypeInstance();
$data['contentLinkText'] = sprintf($this->lang->Get('contentLinkText'), $contentType->Get('name_singular'));
return $data;
}
/**
* Takes a website address string entered into a comment (which may have no leading protocol://) and returns a full URL
*
* @param string $website
* @return string
*/
public function GetWebsiteURL ($website) {
$website = InterspireHTMLCleaner::StripNull($website);
if (substr($website, 0, 7) != 'http://' && substr($website, 0, 8) != 'https://') {
$website = 'http://'. $website;
}
return $website;
}
/**
* This function is called by the module framework to implement the module-specific admin page.
*
* @return void
*/
public function AdminCustom ()
{
$moduleAction = @$_GET['moduleaction'];
// assign the module name so the main admin section knows which module we are in
$this->template->Assign('module', 'comments');
$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/admin.' . $this->moduleName . '.js');
switch ($moduleAction) {
case 'edit':
$this->template->Assign('moduleaction', $moduleAction);
$this->AdminEdit();
break;
default:
$this->template->Assign('moduleaction', 'view');
$this->AdminView();
break;
}
}
/**
* Display fields configuration used for control panel grid display
*
* @var array
*/
public $DisplayFields = array(
array(
'name' => 'Status',
'dbfield' => 'status',
'sortable' => true,
'display' => true,
'default' => '[None]',
'UseLang' => true,
),
);
/**
* Private storage for the control panel view data
*
* @var array
*/
private $ViewList;
/**
* This function returns the member array variable ViewList
*
* @return array The $ViewList variable'
*
* @see $ViewList
*/
public function GetViewList ()
{
return $this->ViewList;
}
/**
* Returns the form used for editing a comment
*
* @return iwp_form
*/
public function GetAdminEditForm ()
{
$form = new iwp_form();
$form->AddGroup('Details');
$form->AddField('textbox', 'fromname', null, $this->lang)
->DisableLabel()
->AddValidation('IsNotBlank')
->Required = true;
$form->AddField('textbox', 'fromemail', null, $this->lang)
->DisableLabel()
->AddValidation('IsNotBlank')
->AddValidation('ValidEmail')
->Required = true;
$form->AddField('textbox', 'website', null, $this->lang)
->DisableLabel();
$form->AddField('select', 'status', null, $this->lang)
->SetAttribute('class', 'Field120')
->AddFieldOption('approved', $this->lang->Get('Approved'))
->AddFieldOption('disapproved', $this->lang->Get('Disapproved'))
->AddFieldOption('pending', $this->lang->Get('Pending'))
->DisableHelpTip();
$wysiwyg = ($this->GetVar('wysiwyg') == 'checked');
if ($wysiwyg) {
$field = $form->AddField('wysiwyg', 'comment', null, $this->lang)->AddValidation('GetWYSIWYGContent');
} else {
$field = $form->AddField('textarea', 'comment', null, $this->lang);
}
$field->AddValidation('IsNotBlank')->Required = true;
$form->AddField('hidden', 'commentid', null, $this->lang);
return $form;
}
/**
* Displays the form for editing an existing comment.
*
* @return void
*/
public function AdminEdit ()
{
$comment = new iwp_module_comments_commentdata();
$comment->Load(intval($_GET['commentid']));
if (!$comment->CanUserAction('edit')) {
iwp_admin_home::getInstance()->ShowDashboard(GetLang('GenericAccessNoPermission'), MSG_ERROR);
die();
}
$this->form = $this->GetAdminEditForm();
$this->template->Assign('PageTitle', $this->lang->Get('BreadCrumbAction_Edit'));
$this->template->Assign('PageIntro', $this->lang->Get('EditIntro'));
$this->template->Assign('BreadCrumbSection', $this->lang->Get('BreadCrumbSection'));
$this->template->Assign('BreadCrumbAction', $this->lang->Get('BreadCrumbAction_Edit'));
if ($comment->GetId()) {
foreach ($this->form->Fields as $fieldname => $field) {
$field->Value($comment->Get($fieldname));
}
$field = $this->form->AddField('hidden', 'commentid', null, $this->lang);
$field->Value($comment->GetId());
}
$this->template->Assign('editFormFieldNamesJavascript', $this->form->GetFieldNamesJavascript());
$this->template->Assign('editFormJSFieldValidation', $this->form->GetJSFieldValidation());
$this->template->Assign('editFormOutput', $this->form->GetOutput());
$this->template->ParseTemplate('header');
$this->template->ParseTemplate('common.form.top');
$this->template->ParseTemplate('comments.edit', false, 'comments');
$this->template->ParseTemplate('footer');
}
/**
* Saves the form for editing an existing comment.
*
* @return void
*/
public function RemoteEditSave ()
{
$comment = new iwp_module_comments_commentdata();
$comment->Load((int)$_POST['commentid']);
if (!$comment->CanUserAction('edit')) {
$this->xml->writeElement('status', 0);
$this->xml->writeElement('message', iwp_language::getInstance()->Get('EditContentNoPermission'));
$this->xml->outputXML();
die();
}
$this->form = $this->GetAdminEditForm();
$valid = $this->form->Validate($_POST, $errorMsgs, $errorFields);
if (!$valid) {
$this->xml->writeElement('status', 0);
$this->xml->writeElement('message', sprintf(iwp_language::getInstance()->Get('ContentSaveErrors') .'<ul><li>'. implode('</li><li>', $errorMsgs) .'</li></ul>', $this->lang->Get('CommentCountSingular')));
$this->xml->outputXML();
die();
}
$oldStatus = $comment->Get('status');
$newStatus = $_POST['status'];
$comment->Set(array(
'fromname' => $_POST['fromname'],
'fromemail' => $_POST['fromemail'],
'website' => $_POST['website'],
'status' => $newStatus,
'comment' => $_POST['comment'],
));
$saved = $comment->Save();
if (!$saved) {
$this->xml->writeElement('status', 0);
$this->xml->writeElement('message', $this->lang->Get('databaseError'));
$this->xml->outputXML();
die();
}
if ($newStatus != $oldStatus) {
// status changed, trigger reactions but don't re-save
switch ($newStatus) {
case 'approved':
$comment->Approve(true, false);
break;
case 'disapproved':
$comment->Disapprove(true, false);
break;
}
}
$this->xml->writeElement('status', 1);
if (@$_POST['savemethod'] == 'andexit') {
iwp_session::Set('CommentsSuccessMessage', $this->lang->Get('CommentSavedSuccessfullyAndExit'));
$this->xml->writeElement('redirect', 'true');
} else {
$this->xml->writeElement('message', $this->lang->Get('CommentSavedSuccessfully'));
}
$this->xml->outputXML();
die();
}
/**
* Process a request for single admin action via AJAX
*
* @return void
*/
public function RemoteSingleAction ()
{
$singleaction = $_GET['singleaction'];
$this->xml->writeElement('action', $singleaction);
$commentId = (int)$_GET['commentid'];
if (!$commentId) {
$this->xml->writeElement('status', 0);
$this->xml->outputXML();
die();
}
$comment = new iwp_module_comments_commentdata();
$comment->Load($commentId);
if ($comment->GetId() != $commentId) {
$this->xml->writeElement('status', 0);
$this->xml->outputXML();
die();
}
// check user permission for this action
$hasperm = false;
switch ($singleaction) {
case 'delete':
case 'approve':
case 'disapprove':
case 'blockip':
case 'unblockip':
$hasperm = $comment->CanUserAction($singleaction);
break;
}
// return an xml error if no permission is granted
if (!$hasperm) {
$this->xml->writeElement('status', 0);
$this->xml->writeElement('error', $this->lang->Get('NoPermissionAction'));
$this->xml->outputXML();
die();
}
$deleted = false;
switch ($singleaction) {
case 'delete':
$comment->Delete($commentId);
$this->xml->writeElement('message', $this->lang->Get('DeletedOk'));
$this->xml->writeElement('refresh', 1);
$refresh = true;
$deleted = true;
break;
case 'approve':
$refresh = $comment->Approve();
$this->xml->writeElement('message', $this->lang->Get('ApprovedOk'));
break;
case 'disapprove':
$refresh = $comment->Disapprove();
$this->xml->writeElement('message', $this->lang->Get('DisapprovedOk'));
break;
case 'blockip':
$refresh = $comment->BlockIP();
$this->xml->writeElement('message', $this->lang->Get('BlockedOk'));
if ($refresh) {
$this->xml->writeElement('blocked', $comment->Get('ip'));
}
break;
case 'unblockip':
$refresh = $comment->UnblockIP();
$this->xml->writeElement('message', $this->lang->Get('UnblockedOk'));
if ($refresh) {
$this->xml->writeElement('unblocked', $comment->Get('ip'));
}
break;
}
$this->xml->writeElement('status', 1);
if ($refresh) {
$this->xml->startElement('row');
$this->xml->writeAttribute('id', $comment->GetId());
if (!$deleted) {
$this->xml->writeCData($comment->RenderAdminViewRow());
}
$this->xml->endElement();
}
$this->xml->outputXML();
die();
}
/**
* Process a request for bulk admin action via AJAX
*
* @return void
*/
public function RemoteBulkAction ()
{
$bulkaction = $_POST['bulkaction'];
$items = iwp_validation::FilterCleanArray($_POST['items']);
$ips = array();
$comment = new iwp_module_comments_commentdata();
foreach ($items as $itemId) {
if (!$itemId = (int)$itemId) {
// not a valid item id, skip
continue;
}
$comment->Load($itemId);
if ($comment->GetId() != $itemId) {
// comment didn't load, skip
continue;
}
// check user permission for this action on this item
$hasperm = false;
switch ($bulkaction) {
case 'delete':
case 'approve':
case 'disapprove':
case 'blockip':
case 'unblockip':
$hasperm = $comment->CanUserAction($bulkaction);
break;
}
if (!$hasperm) {
// no permission on this item, skip
continue;
}
// call the right function
$deleted = false;
switch ($bulkaction) {
case 'delete':
$comment->Delete($itemId);
$refresh = true;
$deleted = true;
break;
case 'approve':
$refresh = $comment->Approve();
break;
case 'disapprove':
$refresh = $comment->Disapprove();
break;
case 'blockip':
$refresh = $this->BlockIP($comment->Get('ip'));
if ($refresh) {
$this->xml->writeElement('blocked', $comment->Get('ip'));
}
break;
case 'unblockip':
$refresh = $this->UnblockIP($comment->Get('ip'));
if ($refresh) {
$this->xml->writeElement('unblocked', $comment->Get('ip'));
}
break;
}
if ($refresh) {
$this->xml->startElement('row');
$this->xml->writeAttribute('id', $comment->GetId());
if (!$deleted) {
$this->xml->writeCData($comment->RenderAdminViewRow(true));
}
$this->xml->endElement();
}
}
if ($bulkaction == 'delete') {
$this->xml->writeElement('refresh', 1);
}
$this->xml->writeElement('status', 1);
$this->xml->outputXML();
}
/**
* Shows the 'view comments' control panel page
*
* @return void
*/
public function AdminView ()
{
if (!iwp_HasPerm('sitemodules', 'comments', '*') && !iwp_HasMultiPerm(IWP_MULTIPERM_MATCHTYPE_ANY, 'content', '*', array('comments_edit', 'comments_delete', 'comments_approve', 'comments_disapprove'))) {
iwp_admin_home::getInstance()->ShowDashboard(GetLang('GenericAccessNoPermission'), MSG_ERROR);
die();
}
// Load and display the template
$this->template->AddRequiredCSS(IWP_MODULES_URI . '/' . $this->moduleName . '/templates/admin.' . $this->moduleName . '.css');
if (iwp_session::Exists('CommentsSuccessMessage')) {
$this->template->Assign('MessageText', iwp_session::Get('CommentsSuccessMessage'));
$this->template->Assign('MessageType', MSG_SUCCESS);
iwp_session::Kill('CommentsSuccessMessage');
}
$this->template->Assign('PageTitle', $this->lang->Get('ManageComments'));
$this->template->Assign('PageIntro', $this->lang->Get('ManageCommentsIntro'));
$this->template->Assign('DisplayFields', $this->GetDisplayFields());
$this->template->Assign('DisplayFilters', 1);
$this->template->Assign('BreadCrumbSection', $this->lang->Get('BreadCrumbSection'));
$this->template->Assign('BreadCrumbAction', $this->lang->Get('BreadCrumbAction_ViewComments'));
$this->template->Assign('ViewType', iwp_strtolower($this->lang->Get('Approved')));
$this->LoadViewList();
$data = array();
$data['lang'] = $this->lang->GetLangVars();
$data['lang_ConfirmDelete_JS'] = iwp_validation::FilterJavascriptString($data['lang']['ConfirmDelete']);
$data['pendingCount'] = $this->db->FetchOne(sprintf("SELECT COUNT(*) FROM `%s` comments WHERE comments.status = '%s'", $this->getTable('commentdata'), IWP_MODULE_COMMENTS_STATUS_PENDING));
$data['approvedCount'] = $this->db->FetchOne(sprintf("SELECT COUNT(*) FROM `%s` comments WHERE comments.status = '%s'", $this->getTable('commentdata'), IWP_MODULE_COMMENTS_STATUS_APPROVED));
$data['disapprovedCount'] = $this->db->FetchOne(sprintf("SELECT COUNT(*) FROM `%s` comments WHERE comments.status = '%s'", $this->getTable('commentdata'), IWP_MODULE_COMMENTS_STATUS_DISAPPROVED));
$this->template->Assign($this->moduleName, $data, false);
$this->template->ParseTemplate('header');
$this->template->ParseTemplate('common.form.top');
$this->template->Assign('paging', $this->template->ParseTemplate('paging', true));
$this->template->ParseTemplate('comments.view', false, 'comments');
$this->template->ParseTemplate('footer');
}
/**
* This function loads the manage 'view' for the lists and puts it into the member variable $ViewList which is then used by the template system to loop over
*
* @return void Doesn't return anything
*
* @see $ViewList
*/
public function LoadViewList($status = null)
{
// order
$sortField = 'date';
$sortDir = 'desc';
if(isset($_GET['sortdir'])){
if(iwp_strtolower($_GET['sortdir']) == "asc"){
$sortDir = "asc";
}
if(iwp_strtolower($_GET['sortdir']) == "desc"){
$sortDir = "desc";
}
}
if(isset($_GET['field'])){
foreach($this->DisplayFields as $_key=>$v) {
if($v['dbfield'] == $_GET['field']){
$sortField = $v['dbfield'];
$this->template->Assign('SortField', $sortField);
$this->template->Assign('SortDir', $sortDir);
break;
}
}
}
if(isset($_GET['page'])){
$requestPage = (int)$_GET['page'];
if($requestPage < 1){
$requestPage = 1;
}
}else{
$requestPage = 1;
}
$searchQuery = '';
if (isset($_GET['searchQuery'])) {
$searchQuery = $_GET['searchQuery'];
}
$this->template->Assign('searchQuery', $searchQuery, true);
$searchField = '';
if (isset($_GET['searchField'])) {
$searchField = $_GET['searchField'];
if ($searchField) {
$this->template->Assign('SearchField_'. $searchField .'_selected', ' selected="selected"');
}
}
$this->template->Assign('searchField', $searchField, true);
$isSearching = $searchQuery != '';
$this->template->Assign('isSearching', $isSearching, false);
$where = array();
if (!is_null($status)) {
$where[] = sprintf("comment.status = '%s'", $this->db->Quote($status));
}
if ($isSearching) {
switch ($searchField) {
case '':
// all fields
$searchQueryQuoted = $this->db->Quote($searchQuery);
$where[] = '(('. implode(') OR (', array(
$this->db->FullText(array('comment.comment'), $searchQueryQuoted),
sprintf("comment.`%s` = '%s'", 'ip', $searchQueryQuoted),
sprintf("comment.`%s` = '%s'", 'contentid', $searchQueryQuoted),
sprintf("comment.`%s` LIKE '%%%s%%'", 'fromname', $searchQueryQuoted),
sprintf("comment.`%s` LIKE '%%%s%%'", 'fromemail', $searchQueryQuoted),
sprintf("comment.`%s` LIKE '%%%s%%'", 'website', $searchQueryQuoted),
)) .'))';
break;
case 'comment':
$where[] = $this->db->FullText(array('comment.comment'), $searchQuery);
break;
case 'ip':
case 'contentid':
case 'status':
$where[] = sprintf("comment.`%s` = '%s'", $this->db->Quote($searchField), $this->db->Quote($searchQuery));
break;
default:
$where[] = sprintf("comment.`%s` like '%%%s%%'", $this->db->Quote($searchField), $this->db->Quote($searchQuery));
break;
}
$this->template->Assign('clearSearchUrl', 'index.php?section=module&action=custom&module=comments&moduleaction='. urlencode($_GET['moduleaction']));
}
// run through permissions to see if we can build a list of items that need to be pulled from the database
$permissions = $this->auth->GetPermissionData();
$permWhere = array();
foreach ($permissions as $perm) {
$allow = ($perm['flag'] == 'allow');
if (!$allow) {
// only process allow flags
continue;
}
if ($perm['scopetype'] == 'core' && $perm['scopename'] == 'root') {
// superuser access, don't bother filtering anything
$permWhere = array();
break;
}
if ($perm['scopetype'] == 'sitemodules' && $perm['scopename'] == $this->moduleName) {
// special case specifically for comments module permissions
// show all comments if any of these permissions are defined
$permWhere = array();
break;
}
if ($perm['scopetype'] != 'content') {
// only process content rules
continue;
}
$thisWhere = array();
if ($perm['scopename'] != '__all') {
$thisWhere['type'] = "content.typeid = ". $perm['scopename'] ."";
}
foreach ($perm['granularity'] as $granularity) {
switch ($granularity) {
case 'own':
if (!isset($thisWhere['granular']))
$thisWhere['granular'] = array();
$thisWhere['granular']['own'] = "content.author=". $this->auth->GetUserId();
break;
case 'specific':
if (count($perm['catcsv'])) {
if (!isset($thisWhere['granular'])) {
$thisWhere['granular'] = array();
}
$thisWhere['granular']['cat'] = "(select count(*) from ". IWP_TABLE_CATASSOC ." ca where ca.contentid = content.contentid and ca.categoryid in(". implode(',', $perm['catcsv']) .")) > 0";
}
if (count($perm['itemcsv'])) {
if (!isset($thisWhere['granular'])) {
$thisWhere['granular'] = array();
}
$thisWhere['granular']['item'] = "content.contentid in(". implode(',', $perm['itemcsv']) .")";
}
break;
}
}
if (isset($thisWhere['granular']))
$thisWhere['granular'] = '('. implode(')OR(', $thisWhere['granular']) .')';
if (count($thisWhere)) {
$thisWhere = '('. implode(')AND(', $thisWhere) .')';
$permWhere[] = $thisWhere;
}
}
if (count($permWhere)) {
$where[] = '(('. implode(') OR (', $permWhere) .'))';
}
$perpage = 20;
$start = ($requestPage * $perpage) - $perpage;
if($this->ViewQuery == null){
if(sizeof($where) > 0){
$where = array_unique($where);
$queryWhere = ' where '.implode(' and ', $where).' ';
} else {
$queryWhere = '';
}
$contentJoin = ' LEFT JOIN '. IWP_TABLE_CONTENT .' content ON content.contentid = comment.contentid ';
$query = 'select SQL_CALC_FOUND_ROWS comment.*, content.title, content.typeid from ' . $this->getTable('commentdata') . ' comment ' . $contentJoin . ' ' . $queryWhere. ' order by `' . $sortField . '` ' . $sortDir . ' limit '.$start.', '.$perpage;
$this->ViewQuery = $this->db->Query($query);
}
$TotalResults = $this->db->FetchOne('select found_rows()');
while(($row = $this->db->Fetch($this->ViewQuery))) {
$row = $this->CommentDataViewList($row);
$this->ViewList[] = $row;
}
$this->paging = new iwp_paging();
$this->paging->ReplaceToken = '{pagenum}';
$this->paging->SetPaging($TotalResults, $perpage, $requestPage, 'index.php?section=module&action=custom&module=comments&moduleaction=' . urlencode($_GET['moduleaction']) . '&searchQuery='. urlencode($searchQuery) .'&searchField='. urlencode($searchField) .'&sortdir='. urlencode($sortDir) .'&field='. urlencode($sortField) .'&page={pagenum}', 10);
$paging['PageCount'] = $this->paging->PageCount;
$paging['CurrentPage'] = $this->paging->CurrentPage;
$paging['PreviousURL'] = $this->paging->PreviousURL;
$paging['PageList'] = $this->paging->PageList;
$paging['NextURL'] = $this->paging->NextURL;
$paging['FirstURL'] = $this->paging->FirstURL;
$paging['LastURL'] = $this->paging->LastURL;
$this->template->AddRequiredJS(IWP_BASE_URI . '/javascript/jquery.form.js');
$this->template->Assign('paging', $paging);
$this->template->Assign('DisplayFields', $this->GetDisplayFields());
$this->template->Assign('ViewList', $this->GetViewList());
$this->template->Assign('ViewListCount', count($this->GetViewList()));
}
/**
* Parses a commentdata row and modifies it as needed for viewing in the admin view list.
*
* @param array $data
* @return array
*/
public function CommentDataViewList ($data)
{
$data['ip_blocked'] = $this->IsBlockedIP($data['ip']);
$date = strtotime($data['date']);
$data['date_html'] = date(iwp_config::Get('ShortDateFormat'), $date);
$data['date_time'] = date('h:ia', $date);
$data['website_url'] = $this->GetWebsiteURL($data['website']);
$content = iwp_content::getInstance();
$content->Load($data['contentid']);
$data['content_url'] = $content->GetUrl();
$data['title'] = $content->Get('title');
$auth = iwp_auth::getInstance();
$comment = iwp_module_comments_commentdata::getInstance();
$data['can_approve'] = $comment->CanUserAction('approve', $content->Get('typeid'));
$data['can_disapprove'] = $comment->CanUserAction('disapprove', $content->Get('typeid'));
$data['can_edit'] = $comment->CanUserAction('edit', $content->Get('typeid'));
$data['can_delete'] = $comment->CanUserAction('delete', $content->Get('typeid'));
$data['can_blockip'] = $comment->CanUserAction('blockip', $content->Get('typeid'));
$data['can_unblockip'] = $comment->CanUserAction('unblockip', $content->Get('typeid'));
$data['can_anyaction'] = ($data['can_approve'] || $data['can_disapprove'] || $data['can_edit'] || $data['can_delete'] || $data['can_blockip'] || $data['can_unblockip']);
$data['comment'] = iwp_content::getInstance()->DecodeSiteURLs($data['comment']);
return $data;
}
/**
* Private storage cache for the blocked ip list.
*
* @var array
*/
private static $blockedIpList = null;
/**
* Loads and returns the blocked IP list as an array. The array is returned by reference and can be modified and saved using SaveBlockedIPList().
*
* @param boolean $forceReload Optional. Default false. If true, this will force a reload from the vars table even if it has been loaded once during this execution already.
*
* @return array
*/
public function &GetBlockedIPList ($forceReload = false)
{
if ($forceReload || iwp_module_comments::$blockedIpList === null) {
$blocked = $this->GetVar('blocked');
if ($blocked === false || $blocked == '') {
iwp_module_comments::$blockedIpList = array();
} else {
iwp_module_comments::$blockedIpList = explode("\n", $blocked);
}
}
return iwp_module_comments::$blockedIpList;
}
/**
* Saves the in-memory blocked IP list. The IP list must have been loaded using GetBlockedIPList first.
*
* @return void
*/
public function SaveBlockedIPList ()
{
if (iwp_module_comments::$blockedIpList === null) {
return;
}
$this->SetVar('blocked', implode("\n", iwp_module_comments::$blockedIpList));
}
/**
* Adds an IP to the block list if it is not already on it.
*
* @param string $ip The ip address to add
* @param boolean $save Optional. Default true. If set to true, this will save the blocked ip list via SaveBlockedIPList(). Useful for single calls to this function but should be set to false if blocking multiple ips at the same time.
*
* @return boolean Returns true if the ip was added, otherwise false.
*/
public function BlockIP ($ip, $save = true)
{
if ($this->IsBlockedIP($ip)) {
// ip is already blocked, return
return false;
}
if (!iwp_event::trigger(new iwp_event_module_comments_beforeipblock($ip, $save))) {
return false;
}
$blocked = &$this->GetBlockedIPList();
$blocked[] = $ip;
if ($save) {
$this->SaveBlockedIPList();
}
iwp_event::trigger(new iwp_event_module_comments_afteripblock($ip, $save));
return true;
}
/**
* Removes an IP from the block list if it is currently on it.
*
* @param string $ip The ip address to remove
* @param boolean $save Optional. Default true. If set to true, this will save the blocked ip list via SaveBlockedIPList(). Useful for single calls to this function but should be set to false if unblocking multiple ips at the same time.
*
* @return boolean Returns true if the ip was removed, otherwise false.
*/
public function UnblockIP ($ip, $save = true)
{
if (!$this->IsBlockedIP($ip)) {
// ip is not blocked, return
return false;
}
if (!iwp_event::trigger(new iwp_event_module_comments_beforeipunblock($ip, $save))) {
return false;
}
$blocked = &$this->GetBlockedIPList();
$offset = array_search($ip, $blocked);
array_splice($blocked, $offset, 1);
if ($save) {
$this->SaveBlockedIPList();
}
iwp_event::trigger(new iwp_event_module_comments_afteripunblock($ip, $save));
return true;
}
/**
* A function to check if a given IP is blocked by the comments system.
*
* @param string $ip The IP address to check
*
* @return boolean Returns true if the IP address $ip has been blocked, otherwise false.
*/
public function IsBlockedIP ($ip)
{
$blocked = &$this->GetBlockedIPList();
return in_array($ip, $blocked);
}
/**
* Returns an array of content display fields to push into the content list grids when appropriate.
*
* @return array
*/
public function GetContentDisplayFields ()
{
// available table aliases for subqueries are c, and ct
// iwp_content c
// iwp_content_types ct
return array(
array(
'name' => 'Comments',
'lang' => $this->lang->Get('ModuleNamePlural'),
'dbfield' => 'count',
'sortable' => true,
'display' => true,
'displaytemplate' => '<a href="index.php?section=module&action=custom&module=comments&moduleaction=view&searchQuery=%%contentid%%&searchField=contentid" title="'. $this->lang->Get('ViewComments') .'">%%value%%</a>',
'default' => '0',
'subquery' => sprintf("SELECT COUNT(*) FROM `%s` comments WHERE comments.contentid = c.contentid AND comments.status = '%s'", $this->getTable('commentdata'), IWP_MODULE_COMMENTS_STATUS_APPROVED),
),
);
}
/**
* Returns a list of this module's database tables and their CREATE SQL statements
*
* @return array Array with table name as key, CREATE SQL statement as value.
*/
public function CreateModuleTable ()
{
// create statements should match a SHOW CREATE result, with cr/lf chars stripped out
$settingsTableName = $this->getTable('comments');
$dataTableName = $this->getTable('commentdata');
$settingsTable = new iwp_tablecreator($settingsTableName);
$settingsTable->AddField('id')->SetAsPrimaryKey();
$settingsTable->AddField('newcomments')->SetAsVarChar('16')->Set('isNull', true)->Set('default', 'NULL');
$settingsTable->AddField('existingcomments')->SetAsVarChar('16');
$settingsTable->AddField('notifyauthor')->SetAsTinyInt();
$dataTable = new iwp_tablecreator($dataTableName);
$dataTable->AddField('id')->SetAsPrimaryKey();
$dataTable->AddField('comment')->Set('type', 'longtext')->Set('hasFullText', true);
$dataTable->AddField('fromname')->SetAsVarChar();
$dataTable->AddField('fromemail')->SetAsVarChar();
$dataTable->AddField('website')->SetAsVarChar();
$dataTable->AddField('ip')->SetAsVarChar()->Set('hasIndex', true);
$dataTable->AddField('contentid')->SetAsInt();
$dataTable->AddField('status')->Set('type', 'enum')->Set('enumValues', array('pending','approved','disapproved'))->Set('isNull', true);
$dataTable->AddField('parentid')->SetAsInt();
$dataTable->AddField('userid')->SetAsInt();
$dataTable->AddField('fromadmin')->SetAsTinyInt();
$dataTable->AddField('notifymoderate')->SetAsTinyInt()->Set('default', '0');
$dataTable->AddField('notifyreply')->SetAsTinyInt()->Set('default', '0');
$dataTable->AddField('sortingnewest')->SetAsVarChar();
$dataTable->AddField('sortingoldest')->SetAsVarChar();
$dataTable->AddField('uniqueid')->Set('type', 'char')->Set('length', '16');
$dataTable->AddField('date')->Set('type', 'timestamp')->Set('default', 'CURRENT_TIMESTAMP')->SetOnUpdateTimeStamp();
$dataTable->AddField('counter')->SetAsInt();
$dataTable->AddIndex('contentid_status_sortingoldest', array('contentid','status','sortingoldest'));
$dataTable->AddIndex('contentid_status_sortingnewest', array('contentid','status','sortingnewest'));
$dataTable->AddIndex('contentid_status', array('contentid','status'));
$dataTable->AddIndex('contentid_date', array('contentid','date'));
$dataTable->AddIndex('status_date', array('status','date'));
$dataTable->AddIndex('contentid_status_date_id', array('contentid','status', 'date', 'id'));
$this->table = array(&$settingsTable, &$dataTable);
$this->tableSchema = array(
$settingsTableName => $settingsTable->GetCreateTable(),
$dataTableName => $dataTable->GetCreateTable(),
);
}
/**
* Defines the section this module's list output is appended to on the front end
*
* @var string
*/
protected $ListAppendSection = 'afterDetails';
/**
* Loads the data used to power the display of this module when listing content on the front end
*
* @param integer $contentId Content id from content table
* @return array
*/
public function LoadListOutput ($contentId)
{
$data = array();
if (!iwp_IsId($contentId)) {
return $data;
}
$data['commentCount'] = $this->db->FetchOne(sprintf("select count(*) from `%s` comments where comments.contentid = %d and comments.status = '%s'", $this->getTable('commentdata'), $contentId, IWP_MODULE_COMMENTS_STATUS_APPROVED));
$data['commentCountFormatted'] = number_format($data['commentCount']);
$data['hasComments'] = ($data['commentCount'] > 0);
if ($data['hasComments']) {
$data['commentCountSuffix'] = $this->lang->Get($data['commentCount'] == 1 ? 'CommentCountSingular' : 'CommentCountPlural');
}
return $data;
}
/**
* Returns the template name used by this module when listing content on the front-end
*
* @return string
*/
public function GetListTemplateName ()
{
return 'listoutput.comments';
}
public function GetActivityLogSubjectURL ($subjectId = '', $subjectText = '') {
return 'index.php?section=module&action=custom&module=comments&moduleaction=edit&commentid='. $subjectId;
}
public function GetActivityLogUserURL ($userId = '', $userText = '') {
if (!$userId) {
return '';
}
if (is_numeric($userId)) {
// action taken in control panel by internal user - userid recorded in userid field
$userId = (int)$userId;
return 'index.php?section=user&action=edit&userid='. $userId;
} else {
// action taken on front end by visitor - email address recorded in userid field
return 'index.php?section=module&action=custom&module=comments&moduleaction=view&searchQuery='. urlencode($userId) .'&searchField=fromemail';
}
}
}
/**
* @see iwp_module::$SitePermissionOptions
*/
iwp_module_comments::$SitePermissionOptions = array(
'full' => new iwp_permissionoption(false, false, false),
'edit' => new iwp_permissionoption(false, false, false),
'delete' => new iwp_permissionoption(false, false, false),
'approve' => new iwp_permissionoption(false, false, false),
'disapprove' => new iwp_permissionoption(false, false, false),
'blockip' => new iwp_permissionoption(false, false, false),
'unblockip' => new iwp_permissionoption(false, false, false),
);
/**
* @see iwp_module::$ContentPermissionOptions
*/
iwp_module_comments::$ContentPermissionOptions = array(
'edit' => new iwp_permissionoption(true, true, false),
'delete' => new iwp_permissionoption(true, true, false),
'approve' => new iwp_permissionoption(true, true, false),
'disapprove' => new iwp_permissionoption(true, true, false),
);