Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 3 Oct 2013 */ namespace IPS; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Abstract Content Model */ abstract class _Content extends \IPS\Patterns\ActiveRecord { /** * @brief [Content\Comment] Database Column Map */ protected static $databaseColumnMap = array(); /** * @brief [Content] Key for hide reasons */ public static $hideLogKey = NULL; /** * @brief [Content\Comment] Language prefix for forms */ public static $formLangPrefix = ''; /** * @brief [Content\Item] Include this content type in user profiles */ public static $includeInUserProfiles = TRUE; /** * @brief Include In Sitemap */ public static $includeInSitemap = TRUE; /** * @brief Reputation Store */ protected $reputation; /** * Should posting this increment the poster's post count? * * @param \IPS\Node\Model|NULL $container Container * @return void */ public static function incrementPostCount( \IPS\Node\Model $container = NULL ) { return TRUE; } /** * Post count for member * * @param \IPS\Member $member The memner * @return int */ public static function memberPostCount( \IPS\Member $member ) { return static::incrementPostCount() ? \IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, array( static::$databasePrefix . static::$databaseColumnMap['author'] . '=?', $member->member_id ) )->first() : 0; } /** * Load and check permissions * * @return static * @throws \OutOfRangeException */ public static function loadAndCheckPerms( $id ) { $obj = static::load( $id ); if ( !$obj->canView( \IPS\Member::loggedIn() ) ) { throw new \OutOfRangeException; } return $obj; } /** * Construct ActiveRecord from database row * * @param array $data Row from database table * @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there? * @return static */ public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE ) { if ( isset( $data[ static::$databaseTable ] ) and is_array( $data[ static::$databaseTable ] ) ) { /* Add author data to multiton store to prevent ->author() running another query later */ if ( isset( $data['author'] ) and is_array( $data['author'] ) ) { $author = \IPS\Member::constructFromData( $data['author'], FALSE ); if ( isset( $data['author_pfields'] ) ) { unset( $data['author_pfields']['member_id'] ); $author->contentProfileFields(); } } /* Load content */ $obj = parent::constructFromData( $data[ static::$databaseTable ], $updateMultitonStoreIfExists ); /* Add reputation if it was passed*/ if ( isset( $data['core_reputation_index'] ) and is_array( $data['core_reputation_index'] ) ) { $obj->_data = array_merge( $obj->_data, $data['core_reputation_index'] ); } /* Return */ return $obj; } else { return parent::constructFromData( $data, $updateMultitonStoreIfExists ); } } /** * Get WHERE clause for Social Group considerations for getItemsWithPermission * * @param string $socialGroupColumn The column which contains the social group ID * @param \IPS\Member $member The member (NULL to use currently logged in member) * @return string */ public static function socialGroupGetItemsWithPermissionWhere( $socialGroupColumn, $member ) { $socialGroups = array(); $member = $member ?: \IPS\Member::loggedIn(); if ( $member->member_id ) { $socialGroups = iterator_to_array( \IPS\Db::i()->select( 'group_id', 'core_sys_social_group_members', array( 'member_id=?', $member->member_id ) ) ); } if ( count( $socialGroups ) ) { return $socialGroupColumn . '=0 OR ( ' . \IPS\Db::i()->in( $socialGroupColumn, $socialGroups ) . ' )'; } else { return $socialGroupColumn . '=0'; } } /** * Check the request for legacy parameters we may need to redirect to * * @return NULL|\IPS\Http\Url */ public function checkForLegacyParameters() { $paramsToSet = array(); $paramsToUnset = array(); /* st=20 needs to go to page=2 (or whatever the comments per page setting is set to) */ if( isset( \IPS\Request::i()->st ) ) { $commentsPerPage = static::getCommentsPerPage(); $paramsToSet['page'] = floor( intval( \IPS\Request::i()->st ) / $commentsPerPage ) + 1; $paramsToUnset[] = 'st'; } /* Did we have any? */ if( count( $paramsToSet ) ) { $url = $this->url(); if( count( $paramsToUnset ) ) { $url = $url->stripQueryString( $paramsToUnset ); } $url = $url->setQueryString( $paramsToSet ); return $url; } return NULL; } /** * Get mapped value * * @param string $key date,content,ip_address,first * @return mixed */ public function mapped( $key ) { if ( isset( static::$databaseColumnMap[ $key ] ) ) { $field = static::$databaseColumnMap[ $key ]; if ( is_array( $field ) ) { $field = array_pop( $field ); } return $this->$field; } return NULL; } /** * Get author * * @return \IPS\Member */ public function author() { if ( $this->mapped('author') or !isset( static::$databaseColumnMap['author_name'] ) or !$this->mapped('author_name') ) { return \IPS\Member::load( $this->mapped('author') ); } else { $guest = new \IPS\Member; $guest->name = $this->mapped('author_name'); return $guest; } } /** * Returns the content * * @return string */ public function content() { return $this->mapped('content'); } /** * Returns the content images * * @param int|null $limit Number of attachments to fetch, or NULL for all * * @return array|NULL * @throws \BadMethodCallException */ public function contentImages( $limit = NULL ) { $idColumn = static::$databaseColumnId; $internal = NULL; $attachments = array(); if ( isset( static::$databaseColumnMap['content'] ) ) { $internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=?', static::$application . '_' . ucfirst( static::$module ), $this->$idColumn ) ); } if ( $internal ) { foreach( \IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id IN(?)', $internal ), array( 'attach_is_image=1' ) ), 'attach_id ASC', $limit ) as $row ) { $attachments[] = array( 'core_Attachment' => $row['attach_location'] ); } } return count( $attachments ) ? $attachments : NULL; } /** * Text for use with data-ipsTruncate * Returns the post with paragraphs turned into line breaks * * @param bool $oneLine If TRUE, will use spaces instead of line breaks. Useful if using a single line display. * @return string * @note For now we are removing all HTML. If we decide to change this to remove specific tags in future, we can use \IPS\Text\Parser::removeElements( $this->content() ) */ public function truncated( $oneLine=FALSE ) { /* Specifically remove quotes, any scripts (which someone with HTML posting allowed may have legitimately enabled, and spoilers (to prevent contents from being revealed) */ $text = \IPS\Text\Parser::removeElements( $this->content(), array( 'blockquote', 'script', 'div[class=ipsSpoiler]' ) ); /* Convert headers and paragraphs into line breaks or just spaces */ $text = str_replace( array( '

', '', '', '', '', '', '' ), ( $oneLine ? ' ' : '
' ), $text ); /* Add a space at the end of list items to prevent two list items from running into each other */ $text = str_replace( '', ' ', $text ); /* Remove all HTML apart from
s*/ $text = strip_tags( $text, '
' ); /* Remove any
s from the start so there isn't just blank space at the top, but maintaining
s elsewhere */ $text = preg_replace( '/^(\s|
|' . chr(0xC2) . chr(0xA0) . ')+?/', '', $text ); /* Return */ return $text; } /** * Delete Record * * @return void */ public function delete() { $idColumn = static::$databaseColumnId; if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) ) { \IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND type_id=?', static::$application, $this->reactionType(), $this->$idColumn ) ); } /* Remove any entries in the promotions table */ \IPS\Db::i()->delete( 'core_social_promote', array( 'promote_class=? AND promote_class_id=?', get_class( $this ), $this->$idColumn ) ); \IPS\Db::i()->delete( 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) ); parent::delete(); $this->expireWidgetCaches(); } /** * Is this a future entry? * * @return bool */ public function isFutureDate() { if ( $this instanceof \IPS\Content\FuturePublishing ) { if ( isset( static::$databaseColumnMap['is_future_entry'] ) and isset( static::$databaseColumnMap['future_date'] ) ) { $column = static::$databaseColumnMap['future_date']; if ( $this->$column > time() ) { return TRUE; } } } return FALSE; } /** * Return the tooltip blurb for future entries * * @return string */ public function futureDateBlurb() { $column = static::$databaseColumnMap['future_date']; $time = \IPS\DateTime::ts( $this->$column ); return \IPS\Member::loggedIn()->language()->addToStack("content_future_date_blurb", FALSE, array( 'sprintf' => array( $time->localeDate(), $time->localeTime() ) ) ); } /** * Content is hidden? * * @return int * @li -2 is pending deletion * @li -1 is hidden having been hidden by a moderator * @li 0 is unhidden * @li 1 is hidden needing approval * @note The actual column may also contain 2 which means the item is hidden because the parent is hidden, but it is not hidden in itself. This method will return -1 in that case. * * @note A piece of content (item and comment) can have an alias for hidden OR approved. * With hidden: 0=not hidden, 1=hidden (needs moderator approval), -1=hidden by moderator, 2=parent item is hidden, -2=pending deletion * With approved: 1=not hidden, 0=hidden (needs moderator approval), -1=hidden by moderator, -2=pending deletion * * User posting has moderator approval set: When adding an unapproved ITEM (approved=0, hidden=1) you should *not* increment container()->_comments but you should update container()->_unapprovedItems * User posting has moderator approval set: When adding an unapproved COMMENT (approved=0, hidden=1) you should *not* increment item()->num_comments in item or container()->_comments but you should update item()->unapproved_comments and container()->_unapprovedComments * * User post is hidden by moderator (approved=-1, hidden=0) you should decrement item()->num_comments and decrement container()->_comments but *not* increment item()->unapproved_comments or container()->_unapprovedComments * User item is hidden by a moderator (approved=-1, hidden=0) you should decrement container()->comments and subtract comment count from container()->_comments, but *not* increment container()->_unapprovedComments * * Moderator hides item (approved=-1, hidden=-1) you should substract num_comments from container()->_comments. Comments inside item are flagged as approved=-1, hidden=2 but item()->num_comments should not be substracted from * * Comments with a hidden value of 2 should increase item()->num_comments but not container()->_comments * @throws \RuntimeException */ public function hidden() { if ( $this instanceof \IPS\Content\Hideable ) { if ( isset( static::$databaseColumnMap['hidden'] ) ) { $column = static::$databaseColumnMap['hidden']; return ( $this->$column == 2 ) ? -1 : intval( $this->$column ); } elseif ( isset( static::$databaseColumnMap['approved'] ) ) { $column = static::$databaseColumnMap['approved']; if ( $this->$column == -2 ) { return intval( $this->$column ); } return $this->$column == -1 ? intval( $this->$column ) : intval( !$this->$column ); } else { throw new \RuntimeException; } } return 0; } /** * Can see moderation tools * * @note This is used generally to control if the user has permission to see multi-mod tools. Individual content items may have specific permissions * @param \IPS\Member|NULL $member The member to check for or NULL for the currently logged in member * @param \IPS\Node\Model|NULL $container The container * @return bool */ public static function canSeeMultiModTools( \IPS\Member $member = NULL, \IPS\Node\Model $container = NULL ) { return static::modPermission( 'pin', $member, $container ) or static::modPermission( 'unpin', $member, $container ) or static::modPermission( 'feature', $member, $container ) or static::modPermission( 'unfeature', $member, $container ) or static::modPermission( 'edit', $member, $container ) or static::modPermission( 'hide', $member, $container ) or static::modPermission( 'unhide', $member, $container ) or static::modPermission( 'delete', $member, $container ); } /** * Check Moderator Permission * * @param string $type 'edit', 'hide', 'unhide', 'delete', etc. * @param \IPS\Member|NULL $member The member to check for or NULL for the currently logged in member * @param \IPS\Node\Model|NULL $container The container * @return bool */ public static function modPermission( $type, \IPS\Member $member = NULL, \IPS\Node\Model $container = NULL ) { /* Compatibility checks */ if ( ( $type == 'hide' or $type == 'unhide' ) and !in_array( 'IPS\Content\Hideable', class_implements( get_called_class() ) ) ) { return FALSE; } if ( ( $type == 'pin' or $type == 'unpin' ) and !in_array( 'IPS\Content\Pinnable', class_implements( get_called_class() ) ) ) { return FALSE; } if ( ( $type == 'feature' or $type == 'unfeature' ) and !in_array( 'IPS\Content\Featurable', class_implements( get_called_class() ) ) ) { return FALSE; } if ( ( $type == 'future_publish' ) and !in_array( 'IPS\Content\FuturePublishing', class_implements( get_called_class() ) ) ) { return FALSE; } /* If this is called from a gateway script, i.e. email piping, just return false as we are a "guest" */ if( $member === NULL AND !\IPS\Dispatcher::hasInstance() ) { return FALSE; } /* Load Member */ $member = $member ?: \IPS\Member::loggedIn(); /* Global permission */ if ( $member->modPermission( "can_{$type}_content" ) ) { return TRUE; } /* Per-container permission */ elseif ( $container ) { return $container->modPermission( $type, $member ); } /* Still here? return false */ return FALSE; } /** * Do Moderator Action * * @param string $action The action * @param \IPS\Member|NULL $member The member doing the action (NULL for currently logged in member) * @param string|NULL $reason Reason (for hides) * @param bool $immediately Delete Immediately * @return void * @throws \OutOfRangeException|\InvalidArgumentException|\RuntimeException */ public function modAction( $action, \IPS\Member $member = NULL, $reason = NULL, $immediately = FALSE ) { if( $action === 'approve' ) { $action = 'unhide'; } /* Check it's a valid action */ if ( !in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'hide', 'unhide', 'move', 'lock', 'unlock', 'delete', 'publish', 'restore', 'restoreAsHidden' ) ) ) { throw new \InvalidArgumentException; } /* And that we can do it */ $toCheck = $action; if ( $action == 'restoreAsHidden' ) { $toCheck = 'restore'; } if ( !call_user_func( array( $this, 'can' . ucfirst( $toCheck ) ), $member ) ) { throw new \OutOfRangeException; } /* Log */ \IPS\Session::i()->modLog( 'modlog__action_' . $action, array( static::$title => TRUE, $this->url()->__toString() => FALSE, $this->mapped('title') ?: ( method_exists( $this, 'item' ) ? $this->item()->mapped('title') : NULL ) => FALSE ), ( $this instanceof \IPS\Content\Item ) ? $this : $this->item() ); /* These ones just need a property setting */ if ( in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'lock', 'unlock' ) ) ) { $val = TRUE; switch ( $action ) { case 'unpin': $val = FALSE; case 'pin': $column = static::$databaseColumnMap['pinned']; break; case 'unfeature': $val = FALSE; case 'feature': $column = static::$databaseColumnMap['featured']; break; case 'unlock': $val = FALSE; case 'lock': if ( isset( static::$databaseColumnMap['locked'] ) ) { $column = static::$databaseColumnMap['locked']; } else { $val = $val ? 'closed' : 'open'; $column = static::$databaseColumnMap['status']; } break; } $this->$column = $val; $this->save(); return; } /* Hide is a tiny bit more complicated */ elseif ( $action === 'hide' ) { $this->hide( $member, $reason ); return; } elseif ( $action === 'unhide' ) { $this->unhide( $member ); return; } /* Delete is just a method */ elseif ( $action === 'delete' ) { /* If we are retaining content for a period of time, we need to just hide it instead for deleting later - this only works, though, with items that implement \IPS\Content\Hideable */ if ( \IPS\Settings::i()->dellog_retention_period AND ( $this instanceof \IPS\Content\Hideable ) AND $immediately === FALSE ) { $this->logDelete(); return; } $idColumn = static::$databaseColumnId; \IPS\Db::i()->delete( 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) ); $this->delete(); return; } /* Restore is just a method */ elseif ( $action === 'restore' ) { $this->restore(); return; } /* Restore As Hidden is just a method */ elseif ( $action === 'restoreAsHidden' ) { $this->restore( TRUE ); return; } /* Publish is just a method */ elseif ( $action === 'publish' ) { $this->publish(); return; } /* Move is just a method */ elseif ( $action === 'move' ) { $args = func_get_args(); $this->move( $args[2][0], $args[2][1] ); return; } } /** * Log for deletion later * * \IPS\Member|NULL $member The member or NULL for currently logged in * @return void */ public function logDelete( $member = NULL ) { $member = $member ?: \IPS\Member::loggedIn(); /* Log it! */ $log = new \IPS\core\DeletionLog; $log->setContentAndMember( $this, $member ); $log->save(); if ( isset( static::$databaseColumnMap['hidden'] ) ) { $column = static::$databaseColumnMap['hidden']; } else if ( isset( static::$databaseColumnMap['approved'] ) ) { $column = static::$databaseColumnMap['approved']; } try { if ( $this->container() ) { if ( $this->container()->_comments !== NULL ) { /* Hidden content already decrements */ if ( !$this->hidden() ) { $this->container()->_comments = ( $this->container()->_comments - $this->mapped('num_comments') ); } } if ( $this->container()->_unapprovedComments !== NULL ) { $this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments - $this->mapped( 'unapproved_comments' ) ); } } } catch( \BadMethodCallException $e ) {} $this->$column = -2; $this->save(); if ( $this instanceof \IPS\Content\Comment ) { $item = $this->item(); /* Update last comment stuff */ $item->resyncLastComment(); /* Update last review stuff */ $item->resyncLastReview(); /* Update number of comments */ $item->resyncCommentCounts(); /* Update number of reviews */ $item->resyncReviewCounts(); /* Save*/ $item->save(); } try { if ( $this->container() ) { $this->container()->setLastComment(); $this->container()->save(); } } catch( \BadMethodCallException $e ) {} } /** * Restore Content * * @param bool Restore as hidden? * @return void * @throws \BadMethodCallException */ public function restore( $hidden = FALSE ) { try { $idColumn = static::$databaseColumnId; $log = \IPS\core\DeletionLog::constructFromData( \IPS\Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id", get_class( $this ), $this->$idColumn ) )->first() ); } catch( \UnderflowException $e ) { throw new \BadMethodCallException; } /* Restoring as hidden? */ if ( $hidden ) { if ( isset( static::$databaseColumnMap['hidden'] ) ) { $column = static::$databaseColumnMap['hidden']; } else if ( isset( static::$databaseColumnMap['approved'] ) ) { $column = static::$databaseColumnMap['approved']; } $this->$column = -1; } else { if ( isset( static::$databaseColumnMap['hidden'] ) ) { $column = static::$databaseColumnMap['hidden']; $this->$column = 0; } else if ( isset( static::$databaseColumnMap['approved'] ) ) { $column = static::$databaseColumnMap['approved']; $this->$column = 1; } } /* Save the changes */ $this->save(); /* Reindex the now hidden content */ if ( $this instanceof \IPS\Content\Item and static::$firstCommentRequired ) { \IPS\Content\Search\Index::i()->index( $this->firstComment() ); } else { \IPS\Content\Search\Index::i()->index( $this ); } /* Delete the log */ $log->delete(); /* Log to moderate logs */ if ( !$hidden ) { try { if ( $this->container() ) { if ( $this->container()->_comments !== NULL ) { if ( $this instanceof \IPS\Content\Item ) { $this->container()->_comments = ( $this->container()->_comments + $this->mapped('num_comments') ); } else { $this->container()->_comments = $this->container()->_comments + 1; } } if ( $this->container()->_unapprovedComments !== NULL ) { if ( $this instanceof \IPS\Content\Item ) { $this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments + $this->mapped('unapproved_comments') ); } } $this->container()->setLastComment(); $this->container()->save(); if ( $this instanceof \IPS\Content\Comment ) { $this->item()->rebuildFirstAndLastCommentData(); } } } catch( \BadMethodCallException $e ) {} } } /** * Can restore?* * * @param \IPS\Member|NULL The member, or currently logged in member * @return bool */ public function canRestore( $member=NULL ) { $member = $member ?: \IPS\Member::loggedIn(); return \IPS\Member::loggedIn()->modPermission('can_restore_deleted_content'); } /** * Hide * * @param \IPS\Member|NULL|FALSE $member The member doing the action (NULL for currently logged in member, FALSE for no member) * @param string $reason Reason * @return void */ public function hide( $member, $reason = NULL ) { if ( isset( static::$databaseColumnMap['hidden'] ) ) { $column = static::$databaseColumnMap['hidden']; } elseif ( isset( static::$databaseColumnMap['approved'] ) ) { $column = static::$databaseColumnMap['approved']; } else { throw new \RuntimeException; } /* Already hidden? */ if( $this->$column == -1 ) { return; } $this->$column = -1; $this->save(); $this->onHide( $member ); if ( static::$hideLogKey ) { $idColumn = static::$databaseColumnId; \IPS\Db::i()->delete( 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) ); \IPS\Db::i()->insert( 'core_soft_delete_log', array( 'sdl_obj_id' => $this->$idColumn, 'sdl_obj_key' => static::$hideLogKey, 'sdl_obj_member_id' => $member === FALSE ? 0 : intval( $member ? $member->member_id : \IPS\Member::loggedIn()->member_id ), 'sdl_obj_date' => time(), 'sdl_obj_reason' => $reason, ) ); } if ( $this instanceof \IPS\Content\Tags ) { \IPS\Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 0 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) ); } /* Update search index */ if ( $this instanceof \IPS\Content\Searchable ) { \IPS\Content\Search\Index::i()->index( $this ); } $this->expireWidgetCaches(); } /** * Unhide * * @param \IPS\Member|NULL|FALSE $member The member doing the action (NULL for currently logged in member, FALSE for no member) * @return void */ public function unhide( $member ) { /* If we're approving, we have to do extra stuff */ $approving = FALSE; if ( $this->hidden() === 1 ) { $approving = TRUE; if ( isset( static::$databaseColumnMap['approved_by'] ) and $member !== FALSE ) { $column = static::$databaseColumnMap['approved_by']; $this->$column = $member ? $member->member_id : \IPS\Member::loggedIn()->member_id; } if ( isset( static::$databaseColumnMap['approved_date'] ) ) { $column = static::$databaseColumnMap['approved_date']; $this->$column = time(); } } /* Now do the actual stuff */ if ( isset( static::$databaseColumnMap['hidden'] ) ) { $column = static::$databaseColumnMap['hidden']; /* Already approved? */ if( $this->$column == 0 ) { return; } $this->$column = 0; } elseif ( isset( static::$databaseColumnMap['approved'] ) ) { $column = static::$databaseColumnMap['approved']; /* Already approved? */ if( $this->$column == 1 ) { return; } $this->$column = 1; } else { throw new \RuntimeException; } $this->save(); $this->onUnhide( $approving, $member ); /* And update the tags perm cache */ if ( $this instanceof \IPS\Content\Tags ) { \IPS\Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 1 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) ); } /* Update search index */ if ( $this instanceof \IPS\Content\Searchable ) { \IPS\Content\Search\Index::i()->index( $this ); } /* Send notifications if necessary */ if ( $approving ) { $this->sendApprovedNotification(); } $this->expireWidgetCaches(); } /** * Blurb for when/why/by whom this content was hidden * * @return string */ public function hiddenBlurb() { if ( !( $this instanceof \IPS\Content\Hideable ) or !static::$hideLogKey ) { throw new \BadMethodCallException; } try { $idColumn = static::$databaseColumnId; $log = \IPS\Db::i()->select( '*', 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) )->first(); return \IPS\Member::loggedIn()->language()->addToStack('hidden_blurb', FALSE, array( 'sprintf' => array( \IPS\Member::load( $log['sdl_obj_member_id'] )->name, \IPS\DateTime::ts( $log['sdl_obj_date'] )->relative(), $log['sdl_obj_reason'] ?: \IPS\Member::loggedIn()->language()->addToStack('hidden_no_reason') ) ) ); } catch ( \UnderflowException $e ) { return \IPS\Member::loggedIn()->language()->addToStack('hidden'); } } /** * Blurb for when/why/by whom this content was deleted * * @return string * @throws \BadMethodCallException */ public function deletedBlurb() { if ( !( $this instanceof \IPS\Content\Hideable ) ) { throw new \BadMethodCallException; } try { $idColumn = static::$databaseColumnId; $log = \IPS\core\DeletionLog::constructFromData( \IPS\Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) )->first() ); return \IPS\Member::loggedIn()->language()->addToStack( 'deletion_blurb', FALSE, array( 'sprintf' => array( $log->_deleted_by->name, $log->deleted_date->fullYearLocaleDate(), $log->deletion_date->fullYearLocaleDate() ) ) ); } catch( \UnderflowException $e ) { return \IPS\Member::loggedIn()->language()->addToStack('deleted'); } } /** * Can promote this comment/item? * * @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member) * @return boolean */ public function canPromoteToSocialMedia( $member=NULL ) { return \IPS\core\Promote::canPromote( $member ); } /** * Can report? * * @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member) * @return TRUE|string TRUE or a language string for why not * @note This requires a few queries, so don't run a check in every template */ public function canReport( $member=NULL ) { $member = $member ?: \IPS\Member::loggedIn(); /* Is this type of comment reportabe? */ if ( !( $this instanceof \IPS\Content\ReportCenter ) ) { return 'generic_error'; } /* Can the member report content? */ if ( !$member->group['g_can_report'] ) { return 'no_module_permission'; } /* Can they view this? */ if ( !$this->canView() ) { return 'no_module_permission'; } /* Have they already subitted a report? */ $idColumn = static::$databaseColumnId; $report = \IPS\Db::i()->select( 'id', 'core_rc_index', array( 'class=? AND content_id=?', get_called_class(), $this->$idColumn ) ); if ( count( $report ) ) { $report = \IPS\Db::i()->select( '*', 'core_rc_reports', array( 'rid=? AND report_by=?', $report->first(), $member->member_id ) ); if ( count( $report ) ) { return 'report_err_already_reported'; } } return TRUE; } /** * Report * * @param string $reportContent Report content message from member * @return \\IPS\core\Reports\Report * @throws \UnexpectedValueException If there is a permission error - you should only call this method after checking canReport */ public function report( $reportContent ) { /* Permission check */ if ( $this->canReport() !== TRUE ) { throw new \UnexpectedValueException; } /* Find or create an index */ $idColumn = static::$databaseColumnId; try { $index = \IPS\core\Reports\Report::load( $this->$idColumn, 'content_id', array( 'class=?', get_called_class() ) ); $index->num_reports = $index->num_reports + 1; } catch ( \OutOfRangeException $e ) { $index = new \IPS\core\Reports\Report; $index->class = get_called_class(); $index->content_id = $this->$idColumn; $index->perm_id = $this->permId(); $index->first_report_by = (int) \IPS\Member::loggedIn()->member_id; $index->first_report_date = time(); $index->last_updated = time(); $index->author = (int) $this->author()->member_id; $index->num_reports = 1; $index->num_comments = 0; } /* Only set this to a new report if it is not already under review */ if( $index->status != 2 ) { $index->status = 1; } $index->save(); /* Create a report */ $reportInsert = array( 'rid' => $index->id, 'report' => $reportContent, 'report_by' => (int) \IPS\Member::loggedIn()->member_id, 'date_reported' => time(), 'ip_address' => \IPS\Request::i()->ipAddress() ); \IPS\Db::i()->insert( 'core_rc_reports', $reportInsert ); /* Send notification to mods */ $moderators = array( 'm' => array(), 'g' => array() ); foreach ( \IPS\Db::i()->select( '*', 'core_moderators' ) as $mod ) { $canView = FALSE; if ( $mod['perms'] == '*' ) { $canView = TRUE; } if ( $canView === FALSE ) { $perms = json_decode( $mod['perms'], TRUE ); if ( isset( $perms['can_view_reports'] ) AND $perms['can_view_reports'] === TRUE ) { $canView = TRUE; } } if ( $canView === TRUE ) { $moderators[ $mod['type'] ][] = $mod['id']; } } $notification = new \IPS\Notification( \IPS\Application::load('core'), 'report_center', $index, array( $index, $reportInsert, $this ) ); foreach ( \IPS\Db::i()->select( '*', 'core_members', ( count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $member ) { $notification->recipients->attach( \IPS\Member::constructFromData( $member ) ); } $notification->send(); /* Return */ return $index; } /** * Change Author * * @param \IPS\Member $newAuthor The new author * @return void */ public function changeAuthor( \IPS\Member $newAuthor ) { $oldAuthor = $this->author(); foreach ( array( 'author', 'author_name', 'edit_member_name' ) as $k ) { if ( isset( static::$databaseColumnMap[ $k ] ) ) { $col = static::$databaseColumnMap[ $k ]; switch ( $k ) { case 'author': $this->$col = $newAuthor->member_id ? $newAuthor->member_id : 0; break; case 'author_name': case 'edit_member_name': /* Real name will contain the custom guest name if available or '' if not */ $this->$col = $newAuthor->member_id ? $newAuthor->name : $newAuthor->real_name; break; } } } $this->save(); if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'front' ) { \IPS\Session::i()->modLog( 'modlog__action_changeauthor', array( static::$title => TRUE, $this->url()->__toString() => FALSE, $this->mapped('title') ?: ( method_exists( $this, 'item' ) ? $this->item()->mapped('title') : NULL ) => FALSE ), ( $this instanceof \IPS\Content\Item ) ? $this : $this->item() ); } } /** * Get HTML for search result display * * @param array $indexData Data from the search index * @param array $authorData Basic data about the author. Only includes columns returned by \IPS\Member::columnsForPhoto() * @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns() * @param array|NULL $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns() * @param array $reputationData Array of people who have given reputation and the reputation they gave * @param int|NULL $reviewRating If this is a review, the rating * @param bool $iPostedIn If the user has posted in the item * @param string $view 'expanded' or 'condensed' * @param bool $asItem Displaying results as items? * @param bool $canIgnoreComments Can ignore comments in the result stream? Activity stream can, but search results cannot. * @param array $template Optional custom template * @param array $reactions Reaction Data * @return string */ public static function searchResult( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $iPostedIn, $view, $asItem, $canIgnoreComments=FALSE, $template=NULL, $reactions=array() ) { /* Item details */ $itemClass = $indexData['index_class']; if ( in_array( 'IPS\Content\Comment', class_parents( get_called_class() ) ) ) { $itemClass = static::$itemClass; $unread = $itemClass::unreadFromData( NULL, $indexData['index_date_updated'], $indexData['index_date_created'], $indexData['index_item_id'], $indexData['index_container_id'], FALSE ); } else { $unread = static::unreadFromData( NULL, $indexData['index_date_updated'], $indexData['index_date_created'], $indexData['index_item_id'], $indexData['index_container_id'], FALSE ); } $itemUrl = $itemClass::urlFromIndexData( $indexData, $itemData ); /* Object URL */ $indefiniteArticle = static::_indefiniteArticle( $containerData ); $definiteArticle = static::_definiteArticle( $containerData ); if ( in_array( 'IPS\Content\Comment', class_parents( get_called_class() ) ) ) { if ( in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) ) { $objectUrl = $itemUrl->setQueryString( array( 'do' => 'findReview', 'review' => $indexData['index_object_id'] ) ); $showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsReview', 'review' => $indexData['index_object_id'] ) ); } else { $objectUrl = $itemUrl->setQueryString( array( 'do' => 'findComment', 'comment' => $indexData['index_object_id'] ) ); $showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsComment', 'comment' => $indexData['index_object_id'] ) ); } $indefiniteArticle = $itemClass::_indefiniteArticle( $containerData ); $definiteArticle = $itemClass::_definiteArticle( $containerData ); } else { $objectUrl = $itemUrl; $showRepUrl = $itemUrl->setQueryString( 'do', 'showReactions' ); } $articles = array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle ); /* Container details */ $containerUrl = NULL; $containerTitle = NULL; if ( isset( $itemClass::$containerNodeClass ) ) { $containerClass = $itemClass::$containerNodeClass; $containerTitle = $containerClass::titleFromIndexData( $indexData, $itemData, $containerData ); $containerUrl = $containerClass::urlFromIndexData( $indexData, $itemData, $containerData ); } /* Reputation */ $repCount = array_sum( $reputationData ); /* Snippet */ $snippet = static::searchResultSnippet( $indexData, $authorData, $itemData, $containerData, $reputationData, $reviewRating, $view ); if ( $template === NULL ) { $template = array( \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' ), 'searchResult' ); } /* Return */ return call_user_func_array( $template, array( $indexData, $articles, $authorData, $itemData, $unread, $asItem ? $itemUrl : $objectUrl, $itemUrl, $containerUrl, $containerTitle, $repCount, $showRepUrl, $snippet, $iPostedIn, $view, $canIgnoreComments, $reactions ) ); } /** * Get snippet HTML for search result display * * @param array $indexData Data from the search index * @param array $authorData Basic data about the author. Only includes columns returned by \IPS\Member::columnsForPhoto() * @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns() * @param array|NULL $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns() * @param array $reputationData Array of people who have given reputation and the reputation they gave * @param int|NULL $reviewRating If this is a review, the rating * @param string $view 'expanded' or 'condensed' * @return callable */ public static function searchResultSnippet( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $view ) { return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData ) : ''; } /** * @brief Return a classname applied to the search result block */ public static $searchResultClassName = ''; /** * Return the filters that are available for selecting table rows * * @return array */ public static function getTableFilters() { $return = array(); if ( in_array( 'IPS\Content\Hideable', class_implements( get_called_class() ) ) ) { $return[] = 'hidden'; $return[] = 'unhidden'; $return[] = 'unapproved'; } return $return; } /** * Get content table states * * @return string */ public function tableStates() { $return = array(); if ( $this instanceof \IPS\Content\Hideable ) { switch ( $this->hidden() ) { case -1: $return[] = 'hidden'; break; case 0: $return[] = 'unhidden'; break; case 1: $return[] = 'unapproved'; break; } } return implode( ' ', $return ); } /* !Follow */ const FOLLOW_PUBLIC = 1; const FOLLOW_ANONYMOUS = 2; const NOTIFICATIONS_PER_BATCH = 50; /** * Send notifications * * @return void */ public function sendNotifications() { /* Send quote and mention notifications */ $sentTo = $this->sendQuoteAndMentionNotifications(); /* How many followers? */ $idColumn = $this::$databaseColumnId; try { $count = $this->notificationRecipients( NULL, NULL, TRUE ); } catch ( \BadMethodCallException $e ) { return; } /* Queue if there's lots, or just send them */ if ( $count > static::NOTIFICATIONS_PER_BATCH ) { \IPS\Task::queue( 'core', 'Follow', array( 'class' => get_class( $this ), 'item' => $this->$idColumn, 'sentTo' => $sentTo ), 1 ); } else { $this->sendNotificationsBatch( 0, $sentTo ); } } /** * Send notifications batch * * @param int $offset Current offset * @param array $sentTo Members who have already received a notification and how - e.g. array( 1 => array( 'inline', 'email' ) * @param string|NULL $extra Additional data * @return int|null New offset or NULL if complete */ public function sendNotificationsBatch( $offset=0, &$sentTo=array(), $extra=NULL ) { $followIds = array(); $followers = $this->notificationRecipients( array( $offset, static::NOTIFICATIONS_PER_BATCH ), $extra ); /* Send notification */ $notification = $this->createNotification( $extra ); $notification->unsubscribeType = 'follow'; foreach ( $followers as $follower ) { $member = \IPS\Member::load( $follower['follow_member_id'] ); if ( $member != $this->author() and $this->canView( $member ) ) { $followIds[] = $follower['follow_id']; $notification->recipients->attach( $member, $follower ); } } /* Log that we sent it */ if( count( $followIds ) ) { \IPS\Db::i()->update( 'core_follow', array( 'follow_notify_sent' => time() ), \IPS\Db::i()->in( 'follow_id', $followIds ) ); } $sentTo = $notification->send( $sentTo ); /* Update the queue */ $newOffset = $offset + static::NOTIFICATIONS_PER_BATCH; if ( $newOffset > $followers->count( TRUE ) ) { return NULL; } return $newOffset; } /** * Send Approved Notification * * @return void */ public function sendApprovedNotification() { $this->sendNotifications(); } /** * Send Unapproved Notification * * @return void */ public function sendUnapprovedNotification() { $moderators = array( 'g' => array(), 'm' => array() ); foreach( \IPS\Db::i()->select( '*', 'core_moderators' ) AS $mod ) { $canView = FALSE; $canApprove = FALSE; if ( $mod['perms'] == '*' ) { $canView = TRUE; $canApprove = TRUE; } else { $perms = json_decode( $mod['perms'], TRUE ); foreach ( array( 'canView' => 'can_view_hidden_', 'canApprove' => 'can_unhide_' ) as $varKey => $modPermKey ) { if ( isset( $perms[ $modPermKey . 'content' ] ) AND $perms[ $modPermKey . 'content' ] ) { $$varKey = TRUE; } else { try { $container = ( $this instanceof \IPS\Content\Comment ) ? $this->item()->container() : $this->container(); $containerClass = get_class( $container ); $title = static::$title; if ( isset( $containerClass::$modPerm ) and ( $perms[ $containerClass::$modPerm ] === -1 or ( is_array( $perms[ $containerClass::$modPerm ] ) and in_array( $container->_id, $perms[ $containerClass::$modPerm ] ) ) ) and $perms["{$modPermKey}{$title}"] ) { $$varKey = TRUE; } } catch ( \BadMethodCallException $e ) { } } } } if ( $canView === TRUE and $canApprove === TRUE ) { $moderators[ $mod['type'] ][] = $mod['id']; } } $notification = new \IPS\Notification( \IPS\Application::load('core'), 'unapproved_content', $this, array( $this, $this->author() ) ); foreach ( \IPS\Db::i()->select( '*', 'core_members', ( count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $member ) { /* We don't need to notify the author of the content */ if( $this->author()->member_id != $member['member_id'] ) { $notification->recipients->attach(\IPS\Member::constructFromData($member)); } } $notification->send(); } /** * Send the notifications after the content has been edited (for any new quotes or mentiones) * * @param string $oldContent The content before the edit * @return void */ public function sendAfterEditNotifications( $oldContent ) { $existingData = static::_getQuoteAndMentionIdsFromContent( $oldContent ); $this->sendQuoteAndMentionNotifications( array_unique( array_merge( $existingData['quotes'], $existingData['mentions'] ) ) ); } /** * Send quote and mention notifications * * @param array $exclude An array of member IDs *not* to send notifications to * @return array The members that were notified and how they were notified */ protected function sendQuoteAndMentionNotifications( $exclude=array() ) { return $this->_sendQuoteAndMentionNotifications( static::_getQuoteAndMentionIdsFromContent( $this->content() ), $exclude ); } /** * Send quote and mention notifications from data * * @param array array( 'quotes' => array( ... member IDs ... ), 'mentions' => array( ... member IDs ... ) ) * @param array $exclude An array of member IDs *not* to send notifications to * @return array The members that were notified and how they were notified */ protected function _sendQuoteAndMentionNotifications( $data, $exclude=array() ) { /* Init */ $sentTo = array(); /* Quotes */ $data['quotes'] = array_filter( $data['quotes'], function( $v ) use ( $exclude ) { return !in_array( $v, $exclude ); } ); if ( !empty( $data['quotes'] ) ) { $notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'quote', ( $this instanceof \IPS\Content\Item ) ? $this : $this->item(), array( $this ), array( $this->author()->member_id ), FALSE ); foreach ( $data['quotes'] as $quote ) { $member = \IPS\Member::load( $quote ); if ( $member->member_id and $member != $this->author() and $this->canView( $member ) ) { $notification->recipients->attach( $member ); } } $sentTo = $notification->send( $sentTo ); } /* Mentions */ $data['mentions'] = array_filter( $data['mentions'], function( $v ) use ( $exclude ) { return !in_array( $v, $exclude ); } ); if ( !empty( $data['mentions'] ) ) { $notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'mention', ( $this instanceof \IPS\Content\Item ) ? $this : $this->item(), array( $this ), array( $this->author()->member_id ), FALSE ); foreach ( $data['mentions'] as $mention ) { $member = \IPS\Member::load( $mention ); if ( $member != $this->author() and $this->canView( $member ) and !$member->isIgnoring( $this->author(), 'mentions' ) ) { $notification->recipients->attach( $member ); } } $sentTo = $notification->send( $sentTo ); } /* Return */ return $sentTo; } /** * Get quote and mention notifications * * @param string $content The content * @return array array( 'quotes' => array( ... member IDs ... ), 'mentions' => array( ... member IDs ... ) ) */ protected static function _getQuoteAndMentionIdsFromContent( $content ) { $return = array( 'quotes' => array(), 'mentions' => array() ); $document = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' ); if ( @$document->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( '
' . $content . '
' ) ) !== FALSE ) { /* Quotes */ foreach( $document->getElementsByTagName('blockquote') as $quote ) { if ( $quote->getAttribute('data-ipsquote-userid') and (int) $quote->getAttribute('data-ipsquote-userid') > 0 ) { $return['quotes'][] = $quote->getAttribute('data-ipsquote-userid'); } } /* Mentions */ foreach( $document->getElementsByTagName('a') as $link ) { if ( $link->getAttribute('data-mentionid') ) { if ( !preg_match( '/\/blockquote(\[\d*\])?\//', $link->getNodePath() ) ) { $return['mentions'][] = $link->getAttribute('data-mentionid'); } } } } return $return; } /** * Expire appropriate widget caches automatically * * @return void */ public function expireWidgetCaches() { \IPS\Widget::deleteCaches( NULL, static::$application ); } /** * Fetch classes from content router * * @param bool|\IPS\Member $member Check member access * @param bool $archived Include any supported archive classes * @param bool $onlyItems Only include item classes * @return array */ public static function routedClasses( $member=FALSE, $archived=FALSE, $onlyItems=FALSE ) { $classes = array(); foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', $member, NULL, NULL, TRUE ) as $router ) { foreach ( $router->classes as $class ) { $classes[] = $class; if( $onlyItems ) { continue; } if ( !( $member instanceof \IPS\Member ) ) { $member = $member ? \IPS\Member::loggedIn() : NULL; } if ( isset( $class::$commentClass ) and $class::supportsComments( $member ) ) { $classes[] = $class::$commentClass; } if ( isset( $class::$reviewClass ) and $class::supportsReviews( $member ) ) { $classes[] = $class::$reviewClass; } if( $archived === TRUE AND isset( $class::$archiveClass ) ) { $classes[] = $class::$archiveClass; } } } return $classes; } /** * Override the HTML parsing enabled flag for rebuilds? * * @note By default this will return FALSE, but classes can override * @see \IPS\forums\Topic\Post * @return bool */ public function htmlParsingEnforced() { return FALSE; } /** * Return any custom multimod actions this content item supports * * @return array */ public function customMultimodActions() { return array(); } /** * Return any available custom multimod actions this content item class supports * * @note Return in format of EITHER * @li array( array( 'action' => ..., 'icon' => ..., 'label' => ... ), ... ) * @li array( array( 'grouplabel' => ..., 'icon' => ..., 'groupaction' => ..., 'action' => array( array( 'action' => ..., 'label' => ... ), ... ) ) ) * @note For an example, look at \IPS\core\Announcements\Announcement * @return array */ public static function availableCustomMultimodActions() { return array(); } /** * Get HTML for search result display * * @return callable */ public function approvalQueueHtml( $ref=NULL, $container, $title ) { return \IPS\Theme::i()->getTemplate( 'modcp', 'core', 'front' )->approvalQueueItem( $this, $ref, $container, $title ); } /** * Indefinite Article * * @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member * @return string */ public function indefiniteArticle( \IPS\Lang $lang = NULL ) { $container = ( $this instanceof \IPS\Content\Comment ) ? $this->item()->containerWrapper() : $this->containerWrapper(); return static::_indefiniteArticle( $container ? $container->_data : array(), $lang ); } /** * Indefinite Article * * @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member * @return string */ public static function _indefiniteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL ) { $lang = $lang ?: \IPS\Member::loggedIn()->language(); return $lang->addToStack( '__indefart_' . static::$title, FALSE ); } /** * Definite Article * * @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member * @return string */ public function definiteArticle( \IPS\Lang $lang = NULL ) { $container = ( $this instanceof \IPS\Content\Comment ) ? $this->item()->containerWrapper() : $this->containerWrapper(); return static::_definiteArticle( $container ? $container->_data : array(), $lang ); } /** * Definite Article * * @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member * @return string */ public static function _definiteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL ) { $lang = $lang ?: \IPS\Member::loggedIn()->language(); return $lang->addToStack( '__defart_' . static::$title, FALSE ); } /** * Get preview image for share services * * @return string */ public function shareImage() { return (string) \IPS\Theme::i()->logo_sharer; } /** * Log keyword usage, if any * * @param string $content Content/text of submission * @param string|NULL $title Title of submission * @return void */ public function checkKeywords( $content, $title=NULL ) { /* Do we have any keywords to track? */ if( !\IPS\Settings::i()->stats_keywords ) { return; } /* We need to know the ID */ $idColumn = static::$databaseColumnId; /* If this is a content item and first comment is required, skip checking the comment */ if ( $this instanceof \IPS\Content\Comment ) { $itemClass = static::$itemClass; if( $itemClass::$firstCommentRequired === TRUE ) { /* During initial post, at this point the firstCommentIdColumn value won't be set, so we check for that or explicitly if this is the first post */ if( !$this->item()->mapped('first_comment_id') OR $this->$idColumn == $this->item()->mapped('first_comment_id') ) { return; } } } $words = preg_split("/[\s]+/", trim( strip_tags( preg_replace( "//", "\n", $content ) ) ), NULL, PREG_SPLIT_NO_EMPTY ); if( $title !== NULL ) { $titleWords = explode( ' ', $title ); $words = array_merge( $words, $titleWords ); } $words = array_unique( $words ); $keywords = json_decode( \IPS\Settings::i()->stats_keywords, true ); $extraData = json_encode( array( 'class' => get_class( $this ), 'id' => $this->$idColumn ) ); foreach( $keywords as $keyword ) { if( in_array( $keyword, $words ) ) { \IPS\Db::i()->insert( 'core_statistics', array( 'time' => time(), 'type' => 'keyword', 'value_4' => $keyword, 'extra_data' => $extraData ) ); } } } /* !Deprecated */ /* The methods below are deprecated and will be removed in Invision Community 4.3 */ /** * Get reputation count * * @return int * @throws \BadMethodCallException */ public function reputation() { if ( !\IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) ) { throw new \BadMethodCallException; } return $this->reactionCount(); } }