Files
IPB/system/Member/Member.php
T
2025-12-19 05:32:19 -08:00

3018 lines
75 KiB
PHP

<?php
/**
* @brief Member Model
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 18 Feb 2013
* @version SVN_VERSION_NUMBER
*/
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;
}
/**
* Member Model
*/
class _Member extends \IPS\Patterns\ActiveRecord
{
/**
* @brief Application
*/
public static $application = 'core';
/* !\IPS\Patterns\ActiveRecord */
/**
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
/**
* @brief [ActiveRecord] Database Table
*/
public static $databaseTable = 'core_members';
/**
* @brief [ActiveRecord] ID Database Column
*/
public static $databaseColumnId = 'member_id';
/**
* @brief [ActiveRecord] Database ID Fields
*/
protected static $databaseIdFields = array( 'name', 'email', 'fb_uid', 'live_id', 'google_id', 'linkedin_id', 'ipsconnect_id', 'twitter_id' );
/**
* @brief Bitwise values for members_bitoptions field
*/
public static $bitOptions = array(
'members_bitoptions' => array(
'members_bitoptions' => array(
'bw_is_spammer' => 1, // Flagged as spam?
// 2 is deprecated
// 4 (bw_vnc_type) is deprecated
// 8 (bw_forum_result_type) is deprecated
'bw_no_status_update' => 16, // Can post status updates?, 1 means they CAN'T post status updates. 0 means they can.
// 32 (bw_status_email_mine) is deprecated
// 64 (bw_status_email_all) is deprecated
// 128 is deprecated (previously bw_disable_customization)
// 256 is deprecated (bw_local_password_set) - used to represent if a local passwnot was set and block the change password form if not
'bw_disable_tagging' => 512, // Tags disabled for this member? 1 means they are, 0 means they aren't.
'bw_disable_prefixes' => 1024, // Tag prefixes disabled? 1 means they are, 0 means they aren't.
'bw_using_skin_gen' => 2048, // 1 means the user has the easy mode editor active, 0 means they do not.
'bw_disable_gravatar' => 4096, // If 0 then gravatar will not be used.
// 8192 (bw_paste_plain) is deprecated
// 16384 (bw_html_sig) is deprecated
// 32768 (allow_admin_mails) is deprecated
'view_sigs' => 65536, // View signatures?
// 131072 (view_img) is deprecated
// 262144 is deprecated
'coppa_user' => 524288, // Was the member validated using coppa?
// 1048576 (login_anonymous) is deprecated
// 2097152 (login_anonymous_online) is deprecated
// 4194304 (members_auto_dst) is deprecated
// 8388608 (members_created_remote) is deprecated
// 16777216 (members_disable_pm) is deprecated
'unacknowledged_warnings' => 33554432, // 1 means the member has at least one warning they have not acknowledged. 0 means they have none.
// 67108864 (pp_setting_moderate_comments) is deprecated and replaced with global setting
'pp_setting_moderate_followers' => 134217728, // Previously pp_setting_moderate_friends. Replaced with setting that toggles whether or not member can be followed.
'pp_setting_count_visitors' => 268435456, // If TRUE, last 5 visitors will be shown on profile
'timezone_override' => 536870912, // If TRUE, user's timezone will not be detected automatically
'validating' => 1073741824, // If TRUE user is validating and should have a corresponding row in core_validating
),
'members_bitoptions2' => array(
'show_pm_popup' => 1, // "Show pop-up when I have a new message"
'remove_gallery_access' => 2, // Remove access to Gallery
'remove_gallery_upload' => 4, // Remove permission to upload images in Gallery
'no_report_count' => 8, // 1 means the report count will not show
'has_no_ignored_users' => 16, // If we know the user has no ignored users, we don't have to query for them
'must_reaccept_privacy' => 32, // 1 means the member needs to re-accept the privacy policy
'must_reaccept_terms' => 64, // 1 means the member needs to re-accept the registration terms
'email_notifications_once' => 128, // 1 means the member only wants one email per notification item until they revisit the community
'disable_notification_sounds' => 256 // 1 means notification sounds are disabled, 0 is enabled
)
)
);
/* !Follow */
/**
* @brief Cache for current follow data, used on "My Followed Content" screen
*/
public $_followData;
const FOLLOW_PUBLIC = 1;
const FOLLOW_ANONYMOUS = 2;
/**
* @brief Cached logged in member
*/
protected static $loggedInMember = NULL;
/**
* Get logged in member
*
* @return \IPS\Member
*/
public static function loggedIn()
{
if( static::$loggedInMember === NULL )
{
static::$loggedInMember = static::load( \IPS\Session::i()->member->member_id );
if ( isset( $_SESSION['logged_in_as_key'] ) )
{
if ( static::$loggedInMember->isAdmin() AND static::$loggedInMember->hasAcpRestriction( 'core', 'members', 'member_login' ) )
{
$key = $_SESSION['logged_in_as_key'];
if ( isset( \IPS\Data\Store::i()->$key ) )
{
static::$loggedInMember = static::load( \IPS\Data\Store::i()->$key );
}
}
}
if ( !static::$loggedInMember->member_id and isset( \IPS\Request::i()->cookie['ipsTimezone'] ) )
{
static::$loggedInMember->timezone = \IPS\Request::i()->cookie['ipsTimezone'];
}
}
return static::$loggedInMember;
}
/**
* Load Record
* We override it so we return a guest object for a non-existant member
*
* @see \IPS\Db::build
* @param int|string $id ID
* @param string $idField The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
* @param mixed $extraWhereClause Additional where clause (see \IPS\Db::build for details)
* @return static
*/
public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
{
try
{
if( $id === NULL OR $id === 0 OR $id === '' )
{
$classname = get_called_class();
return new $classname;
}
else
{
$member = parent::load( $id, $idField, $extraWhereClause );
if ( $member->restrict_post > 0 and $member->restrict_post <= time() )
{
$member->restrict_post = 0;
$member->save();
}
return $member;
}
}
catch ( \OutOfRangeException $e )
{
$classname = get_called_class();
return new $classname;
}
}
/**
* Load record based on a URL
*
* @param \IPS\Http\Url $url URL to load from
* @return static
* @throws \InvalidArgumentException
* @throws \OutOfRangeException
*/
public static function loadFromUrl( \IPS\Http\Url $url )
{
$member = parent::loadFromUrl( $url );
if ( !$member->member_id )
{
throw new \OutOfRangeException;
}
return $member;
}
/**
* Set Default Values
*
* @return void
*/
public function setDefaultValues()
{
/* If we're in the installer - don't do this */
if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation === 'setup' )
{
return;
}
$this->member_group_id = \IPS\Settings::i()->guest_group;
$this->mgroup_others = '';
$this->joined = time();
$this->ip_address = \IPS\Request::i()->ipAddress();
$this->timezone = 'UTC';
$this->allow_admin_mails = 1;
$this->pp_photo_type = '';
$this->member_posts = 0;
$this->_data['pp_main_photo'] = NULL;
$this->_data['pp_thumb_photo'] = NULL;
$this->_data['failed_logins'] = NULL;
$this->_data['pp_reputation_points'] = 0;
$this->_data['signature'] = '';
$this->_data['auto_track'] = 0;
if( isset( \IPS\Request::i()->cookie['language'] ) AND \IPS\Request::i()->cookie['language'] )
{
$this->language = \IPS\Request::i()->cookie['language'];
}
if( isset( \IPS\Request::i()->cookie['theme'] ) AND \IPS\Request::i()->cookie['theme'] )
{
$this->skin = \IPS\Request::i()->cookie['theme'];
}
}
/**
* [ActiveRecord] Delete Record
*
* @return void
*/
public function delete( $setAuthorToGuest = TRUE )
{
if( $setAuthorToGuest )
{
/* Clean up content - set to member ID 0 - We check $setAuthorToGuest because of member merging.
As the member is immediately deleted we do not want to compete with the existing merge in progress. */
$this->hideOrDeleteAllContent('merge', array('merge_with_id' => 0, 'merge_with_name' => ''));
}
/* Let apps do their stuff */
$this->memberSync( 'onDelete' );
/* Actually delete from database */
parent::delete();
/* Login handler callback */
foreach ( \IPS\Login::handlers( TRUE ) as $k => $handler )
{
try
{
$handler->deleteAccount( $this );
}
catch( \Exception $e ){}
}
/* Reset statistics */
\IPS\Widget::deleteCaches();
}
/**
* [ActiveRecord] Save Changed Columns
*
* @return void
* @note We have to be careful when upgrading in case we are coming from an older version
*/
public function save()
{
$new = $this->_new;
$changes = $this->changed;
if ( $this->member_id AND ( !\IPS\Dispatcher::hasInstance() OR \IPS\Dispatcher::i()->controllerLocation != 'setup' ) )
{
$this->checkGroupPromotion();
}
parent::save();
if ( $new )
{
$this->memberSync( 'onCreateAccount' );
/* Login handler callback */
foreach ( \IPS\Login::handlers( TRUE ) as $k => $handler )
{
try
{
$handler->createAccount( $this );
}
catch( \Exception $e ){}
}
if ( $this->members_bitoptions['validating'] )
{
/* Set the member to validating and add core_validating row */
$vid = md5( $this->members_pass_hash . uniqid( mt_rand(), TRUE ) );
$validatingOptions = array(
'vid' => $vid,
'member_id' => $this->member_id,
'entry_date' => time(),
'new_reg' => 1,
'ip_address' => $this->ip_address,
'spam_flag' => ( $this->members_bitoptions['bw_is_spammer'] ) ?: FALSE,
'user_verified' => ( \IPS\Settings::i()->reg_auth_type == 'admin' ) ?: FALSE,
);
\IPS\Db::i()->insert( 'core_validating', $validatingOptions );
}
/* Profile Fields */
\IPS\Db::i()->insert( 'core_pfields_content', array( 'member_id' => $this->member_id ), TRUE );
}
else
{
/* Log display name history if that is a change */
if( isset( $changes['name'] ) AND $this->previousName !== NULL )
{
\IPS\Db::i()->insert( 'core_dnames_change', array(
'dname_member_id' => $this->member_id,
'dname_date' => time(),
'dname_ip_address' => \IPS\Request::i()->ipAddress(),
'dname_previous' => $this->previousName,
'dname_current' => $changes['name'],
'dname_discount' => FALSE,
) );
}
/* Run member sync, but not if the only change is the last_activity timestamp */
if( count( $changes ) > 1 OR !isset( $changes['last_activity'] ) )
{
$this->memberSync( 'onProfileUpdate', array( 'changes' => array_merge( $changes, $this->changedCustomFields ) ) );
}
}
/* rebuild create menu if the user changed status settings or group(s) */
if ( !$new AND ( isset( $changes['pp_setting_count_comments'] ) or isset( $changes['member_group_id'] ) or isset( $changes['mgroup_others'] ) ) )
{
$this->create_menu = NULL;
parent::save();
}
}
/* !Getters/Setters Data */
/**
* Group Data, taking into consideration secondary groups
*/
public $_group = NULL;
/**
* @brief Admin CP Restrictions
*/
protected $restrictions = NULL;
/**
* @brief Moderator Permissions
*/
protected $modPermissions = NULL;
/**
* @brief Calculated language ID
*/
protected $calculatedLanguageId = NULL;
/**
* @brief Marker Cache
*/
public $markers = array();
protected $markersResetTimes = array();
protected $haveAllMarkers = FALSE;
/**
* @brief Default stream ID
*/
protected $defaultStreamId = FALSE;
/**
* @brief Keep track of any changed profile fields
*/
public $changedCustomFields = array();
/**
* Get name, do not return "guest" name if not set
*
* @return string
*/
public function get_real_name()
{
return ( isset( $this->_data['name'] ) ) ? $this->_data['name'] : '';
}
/**
* Get name
*
* @return string
*/
public function get_name()
{
if( !isset( $this->_data['name'] ) )
{
return \IPS\Member::loggedIn()->language()->addToStack('guest');
}
return $this->member_id ? $this->_data['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'guest_name_shown', NULL, array( 'sprintf' => array( $this->_data['name'] ) ) );
}
/**
* @brief Previous name - stored temporarily for display name history log
*/
protected $previousName = NULL;
/**
* Set name
*
* @param string $value Value
* @return void
*/
public function set_name( $value )
{
if( isset( $this->_data['name'] ) )
{
$this->previousName = $this->_data['name'];
}
$this->_data['name'] = $value;
$this->_data['members_seo_name'] = \IPS\Http\Url::seoTitle( $value );
}
/**
* Set group
*
* @see \IPS\Patterns\ActiveRecord::__set
* @param int $value Value
* @return void
*/
public function set_member_group_id( $value )
{
$this->_data['member_group_id'] = $value;
$this->_group = NULL;
}
/**
* Set Secondary Groups
*
* @see \IPS\Patterns\ActiveRecord::__set
* @param string $value Value
* @return void
*/
public function set_mgroup_others( $value )
{
$groups = explode( ",", $value );
if ( in_array( \IPS\Settings::i()->guest_group, $groups ) )
{
throw new \InvalidArgumentException;
}
$this->_data['mgroup_others'] = $value;
$this->_group = NULL;
}
/**
* Flag as spammer
*
* @return void
*/
public function flagAsSpammer()
{
if ( !$this->members_bitoptions['bw_is_spammer'] )
{
$actions = explode( ',', \IPS\Settings::i()->spm_option );
/* Hide or delete */
if ( in_array( 'unapprove', $actions ) or in_array( 'delete', $actions ) )
{
/* Send to queue */
$this->hideOrDeleteAllContent( in_array( 'delete', $actions ) ? 'delete' : 'hide' );
/* Clear out their profile */
if ( in_array( 'delete', $actions ) )
{
$this->member_title = '';
$this->signature = '';
$this->pp_main_photo = NULL;
try
{
\IPS\File::get( 'core_Profile', $this->pp_main_photo )->delete();
}
catch ( \Exception $e ) { }
\IPS\Db::i()->delete( 'core_pfields_content', array( 'member_id=?', $this->member_id ) );
}
}
/* Restrict from posting or ban */
if ( in_array( 'disable', $actions ) or in_array( 'ban', $actions ) )
{
if ( in_array( 'ban', $actions ) )
{
$this->temp_ban = -1;
}
else
{
$this->restrict_post = -1;
$this->members_disable_pm = 2;
}
}
/* Save */
$this->members_bitoptions['bw_is_spammer'] = TRUE;
$this->save();
/* Run sync */
$this->memberSync( 'onSetAsSpammer' );
/* Notify admin */
if ( \IPS\Settings::i()->spm_notify )
{
\IPS\Email::buildFromTemplate( 'core', 'admin_spammer', array( $this ) )->send( \IPS\Settings::i()->email_in );
}
/* Feedback to Spam Monitoring Service */
if ( \IPS\Settings::i()->spam_service_enabled and \IPS\Settings::i()->spam_service_send_to_ips )
{
$this->spamService( 'markspam' );
}
}
}
/**
* Hide/Delete All Content
*
* @param string $action 'hide' or 'delete' or 'merge'
* @param array $extra Extra data needed by the MemberContent plugin
* @return void
*/
public function hideOrDeleteAllContent( $action, $extra=array() )
{
/* Edited member, so clear widget caches (stats, widgets that contain photos, names and so on) */
\IPS\Widget::deleteCaches();
/* Send to the queue, include archived content */
foreach ( \IPS\Content::routedClasses( FALSE, TRUE, FALSE ) as $class )
{
if ( isset( $class::$databaseColumnMap['author'] ) and ( $action == 'delete' or in_array( 'IPS\Content\Hideable', class_implements( $class ) ) ) )
{
\IPS\Task::queue( 'core', 'MemberContent', array_merge( array( 'initiated_by_member_id' => \IPS\Member::loggedIn()->member_id, 'member_id' => $this->member_id, 'name' => $this->name, 'class' => $class, 'action' => $action ), $extra ), 1 );
}
}
/* And private messages */
\IPS\Task::queue( 'core', 'MemberContent', array_merge( array( 'initiated_by_member_id' => \IPS\Member::loggedIn()->member_id, 'member_id' => $this->member_id, 'name' => $this->name, 'class' => 'IPS\\core\\Messenger\\Conversation', 'action' => $action ), $extra ), 1 );
}
/**
* Unflag as spammer
*
* @return void
*/
public function unflagAsSpammer()
{
if ( $this->members_bitoptions['bw_is_spammer'] )
{
/* Save */
$this->members_bitoptions['bw_is_spammer'] = FALSE;
$this->save();
/* Remove any pending hide or delete content queued tasks */
foreach( \IPS\Db::i()->select( '*', 'core_queue', array( '`key`=?', 'MemberContent' ) ) as $task )
{
$data = json_decode( $task['data'], true );
if( $data['member_id'] == $this->member_id )
{
\IPS\Db::i()->delete( 'core_queue', array( 'id=?', $task['id'] ) );
}
}
/* Report back to spam service */
if ( \IPS\Settings::i()->spam_service_enabled and \IPS\Settings::i()->spam_service_send_to_ips )
{
$this->spamService( 'notspam' );
}
}
}
/**
* Get auto-track data
*
* @return array
*/
public function get_auto_follow()
{
return ( mb_substr( $this->_data['auto_track'], 0, 1 ) !== '{' ) ?
array( 'method' => 'immediate', 'content' => 0, 'comments' => (int) $this->_data['auto_track'] ) :
json_decode( $this->_data['auto_track'], TRUE );
}
/**
* Set banned
*
* @param string $value Value
* @return void
*/
public function set_temp_ban( $value )
{
$this->_data['temp_ban'] = $value;
if ( $value == -1 )
{
\IPS\Db::i()->delete( 'core_validating', array( 'member_id=?', $this->member_id ) );
}
else
{
$this->members_bitoptions['validating'] = FALSE;
}
}
/**
* Get Group Data
*
* @return array
*/
public function get_group()
{
if ( $this->_group === NULL )
{
/* Load primary group */
try
{
$group = \IPS\Member\Group::load( $this->_data['member_group_id'] );
}
catch ( \OutOfRangeException $e )
{
$group = \IPS\Member\Group::load( \IPS\Settings::i()->member_group );
}
$this->_group = array_merge( $group->data(), $group->g_bitoptions->asArray() );
/* Merge in secondary group data */
if ( !empty( $this->_data['mgroup_others'] ) )
{
$groups = array_filter( explode( ',', $this->_data['mgroup_others'] ) );
$exclude = array();
$lessIsMore = array();
$neg1IsBest = array();
$zeroIsBest = array();
$callback = array();
/* Get the limits we need to work out from apps */
foreach ( \IPS\Application::allExtensions( 'core', 'GroupLimits', FALSE, 'core' ) as $key => $extension )
{
if( method_exists( $extension, 'getLimits' ) )
{
$appLimits = $extension->getLimits();
if( !empty( $appLimits['neg1IsBest'] ) )
{
$neg1IsBest = array_merge( $neg1IsBest, $appLimits['neg1IsBest'] );
}
if( !empty( $appLimits['zeroIsBest'] ) )
{
$zeroIsBest = array_merge( $zeroIsBest, $appLimits['zeroIsBest'] );
}
if( !empty( $appLimits['lessIsMore'] ) )
{
$lessIsMore = array_merge( $lessIsMore, $appLimits['lessIsMore'] );
}
if( !empty( $appLimits['exclude'] ) )
{
$exclude = array_merge( $exclude, $appLimits['exclude'] );
}
if( !empty( $appLimits['callback'] ) )
{
$callback = array_merge( $callback, $appLimits['callback'] );
}
}
}
/* Do the merging */
$skippedGroups = array();
foreach( $groups as $gid )
{
try
{
$group = \IPS\Member\Group::load( $gid );
}
catch( \OutOfRangeException $e )
{
$skippedGroups[] = $gid;
continue;
}
$_data = array_merge( $group->_data, $group->g_bitoptions->asArray() );
foreach( $_data as $k => $v )
{
if ( ! in_array( $k, $exclude ) )
{
if ( in_array( $k, $zeroIsBest ) )
{
if ( empty( $this->_group[ $k ] ) )
{
continue;
}
else if( $v == 0 )
{
$this->_group[ $k ] = 0;
}
else if ( $v > $this->_group[ $k ] )
{
$this->_group[ $k ] = $v;
}
}
else if( in_array( $k, $neg1IsBest ) )
{
if ( $this->_group[ $k ] == -1 )
{
continue;
}
else if( $v == -1 )
{
$this->_group[ $k ] = -1;
}
else if ( $v > $this->_group[ $k ] )
{
$this->_group[ $k ] = $v;
}
}
else if ( in_array( $k, $lessIsMore ) )
{
if ( $v < $this->_group[ $k ] )
{
$this->_group[ $k ] = $v;
}
}
else if ( array_key_exists( $k, $callback ) )
{
$result = call_user_func( $callback[ $k ], $this->_group, $_data, $k, $this->_data );
if( is_array( $result ) )
{
$this->_group = array_merge( $this->_group, $result );
}
else if( $result !== NULL )
{
$this->_group[ $k ] = $result;
}
}
else
{
if ( !isset( $this->_group[ $k ] ) OR $v > $this->_group[ $k ] )
{
$this->_group[ $k ] = $v;
}
}
}
}
}
if( count( $skippedGroups ) )
{
$this->mgroup_others = implode( ',', array_diff( $groups, $skippedGroups ) );
parent::save();
}
}
}
return $this->_group;
}
/**
* Retrieve the group name
*
* @return string
*/
public function get_groupName()
{
if ( $this->_group === NULL )
{
$group = $this->group;
}
if( $this->_data['member_group_id'] )
{
$group = \IPS\Member\Group::load( $this->_data['member_group_id'] );
$this->_group['name'] = $group->formatName( \IPS\Member::loggedIn()->language()->addToStack( "core_group_{$group->g_id}" ) );
}
return $this->_group['name'];
}
/**
* Get an array of the group IDs (including secondary groups) this member belongs to
*
* @return array
*/
public function get_groups()
{
$groups = array( $this->_data['member_group_id'] );
if( $this->_data['mgroup_others'] )
{
$groups = array_merge( $groups, array_map( "intval", explode( ',', $this->_data['mgroup_others'] ) ) );
}
/* Sort for consistency when using permissions as part of a cache key */
sort( $groups, SORT_NUMERIC );
return $groups;
}
/**
* Social Groups
*/
protected $_socialGroups = NULL;
/**
* Social Groups
*
* @return array
*/
public function socialGroups()
{
if ( $this->_socialGroups === NULL )
{
$this->_socialGroups = iterator_to_array( \IPS\Db::i()
->select( 'group_id', 'core_sys_social_group_members', array( 'member_id=?', $this->member_id ) )
->setKeyField( 'group_id' )
->setValueField( 'group_id' ) );
}
return $this->_socialGroups;
}
/**
* Permission Array
*
* @return array
*/
public function permissionArray()
{
$return = $this->groups;
if ( $this->member_id )
{
$return[] = "m{$this->member_id}";
foreach ( $this->socialGroups() as $socialGroupId )
{
$return[] = "s{$socialGroupId}";
}
}
return $return;
}
/**
* Get Joined Date
*
* @return \IPS\DateTime
*/
public function get_joined()
{
return \IPS\DateTime::ts( $this->_data['joined'] );
}
/**
* Get Photo Type
*
* @return string
*/
public function get_pp_photo_type()
{
if ( !$this->_data['pp_photo_type'] and \IPS\Settings::i()->allow_gravatars and $this->member_id and !$this->members_bitoptions['bw_disable_gravatar'] )
{
return 'gravatar';
}
return $this->_data['pp_photo_type'];
}
/**
* Get SEO Name
*
* @return string
*/
public function get_members_seo_name()
{
/* Set it so it will be saved */
if( !isset( $this->_data['members_seo_name'] ) or !$this->_data['members_seo_name'] )
{
if ( !$this->name )
{
return NULL;
}
$this->members_seo_name = \IPS\Http\Url::seoTitle( $this->name );
}
return $this->_data['members_seo_name'] ?: \IPS\Http\Url::seoTitle( $this->name );
}
/**
* Get localized birthday, taking into account optional year
*
* @return string|null
*/
public function get_birthday()
{
if( $this->_data['bday_year'] )
{
$date = new \IPS\DateTime( str_pad( $this->_data['bday_year'], 4, 0, STR_PAD_LEFT ) . str_pad( $this->_data['bday_month'], 2, 0, STR_PAD_LEFT ) . str_pad( $this->_data['bday_day'], 2, 0, STR_PAD_LEFT ) );
return $date->localeDate();
}
else if( $this->_data['bday_month'] )
{
$date = new \IPS\DateTime( $this->_data['bday_month'] . '/' . $this->_data['bday_day'] );
return $date->dayAndMonth();
}
else
{
return NULL;
}
}
/**
* Get the member's age
*
* @param \IPS\DateTime|null $date If supplied, birthday is calculated from this point
* @note If the member has not specified a birth year (which is optional), NULL is returned
* @return int|null
*/
public function age( $date=NULL )
{
if( $this->_data['bday_year'] )
{
/* We use dashes because DateTime accepts two digit years with it */
$birthday = new \IPS\DateTime( $this->_data['bday_year'] . '-' . $this->_data['bday_month'] . '-' . $this->_data['bday_day'] );
$birthday->setTimezone( new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) );
$today = $date ?: new \IPS\DateTime();
$today->setTime( 23, 59, 59 ); // We want how old they'll be at the end of the provided date
$today->setTimezone( new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) );
return $birthday->diff( $today )->y;
}
else
{
return NULL;
}
}
/**
* User's photo URL
*
* @param bool $thumb Use thumbnail?
* @return \IPS\Http\Url
*/
public function get_photo( $thumb=TRUE )
{
return static::photoUrl( $this->_data, $thumb );
}
/**
* Set Photo Type
*
* @param string $type Photo type
* @return void
*/
public function set_pp_photo_type( $type )
{
$this->_data['pp_photo_type'] = $type;
}
/**
* Set Photo
*
* @param string $photo Photo location
* @return void
*/
public function set_pp_main_photo( $photo )
{
if ( $this->_data['pp_main_photo'] )
{
try
{
\IPS\File::get( 'core_Profile', $this->_data['pp_main_photo'] )->delete();
}
catch ( \Exception $e ) {}
}
if ( $this->_data['pp_thumb_photo'] )
{
try
{
\IPS\File::get( 'core_Profile', $this->_data['pp_thumb_photo'] )->delete();
}
catch ( \Exception $e ) {}
}
$this->_data['pp_main_photo'] = $photo;
}
/**
* Get reputation points
*
* @return int
*/
public function get_pp_reputation_points()
{
return (int) $this->_data['pp_reputation_points'];
}
/**
* Get warning points
*
* @return int
*/
public function get_warn_level()
{
return (int) $this->_data['warn_level'];
}
/**
* Get failed login details
*
* @return array
*/
public function get_failed_logins()
{
return json_decode( $this->_data['failed_logins'], TRUE ) ?: array();
}
/**
* Fetch the ranks - abstracted to a static method for caching
*
* @return array
*/
public static function getRanks()
{
static $ranks = NULL;
if( $ranks !== NULL )
{
return $ranks;
}
if ( isset( \IPS\Data\Store::i()->ranks ) )
{
$ranks = \IPS\Data\Store::i()->ranks;
}
else
{
$ranks = iterator_to_array( \IPS\Db::i()->select( '*', 'core_member_ranks', NULL, 'posts DESC' ) );
\IPS\Data\Store::i()->ranks = $ranks;
}
return $ranks;
}
/**
* Get member title
*
* @return array
*/
public function get_rank()
{
$title = NULL;
$image = NULL;
/* Does this member have a custom title? */
if ( isset( $this->member_title ) )
{
$title = $this->member_title;
}
foreach( static::getRanks() as $rank )
{
if ( $this->member_posts >= $rank['posts'] )
{
/* Pips or Image */
if ( $rank['use_icon'] and $rank['icon'] )
{
$image = "<img src='" . \IPS\File::get( 'core_Theme', $rank['icon'] )->url . "' alt=''>";
}
else
{
$image = str_repeat( "<span class='ipsPip'></span>", intval( $rank['pips'] ) );
}
/* Get member title from rank */
if ( !isset( $title ) )
{
$title = \IPS\Member::loggedIn()->language()->addToStack( 'core_member_rank_' . $rank['id']);
}
break;
}
}
return array( 'title' => $title, 'image' => $image );
}
/**
* Get member location
*
* @return string|null
*/
public function get_location()
{
return $this->location();
}
/**
* Get members posts for today
*
* @return array
*/
public function get_members_day_posts()
{
return explode( ',', $this->_data['members_day_posts'] );
}
/**
* Set members posts for today
*
* @param array $value Array of daily post data. Index 0 is the amount of posts posted in this time period, and optional index 1 is a timestamp of when we started counting
* @return void
*/
public function set_members_day_posts( $value )
{
/* Are we updating time? */
if ( ! isset( $value[1] ) )
{
$value[1] = $this->members_day_posts[1];
}
$this->_data['members_day_posts'] = implode( ',', $value );
}
/**
* Get member's default stream
*
* @return int|null
*/
public function get_defaultStream()
{
if ( $this->defaultStreamId === FALSE )
{
if ( $this->member_streams and $streams = json_decode( $this->member_streams, TRUE ) and count( $streams ) )
{
$this->defaultStreamId = ( isset( $streams['default'] ) ? $streams['default'] : NULL );
}
else
{
$this->defaultStreamId = NULL;
}
}
return $this->defaultStreamId;
}
/**
* Set member's default stream
*
* @param null|int $value Null or stream ID. 0 is for 'all activity'
* @return void
*/
public function set_defaultStream( $value )
{
if ( $this->member_streams and $streams = json_decode( $this->member_streams, TRUE ) and count( $streams ) )
{
$streams['default'] = $value;
}
else
{
$streams = array( 'streams' => array(), 'default' => $value );
}
$this->member_streams = json_encode( $streams );
$this->save();
$this->defaultStreamId = $value;
}
/**
* Get member location
*
* @return string|null
*/
public function location()
{
if( $this->sessionData === FALSE )
{
return NULL;
}
if( $this->sessionData === NULL )
{
try
{
$this->sessionData = \IPS\Db::i()->select( '*', 'core_sessions', array( 'member_id=?', $this->member_id ) )->first();
}
catch ( \UnderflowException $e ) {}
}
return ( $this->sessionData ) ? \IPS\Session::i()->getLocation( $this->sessionData ) : NULL;
}
/**
* Get validating description
*
* @param null $validatingRow
* @return string
*/
public function validatingDescription( $validatingRow=NULL )
{
try
{
$validatingRow = ( $validatingRow ) ?: \IPS\Db::i()->select( '*', 'core_validating', array( 'member_id=?', $this->member_id ) )->first();
}
catch( \UnderflowException $ex )
{
return '';
}
$validatingDescription = '';
if ( $validatingRow['new_reg'] )
{
if ( $validatingRow['user_verified'] )
{
$validatingDescription = \IPS\Member::loggedIn()->language()->addToStack('members_validating_admin');
}
else
{
$validatingDescription = \IPS\Member::loggedIn()->language()->addToStack('members_validating_user');
}
if ( $validatingRow['coppa_user'] )
{
$validatingDescription .= \IPS\Member::loggedIn()->language()->addToStack('members_validating_coppa');
}
if ( $validatingRow['spam_flag'] )
{
$validatingDescription .= \IPS\Member::loggedIn()->language()->addToStack('members_validating_spam');
}
}
elseif ( $validatingRow['email_chg'] )
{
$validatingDescription .= \IPS\Member::loggedIn()->language()->addToStack('members_validating_email_chg');
}
return $validatingDescription;
}
/**
* Followers
*
* @param int $privacy static::FOLLOW_PUBLIC + static::FOLLOW_ANONYMOUS
* @param array $frequencyTypes array( 'immediate', 'daily', 'weekly' )
* @param \IPS\DateTime|int|NULL $date Only users who started following before this date will be returned. NULL for no restriction
* @param int|array $limit LIMIT clause
* @param string $order Column to order by
* @param int $flags Flags to pass to select (e.g. \IPS\DB::SELECT_SQL_CALC_FOUND_ROWS)
* @param int
* @return \IPS\Db\Select|NULL
* @throws \BadMethodCallException
*/
public function followers( $privacy=3, $frequencyTypes=array( 'immediate', 'daily', 'weekly' ), $date=NULL, $limit=array( 0, 25 ), $order=NULL, $flags=\IPS\DB::SELECT_SQL_CALC_FOUND_ROWS )
{
if( $this->members_bitoptions['pp_setting_moderate_followers'] )
{
return NULL;
}
return static::_followers( 'member', $this->member_id, $privacy, $frequencyTypes, $date, $limit, $order, $flags );
}
/**
* Set failed login details
*
* @param array $data Data
*/
public function set_failed_logins( $data )
{
$this->_data['failed_logins'] = json_encode( $data );
$highest = 0;
foreach ( $data as $ipAddress => $times )
{
if ( $highest < count( $times ) )
{
$highest = count( $times );
}
}
$this->failed_login_count = $highest;
}
/* !Photos */
/**
* Columns needed to build photos
*
* @return array
*/
public static function columnsForPhoto()
{
$return = array( 'member_id', 'name', 'members_seo_name', 'member_group_id', 'pp_photo_type', 'pp_main_photo', 'pp_thumb_photo' );
if ( true or \IPS\Settings::i()->allow_gravatars )
{
$return[] = 'pp_gravatar';
$return[] = 'email';
}
return $return;
}
/**
* Get photo from data
*
* @param array $memberData Array of member data, must include values for at least the keys returned by columnsForPhoto()
* @param bool $thumb Use thumbnail?
* @return string
*/
public static function photoUrl( $memberData, $thumb = TRUE )
{
$url = NULL;
/* Either uploaded or synced from social media */
if ( $memberData['pp_main_photo'] and ( mb_substr( $memberData['pp_photo_type'], 0, 5 ) === 'sync-' or $memberData['pp_photo_type'] === 'custom' ) )
{
try
{
return (string) \IPS\File::get( 'core_Profile', ( $thumb and $memberData['pp_thumb_photo'] ) ? $memberData['pp_thumb_photo'] : $memberData['pp_main_photo'] )->url;
}
catch ( \InvalidArgumentException $e ) { }
}
/* Gravatar */
elseif ( \IPS\Settings::i()->allow_gravatars and $memberData['pp_photo_type'] === 'gravatar' )
{
/* Default URL we send to Gravatar for it to fallback for. Gravatar will error for localhost URLs, so if IN_DEV, don't send this (this way also allows us to easily see what is loading from Gravatar). */
$defaultUrl = \IPS\IN_DEV ? '' : \IPS\Theme::i()->resource( 'default_photo.png', 'core', 'global' );
/* We need to know how big the photo can be */
$photoVars = explode( ':', \IPS\Member::load( $memberData['member_id'] )->group['g_photo_max_vars'] );
/* Construct the URL */
$url = \IPS\Http\Url::external( "https://secure.gravatar.com/avatar/" . md5( trim( mb_strtolower( $memberData['pp_gravatar'] ?: $memberData['email'] ) ) ) )->setQueryString( array(
'd' => $defaultUrl
) );
/* If we're in the ACP, munge because this is an external resource */
if ( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'admin' )
{
$url = $url->makeSafeForAcp( TRUE );
}
/* Return */
return (string) $url;
}
/* Other - This allows an app (such as Gallery) to set the pp_photo_type to a storage container to support custom images without duplicating them */
elseif( $memberData['pp_photo_type'] and $memberData['pp_photo_type'] != 'none' )
{
try
{
return (string) \IPS\File::get( $memberData['pp_photo_type'], $memberData['pp_main_photo'] )->url;
}
catch ( \InvalidArgumentException $e )
{
/* If there was an exception, clear these values out - most likely the image or storage container is no longer valid */
$member = \IPS\Member::load( $memberData['member_id'] );
$member->pp_photo_type = NULL;
$member->pp_main_photo = NULL;
$member->save();
}
}
/* Still here? Return default photo */
if ( !$url )
{
return \IPS\Theme::i()->resource( 'default_photo.png', 'core', 'global' );
}
}
/* !Get Calculated Properties */
/**
* Is an admin?
*
* @return bool
*/
public function isAdmin()
{
return $this->acpRestrictions() !== FALSE;
}
/**
* @brief Cache the session data if we pull it for location, etc.
*/
protected $sessionData = NULL;
/**
* Is online?
*
* @return bool
*/
public function isOnline()
{
if( !$this->member_id )
{
return FALSE;
}
if ( $this->sessionData === NULL )
{
try
{
$this->sessionData = \IPS\Db::i()->select( '*', 'core_sessions', array( 'member_id = ?', $this->member_id ) )->first();
}
catch ( \UnderflowException $e )
{
$this->sessionData = FALSE;
}
}
if( $this->sessionData === FALSE )
{
return FALSE;
}
$diff = \IPS\DateTime::ts( $this->last_activity )->diff( \IPS\DateTime::create() );
if ( $diff->y or $diff->m or $diff->d or $diff->h or $diff->i > 15 )
{
return FALSE;
}
return TRUE;
}
/**
* Is banned?
* If is banned until a certain time, returns an \IPS\DateTime object
*
* @return FALSE|\IPS\DateTime|TRUE
*/
public function isBanned()
{
if ( $this->temp_ban != 0 )
{
if ( $this->temp_ban != -1 and time() >= $this->temp_ban )
{
$this->temp_ban = 0;
$this->save();
return FALSE;
}
elseif ( $this->temp_ban > 0 )
{
return \IPS\DateTime::ts( $this->temp_ban );
}
return TRUE;
}
if( !$this->group['g_view_board'] )
{
return TRUE;
}
return FALSE;
}
/**
* Is the member in a certain group (including secondary groups)
*
* @param int|\IPS\Member\Group|array $group The group, or array of groups
* @param bool $permissionArray If TRUE, checks the permission array rather than the groups
* @return bool
*/
public function inGroup( $group, $permissionArray=FALSE )
{
$group = array_filter( is_array( $group ) ? $group : array( $group ) );
$check = array_filter( $permissionArray ? $this->permissionArray() : $this->groups );
foreach ( $group as $_group )
{
$groupId = ( $_group instanceof \IPS\Member\Group ) ? $_group->g_id : $_group;
if ( in_array( $groupId, $check ) )
{
return TRUE;
}
}
return FALSE;
}
/**
* Store a reference to the language object
*/
protected $_lang = NULL;
/**
* Return the language object to use for this member - returns default if member has not selected a language
*
* @param bool $frontOnly If TRUE, will only look at the the langauge for the front-end, not the AdminCP
* @return \IPS\Lang
*/
public function language( $frontOnly=FALSE )
{
/* Did we already load the language object? */
if( $this->_lang !== NULL )
{
return $this->_lang;
}
/* If in setup, create a "dummy" language */
if ( \IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'setup' AND \IPS\Dispatcher::i()->setupLocation === 'install' )
{
$this->_lang = \IPS\Lang::setupLanguage();
return $this->_lang;
}
else if ( \IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'setup' AND \IPS\Dispatcher::i()->setupLocation === 'upgrade' )
{
$this->_lang = \IPS\Lang::upgraderLanguage();
return $this->_lang;
}
/* Work out if we are getting the ACP language or the normal language */
$column = 'language';
if ( !$frontOnly and \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'admin' and $this->member_id and $this->member_id == static::loggedIn()->member_id )
{
$column = 'acp_language';
}
/* If the member has a language set, try that */
if( $this->calculatedLanguageId !== NULL or $this->$column )
{
try
{
$this->_lang = \IPS\Lang::load( $this->calculatedLanguageId ?: $this->$column );
if( $this->_lang->enabled )
{
return $this->_lang;
}
}
catch ( \OutOfRangeException $e ) { }
}
/* Otherwise, if this is us, try looking at HTTP_ACCEPT_LANGUAGE */
if ( \IPS\Dispatcher::hasInstance() and $this->member_id == static::loggedIn()->member_id )
{
/* Work out what's in HTTP_ACCEPT_LANGUAGE */
$preferredLanguage = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? \IPS\Lang::autoDetectLanguage( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) : NULL;
/* If we worked one out, use that and save it on the account so it gets used for emails etc */
if ( $preferredLanguage )
{
$this->calculatedLanguageId = $preferredLanguage;
if ( $this->member_id )
{
$this->$column = $preferredLanguage;
$this->save();
}
}
/* Otherwise, just use the default */
else
{
$this->calculatedLanguageId = \IPS\Lang::defaultLanguage();
}
}
else
{
/* Just return the default language */
$this->calculatedLanguageId = \IPS\Lang::defaultLanguage();
}
/* Set it */
$this->_lang = \IPS\Lang::load( $this->calculatedLanguageId );
/* Add upgrader language bits if appropriate */
if ( \IPS\Dispatcher::hasInstance() AND class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'setup' AND \IPS\Dispatcher::i()->setupLocation === 'upgrade' )
{
$this->_lang->upgraderLanguage();
}
/* Return */
return $this->_lang;
}
/**
* @brief Cached URL
*/
protected $_url = NULL;
/**
* Get URL
*
* @return \IPS\Http\Url
*/
public function url()
{
if( $this->_url === NULL )
{
$this->_url = \IPS\Http\Url::internal( "app=core&module=members&controller=profile&id={$this->member_id}", 'front', 'profile', $this->members_seo_name );
}
return $this->_url;
}
/**
* URL to ACP "Edit Member"
*
* @return \IPS\Http\Url
*/
public function acpUrl()
{
return \IPS\Http\Url::internal( "app=core&module=members&controller=members&do=edit&id={$this->member_id}", 'admin' );
}
/**
* HTML link to profile with hovercard
*
* @param string|NULL $warningRef The reference key for warnings
* @param boolean $groupFormatting Apply the group prefix/suffix to the name?
* @return string
*/
public function link( $warningRef=NULL, $groupFormatting=FALSE )
{
if ( !\IPS\Settings::i()->warn_on )
{
$warningRef = NULL;
}
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( $this, $warningRef, $groupFormatting );
}
/**
* Profile Fields
*
*/
public function profileFields()
{
if ( !$this->member_id )
{
return array();
}
$profileFields = array();
$values = array();
try
{
$values = \IPS\Db::i()->select( '*', 'core_pfields_content', array( 'member_id = ?', $this->member_id ) )->first();
}
catch ( \UnderflowException $e ) {}
if( !empty( $values ) )
{
foreach ( \IPS\core\ProfileFields\Field::values( $values, \IPS\core\ProfileFields\PROFILE ) as $group => $fields )
{
$profileFields[ 'core_pfieldgroups_' . $group ] = $fields;
}
}
return $profileFields;
}
/**
* Profile Fields shown next to users content
*/
public $profileFields;
/**
* Profile Fields shown next to users content
*
* @return array
*/
public function contentProfileFields()
{
if ( $this->profileFields === NULL )
{
$this->profileFields = array();
if ( $this->member_id AND \IPS\core\ProfileFields\Field::fieldsForContentView() )
{
$select = '*';
/* Can we view private fields? */
if( !( \IPS\Member::loggedIn()->isAdmin() OR \IPS\Member::loggedIn()->member_id === $this->member_id ) )
{
$select = 'member_id';
$publicFields = \IPS\Db::i()->select( 'pf_id', 'core_pfields_data', array( 'pf_member_hide = ?', 0 ) );
foreach( $publicFields as $field)
{
$select .= ", field_{$field}";
}
}
try
{
$values = \IPS\Db::i()->select( $select, 'core_pfields_content', array( 'member_id = ?', $this->member_id ) )->first();
if ( is_array( $values ) )
{
$this->setProfileFieldValuesInMemory( $values );
}
}
catch ( \UnderflowException $e ) {}
}
}
return $this->profileFields;
}
/**
* Store profile field values in memory
*
* @param array $values
* @return void
*/
public function setProfileFieldValuesInMemory( array $values )
{
$this->profileFields = array();
$values = array_filter( $values, function ( $val) { return ( $val ==! '' AND $val !== NULL ); } );
if( !empty( $values ) )
{
foreach ( \IPS\core\ProfileFields\Field::values( $values, \IPS\core\ProfileFields\CONTENT ) as $group => $fields )
{
$this->profileFields[ 'core_pfieldgroups_' . $group ] = str_replace( '{member_id}', $this->member_id, $fields );
}
}
}
/**
* IP Addresses
*
* @code
return array(
'::1' => array(
'count' => ... // int (number of times this member has used this IP)
'first' => ... // \IPS\DateTime (first use)
'last' => ... // \IPS\DateTime (last use)
),
...
);
* @endcode
* @return array
*/
public function ipAddresses()
{
$return = array();
foreach ( \IPS\Application::allExtensions( 'core', 'IpAddresses' ) as $class )
{
$results = $class->findByMember( $this );
if( $results === NULL )
{
continue;
}
foreach ( $results as $ip => $data )
{
if ( isset( $return[ $ip ] ) )
{
$return[ $ip ]['count'] += $data['count'];
if ( $data['first'] < $return[ $ip ]['first'] )
{
$return[ $ip ]['first'] = $data['first'];
}
if ( $data['last'] > $return[ $ip ]['last'] )
{
$return[ $ip ]['last'] = $data['last'];
}
}
else
{
if ( $ip )
{
$return[ $ip ] = $data;
}
}
}
}
return $return;
}
/**
* Mark the entire site as read
*
* @return void
*/
public function markAllAsRead()
{
/* Delete all member markers */
\IPS\Db::i()->delete( 'core_item_markers', array( 'item_member_id=?', $this->member_id ) );
$this->marked_site_read = time();
$this->save();
}
/**
* Get read/unread markers
*
* @param string $app Application key
* @param string $key Marker key
* @return array
*/
public function markersItems( $app, $key )
{
if ( !isset( $this->markers[ $app ] ) or !array_key_exists( $key, $this->markers[ $app ] ) )
{
try
{
$marker = \IPS\Db::i()->select( '*', 'core_item_markers', array( 'item_key=? AND item_member_id=? AND item_app=?', $key, $this->member_id, $app ) )->first();
$this->markers[ $app ][ $key ] = $marker;
}
catch ( \UnderflowException $e )
{
$this->markers[ $app ][ $key ] = NULL;
}
}
return $this->markers[ $app ][ $key ] ? json_decode( $this->markers[ $app ][ $key ]['item_read_array'], TRUE ) : array();
}
/**
* Get read/unread markers for containers
*
* @param string|NULL $app Application key or NULL for all applications
* @return array
*/
public function markersResetTimes( $app )
{
if ( ( !$app and !$this->haveAllMarkers ) or ( $app and !isset( $this->markersResetTimes[ $app ] ) ) )
{
try
{
$where = array( array( 'item_member_id=?', $this->member_id ) );
if ( $app )
{
$this->markersResetTimes[ $app ] = array();
$where[] = array( 'item_app=?', $app );
}
else
{
$this->markersResetTimes = array();
}
foreach ( \IPS\Db::i()->select( '*', 'core_item_markers', $where ) as $row )
{
if( !isset( $this->markersResetTimes[ $row['item_app'] ] ) or !is_array( $this->markersResetTimes[ $row['item_app'] ] ) )
{
$this->markersResetTimes[ $row['item_app'] ] = array();
}
if ( $row['item_app_key_1'] )
{
if ( $row['item_app_key_2'] )
{
if( !isset( $this->markersResetTimes[ $row['item_app'] ][ $row['item_app_key_1'] ] ) OR !is_array( $this->markersResetTimes[ $row['item_app'] ][ $row['item_app_key_1'] ] ) )
{
$this->markersResetTimes[ $row['item_app'] ][ $row['item_app_key_1'] ] = array();
}
if ( $row['item_app_key_3'] )
{
if( !isset( $this->markersResetTimes[ $row['item_app'] ][ $row['item_app_key_1'] ][ $row['item_app_key_2'] ] ) OR !is_array( $this->markersResetTimes[ $row['item_app'] ][ $row['item_app_key_1'] ][ $row['item_app_key_2'] ] ) )
{
$this->markersResetTimes[ $row['item_app'] ][ $row['item_app_key_1'] ][ $row['item_app_key_2'] ] = array();
}
$this->markersResetTimes[ $row['item_app'] ][ $row['item_app_key_1'] ][ $row['item_app_key_2'] ][ $row['item_app_key_3'] ] = $row['item_global_reset'];
}
else
{
$this->markersResetTimes[ $row['item_app'] ][ $row['item_app_key_1'] ][ $row['item_app_key_2'] ] = $row['item_global_reset'];
}
}
else
{
$this->markersResetTimes[ $row['item_app'] ][ $row['item_app_key_1'] ] = $row['item_global_reset'];
}
}
else
{
$this->markersResetTimes[ $row['item_app'] ] = $row['item_global_reset'];
}
$this->markers[ $row['item_app'] ][ $row['item_key'] ] = $row;
}
if ( !$app )
{
$this->haveAllMarkers = TRUE;
}
}
catch ( \UnderflowException $e )
{
if ( $app )
{
$this->markersResetTimes[ $app ] = NULL;
}
else
{
$this->markersResetTimes = array();
}
}
}
if ( $app )
{
return $this->markersResetTimes[ $app ];
}
else
{
return $this->markersResetTimes;
}
}
/**
* Get Warnings
*
* @param int $limit The number to get
* @param bool|NULL $acknowledged If true, will only get warnings that have been acknowledged, if false will only get warnings that have not been knowledged. If NULL, will get both.
* @param string|NULL $type If specified, will only pull warnings that applied a specific action.
* @return \IPS\Patterns\ActiveRecordIterator
*/
public function warnings( $limit, $acknowledged=NULL, $type=NULL )
{
if ( !$this->member_id )
{
return array();
}
$where = array( array( 'wl_member=?', $this->member_id ) );
if ( $acknowledged !== NULL )
{
$where[] = array( 'wl_acknowledged=?', $acknowledged );
}
switch ( $type )
{
case 'mq':
$where[] = array( 'wl_mq<>0' );
break;
case 'rpa':
$where[] = array( 'wl_rpa<>0' );
break;
case 'suspend':
$where[] = array( 'wl_suspend<>0' );
break;
}
return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members_warn_logs', $where, 'wl_date DESC', $limit, NULL, NULL, \IPS\Db::SELECT_DISTINCT ), 'IPS\core\Warnings\Warning' );
}
/**
* @brief Cached reputation data
*/
protected $_reputationData = NULL;
/**
* Calculate and cache the member's reputation level data
*
* @return void
*/
protected function getReputationData()
{
if( $this->_reputationData === NULL )
{
$this->_reputationData = array();
if ( isset( \IPS\Data\Store::i()->reputationLevels ) )
{
$reputationLevels = \IPS\Data\Store::i()->reputationLevels;
}
else
{
$reputationLevels = iterator_to_array( \IPS\Db::i()->select( '*', 'core_reputation_levels', NULL, 'level_points DESC' ) );
\IPS\Data\Store::i()->reputationLevels = $reputationLevels;
}
foreach ( $reputationLevels as $level )
{
if ( $this->pp_reputation_points >= $level['level_points'] )
{
$this->_reputationData = $level;
break;
}
}
}
return $this->_reputationData;
}
/**
* Reputation level description
*
* @return string|NULL
*/
public function reputation()
{
$level = $this->getReputationData();
if( isset( $level['level_id'] ) )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'core_reputation_level_' . $level['level_id'] );
}
else
{
return NULL;
}
}
/**
* Reputation image
*
* @return string|NULL
*/
public function reputationImage()
{
$level = $this->getReputationData();
if( isset( $level['level_id'] ) )
{
return $level['level_image'];
}
else
{
return NULL;
}
}
/**
* Encrypt a plaintext password
*
* @param string $password Password to encrypt
* @return string Encrypted password
* @todo [Future] When we increase minimum PHP version, adjust blowfish to $2y$
*/
public function encryptedPassword( $password )
{
/* New password style introduced in IPS4 using Blowfish */
if ( mb_strlen( $this->members_pass_salt ) === 22 )
{
return crypt( $password, '$2a$13$' . $this->members_pass_salt );
}
/* Old encryption style using md5 */
else
{
return md5( md5( $this->members_pass_salt ) . md5( \IPS\Request::legacyEscape( $password ) ) );
}
}
/**
* Notifications Configuration
*
* @return array
*/
public function notificationsConfiguration()
{
$return = array();
foreach (
\IPS\Db::i()->select(
'd.*, p.preference',
array( 'core_notification_defaults', 'd' )
)->join(
array( 'core_notification_preferences', 'p' ),
array( 'd.notification_key=p.notification_key AND p.member_id=?', $this->member_id )
)
as $row
) {
if ( $row['preference'] === NULL or !$row['editable'] )
{
$return[ $row['notification_key'] ] = explode( ',', $row['default'] );
}
else
{
$return[ $row['notification_key'] ] = array_diff( explode( ',', $row['preference'] ), explode( ',', $row['disabled'] ) );
}
}
return $return;
}
/**
* @brief Following?
*/
protected $_following = array();
/**
* Following
*
* @param string $app Application key
* @param string $area Area
* @param int $id Item ID
* @return bool
*/
public function following( $app, $area, $id )
{
$_key = md5( $app . $area . $id );
if( isset( $this->_following[ $_key ] ) )
{
return $this->_following[ $_key ];
}
try
{
\IPS\Db::i()->select( 'follow_id', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_rel_id=? AND follow_member_id=?', $app, $area, $id, $this->member_id ) )->first();
$this->_following[ $_key ] = TRUE;
}
catch ( \UnderflowException $e )
{
$this->_following[ $_key ] = FALSE;
}
return $this->_following[ $_key ];
}
/**
* Admin CP Restrictions
*
* @return array
*/
protected function acpRestrictions()
{
if ( !$this->member_id )
{
return FALSE;
}
if ( $this->restrictions === NULL )
{
if ( !isset( \IPS\Data\Store::i()->administrators ) )
{
\IPS\Data\Store::i()->administrators = array(
'm' => iterator_to_array( \IPS\Db::i()->select( '*', 'core_admin_permission_rows', array( 'row_id_type=?', 'member' ) )->setKeyField( 'row_id' ) ),
'g' => iterator_to_array( \IPS\Db::i()->select( '*', 'core_admin_permission_rows', array( 'row_id_type=?', 'group' ) )->setKeyField( 'row_id' ) ),
);
}
$rows = array();
if ( isset( \IPS\Data\Store::i()->administrators['m'][ $this->member_id ] ) )
{
$rows[] = \IPS\Data\Store::i()->administrators['m'][ $this->member_id ];
}
foreach ( $this->groups as $id )
{
if ( isset( \IPS\Data\Store::i()->administrators['g'][ $id ] ) )
{
$rows[] = \IPS\Data\Store::i()->administrators['g'][ $id ];
}
}
$this->restrictions = FALSE;
if ( count( $rows ) > 0 )
{
$this->restrictions = array();
foreach ( $rows as $row )
{
if ( $row['row_perm_cache'] === '*' )
{
$this->restrictions = '*';
break;
}
$perms = json_decode( $row['row_perm_cache'], TRUE );
if ( $row['row_id_type'] === 'member' )
{
$this->restrictions = $perms;
break;
}
else if( is_array( $perms ) )
{
$this->restrictions = array_merge( $this->restrictions, $perms );
}
}
}
}
return $this->restrictions;
}
/**
* Moderator Permissions
*
* @return array
*/
public function modPermissions()
{
/* Only guests can be moderators of course */
if ( !$this->member_id )
{
return FALSE;
}
/* Work out the permissions... */
if ( $this->modPermissions === NULL )
{
/* Start with FALSE (no moderator permissions) */
$this->modPermissions = FALSE;
/* If we don't have a datastore of moderator configuration, load that now */
if ( !isset( \IPS\Data\Store::i()->moderators ) )
{
\IPS\Data\Store::i()->moderators = array(
'm' => iterator_to_array( \IPS\Db::i()->select( '*', 'core_moderators', array( 'type=?', 'm' ) )->setKeyField( 'id' ) ),
'g' => iterator_to_array( \IPS\Db::i()->select( '*', 'core_moderators', array( 'type=?', 'g' ) )->setKeyField( 'id' ) ),
);
}
/* Member-level moderator permissions override all group-level permissions, so if this member is a moderator at a member-level, just use that */
if ( isset( \IPS\Data\Store::i()->moderators['m'][ $this->member_id ] ) )
{
$perms = \IPS\Data\Store::i()->moderators['m'][ $this->member_id ]['perms'];
$this->modPermissions = $perms == '*' ? '*' : json_decode( $perms, TRUE );
}
/* Otherwise, examine the groups and combine the permissions each group awards */
else
{
/* Get all the groups the member is in which have moderator permissions... */
$rows = array();
foreach ( $this->groups as $id )
{
if ( isset( \IPS\Data\Store::i()->moderators['g'][ $id ] ) )
{
$rows[] = \IPS\Data\Store::i()->moderators['g'][ $id ];
}
}
/* And if we have any... */
if ( count( $rows ) > 0 )
{
/* Start with an empty array (indicates they are a moderator, but haven't get given them any permissions) */
$this->modPermissions = array();
/* Loop the groups and combine the permissions... */
foreach ( $rows as $row )
{
/* If any group has all permissions, this user has all moderator permissions and we don't need to go further */
if ( $row['perms'] === '*' )
{
$this->modPermissions = '*';
break;
}
/* Otherwise, examine what permissions this group has... */
$perms = json_decode( $row['perms'], TRUE );
if( !empty( $perms ) )
{
foreach ( $perms as $k => $v )
{
/* If we haven't seen this permission key at all, give them the value */
if ( !isset( $this->modPermissions[ $k ] ) )
{
$this->modPermissions[ $k ] = $v;
}
/* If it's an array, combine the values */
elseif ( is_array( $this->modPermissions[ $k ] ) AND is_array( $v ) )
{
foreach( $v as $_k => $_v )
{
/* If this key isn't set yet, then use this value */
if( !isset( $this->modPermissions[ $k ][ $_k ] ) )
{
$this->modPermissions[ $k ][ $_k ] = $_v;
}
/* Otherwise, we only set the value if it's better than what is already set */
elseif ( $_v == -1 or ( $this->modPermissions[ $k ][ $_k ] != -1 and $_v > $this->modPermissions[ $k ][ $_k ] ) )
{
$this->modPermissions[ $k ][ $_k ] = $_v;
}
}
}
/* If it's a number, they get the higher one, or -1 is best */
elseif ( $v == -1 or ( $this->modPermissions[ $k ] != -1 and $v > $this->modPermissions[ $k ] ) )
{
$this->modPermissions[ $k ] = $v;
}
}
}
}
}
}
}
/* Return */
return $this->modPermissions;
}
/**
* @brief Report count
*/
protected $reportCount = NULL;
/**
* Get number of open reports that this member can see
*
* @return int
*/
public function reportCount()
{
if ( $this->reportCount === NULL )
{
if ( \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'modcp' ) ) )
{
if( !\IPS\Member::loggedIn()->members_bitoptions['no_report_count'] )
{
$this->reportCount = \IPS\Db::i()->select(
'COUNT(*)',
'core_rc_index',
array(
array( 'status IN( 1, 2 )' ),
array(
'( perm_id IN (?) OR perm_id IS NULL )',
\IPS\Db::i()->select( 'perm_id', 'core_permission_index', \IPS\Db::i()->findInSet( 'perm_view', array_merge( array( '*' ), $this->groups ) ) )
)
)
)->first();
}
}
else
{
$this->reportCount = 0;
}
}
return (int) $this->reportCount;
}
/**
* @brief Ignore Preferences
* @see \IPS\Member::isIgnoring()
*/
protected $ignorePreferences = NULL;
/**
* Is this member ignoring another member?
*
* @param \IPS\Member $member The member
* @param string $type The type (topics, messages, signatures)
* @return bool
*/
public function isIgnoring( \IPS\Member $member, $type )
{
if ( $member->group['gbw_cannot_be_ignored'] or !$this->member_id )
{
return FALSE;
}
if ( $this->ignorePreferences === NULL )
{
if ( $this->members_bitoptions['has_no_ignored_users'] )
{
$this->ignorePreferences = array();
}
else
{
$this->ignorePreferences = iterator_to_array( \IPS\Db::i()->select( '*', 'core_ignored_users', array( 'ignore_owner_id=?', $this->member_id ) )->setKeyField( 'ignore_ignore_id' ) );
if ( empty( $this->ignorePreferences ) )
{
$this->members_bitoptions['has_no_ignored_users'] = TRUE;
$this->save();
}
}
}
if ( isset( $this->ignorePreferences[ $member->member_id ] ) )
{
return (bool) $this->ignorePreferences[ $member->member_id ][ 'ignore_' . $type ];
}
return FALSE;
}
/**
* Build the "Create" menu
*
* @return array
*/
public function createMenu()
{
if ( $this->create_menu === NULL or ( \IPS\IN_DEV and !\IPS\DEV_USE_MENU_CACHE ) )
{
$createMenu = array();
foreach ( \IPS\Application::allExtensions( 'core', 'CreateMenu', $this ) as $ext )
{
$createMenu = array_merge( $createMenu, array_map( function( $val )
{
$val['link'] = (string) $val['link'];
return $val;
}, $ext->getItems() ) );
}
$this->create_menu = json_encode( $createMenu );
$this->save();
return $createMenu;
}
else
{
return json_decode( $this->create_menu, TRUE );
}
}
/**
* Moderate New Content
*
* @return bool
*/
public function moderateNewContent()
{
$modQueued = FALSE;
if ( $this->group['g_mod_preview'] )
{
if ( $this->group['g_mod_post_unit'] )
{
/* Days since joining */
if ( $this->group['gbw_mod_post_unit_type'] )
{
$modQueued = $this->joined->add( new \DateInterval( "P{$this->group['g_mod_post_unit']}D" ) )->getTimestamp() > time();
}
/* Content items */
else
{
$modQueued = $this->member_posts < $this->group['g_mod_post_unit'];
}
}
else
{
$modQueued = TRUE;
}
}
/* If we're not group moderated what about individual member */
if ( !$modQueued )
{
if( $this->mod_posts == -1 or ( $this->mod_posts > 0 and $this->mod_posts > time() ) )
{
$modQueued = TRUE;
}
}
return $modQueued;
}
/**
* Cover Photo
*
* @return \IPS\Helpers\CoverPhoto
*/
public function coverPhoto()
{
$photo = new \IPS\Helpers\CoverPhoto;
if ( $this->pp_cover_photo )
{
$photo->file = \IPS\File::get( 'core_Profile', $this->pp_cover_photo );
$photo->offset = $this->pp_cover_offset;
}
$photo->editable = ( \IPS\Member::loggedIn()->modPermission('can_modify_profiles') or ( \IPS\Member::loggedIn()->member_id == $this->member_id and $this->group['g_edit_profile'] and $this->group['gbw_allow_upload_bgimage'] ) );
$photo->maxSize = $this->group['g_max_bgimg_upload'];
$photo->object = $this;
return $photo;
}
/**
* Get HTML for search result display
*
* @return callable
*/
public function searchResultHtml()
{
return \IPS\Theme::i()->getTemplate('search')->member( $this );
}
/* !Permissions */
/**
* Has access to a restricted ACP area?
*
* @param \IPS\Application|string $app Application
* @param \IPS\Application\Module|string|null $module Module
* @param string|null $key Restriction Key
*/
public function hasAcpRestriction( $app, $module=NULL, $key=NULL )
{
/* Load our ACP restrictions */
$restrictions = $this->acpRestrictions();
if ( $restrictions === FALSE )
{
return FALSE;
}
/* If we have all permissions, return true */
if ( $restrictions === '*' )
{
return TRUE;
}
/* If we don't have any permissions, return false */
if( !count( $restrictions ) )
{
return FALSE;
}
/* Otherwise, check 'em! */
if ( in_array( is_string( $app ) ? $app : $app->directory, $restrictions['applications'] ) )
{
if ( $module === NULL or in_array( is_string( $module ) ? $module : $module->key, $restrictions['modules'] ) )
{
if( isset( $restrictions['items'][ is_string( $app ) ? $app : $app->directory ][ ( is_string( $module ) ? $module : $module->key ) ] ) )
{
if ( $key === NULL or in_array( $key, $restrictions['items'][ is_string( $app ) ? $app : $app->directory ][ ( is_string( $module ) ? $module : $module->key ) ] ) )
{
return TRUE;
}
}
}
}
return FALSE;
}
/**
* Get moderator permission
*
* @param string|NULL $key Permission Key to check, or NULL to just test if they have any moderator permissions.
* @return mixed
*/
public function modPermission( $key=NULL )
{
/* Load our permissions */
$permissions = $this->modPermissions();
if ( $permissions == FALSE )
{
return FALSE;
}
/* If we have all permissions, return true */
if ( $permissions === '*' or $key === NULL )
{
return TRUE;
}
/* Otherwise return it */
return isset( $permissions[ $key ] ) ? $permissions[ $key ] : NULL;
}
/**
* Can warn
*
* @param \IPS\Member $member The member to warn
* @return bool
*/
public function canWarn( \IPS\Member $member )
{
if( !$this->modPermission('mod_can_warn') OR !$this->modPermission('mod_see_warn') )
{
return FALSE;
}
if( $member->inGroup( explode( ',', \IPS\Settings::i()->warn_protected ) ) or $member == \IPS\Member::loggedIn() )
{
return FALSE;
}
if ( $this->modPermission('warn_mod_day') !== TRUE and $this->modPermission('warn_mod_day') != -1 )
{
$oneDayAgo = \IPS\DateTime::create()->sub( new \DateInterval( 'P1D' ) );
$warningsGivenInTheLastDay = \IPS\Db::i()->select( 'COUNT(*)', 'core_members_warn_logs', array( 'wl_moderator=? AND wl_date>?', $this->member_id, $oneDayAgo->getTimestamp() ) )->first();
if( $warningsGivenInTheLastDay >= $this->modPermission('warn_mod_day') )
{
return FALSE;
}
}
return TRUE;
}
/**
* Check that the login key is not expired and is present
*
* @return void
*/
public function checkLoginKey()
{
$keyExpiry = \IPS\DateTime::ts( $this->member_login_key_expire );
if ( !$this->member_login_key or $keyExpiry->diff( \IPS\DateTime::create() )->days > 7 )
{
$this->member_login_key = md5( uniqid() );
$this->member_login_key_expire = \IPS\DateTime::create()->add( new \DateInterval( 'P7D' ) )->getTimestamp();
$this->save();
}
}
/* !Recounting */
/**
* Recalculate notification count
*
* @return void
*/
public function recountNotifications()
{
$this->notification_cnt = \IPS\Db::i()->select( 'COUNT(*)', 'core_notifications', array( 'member=? AND read_time IS NULL', $this->member_id ) )->first();
$this->save();
}
/**
* Recounts content for this member
*
* @return void
*/
public function recountContent()
{
$this->member_posts = 0;
foreach ( \IPS\Content::routedClasses( $this, TRUE, FALSE ) as $class )
{
$this->member_posts += $class::memberPostCount( $this );
}
$this->save();
}
/**
* Recounts reputation for this member
*
* @return void
*/
public function recountReputation()
{
$this->pp_reputation_points = \IPS\Db::i()->select( 'SUM(rep_rating)', 'core_reputation_index', array( 'member_received=?', $this->member_id ) );
$this->save();
}
/* !Do Stuff */
/**
* Generate a salt
*
* @return string
*/
public function generateSalt()
{
$salt = '';
for ( $i=0; $i<22; $i++ )
{
do
{
$chr = rand( 48, 122 );
}
while ( in_array( $chr, range( 58, 64 ) ) or in_array( $chr, range( 91, 96 ) ) );
$salt .= chr( $chr );
}
return $salt;
}
/**
* Can use module
*
* @param \IPS\Application\Module $module The module to test
* @return bool
* @throws \InvalidArgumentException
*/
public function canAccessModule( $module )
{
if ( !( $module instanceof \IPS\Application\Module ) )
{
throw new \InvalidArgumentException;
}
return \IPS\Application::load( $module->application )->canAccess( $this ) and ( $module->protected or $module->can( 'view', $this ) );
}
/**
* IPS Spam Monitoring Service
*
* @param string $type Request type
* @return int|NULL Action code based on spam service response, or NULL for no action
*/
public function spamService( $type='register' )
{
try
{
$response = \IPS\Http\Url::ips( 'spam/' . $type )->request()->login( \IPS\Settings::i()->ipb_reg_number, '' )->post( array(
'email' => $this->email,
'ip' => $this->ip_address,
) );
if ( $response->httpResponseCode !== 200 )
{
$spamCode = intval( (string) $response );
}
else
{
$spamCode = 0;
}
}
catch ( \IPS\Http\Request\Exception $e )
{
$spamCode = 0;
}
$action = NULL;
if( $type == 'register' and $spamCode )
{
/* Log Request */
\IPS\Db::i()->insert( 'core_spam_service_log', array(
'log_date' => time(),
'log_code' => $spamCode,
'log_msg' => '', // No value is returned unless it's a developer account making the call
'email_address' => $this->email,
'ip_address' => $this->ip_address
) );
/* Action to perform */
$key = "spam_service_action_{$spamCode}";
$action = \IPS\Settings::i()->$key;
/* Perform Action */
switch( $action )
{
/* Proceed with registration */
case 1:
break;
/* Flag for admin approval */
case 2:
\IPS\Settings::i()->reg_auth_type = 'admin';
break;
/* Approve the account, but ban it */
case 3:
$this->temp_ban = -1;
$this->members_bitoptions['bw_is_spammer'] = TRUE;
break;
/* Deny registration - we return the code and the controller is expected to show an error */
case 4:
break;
}
}
return $action;
}
/**
* Member Sync
*
* @param string $method Method
* @param array $params Additional parameters to pass
* @return void
*/
public function memberSync( $method, $params=array() )
{
/* Don't do this during an upgrade */
if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'setup' )
{
return;
}
foreach ( \IPS\Application::allExtensions( 'core', 'MemberSync', FALSE ) as $class )
{
if ( method_exists( $class, $method ) )
{
call_user_func_array( array( $class, $method ), array_merge( array( $this ), $params ) );
}
}
}
/**
* Validate
*
* @return void
*/
public function validate()
{
/* Do stuff */
foreach ( \IPS\Db::i()->select( '*', 'core_validating', array( 'member_id=?', $this->member_id ) ) as $row )
{
/* For new registrations, send a success email */
if ( $row['new_reg'] )
{
try
{
\IPS\Email::buildFromTemplate( 'core', 'registration_complete', array( $this ) )->send( $this );
}
catch( \ErrorException $e )
{
/* @link http://community.invisionpower.com/4bugtrack/beta-3a-possible-problems-if-no-email-server-setup-for-new-registrations-r627/ */
/* If mail isn't working, we don't want to stop the validation from occurring - the error will already be logged to the email error logs */
}
}
}
/* Reset the bitwise flag */
$this->members_bitoptions['validating'] = FALSE;
$this->save();
/* Delete records */
\IPS\Db::i()->delete( 'core_validating', array( 'member_id=?', $this->member_id ) );
$this->memberSync( 'onValidate' );
/* Login handler callback */
foreach ( \IPS\Login::handlers( TRUE ) as $k => $handler )
{
try
{
$handler->validateAccount( $this );
}
catch( \Exception $e ){}
}
}
/**
* Merge
*
* @param \IPS\Member $otherMember Member to merge with
* @return void
*/
public function merge( \IPS\Member $otherMember )
{
if ( $this == $otherMember )
{
throw new \InvalidArgumentException( 'merge_self_error' );
}
/* Merge content */
$otherMember->hideOrDeleteAllContent( 'merge', array( 'merge_with_id' => $this->member_id, 'merge_with_name' => $this->name ) );
/* Let apps do their stuff */
$this->memberSync( 'onMerge', array( $otherMember ) );
/* Login handler callback */
foreach ( \IPS\Login::handlers( TRUE ) as $k => $handler )
{
try
{
$handler->mergeAccounts( $this, $otherMember );
}
catch( \Exception $e ){}
}
}
/**
* Add profile visitor
*
* @param \IPS\Member $visitor Member that viewed profile
* @return void
*/
public function addVisitor( $visitor )
{
$visitors = json_decode( $this->pp_last_visitors, TRUE );
/* If this member is already in the visitor list remove the entry so we can add back in the correct order */
if( isset( $visitors[ $visitor->member_id ] ) )
{
unset( $visitors[ $visitor->member_id ] );
}
/* We want to limit to 5 members */
else if ( count( $visitors ) >= 5 )
{
$visitors = array_reverse( $visitors, TRUE );
array_pop( $visitors );
$visitors = array_reverse( $visitors, TRUE );
}
/* Add the new entry */
$visitors[ $visitor->member_id ] = time();
/* Encode and save*/
$this->pp_last_visitors = json_encode( $visitors );
$this->save();
}
/**
* @brief Posts Per Day Storage
*/
protected $_ppdLimit = NULL;
/**
* Check posts per day to see if this member can post.
*
* @return bool
*/
public function checkPostsPerDay()
{
/* Fetch our PPD limit - we should only need to do this once */
if ( $this->_ppdLimit === NULL )
{
$this->_ppdLimit = $this->group['g_ppd_limit'];
}
/* We can't actually check guests as we can't store how often they have posted - simply counting content is not viable */
if ( ! $this->member_id )
{
return TRUE;
}
/* Is there any limit at all? */
if ( ! $this->_ppdLimit )
{
return TRUE;
}
$count = $this->members_day_posts[0];
$time = $this->members_day_posts[1];
/* Have we posted at all yet? */
if ( ! $count OR ! $time )
{
return TRUE;
}
/* Are we beyond our 24 hours? */
if ( $time AND $time < \IPS\DateTime::create()->sub( new \DateInterval( 'P1D' ) )->getTimestamp() )
{
/* Update member immediately */
$this->members_day_posts = array( 0, 0 );
$this->save();
return TRUE;
}
/* Still within 24 hours... have we hit the limit? */
if ( $count >= $this->_ppdLimit )
{
return FALSE;
}
/* Still here? */
return TRUE;
}
/**
* Check Group Promotion
*
* @return void
*/
public function checkGroupPromotion()
{
if ( $this->group['g_promotion'] != '-1&-1' )
{
/* G-g-gunit, like the hip hop group */
list( $gid, $gUnit ) = explode( '&', $this->group['g_promotion'] );
/* Are we checking for post based auto incrementation? 0 is post based, 1 is date based, so... */
if ( $this->group['gbw_promote_unit_type'] )
{
if ( $gUnit > 0 )
{
if ( $this->joined->getTimestamp() <= ( time() - ( $gUnit * 86400 ) ) )
{
$this->member_group_id = $gid;
}
}
}
else
{
if ( $gUnit > 0 )
{
if ( $this->member_posts >= $gUnit )
{
$this->member_group_id = $gid;
}
}
}
}
}
/**
* Clear out cached "create menu"
*
* @return void
*/
public static function clearCreateMenu()
{
\IPS\Db::i()->update( 'core_members', array( 'create_menu' => NULL ) );
}
}