<?php
/**
* @brief Member Model
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 18 Feb 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;
}
/**
* 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' );
/**
* @brief [ActiveRecord] Multiton Map
*/
protected static $multitonMap = array();
/**
* @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
'has_security_answers' => 512,
'security_questions_opt_out' => 1024,
'ignore_promotions' => 2048,// 1 means that the user will not be checked against group promotion rules. Set by Commerce to prevent "promoting" users incorrectly after a purchase.
'profile_completed' => 4096,
'profile_completion_dismissed' => 8192,
'created_externally' => 16384
)
)
);
/* !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
*/
public static $loggedInMember = NULL;
/**
* @brief If we change the photo_type, then we need to record the previous photo type to determin if set_pp_main_photo should attempt removal of existing images
*/
protected $_previousPhotoType = NULL;
/**
* Get logged in member
*
* @return \IPS\Member
*/
public static function loggedIn()
{
/* If we haven't loaded the member yet, or if the session member has changed since we last loaded the member, reload and cache */
if( static::$loggedInMember === NULL )
{
static::$loggedInMember = \IPS\Session::i()->member;
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 )
{
unset( \IPS\Data\Store::i()->$key );
unset( $_SESSION['logged_in_as_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 )
{
try
{
$member = parent::loadFromUrl( $url );
}
catch( \InvalidArgumentException $e )
{
throw new \OutOfRangeException;
}
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 = ( \IPS\Settings::i()->updates_consent_default == 'enabled' );
$this->pp_photo_type = '';
$this->member_posts = 0;
$this->last_visit = NULL;
$this->_data['pp_main_photo'] = NULL;
$this->_data['pp_thumb_photo'] = NULL;
$this->_data['pp_gravatar'] = NULL;
$this->_data['failed_logins'] = NULL;
$this->_data['mfa_details'] = NULL;
$this->_data['pp_reputation_points'] = 0;
$this->_data['signature'] = '';
$this->_data['auto_track'] = json_encode( array(
'content' => \IPS\Settings::i()->auto_follow_new_content ? 1 : 0,
'comments' => \IPS\Settings::i()->auto_follow_replied_to ? 1 : 0,
'method' => 'immediate'
) );
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
*
* @param bool $setAuthorToGuest Sets the author id of all content to 0 ( guest )
* @param bool $keepAuthorName Keeps the author name
* @return void
*/
public function delete( $setAuthorToGuest = TRUE, $keepAuthorName = 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' => $keepAuthorName ? $this->name : '' ) );
}
/* Let apps do their stuff */
$this->memberSync( 'onDelete' );
/* Actually delete from database */
parent::delete();
/* 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()
{
if ( $this->member_id AND ( !\IPS\Dispatcher::hasInstance() OR \IPS\Dispatcher::i()->controllerLocation != 'setup' ) )
{
$this->checkGroupPromotion();
}
$new = $this->_new;
$changes = $this->changed;
/* Set default status updates enabled/disabled status */
if( $new AND !isset( $this->_data['pp_setting_count_comments'] ) )
{
$this->_data['pp_setting_count_comments'] = \IPS\Settings::i()->status_updates_mem_enable;
}
parent::save();
if ( $new )
{
$this->memberSync( 'onCreateAccount' );
/* Profile Fields */
\IPS\Db::i()->insert( 'core_pfields_content', array( 'member_id' => $this->member_id ), TRUE );
}
else
{
/* Run member sync, but not if the only change is the last_activity timestamp */
if( count( $this->changedCustomFields ) > 0 or count( $changes ) > 1 OR !isset( $changes['last_activity'] ) )
{
$this->memberSync( 'onProfileUpdate', array( 'changes' => array_merge( $changes, $this->changedCustomFields ) ) );
}
}
/* If we have updated custom fields, make sure we don't have any cached in the class - just wipe out cache so we will refetch if needed */
if( count( $this->changedCustomFields ) )
{
$this->rawProfileFieldsData = NULL;
$this->profileFields = NULL;
}
/* 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\Friendly::seoTitle( $value );
if( $this->_data['pp_photo_type'] == 'letter' )
{
$this->deletePhoto();
$this->_data['pp_photo_type'] = '';
$this->_data['pp_main_photo'] = NULL;
$this->_data['pp_thumb_photo'] = NULL;
}
}
/**
* 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'] = (int) $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 = array_filter( explode( ",", $value ) );
if ( in_array( \IPS\Settings::i()->guest_group, $groups ) )
{
throw new \InvalidArgumentException;
}
$this->_data['mgroup_others'] = implode( ',', $groups );
$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;
\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->logHistory( 'core', 'account', array( 'type' => 'spammer', 'set' => TRUE, 'actions' => $actions ) );
$this->memberSync( 'onSetAsSpammer' );
/* Notify admin */
if ( \IPS\Settings::i()->spm_notify )
{
\IPS\Email::buildFromTemplate( 'core', 'admin_spammer', array( $this ), \IPS\Email::TYPE_LIST )->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 ) ) ) )
{
/* Comments run first so rebuilding topic doesn't fail with incorrect author ID */
$order = ( is_subclass_of( $class, '\IPS\Content\Comment' ) ) ? 1 : 2;
\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 ), $order );
}
}
/* 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 ), 2 );
}
/**
* 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();
/* Log */
$this->logHistory( 'core', 'account', array( 'type' => 'spammer', 'set' => FALSE ) );
/* 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' );
}
/* Run sync */
$this->memberSync( 'onUnSetAsSpammer' );
}
}
/**
* 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'];
}
/**
* @brief Cached groups check
*/
protected $_groups = NULL;
/**
* Get an array of the group IDs (including secondary groups) this member belongs to
*
* @return array
*/
public function get_groups()
{
if ( $this->_groups !== NULL )
{
return $this->_groups;
}
$this->_groups = array( $this->_data['member_group_id'] );
if( $this->_data['mgroup_others'] )
{
foreach( explode( ',', $this->_data['mgroup_others'] ) as $id )
{
$this->_groups[] = intval( $id );
}
}
/* Sort for consistency when using permissions as part of a cache key */
sort( $this->_groups, SORT_NUMERIC );
return $this->_groups;
}
/**
* Social Groups
*/
protected $_socialGroups = NULL;
/**
* Social Groups
*
* @param bool $fromWriteServer If Read/Write separation is enabled, this flag can be used to force reading from the write server, which can be used when rebuilding cached permission strings
* @return array
*/
public function socialGroups( $fromWriteServer = FALSE )
{
if ( $this->_socialGroups === NULL )
{
/* If this is a guest, they will not have any social groups - save the query */
if( !$this->member_id )
{
$this->_socialGroups = array();
}
else
{
$this->_socialGroups = iterator_to_array( \IPS\Db::i()
->select( 'group_id', 'core_sys_social_group_members', array( 'member_id=?', $this->member_id ), NULL, NULL, NULL, NULL, $fromWriteServer ? \IPS\Db::SELECT_FROM_WRITE_SERVER : 0 )
->setKeyField( 'group_id' )
->setValueField( 'group_id' ) );
}
}
return $this->_socialGroups;
}
/**
* Clubs
*/
protected $_clubs = NULL;
/**
* Clubs
*
* @param bool $fromWriteServer If Read/Write separation is enabled, this flag can be used to force reading from the write server, which can be used when rebuilding cached permission strings
* @return array
*/
public function clubs( $fromWriteServer = FALSE )
{
if ( !\IPS\Settings::i()->clubs )
{
return array();
}
if ( $this->_clubs === NULL )
{
/* If this is a guest, they will not have any clubs - save the query */
if( !$this->member_id )
{
$this->_clubs = array();
}
else
{
$this->_clubs = iterator_to_array( \IPS\Db::i()->select( 'club_id', 'core_clubs_memberships', array( "member_id=? AND status IN('" . \IPS\Member\Club::STATUS_MEMBER . "','" . \IPS\Member\Club::STATUS_MODERATOR . "','" . \IPS\Member\Club::STATUS_LEADER . "')", $this->member_id ), NULL, NULL, NULL, NULL, $fromWriteServer ? \IPS\Db::SELECT_FROM_WRITE_SERVER : 0 ) );
}
}
return $this->_clubs;
}
/**
* Permission Array
*
* @return array
*/
public function permissionArray()
{
$return = $this->groups;
if ( $this->member_id )
{
$return[] = "m{$this->member_id}";
if ( \IPS\Settings::i()->clubs )
{
$return[] = "ca"; // Public clubs, which is everyone except guests
}
if ( $this->modPermission('can_access_all_clubs') )
{
$return[] = 'cm';
}
if ( $this->permission_array === NULL )
{
$this->rebuildPermissionArray();
}
if( $this->permission_array )
{
$return = array_merge( $return, explode( ',', $this->permission_array ) );
}
}
return $return;
}
/**
* Permission Array
*
* @return void
*/
public function rebuildPermissionArray()
{
$permissionArray = array();
foreach ( $this->socialGroups( TRUE ) as $socialGroupId )
{
$permissionArray[] = "s{$socialGroupId}";
}
if ( \IPS\Settings::i()->clubs )
{
/* Wipe club cache as when we are added, perm is rebuilt but the new club membership is not detected as we are using cached value */
$this->_clubs = NULL;
foreach ( $this->clubs( TRUE ) as $clubId )
{
$permissionArray[] = "c{$clubId}";
}
}
$this->permission_array = implode( ',', $permissionArray );
$this->save();
}
/**
* 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\Friendly::seoTitle( $this->name );
}
return $this->_data['members_seo_name'] ?: \IPS\Http\Url\Friendly::seoTitle( $this->name );
}
/**
* Get localized birthday, taking into account optional year
*
* @return string|null
*/
public function get_birthday()
{
try
{
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->fullYearLocaleDate();
}
else if( $this->_data['bday_month'] )
{
$date = new \IPS\DateTime( $this->_data['bday_month'] . '/' . $this->_data['bday_day'] );
return $date->dayAndMonth();
}
else
{
return NULL;
}
}
catch ( \Exception $e )
{
\IPS\Log::debug( "Member " . $this->id ." has a not valid birthday date" , 'birthday_error' );
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'] AND checkdate( $this->_data['bday_month'], $this->_data['bday_day'], $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'] );
if ( \IPS\Member::loggedIn()->timezone )
{
$birthday->setTimezone( new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) );
}
$today = $date ? clone $date : new \IPS\DateTime();
$today->setTime( 23, 59, 59 ); // We want how old they'll be at the end of the provided date
if ( \IPS\Member::loggedIn()->timezone )
{
$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?
* @param bool $email Is the photo going to be used in an email?
* @return string
*/
public function get_photo( $thumb=TRUE, $email=FALSE )
{
return static::photoUrl( $this->_data, $thumb, $email );
}
/**
* Set Photo Type
*
* @param string $type Photo type
* @return void
*/
public function set_pp_photo_type( $type )
{
if ( $this->_previousPhotoType === NULL and isset( $this->_data['pp_photo_type'] ) )
{
$this->_previousPhotoType = $this->_data['pp_photo_type'];
}
$this->_data['pp_photo_type'] = $type;
}
/**
* Set Photo
*
* @param string $photo Photo location
* @return void
*/
public function set_pp_main_photo( $photo )
{
$this->deletePhoto();
$this->_data['pp_main_photo'] = $photo;
}
/**
* Delete the profile photo
*
* @return void
*/
public function deletePhoto()
{
/* It is common to update pp_photo_type before pp_main_photo */
$photoType = ( $this->_previousPhotoType !== NULL ) ? $this->_previousPhotoType : $this->_data['pp_photo_type'];
/* Attempt to delete existing images if they are from a profile sync, or uploaded/imported from URL */
if ( mb_substr( $photoType, 0, 5 ) === 'sync-' or $photoType === 'custom' or $photoType === 'letter' )
{
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();
$this->_data['pp_thumb_photo'] = NULL;
}
catch ( \Exception $e ) {}
}
}
}
/**
* Get reputation points
*
* @return int
*/
public function get_pp_reputation_points()
{
return isset( $this->_data['pp_reputation_points'] ) ? (int) $this->_data['pp_reputation_points'] : 0;
}
/**
* Get warning points
*
* @return int
*/
public function get_warn_level()
{
return isset( $this->_data['warn_level'] ) ? (int) $this->_data['warn_level'] : 0;
}
/**
* 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 ( !$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 )
{
$this->sessionData = \IPS\Session\Store::i()->getLatestMemberSession( $this->member_id );
}
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['reg_cancelled'] )
{
$validatingDescription = \IPS\Member::loggedIn()->language()->addToStack('members_validating_cancelled');
}
elseif ( $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;
}
/**
* Get MFA details
*
* @return array
*/
public function get_mfa_details()
{
return json_decode( $this->_data['mfa_details'], TRUE ) ?: array();
}
/**
* Set MFA details
*
* @param array $data Data
*/
public function set_mfa_details( $data )
{
$this->_data['mfa_details'] = json_encode( $data );
}
/**
* Get profile sync settings
*
* @return array
*/
public function get_profilesync()
{
$return = isset( $this->_data['profilesync'] ) ? ( json_decode( $this->_data['profilesync'], TRUE ) ?: array() ) : array();
if ( isset( $return['facebook'] ) or isset( $return['Facebook'] ) or isset( $return['google'] ) or isset( $return['Google'] ) or isset( $return['linkedin'] ) or isset( $return['Linkedin'] ) or isset( $return['LinkedIn'] ) or isset( $return['microsoft'] ) or isset( $return['Microsoft'] ) or isset( $return['live'] ) or isset( $return['Live'] ) or isset( $return['twitter'] ) or isset( $return['Twitter'] ) )
{
$newVal = array();
foreach ( $return as $loginKey => $prefs )
{
$classname = NULL;
switch ( mb_strtolower( $loginKey ) )
{
case 'internal':
$classname = 'IPS\\Login\\Handler\\Standard';
break;
case 'facebook':
$classname = 'IPS\\Login\\Handler\\OAuth2\\Facebook';
break;
case 'google':
$classname = 'IPS\\Login\\Handler\\OAuth2\\Google';
break;
case 'linkedin':
$classname = 'IPS\\Login\\Handler\\OAuth2\\LinkedIn';
break;
case 'live':
case 'microsoft':
$classname = 'IPS\\Login\\Handler\\OAuth2\\Microsoft';
$settings['legacy_redirect'] = TRUE;
break;
case 'twitter':
$classname = 'IPS\\Login\\Handler\\OAuth1\\Twitter';
break;
}
if ( $classname and class_exists( $classname ) )
{
try
{
$methodId = \IPS\Db::i()->select( 'login_id', 'core_login_methods', array( 'login_classname=? AND login_enabled=1', $classname ) )->first();
foreach ( $prefs as $option => $v )
{
if ( $v )
{
$newVal[ $option ] = array( 'handler' => $methodId, 'ref' => NULL, 'error' => NULL );
}
}
}
catch ( \UnderflowException $e ) { }
}
}
$this->_data['profilesync'] = json_encode( $newVal );
return $newVal;
}
return $return;
}
/**
* Set profile sync settings
*
* @param array $data Data
*/
public function set_profilesync( $data )
{
$this->_data['profilesync'] = json_encode( $data );
if ( !$data )
{
$this->_data['profilesync_lastsync'] = 0;
}
}
/* !Photos */
/**
* Columns needed to build photos
*
* @return array
*/
public static function columnsForPhoto()
{
$return = array( 'member_id', 'name', 'members_seo_name', 'member_group_id', 'mgroup_others', 'pp_photo_type', 'pp_main_photo', 'pp_thumb_photo' );
if ( \IPS\Settings::i()->allow_gravatars )
{
$return[] = 'pp_gravatar';
$return[] = 'email';
$return[] = 'members_bitoptions';
}
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?
* @param bool $email Is the photo going to be used in an email?
* @param bool $useDefaultPhoto If there is no photo, should the default (rather than NULL) be returned? (If Gravatar is enabled, this will have no effect)
* @return string
*/
public static function photoUrl( $memberData, $thumb=TRUE, $email=FALSE, $useDefaultPhoto=TRUE )
{
$gravatar = FALSE;
$photoUrl = NULL;
/* All this only applies to members... */
if ( isset( $memberData['member_id'] ) and $memberData['member_id'] )
{
/* Is Gravatar disabled for them? */
$gravatarDisabled = FALSE;
if ( isset( $memberData['members_bitoptions'] ) )
{
if ( is_object( $memberData['members_bitoptions'] ) )
{
$gravatarDisabled = $memberData['members_bitoptions']['bw_disable_gravatar'];
}
else
{
$gravatarDisabled = $memberData['members_bitoptions'] & static::$bitOptions['members_bitoptions']['members_bitoptions']['bw_disable_gravatar'];
}
}
/* 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' or ( \IPS\Settings::i()->letter_photos == 'letters' AND $memberData['pp_photo_type'] == 'letter' and $useDefaultPhoto and ( $gravatarDisabled OR !\IPS\Settings::i()->allow_gravatars ) ) ) )
{
try
{
$photoUrl = \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'] === 'letter' OR $memberData['pp_photo_type'] === 'gravatar' ) or ( !$memberData['pp_photo_type'] and !$gravatarDisabled ) ) )
{
$photoUrl = \IPS\Theme::i()->resource( 'default_photo.png', 'core', 'global' );
if( empty( $memberData['pp_main_photo'] ) )
{
if( $photo = static::generateLetterPhoto( $memberData ) )
{
$photoUrl = $photo;
}
}
else
{
$photoUrl = \IPS\File::get( 'core_Profile', ( $thumb and $memberData['pp_thumb_photo'] ) ? $memberData['pp_thumb_photo'] : $memberData['pp_main_photo'] )->url;
}
$gravatar = TRUE;
}
elseif( \IPS\Settings::i()->letter_photos == 'letters' AND empty( $memberData['pp_main_photo'] ) )
{
if( $photo = static::generateLetterPhoto( $memberData ) )
{
$photoUrl = $photo;
if( !$gravatarDisabled AND \IPS\Settings::i()->allow_gravatars )
{
$gravatar = TRUE;
}
}
}
/* 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' and mb_strpos( $memberData['pp_photo_type'], '_' ) !== FALSE )
{
try
{
$photoUrl = \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();
}
}
if( $gravatar )
{
/* Construct the URL - 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).*/
$photoUrl = \IPS\Http\Url::external( "https://secure.gravatar.com/avatar/" . md5( trim( mb_strtolower( $memberData['pp_gravatar'] ?: $memberData['email'] ) ) ) )->setQueryString( array(
'd' => \IPS\IN_DEV ? '' : ( $photoUrl instanceof \IPS\Http\Url ? (string) $photoUrl->setScheme( \IPS\Request::i()->isSecure() ? 'https' : 'http' ) : '' )
) );
}
/* If we're in the ACP, munge because this is an external resource, but not for locally uploaded files or letter avatars */
if (
\IPS\Dispatcher::hasInstance() AND
\IPS\Dispatcher::i()->controllerLocation === 'admin' AND
( $photoUrl instanceof \IPS\Http\Url ) AND
( $gravatar === TRUE OR !in_array( $memberData['pp_photo_type'], array( 'custom', 'letter' ) ) )
)
{
$photoUrl = $photoUrl->makeSafeForAcp( TRUE );
}
/* Return */
if( $photoUrl !== NULL )
{
return (string) $photoUrl;
}
}
/* Still here? Return default photo */
if ( !$photoUrl and $useDefaultPhoto )
{
if( $email )
{
return rtrim( \IPS\Settings::i()->base_url, '/' ) . '/applications/core/interface/email/default_photo.png';
}
else
{
if( $photo = static::generateLetterPhoto( $memberData ) )
{
return (string) $photo;
}
return (string) \IPS\Theme::i()->resource( 'default_photo.png', 'core', 'global' );
}
}
return NULL;
}
/**
* Generate Letter Photo
*
* @param array $memberData
* @return boolean
*/
public static function generateLetterPhoto( $memberData)
{
/* Letter-photos are enabled and this user has no photo, so generate a new letter photo if possible. */
if( \IPS\Settings::i()->letter_photos == 'letters' AND \IPS\Image::canWriteText() AND isset( $memberData['member_id'] ) AND $memberData['member_id'] )
{
$member = \IPS\Member::load( $memberData['member_id'] );
if( $member->pp_photo_type == 'letter' AND !empty( $member->pp_main_photo ) )
{
return \IPS\File::get( 'core_Profile', $member->pp_main_photo )->url;
}
$letterPhoto = new \IPS\Member\LetterPhoto( $member );
$photoVars = static::getLetterPhotoDimensions( $member );
try
{
if( $newPhoto = $letterPhoto->create( $photoVars['width'], $photoVars['height'] ) )
{
$photoUrl = $newPhoto->container . '/' . $newPhoto->filename;
$member->pp_photo_type = 'letter';
$member->pp_main_photo = $photoUrl;
$member->pp_thumb_photo = NULL;
$member->save();
return $newPhoto->url;
}
}
catch( \Exception $e )
{
\IPS\Log::log( $e, 'letter_photo' );
return FALSE;
}
}
return FALSE;
}
/**
* Get dimensions for letter photo
*
* @note Abstracted so third parties can override if desired
* @param \IPS\Member $member Member
* @return array
*/
protected static function getLetterPhotoDimensions( $member )
{
$photoVars = explode( 'x', \IPS\THUMBNAIL_SIZE );
return array( 'width' => $photoVars[0], 'height' => $photoVars[1] );
}
/* !Get Calculated Properties */
/**
* Get administrators
*
* @return array
*/
public static function administrators()
{
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' ) ),
);
}
return \IPS\Data\Store::i()->administrators;
}
/**
* 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 )
{
$this->sessionData = \IPS\Session\Store::i()->getLatestMemberSession( $this->member_id );
}
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 Online Anonymously
*
* @return bool
*/
public function isOnlineAnonymously()
{
if ( !$this->member_id )
{
return FALSE;
}
if ( $this->sessionData === NULL )
{
$this->sessionData = \IPS\Session\Store::i()->getLatestMemberSession( $this->member_id );
}
if ( $this->sessionData === FALSE )
{
return FALSE;
}
return ( $this->sessionData['login_type'] == \IPS\Session\Front::LOGIN_TYPE_ANONYMOUS );
}
/**
* 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 );
/* Disabled Languages are allowed to be used in the ACP */
if( $this->_lang->enabled OR $column == 'acp_language' )
{
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=view&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 shown next to users content
*/
public $rawProfileFieldsData = NULL;
/**
* Profile Fields
*
* @param int $location \IPS\core\ProfileFields\Field::PROFILE for profile, \IPS\core\ProfileFields\Field::REG for registration screen or \IPS\core\ProfileFields\Field::STAFF for ModCP/ACP
* @return array
*/
public function profileFields( $location = 0 )
{
if ( !$this->member_id )
{
return array();
}
if( $this->rawProfileFieldsData !== NULL )
{
return $this->rawProfileFieldsData;
}
$this->rawProfileFieldsData = 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, $location ) as $group => $fields )
{
$this->rawProfileFieldsData[ 'core_pfieldgroups_' . $group ] = $fields;
}
}
return $this->rawProfileFieldsData;
}
/**
* 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\Dispatcher::hasInstance() OR !( \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\Field::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 ] = array();
}
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();
}
if ( !\IPS\Settings::i()->warn_on )
{
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;
}
/**
* @brief Cached reputation last day won
*/
protected $_reputationLastDayWon = NULL;
/**
* Return the 'date' of the last day won, along with the 'rep_total'.
*
* @return array( 'date' => \IPS\DateTime, 'rep_total' => int )|FALSE
*/
public function getReputationLastDayWon()
{
if ( $this->_reputationLastDayWon === NULL )
{
try
{
$dayWon = \IPS\Db::i()->select( 'leader_date, leader_rep_total', 'core_reputation_leaderboard_history', array( 'leader_position=1 AND leader_member_id=?', $this->member_id ), 'leader_date DESC', array( 0, 1 ) )->first();
/* The 'day won' must be in the leaderboard timezone otherwise it will be off for people in significantly different timezones */
$this->_reputationLastDayWon = array( 'date' => \IPS\DateTime::ts( $dayWon['leader_date'], true )->setTimezone( new \DateTimeZone( \IPS\Settings::i()->reputation_timezone ) ), 'rep_total' => $dayWon['leader_rep_total'] );
}
catch( \UnderflowException $ex )
{
$this->_reputationLastDayWon = FALSE;
}
}
return $this->_reputationLastDayWon;
}
/**
* @brief Cached reputation days won count
*/
protected $_reputationDaysWonCount = NULL;
/**
* Return the total number of days won
*
* @return int
*/
public function getReputationDaysWonCount()
{
if ( $this->_reputationDaysWonCount === NULL )
{
$this->_reputationDaysWonCount = \IPS\Db::i()->select( 'COUNT(*)', 'core_reputation_leaderboard_history', array( 'leader_position=1 AND leader_member_id=?', $this->member_id ) )->first();
}
return $this->_reputationDaysWonCount;
}
/**
* 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;
}
}
/**
* Verify legacy password
*
* @return bool
*/
public function verifyLegacyPassword( $password )
{
return \IPS\Login::compareHashes( $this->members_pass_hash, md5( md5( $this->members_pass_salt ) . md5( \IPS\Request::legacyEscape( $password ) ) ) );
}
/**
* Set local password
*
* BE CAREFUL: The standard login handler may be disabled, only call this method
* if you have alreadu checked it is enabled. In most cases, it is better to let
* the available login handlers handle password management
*
* @param string $password Password to encrypt
* @return void
*/
public function setLocalPassword( $password )
{
$this->members_pass_hash = password_hash( $password, PASSWORD_DEFAULT );
$this->members_pass_salt = NULL;
}
/**
* Change member's password for all applicable login handlers
*
* @param string $newPassword The new password in plaintext
* @param string $type Type of change for log
* @return bool
*/
public function changePassword( $newPassword, $type='manual' )
{
$return = FALSE;
foreach ( \IPS\Login::methods() as $method )
{
if ( $method->canChangePassword( $this ) )
{
try
{
$method->changePassword( $this, $newPassword );
$return = TRUE;
}
catch( \BadMethodCallException $e ){}
}
}
$this->memberSync( 'onPassChange', array( $newPassword ) );
$this->logHistory( 'core', 'password_change', $type );
return $return;
}
/**
* 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 ) )
{
if ( empty( $this->restrictions ) )
{
$this->restrictions = $perms;
}
else
{
if( isset( $perms['applications'] ) )
{
foreach ( $perms['applications'] as $app => $modules )
{
if ( !isset( $this->restrictions['applications'][ $app ] ) )
{
$this->restrictions['applications'][ $app ] = array();
}
foreach ( $modules as $module )
{
if ( !isset( $this->restrictions['applications'][ $app ][ $module ] ) )
{
$this->restrictions['applications'][ $app ][ $module ] = $module;
}
}
}
}
if( isset( $perms['items'] ) )
{
foreach ( $perms['items'] as $app => $modules )
{
if ( !isset( $this->restrictions['items'][ $app ] ) )
{
$this->restrictions['items'][ $app ] = array();
}
foreach ( $modules as $module => $items )
{
if ( !isset( $this->restrictions['items'][ $app ][ $module ] ) )
{
$this->restrictions['items'][ $app ][ $module ] = array();
}
foreach ( $items as $item )
{
if ( !in_array( $item, $this->restrictions['items'][ $app ][ $module ] ) )
{
$this->restrictions['items'][ $app ][ $module ][] = $item;
}
}
}
}
}
}
}
}
}
}
return $this->restrictions;
}
/**
* Moderator Permissions
*
* @return array
*/
public function modPermissions()
{
/* Only members 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 ) )
{
$this->modPermissions[ $k ] = array_merge( $this->modPermissions[ $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
*
* @param bool $force Fetch the count even if the notification is off
* @return int
*/
public function reportCount( $force=FALSE )
{
if ( $this->reportCount === NULL OR $force === TRUE )
{
if ( \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'modcp' ) ) )
{
if( !\IPS\Member::loggedIn()->members_bitoptions['no_report_count'] OR $force === TRUE )
{
$where = '( perm_id IN (?) OR perm_id IS NULL ) AND status IN( 1,2 )';
/* fetch only reports for content of enabled applications */
$where .= " AND " . \IPS\Db::i()->in( 'class', array_merge( array( 'IPS\core\Messenger\Conversation', 'IPS\core\Messenger\Message' ), array_values( \IPS\Content::routedClasses( FALSE, TRUE ) ) ) );
$reportCount = \IPS\Db::i()->select(
'COUNT(*)',
'core_rc_index',
array( $where, \IPS\Db::i()->select( 'perm_id', 'core_permission_index', \IPS\Db::i()->findInSet( 'perm_view', array_merge( array( \IPS\Member::loggedIn()->member_group_id ), array_filter( explode( ',', \IPS\Member::loggedIn()->mgroup_others ) ) ) ) . " OR perm_view='*'" ) )
)->first();
if( $force === FALSE )
{
$this->reportCount = $reportCount;
}
else
{
return $reportCount;
}
}
}
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|array $member The member
* @param string $type The type (topics, messages, signatures)
* @return bool
*/
public function isIgnoring( $member, $type )
{
if( is_array( $member ) )
{
$member = \IPS\Member::load( $member['member_id'] );
}
$group = $member->group;
$id = $member->member_id;
$cannotBeIgnored = !$member->canBeIgnored();
if ( $cannotBeIgnored 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[ $id ] ) )
{
return (bool) $this->ignorePreferences[ $id ][ 'ignore_' . $type ];
}
return FALSE;
}
/**
* Build the "Create" menu
*
* @return array
*/
public function createMenu()
{
$menu = NULL;
$rebuild = FALSE;
/* Make sure that this is a valid member */
if( !$this->member_id )
{
return array();
}
if ( ! \IPS\Settings::i()->member_menu_create_key )
{
/* Generate a new key */
static::clearCreateMenu();
}
if ( \IPS\IN_DEV and !\IPS\DEV_USE_MENU_CACHE )
{
$rebuild = TRUE;
}
else if ( $this->create_menu !== NULL )
{
$menu = json_decode( $this->create_menu, TRUE );
if ( ! isset( $menu['menu_key'] ) or $menu['menu_key'] != \IPS\Settings::i()->member_menu_create_key )
{
$rebuild = TRUE;
}
}
else
{
$rebuild = TRUE;
}
if ( $rebuild === TRUE )
{
$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( array( 'menu_key' => \IPS\Settings::i()->member_menu_create_key, 'menu' => $createMenu ) );
$this->save();
return $createMenu;
}
else
{
return $menu['menu'];
}
}
/**
* 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 );
}
/**
* Should a reply made by this member be highlighted?
*
* @return boolean
*/
public function hasHighlightedReplies()
{
return (boolean) $this->group['gbw_post_highlight'];
}
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @param array|NULL $otherFields Array of additional fields to return (raw values)
* @return array
* @apiresponse int id ID number
* @apiresponse string name Username
* @apiresponse string title Member title
* @clientapiresponse string timezone Member timezone
* @apiresponse string formattedName Username with group formatting
* @apiresponse string ipAddress IP address used during registration
* @apiresponse \IPS\Member\Group primaryGroup Primary group
* @clientapiresponse [\IPS\Member\Group] secondaryGroups Secondary groups
* @clientapiresponse string email Email address
* @apiresponse datetime joined Registration date
* @clientapiresponse string registrationIpAddress IP address when registered
* @clientapiresponse int warningPoints Number of active warning points
* @apiresponse int reputationPoints Number of reputation points
* @apiresponse string photoUrl URL to photo (which will be the site's default if they haven't set one)
* @apiresponse bool photoUrlIsDefault Indicates if the value of photoUrl is the site's default
* @apiresponse string coverPhotoUrl URL to profile cover photo (will be blank if there isn't one)
* @apiresponse string profileUrl URL to profile
* @clientapiresponse bool validating Whether or not the validating flag is set on the member account
* @apiresponse int posts Number of content item submissions member has made
* @apiresponse datetime|null lastActivity Last activity date on the site
* @apiresponse datetime|null lastVisit Last distinct visit date on the site
* @apiresponse datetime|null lastPost Latest content submission date
* @apiresponse int profileViews Number of times member's profile has been viewed
* @apiresponse string birthday Member birthday in MM/DD/YYYY format (or MM/DD format if no year has been supplied)
* @apiresponse [\IPS\core\ProfileFields\Api\FieldGroup] customFields Custom profile fields. For requests using an OAuth Access Token for a particular member, only fields the authorized user can view will be included
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL, $otherFields = NULL )
{
$group = \IPS\Member\Group::load( $this->_data['member_group_id'] );
$secondaryGroups = array();
foreach ( array_filter( array_map( "intval", explode( ',', $this->_data['mgroup_others'] ) ) ) as $secondaryGroupId )
{
try
{
$secondaryGroups[] = \IPS\Member\Group::load( $secondaryGroupId )->apiOutput( $authorizedMember );
}
catch ( \OutOfRangeException $e ) { }
}
/* Figure out custom fields if any */
$fields = array();
try
{
$fieldData = \IPS\core\ProfileFields\Field::fieldData();
$fieldValues = \IPS\Db::i()->select( '*', 'core_pfields_content', array( 'member_id=?', $this->member_id ) )->first();
foreach( $fieldData as $profileFieldGroup => $profileFields )
{
$groupValues = array();
foreach( $profileFields as $field )
{
if ( !$authorizedMember or ( !$field['pf_admin_only'] and ( !$field['pf_member_hide'] or $authorizedMember->member_id == $this->member_id ) ) )
{
$groupValues[ $field['pf_id'] ] = new \IPS\core\ProfileFields\Api\Field( $this->language()->get( 'core_pfield_' . $field['pf_id'] ), $fieldValues[ 'field_' . $field['pf_id'] ] );
}
}
$fields[ $profileFieldGroup ] = ( new \IPS\core\ProfileFields\Api\FieldGroup( $this->language()->get( 'core_pfieldgroups_' . $profileFieldGroup ), $groupValues ) )->apiOutput( $authorizedMember );
}
}
catch( \UnderflowException $e ) { } # Guests will not have any profile field information.
$return = array();
$return['id'] = $this->member_id;
$return['name'] = $this->name;
$return['title'] = $this->member_title;
if ( !$authorizedMember )
{
$return['timeZone'] = $this->timezone;
}
$return['formattedName'] = $group->formatName( $this->name );
$return['primaryGroup'] = $group->apiOutput( $authorizedMember );
if ( !$authorizedMember )
{
$return['secondaryGroups'] = $secondaryGroups;
$return['email'] = $this->email;
}
$return['joined'] = $this->joined->rfc3339();
if ( !$authorizedMember )
{
$return['registrationIpAddress']= $this->ip_address;
$return['warningPoints'] = $this->warn_level;
}
$return['reputationPoints'] = $this->pp_reputation_points;
$return['photoUrl'] = static::photoUrl( $this->_data, FALSE );
$return['photoUrlIsDefault'] = static::photoUrl( $this->_data, FALSE, FALSE, FALSE ) != $return['photoUrl'];
$return['coverPhotoUrl'] = $this->pp_cover_photo ? ( (string) \IPS\File::get( 'core_Profile', $this->pp_cover_photo )->url ) : '';
$return['profileUrl'] = (string) $this->url();
if ( !$authorizedMember )
{
$return['validating'] = (bool) $this->members_bitoptions['validating'];
}
$return['posts'] = $this->member_posts;
$return['lastActivity'] = $this->last_activity ? \IPS\DateTime::ts( $this->last_activity )->rfc3339() : NULL;
$return['lastVisit'] = $this->last_visit ? \IPS\DateTime::ts( $this->last_visit )->rfc3339() : NULL;
$return['lastPost'] = $this->member_last_post ? \IPS\DateTime::ts( $this->member_last_post )->rfc3339() : NULL;
$return['profileViews'] = $this->members_profile_views;
$return['birthday'] = $this->bday_month ? ( $this->bday_month . '/' . $this->bday_day . ( $this->bday_year ? '/' . $this->bday_year : '' ) ) : NULL;
$return['customFields'] = $fields;
if ( !$authorizedMember )
{
if( $otherFields !== NULL AND is_array( $otherFields ) )
{
foreach( $otherFields as $property )
{
$return[ $property ] = $this->$property;
}
}
}
return $return;
}
/**
* Answers to security questions
*
* @return \IPS\Db\Select
*/
public function securityAnswers()
{
return \IPS\Db::i()->select( array( 'answer_question_id', 'answer_answer' ), 'core_security_answers', array( 'answer_member_id=?', $this->member_id ) )->setKeyField('answer_question_id')->setValueField('answer_answer');
}
/**
* Last used device
*
* @return \IPS\Member\Device|NULL
*/
public function lastUsedDevice()
{
try
{
return \IPS\Member\Device::constructFromData( \IPS\Db::i()->select( '*', 'core_members_known_devices', array( 'member_id=?', $this->member_id ), 'last_seen DESC', 1 )->first() );
}
catch ( \Exception $e )
{
return NULL;
}
}
/**
* Last used IP address
*
* @return \IPS\Member\Device|NULL
*/
public function lastUsedIp()
{
try
{
return \IPS\Db::i()->select( 'ip_address', 'core_members_known_ip_addresses', array( 'member_id=?', $this->member_id ), 'last_seen DESC', 1 )->first();
}
catch ( \Exception $e )
{
return NULL;
}
}
/**
* Device count
*
* @return int
*/
public function deviceCount()
{
return \IPS\Db::i()->select( 'COUNT(*)', 'core_members_known_devices', array( 'member_id=?', $this->member_id ) )->first();
}
/**
* Check if account is locked - returns FALSE if account is unlocked, an \IPS\DateTime object if the account is locked until a certain time, or TRUE if account is unlocked indefinitely
*
* @param \IPS\Member $member The account
* @return \IPS\DateTime|bool
*/
public function unlockTime()
{
if ( \IPS\Settings::i()->ipb_bruteforce_attempts and isset( $this->failed_logins[ \IPS\Request::i()->ipAddress() ] ) and count( $this->failed_logins[ \IPS\Request::i()->ipAddress() ] ) >= \IPS\Settings::i()->ipb_bruteforce_attempts )
{
if ( \IPS\Settings::i()->ipb_bruteforce_period and \IPS\Settings::i()->ipb_bruteforce_unlock )
{
$failedLogins = $this->failed_logins[ \IPS\Request::i()->ipAddress() ];
sort( $failedLogins );
while ( count( $failedLogins ) > \IPS\Settings::i()->ipb_bruteforce_attempts )
{
array_pop( $failedLogins );
}
$unlockTime = \IPS\DateTime::ts( array_pop( $failedLogins ) );
$unlockTime = $unlockTime->add( new \DateInterval( 'PT' . \IPS\Settings::i()->ipb_bruteforce_period . 'M' ) );
/* If Unlock Time is in the past, return FALSE to avoid the exception and allow login */
if ( $unlockTime->getTimestamp() < time() )
{
return FALSE;
}
/* Otherwise that is what we're returning */
return $unlockTime;
}
return TRUE;
}
return FALSE;
}
/* !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! */
$appKey = is_string( $app ) ? $app : $app->directory;
if ( array_key_exists( $appKey, $restrictions['applications'] ) )
{
if ( $module === NULL )
{
return TRUE;
}
else
{
$moduleKey = ( $module === NULL or is_string( $module ) ) ? $module : $module->key;
if ( in_array( $moduleKey, $restrictions['applications'][ $appKey ] ) )
{
if ( $key === NULL )
{
return TRUE;
}
elseif ( isset( $restrictions['items'][ $appKey ][ $moduleKey ] ) and in_array( $key, $restrictions['items'][ $appKey ][ $moduleKey ] ) )
{
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;
}
/* !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 ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->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();
}
/**
* Removes reputation for this member
*
* @param string $type given|received The type of reputation to remove
* @return void
*/
public function removeReputation( $type )
{
$where = ( $type == 'given' ) ? array( 'member_id=?', $this->member_id ) : array( 'member_received=?', $this->member_id );
\IPS\Db::i()->delete( 'core_reputation_index', $where );
if( $type == 'received' )
{
$this->recountReputation();
}
else
{
\IPS\Task::queue( 'core', 'RecountMemberReputation', array(), 4 );
}
}
/* !Do Stuff */
/**
* 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 ) );
}
/**
* @brief Store whitelist filters
*/
public static $whitelistFilters = NULL;
/**
* Check Spam Defense Whitelist
*
* @param string|NULL $emailAddress Email address to check, NULL for existing email address
* @return boolean
*/
public function spamDefenseWhitelist( $emailAddress=NULL )
{
$email = $emailAddress ?: $this->email;
if( static::$whitelistFilters === NULL )
{
static::$whitelistFilters = iterator_to_array( \IPS\Db::i()->select( 'whitelist_type, whitelist_content', 'core_spam_whitelist' ) );
}
foreach( static::$whitelistFilters as $whitelist )
{
if( $whitelist['whitelist_type'] == 'ip' )
{
if( preg_match( '/^' . str_replace( '\*', '.*', preg_quote( $whitelist['whitelist_content'], '/' ) ) . '$/i', $this->ip_address ) )
{
return TRUE;
}
}
else
{
if( preg_match( '/' . str_replace( '\*', '.*', preg_quote( '@' . $whitelist['whitelist_content'], '/' ) ) . '$/i', $email ) )
{
return TRUE;
}
}
}
return FALSE;
}
/**
* IPS Spam Defense Service
*
* @param string $type Request type
* @param string $emailAddress Email address to check, NULL for existing email address
* @param int $spamCode If set, will modify by reference with the raw value from the spam service
* @return int|NULL Action code based on spam service response, or NULL for no action
*/
public function spamService( $type='register', $emailAddress=NULL, &$spamCode=NULL )
{
$email = $emailAddress ?: $this->email;
/* Check Whitelist */
if( $this->spamDefenseWhitelist( $email ) )
{
/* Account details were whitelisted */
return NULL;
}
try
{
$response = \IPS\Http\Url::ips( 'spam/' . $type )->request()->login( \IPS\Settings::i()->ipb_reg_number, '' )->post( array(
'email' => $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' => $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 ) )
{
try
{
call_user_func_array( array($class, $method), array_merge( array($this), $params ) );
}
/* 4.3 backwards compatibility for 4.2 applications */
catch( \ArgumentCountError $e )
{
if ( \IPS\IN_DEV )
{
throw $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 ) );
/* Log */
$this->logHistory( 'core', 'account', array( 'type' => 'merge', 'id' => $otherMember->member_id, 'name' => $otherMember->name, 'email' => $otherMember->email ) );
/* Let apps do their stuff */
$this->memberSync( 'onMerge', array( $otherMember ) );
}
/**
* 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 ( is_array( $visitors ) AND 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 )
{
if ( $this->group['g_ppd_unit'] )
{
/* The limit may have been removed due to number of total posts or days since joining */
if( $this->group['gbw_ppd_unit_type'] )
{
/* Days */
if ( $this->joined->add( new \DateInterval( "P{$this->group['g_ppd_unit']}D" ) )->getTimestamp() < time() )
{
return TRUE;
}
}
else
{
/* Posts */
if ( $this->member_posts >= $this->group['g_ppd_unit'] )
{
return TRUE;
}
}
}
return FALSE;
}
/* Still here? */
return TRUE;
}
/**
* Check Group Promotion
*
* @return void
*/
public function checkGroupPromotion()
{
/* If we should ignore promotions for this member, do so */
if( $this->members_bitoptions['ignore_promotions'] )
{
return;
}
/* Just check the primary group, secondary groups should not prevent promoting */
if( \IPS\Member\Group::load( $this->member_group_id )->g_promote_exclude )
{
return;
}
$ruleToUse = NULL;
/* Loop over all group promotion rules and get the last one that matches us */
foreach( \IPS\Member\GroupPromotion::roots() as $rule )
{
if( $rule->enabled and $rule->matches( $this ) )
{
$ruleToUse = $rule->id;
}
}
/* If there's no rule, return now */
if( $ruleToUse === NULL )
{
return;
}
/* If we matched a rule, get that rule now */
$ruleToUse = \IPS\Member\GroupPromotion::load( $ruleToUse );
/* Set the primary and secondary groups as appropriate */
$action = $ruleToUse->_actions;
if( $action['primary_group'] AND $this->member_group_id != $action['primary_group'] )
{
try
{
$group = \IPS\Member\Group::load( $action['primary_group'] );
$this->member_group_id = $action['primary_group'];
$this->logHistory( 'core', 'group', array( 'type' => 'primary', 'by' => 'promotion', 'id' => $ruleToUse->id, 'old' => $this->member_group_id, 'new' => $action['primary_group'] ) );
/* Reset profile completion flag to see if there are any items to complete now permissions are elevated */
$this->members_bitoptions['profile_completed'] = 0;
}
catch ( \OutOfRangeException $e )
{
\IPS\Log::debug( 'Promotion ' . $ruleToUse->id . ' tried to set not existing member group' );
}
}
if( count( $action['secondary_group'] ) OR count( $action['secondary_remove'] ) )
{
$secondaryGroups = array_filter( explode( ',', $this->_data['mgroup_others'] ) );
$oldSecondaryGroups = $secondaryGroups;
foreach ( $action['secondary_group'] as $key => $group )
{
try
{
$group = \IPS\Member\Group::load( $group );
}
catch ( \OutOfRangeException $e )
{
unset( $action['secondary_group'][$key] );
\IPS\Log::debug( 'Promotion ' . $ruleToUse->id . ' tried to set not existing member group' );
}
}
if( count( $action['secondary_group'] ) )
{
if( array_diff( $action['secondary_group'], $secondaryGroups ) )
{
$secondaryGroups = array_merge( $secondaryGroups, $action['secondary_group'] );
}
}
if( count( $action['secondary_remove'] ) )
{
foreach( $action['secondary_remove'] as $groupToRemove )
{
while( in_array( $groupToRemove, $secondaryGroups ) )
{
$key = array_search( $groupToRemove, $secondaryGroups );
unset( $secondaryGroups[ $key ] );
}
}
}
if ( array_diff( $secondaryGroups, $oldSecondaryGroups ) or array_diff( $oldSecondaryGroups, $secondaryGroups ) )
{
$this->logHistory( 'core', 'group', array( 'type' => 'secondary', 'by' => 'promotion', 'id' => $ruleToUse->id, 'old' => $oldSecondaryGroups, 'new' => $secondaryGroups ) );
}
$this->mgroup_others = implode( ',', array_unique( $secondaryGroups ) );
/* Reset profile completion flag to see if there are any items to complete now permissions are elevated */
$this->members_bitoptions['profile_completed'] = 0;
}
}
/**
* Is the current user allowed to use the contact us form
*
* @return bool
*/
public function canUseContactUs()
{
try
{
$module = \IPS\Application\Module::get( 'core', 'contact', 'front' );
}
catch ( \OutOfRangeException $e )
{
return FALSE;
}
if ( !$this->canAccessModule( $module ) )
{
return FALSE;
}
/* If all groups have access, we can */
if( \IPS\Settings::i()->contact_access != '*' )
{
/* Check member */
$memberGroups = array_merge( array( $this->member_group_id ), array_filter( explode( ',', $this->mgroup_others ) ) );
$accessGroups = explode( ',', \IPS\Settings::i()->contact_access );
/* Are we in an allowed group? */
if( count( array_intersect( $accessGroups, $memberGroups ) ) )
{
return TRUE;
}
else
{
return FALSE;
}
}
return TRUE;
}
/**
* Perform a database update on all members
*
* Typically, updating the entire table locks the table which makes other queries stack up.
* On busy sites this is a real problem. We mitigate this by updating in batches via a background task
*
* We replace if the fields we want to update are the exact same on a subsequent call to this method, so if you had ( 'skin' => 2 ) and then ( 'skin' => 3 ),
* the row matching 'skin' => 2 will be removed from the queue table and the 'skin' => 3 row will replace it. This is to ensure that if you update the same fields while
* an existing task is running, it will update the members with the latest data.
*
* @note This task can be processing or waiting to process and the member can still change the value, so it could be possible for the member to set a 'skin' parameter and then have this
* overwritten when this task processes.
*
* @param array $update array( field => value ) pairs to be used directly in a \IPS\Db::i()->update( 'core_members', $update ) query
* @param int $severity Severity level. 1 being highest, 5 lowest
* @return void
*/
public static function updateAllMembers( $update, $severity=3 )
{
\IPS\Task::queue( 'core', 'UpdateMembers', array( 'update' => $update ), $severity, array( 'update' ) );
}
/**
* Invalidate the current "create menu" key
*
* @return void
*/
public static function clearCreateMenu()
{
\IPS\Settings::i()->changeValues( array( 'member_menu_create_key' => mt_rand() ) );
}
/**
* Invalidate all sessions and auto-login-keys. Called after the user changes their email address or password.
*
* @param bool|string $frontEndSessions Boolean value indicating if front-end sessions should be cleared, or a string containing a session ID to wipe all except that one
* @param bool|string $acpSessions Boolean value indicating if acp sessions should be cleared, or a string containing a session ID to wipe all except that one
* @param bool $loginKeys Boolean value indicating if login keys (used for "Remember Me" logins) should be wiped
* @return void
*/
public function invalidateSessionsAndLogins( $frontEndSessions=TRUE, $acpSessions=TRUE, $loginKeys=TRUE )
{
/* Terminate any active sessions */
if ( $frontEndSessions !== FALSE )
{
\IPS\Session\Store::i()->deleteByMember( $this->member_id, NULL, $frontEndSessions );
}
if ( $acpSessions !== FALSE )
{
$where = array( array( 'session_member_id=?', $this->member_id ) );
if ( is_string( $acpSessions ) )
{
$where[] = array( 'session_id<>?', $acpSessions );
}
\IPS\Db::i()->delete( 'core_sys_cp_sessions', $where );
}
/* Wipe login keys to stop "Remember Me" cookies automatically logging us in */
if ( $loginKeys )
{
\IPS\Db::i()->update( 'core_members_known_devices', array( 'login_key' => NULL ), array( 'member_id=?', $this->member_id ) );
}
/* Invalidate any pending "Forgot Password" or 2FA recovery emails, because they provide a doorway into accessing the account */
\IPS\Db::i()->delete( 'core_validating', array( 'member_id=? AND ( lost_pass=1 OR forgot_security=1 )', $this->member_id ) );
}
/* !Registration/Validation */
/**
* Call after completed registration to send email for validation if required or flag for admin validation
*
* @param bool $noEmailValidationRequired If the user's email is implicitly trusted (for example, provided by a third party), set this to TRUE to bypass email validation
* @param bool $doNotDelete If TRUE, the account will not be deleted in the normal cleanup of unvalidated accounts. Used for accounts created in Commerce checkout.
* @return void
*/
public function postRegistration( $noEmailValidationRequired = FALSE, $doNotDelete = FALSE )
{
/* Work out validation type */
$validationType = \IPS\Settings::i()->reg_auth_type;
if ( $noEmailValidationRequired )
{
switch ( $validationType )
{
case 'user':
$validationType = 'none';
break;
case 'admin_user':
$validationType = 'admin';
break;
}
}
/* Validation */
if ( $validationType != 'none' )
{
/* Set the flag */
$this->members_bitoptions['validating'] = TRUE;
$this->save();
/* Prevent duplicates from double clicking, etc */
\IPS\Db::i()->delete( 'core_validating', array( 'member_id=? and new_reg=1', $this->member_id ) );
/* Insert a record */
$vid = md5( $this->members_pass_hash . \IPS\Login::generateRandomString() );
\IPS\Db::i()->insert( 'core_validating', 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' => ( $validationType == 'admin' ) ?: FALSE,
'email_sent' => ( $validationType != 'admin' ) ? time() : NULL,
'do_not_delete' => $doNotDelete
) );
/* Send email for validation */
if ( $validationType != 'admin' )
{
\IPS\Email::buildFromTemplate( 'core', 'registration_validate', array( $this, $vid ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $this );
}
}
/* If no email-related validation is required, notify the incoming mail address */
if( $validationType == 'none' or $validationType == 'admin' )
{
$this->_registrationNotifyAdmin();
}
/* Send a welcome email if validation is disabled */
if( $validationType == 'none' )
{
$this->_sendWelcomeEmail();
}
}
/**
* Send email to admin telling them the user has registered
* If no validation, or admin-only validation is enabled: this is called immediately after registration
* If email-only or email-and-admin validation is enabled: this is called after the user has validated their email address
* i.e. in all cases, it is when the admin needs to approve the account, or, if no admin validation is enabled, when the account is ready to use
*
* @return void
*/
protected function _registrationNotifyAdmin()
{
if( \IPS\Settings::i()->new_reg_notify )
{
try
{
$values = \IPS\Db::i()->select( '*', 'core_pfields_content', array( 'member_id=?', $this->member_id ) )->first();
}
catch ( \UnderflowException $e )
{
$values = array();
}
$profileFields = array();
foreach ( \IPS\core\ProfileFields\Field::fields( $values, \IPS\core\ProfileFields\Field::REG ) as $group => $fields )
{
foreach ( $fields as $id => $field )
{
if ( $field instanceof \IPS\Helpers\Form\Address )
{
$profileFields[ "field_{$id}" ] = (string) $field->value;
}
else
{
$profileFields[ "field_{$id}" ] = $field::stringValue( $field->value );
}
}
}
\IPS\Email::buildFromTemplate( 'core', 'registration_notify', array( $this, $profileFields ), \IPS\Email::TYPE_LIST )->send( \IPS\Settings::i()->email_in );
}
}
/**
* Email Validation Confirmed
*
* @param array $record validating record
* @return void
*/
public function emailValidationConfirmed( $record )
{
/* Notify the admin only if they have not been notified before */
if( !$record['user_verified'] )
{
$this->_registrationNotifyAdmin();
}
/* Log */
$this->logHistory( 'core', 'account', array( 'type' => 'email_validated' ) );
/* If admin validation is required, set the flag */
if ( \IPS\Settings::i()->reg_auth_type == 'admin_user' )
{
\IPS\Db::i()->update( 'core_validating', array( 'user_verified' => TRUE ), array( 'member_id=?', $this->member_id ) );
}
/* Otherwise, validation is complete */
else
{
$this->validationComplete();
}
}
/**
* Final Validation Complete
* If no validation is enabled: this is never called
* If email-only validation is enabled: this is called after the user has validated their email address or if the admin manually validates the account
* If admin (including email and admin) validation is enabled: this is called after the admin has validated the account
*
* @return void
*/
public function validationComplete()
{
/* Send a success email */
$this->_sendWelcomeEmail();
/* Delete rows */
\IPS\Db::i()->delete( 'core_validating', array( 'member_id=?', $this->member_id ) );
/* Reset the flag */
$this->members_bitoptions['validating'] = FALSE;
$this->save();
/* Sync */
$this->memberSync( 'onValidate' );
}
/**
* Send a welcome email
* Called if no validation is enabled or after validation is complete
*
* @return void
*/
protected function _sendWelcomeEmail()
{
try
{
\IPS\Email::buildFromTemplate( 'core', 'registration_complete', array( $this ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $this );
}
catch( \ErrorException $e ) { }
}
/**
* Get how often the member changed his name
*
* @return bool
*/
public function hasNameChanges()
{
try
{
return \IPS\Db::i()->select( 'count(*)', 'core_member_history', array( 'log_member=? AND log_app=? AND log_type=?', $this->member_id, 'core', 'display_name' ) )->first();
}
catch ( \UnderflowException $e )
{
return FALSE;
}
}
/**
* Profile Sync
*
* @return array
*/
public function profileSync()
{
$profileSync = $this->profilesync;
if ( is_array( $profileSync ) )
{
foreach ( $profileSync as $k => $v )
{
if ( $k === 'status' )
{
foreach ( $v as $methodId => $data )
{
try
{
$method = \IPS\Login\Handler::load( $methodId );
}
catch ( \OutOfRangeException $e )
{
unset( $profileSync['status'][ $methodId ] );
continue;
}
try
{
$profileSync['status'][ $methodId ]['error'] = NULL;
foreach ( $method->userStatuses( $this, $profileSync['status'][ $methodId ]['lastsynced'] ? \IPS\DateTime::ts( $profileSync['status'][ $methodId ]['lastsynced'] ) : NULL ) as $status )
{
$status->member_id = $this->member_id;
$status->imported = TRUE;
$status->save();
\IPS\Content\Search\Index::i()->index( $status );
}
$profileSync['status'][ $methodId ]['lastsynced'] = time();
}
catch ( \IPS\Login\Exception $e )
{
unset( $profileSync['status'][ $methodId ] );
}
catch ( \DomainException $e )
{
$profileSync['status'][ $methodId ]['error'] = $e->getMessage();
}
catch ( \Exception $e )
{
\IPS\Log::log( $e, 'profilesync' );
$profileSync['status'][ $methodId ]['error'] = 'profilesync_generic_error';
}
}
}
else
{
try
{
$method = \IPS\Login\Handler::load( $v['handler'] );
}
catch ( \OutOfRangeException $e )
{
unset( $profileSync[ $k ] );
continue;
}
try
{
$profileSync[ $k ]['error'] = NULL;
switch ( $k )
{
case 'email':
$email = $method->userEmail( $this );
if ( $email and $email != $this->email )
{
if ( $error = \IPS\Login::emailIsInUse( $email, $this ) )
{
throw new \DomainException('member_email_exists');
}
foreach ( \IPS\Db::i()->select( 'ban_content', 'core_banfilters', array( "ban_type=?", 'email' ) ) as $bannedEmail )
{
if ( preg_match( '/^' . str_replace( '\*', '.*', preg_quote( $bannedEmail, '/' ) ) . '$/i', $this->value ) )
{
throw new \DomainException( 'form_email_banned' );
}
}
if ( \IPS\Settings::i()->allowed_reg_email !== '' AND $allowedEmailDomains = explode( ',', \IPS\Settings::i()->allowed_reg_email ) )
{
$matched = FALSE;
foreach ( $allowedEmailDomains AS $domain )
{
if( \mb_stripos( $this->value, "@" . $domain ) !== FALSE )
{
$matched = TRUE;
}
}
if ( count( $allowedEmailDomains ) AND !$matched )
{
throw new \DomainException( 'form_email_banned' );
}
}
$this->logHistory( 'core', 'email_change', array( 'old' => $this->email, 'new' => $email, 'by' => 'profilesync', 'id' => $method->id, 'service' => $method::getTitle() ) );
$this->email = $email;
}
break;
case 'name':
$name = $method->userProfileName( $this );
if ( $name != $this->name )
{
if ( mb_strlen( $name ) < \IPS\Settings::i()->min_user_name_length )
{
throw new \DomainException('form_minlength_unspecific');
}
if ( mb_strlen( $name ) > \IPS\Settings::i()->max_user_name_length )
{
throw new \DomainException('form_minlength_unspecific');
}
if ( \IPS\Settings::i()->username_characters )
{
if ( !preg_match( '/^[' . str_replace( '\-', '-', preg_quote( \IPS\Settings::i()->username_characters, '/' ) ) . ']*$/iu', $name ) )
{
throw new \DomainException('form_name_banned');
}
}
if ( \IPS\Login::usernameIsInUse( $name, $this ) )
{
throw new \DomainException('member_name_exists');
}
foreach( \IPS\Db::i()->select( 'ban_content', 'core_banfilters', array("ban_type=?", 'name') ) as $bannedName )
{
if( preg_match( '/^' . str_replace( '\*', '.*', preg_quote( $bannedName, '/' ) ) . '$/i', $this->value ) )
{
throw new \DomainException( 'form_name_banned' );
}
}
$this->logHistory( 'core', 'display_name', array( 'old' => $this->name, 'new' => $name, 'by' => 'profilesync', 'id' => $method->id, 'service' => $method::getTitle() ) );
$this->name = $name;
}
break;
case 'photo':
$photoUrl = $method->userProfilePhoto( $this );
if ( (string) $photoUrl )
{
$contents = $photoUrl->request()->get();
$md5 = md5( $contents );
if ( $contents AND ( !isset( $v['ref'] ) or $md5 != $v['ref'] ) )
{
$photoVars = explode( ':', $this->group['g_photo_max_vars'] );
try
{
$image = \IPS\Image::create( $contents );
}
catch( \Exception $e )
{
throw new \DomainException('member_photo_bad_url');
}
if( $image->isAnimatedGif and !$this->group['g_upload_animated_photos'] )
{
throw new \DomainException('member_photo_upload_no_animated');
}
if ( $image->width > $photoVars[1] or $image->height > $photoVars[2] )
{
$image->resizeToMax( $photoVars[1], $photoVars[2] );
}
if ( $photoVars[0] and \strlen( $image ) > ( $photoVars[0] * 1024 ) )
{
throw new \DomainException('upload_too_big_unspecific');
}
$newFile = \IPS\File::create( 'core_Profile', 'imported-photo-' . $this->member_id . '.' . $image->type, (string) $image );
$this->pp_photo_type = 'custom';
$this->pp_main_photo = NULL;
$this->pp_main_photo = (string) $newFile;
$thumbnail = $newFile->thumbnail( 'core_Profile', \IPS\PHOTO_THUMBNAIL_SIZE, \IPS\PHOTO_THUMBNAIL_SIZE, TRUE );
$this->pp_thumb_photo = (string) $thumbnail;
if ( isset( $v['ref'] ) )
{
$this->photo_last_update = time();
}
$this->logHistory( 'core', 'photo', array( 'action' => 'new', 'type' => 'profilesync', 'id' => $method->id, 'service' => $method::getTitle() ) );
$profileSync['photo']['ref'] = $md5;
}
}
break;
case 'cover':
$coverPhotoUrl = $method->userCoverPhoto( $this );
if ( (string) $coverPhotoUrl )
{
$contents = $coverPhotoUrl->request()->get();
$md5 = md5( $contents );
if ( !isset( $v['ref'] ) or $md5 != $v['ref'] )
{
try
{
$image = \IPS\Image::create( $contents );
}
catch( \Exception $e )
{
throw new \DomainException('member_photo_bad_url');
}
if ( $this->group['g_max_bgimg_upload'] != -1 and \strlen( $image ) > ( $this->group['g_max_bgimg_upload'] * 1024 ) )
{
throw new \DomainException('upload_too_big_unspecific');
}
$newFile = \IPS\File::create( 'core_Profile', 'imported-cover-photo-' . $this->member_id . '.' . $image->type, (string) $image );
if ( $this->pp_cover_photo )
{
try
{
\IPS\File::get( 'core_Profile', $this->pp_cover_photo )->delete();
}
catch ( \Exception $e ) { }
}
$this->pp_cover_photo = (string) $newFile;
$this->logHistory( 'core', 'photo', array( 'action' => 'new', 'type' => 'profilesync', 'id' => $method->id, 'service' => $method::getTitle() ) );
$profileSync['cover']['ref'] = $md5;
}
}
break;
}
}
catch ( \IPS\Login\Exception $e )
{
unset( $profileSync[ $k ] );
}
catch ( \DomainException $e )
{
$profileSync[ $k ]['error'] = $e->getMessage();
}
catch ( \Exception $e )
{
\IPS\Log::log( $e, 'profilesync' );
$profileSync[ $k ]['error'] = 'profilesync_generic_error';
}
}
}
}
$this->profilesync = $profileSync;
$this->profilesync_lastsync = time();
$this->save();
}
/**
* Can this member be ignored?
*
* @return bool
*/
public function canBeIgnored()
{
if ( !\IPS\Settings::i()->ignore_system_on )
{
return FALSE;
}
if( !$this->member_id )
{
return FALSE;
}
if ( $this->modPermission() AND !$this->modPermission( 'can_moderator_be_ignored' ) )
{
return FALSE;
}
if ( $this->group['gbw_cannot_be_ignored'] )
{
return FALSE;
}
return TRUE;
}
/**
* Log Member Action
*
* @param mixed $app The application action applies to
* @param string $type Log type
* @param mixed $extra Any extra data for the type
* @param mixed $by The member performing the action. NULL for currently logged in member or FALSE for no member
*
* @return void
*/
public function logHistory( $app, $type, $extra=NULL, $by=NULL )
{
if ( $this->member_id )
{
if ( $by === NULL and \IPS\Dispatcher::hasInstance() )
{
$by = \IPS\Session::i()->member; // Not \IPS\Member::loggedIn() because if this is an admin logged in as a member, we want to log that the action was done by the admin
}
\IPS\Db::i()->insert( 'core_member_history', array(
'log_app' => $app,
'log_member' => (int) $this->member_id,
'log_by' => $by ? $by->member_id : NULL,
'log_type' => $type,
'log_data' => json_encode( $extra ),
'log_date' => microtime( TRUE ),
'log_ip_address' => \IPS\Request::i()->ipAddress()
) );
}
}
/* !Top Members */
const TOP_MEMBERS_OVERVIEW = 1;
const TOP_MEMBERS_FILTERS = 2;
/**
* @brief Custom count for a top member resu;t
*/
public $_customCount = NULL;
/**
* Get available Top Members options
*
* @param int $filter See TOP_MEMBERS_* constants
* @return array
*/
public static function topMembersOptions( $filter = 0 )
{
$filters = array(
'pp_reputation_points' => \IPS\Member::loggedIn()->language()->addToStack('leaderboard_tab_reputation'),
'member_posts' => \IPS\Member::loggedIn()->language()->addToStack('leaderboard_tab_posts')
);
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', TRUE ) as $object )
{
foreach( $object->classes as $item )
{
$commentClass = NULL;
if ( isset( $item::$commentClass ) )
{
$commentClass = $item::$commentClass;
}
if ( $item::$firstCommentRequired and isset( $commentClass::$databaseColumnMap['author'] ) )
{
$filters[ $commentClass ] = \IPS\Member::loggedIn()->language()->addToStack('leaderboard_tab_x', NULL, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack("{$commentClass::$title}_pl_lc") ) ) );
}
elseif ( isset( $item::$databaseColumnMap['author'] ) )
{
$filters[ $item ] = \IPS\Member::loggedIn()->language()->addToStack('leaderboard_tab_x', NULL, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack("{$item::$title}_pl_lc") ) ) );
}
}
}
if ( $filter )
{
$available = $filter === static::TOP_MEMBERS_OVERVIEW ? \IPS\Settings::i()->reputation_top_members_overview : \IPS\Settings::i()->reputation_top_members_filters;
if ( $available != '*' )
{
$available = explode( ',', $available );
$filters = array_filter( $filters, function( $k ) use ( $available ) {
return in_array( $k, $available );
}, ARRAY_FILTER_USE_KEY );
}
}
return $filters;
}
/**
* Get top members for a particular type
*
* @param string $type The type (as returned by topMembersOptions())
* @param int $limit Number to get
* @return \Traversable
*/
public static function topMembers( $type, $limit )
{
if ( in_array( $type, array( 'pp_reputation_points', 'member_posts' ) ) )
{
$where = array(
array( "name IS NOT NULL" ),
array( "name != ''" ),
array( "temp_ban != -1" ),
array( "email != ''" ),
array( $type . '>0' ),
\IPS\Db::i()->in( 'member_group_id', \IPS\Settings::i()->leaderboard_excluded_groups, TRUE )
);
$orderBy = $type . ' DESC';
return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', $where, $orderBy, $limit ), 'IPS\Member' );
}
else
{
$storeKey = 'store_' . str_replace( '\\', '-', $type );
$stored = isset( \IPS\Data\Store::i()->$storeKey ) ? \IPS\Data\Store::i()->$storeKey : NULL;
if ( ! $stored or ( ( time() - $stored['time'] ) > 300 ) )
{
$contentWhere = array( array( $type::$databasePrefix . $type::$databaseColumnMap['author'] . '<>?', 0 ) );
if ( isset( $type::$databaseColumnMap['hidden'] ) )
{
$contentWhere[] = array( $type::$databasePrefix . $type::$databaseColumnMap['hidden'] . '=0' );
}
else if ( isset( $type::$databaseColumnMap['approved'] ) )
{
$contentWhere[] = array( $type::$databasePrefix . $type::$databaseColumnMap['approved'] . '=1' );
}
$authorField = $type::$databasePrefix . $type::$databaseColumnMap['author'];
$members = array();
foreach( \IPS\Db::i()->select( 'COUNT(*) as count, ' . $type::$databaseTable . '.' . $authorField, $type::$databaseTable, $contentWhere, 'count DESC', $limit, $authorField ) as $row )
{
$members[ $row[ $authorField ] ] = $row['count'];
}
/* Make sure these members actually exist */
$memberIds = array();
/* Make sure they're not in an excluded group */
$memberWhere = array();
$memberWhere[] = \IPS\Db::i()->in( 'member_id', array_keys( $members ) );
$memberWhere[] = \IPS\Db::i()->in( 'member_group_id', \IPS\Settings::i()->leaderboard_excluded_groups, TRUE );
foreach( \IPS\Db::i()->select( 'member_id', 'core_members', $memberWhere ) AS $member_id )
{
$memberIds[ $member_id ] = $members[ $member_id ];
}
\IPS\Data\Store::i()->$storeKey = array( 'time' => time(), 'memberIds' => $memberIds );
$stored = \IPS\Data\Store::i()->$storeKey;
}
$results = array();
foreach ( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', \IPS\Db::i()->in( 'member_id', array_keys( $stored['memberIds'] ) ), NULL, $limit ), 'IPS\Member' ) as $member )
{
$member->_customCount = $stored['memberIds'][ $member->member_id ];
$results[ $member->member_id ] = $member;
}
/* Sort by custom value DESC */
uasort( $results, function( $a, $b ) {
return ( $a->_customCount == $b->_customCount ) ? 0 : ( ( $a->_customCount < $b->_customCount ) ? 1 : -1 );
});
return $results;
}
}
/* !Profile Completion */
/**
* @brief Profile Completion Cache
*/
public $_profileCompletion = NULL;
/**
* Returns suggested profile items
*
* @return array
*/
public function profileCompletion()
{
if ( $this->_profileCompletion === NULL )
{
$this->_profileCompletion = array();
$this->_profileCompletion['required'] = array();
$this->_profileCompletion['suggested'] = array();
if ( $this->member_id AND !$this->members_bitoptions['profile_completed'] )
{
foreach( Member\ProfileStep::loadAll() AS $id => $step )
{
$this->_profileCompletion[ ( $step->required ) ? 'required' : 'suggested' ][ $step->id ] = $step->completed( $this );
}
}
}
return $this->_profileCompletion;
}
/**
* Profile Completion Percentage
*
* @return \IPS\Math\Number
*/
public function profileCompletionPercentage()
{
if ( $this->members_bitoptions['profile_completed'] )
{
return new \IPS\Math\Number( '100' );
}
$total = 0;
$done = 0;
/* Check Required Ones */
$completion = $this->profileCompletion();
foreach( $completion['required'] AS $key => $value )
{
$total++;
if ( $value )
{
$done++;
}
}
/* Now Suggested */
foreach( $completion['suggested'] AS $key => $value )
{
$total++;
if ( $value )
{
$done++;
}
}
/* Are we actually done? */
if ( $done === $total AND $this->members_bitoptions['profile_completed'] === FALSE )
{
$this->members_bitoptions['profile_completed'] = TRUE;
$this->save();
}
if ( $this->members_bitoptions['profile_completed'] )
{
return new \IPS\Math\Number( '100' );
}
elseif( !$total )
{
return new \IPS\Math\Number( '100' );
}
else
{
return new \IPS\Math\Number( (string) round( 100 / $total * $done ) );
}
}
/**
* Next Profile Step
*
* @return \IPS\Member\ProfileStep|NULL
*/
public function nextProfileStep()
{
if ( $this->members_bitoptions['profile_completed'] )
{
return NULL;
}
$completed = $this->profileCompletion();
if ( !count( $completed['suggested'] ) )
{
return NULL;
}
$nextStep = NULL;
foreach( $completed['suggested'] AS $id => $complete )
{
if ( !$complete )
{
$nextStep = \IPS\Member\ProfileStep::load( $id );
break;
}
}
return $nextStep;
}
/**
* Determine if the member can use signatures
*
* @return bool
*/
public function canEditSignature()
{
/* If signatures are globally disabled, we can't edit them */
if( !\IPS\Settings::i()->signatures_enabled )
{
return FALSE;
}
/* Are they enabled for our group? */
$sigLimits = explode( ":", $this->group['g_signature_limits'] );
if( $sigLimits[0] )
{
return FALSE;
}
/* Are there post count or day restrictions? */
if( $this->group['g_sig_unit'] )
{
if( $this->group['gbw_sig_unit_type'] )
{
if ( $this->joined->diff( \IPS\DateTime::create() )->days < $this->group['g_sig_unit'] )
{
return FALSE;
}
}
else
{
if ( $this->member_posts < $this->group['g_sig_unit'] )
{
return FALSE;
}
}
}
return TRUE;
}
/**
* Produce a random hex color for a background
*
* @return string
*/
public function coverPhotoBackgroundColor()
{
return $this->staticCoverPhotoBackgroundColor( $this->name );
}
/**
* returns the recent profile visitors
*
* @return array
*/
public function get_profileVisitors()
{
$visitors = array();
$visitorData = array();
$visitorInfo = json_decode( $this->pp_last_visitors, TRUE );
if ( !is_array( $visitorInfo ) )
{
$visitorInfo = array();
}
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', array( \IPS\Db::i()->in( 'member_id', array_keys( array_reverse( $visitorInfo, TRUE ) ) ) ) ), 'IPS\Member' ) AS $row )
{
$visitorData[$row->member_id] = $row;
}
foreach( array_reverse( $visitorInfo, TRUE ) as $id => $time )
{
if ( isset( $visitorData[$id] ) )
{
$visitors[$id]['member'] = $visitorData[$id];
$visitors[$id]['visit_time'] = $time;
}
}
return $visitors;
}
}