Invision Power Services, Inc. * @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', 'fb_uid', 'live_id', 'google_id', 'linkedin_id', 'ipsconnect_id', 'twitter_id' ); /** * @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 ) ) ); /* !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 and isset( \IPS\Request::i()->cookie['ipsTimezone'] ) ) { static::$loggedInMember->timezone = \IPS\Request::i()->cookie['ipsTimezone']; } } return static::$loggedInMember; } /** * Load Record * We override it so we return a guest object for a non-existant member * * @see \IPS\Db::build * @param int|string $id ID * @param string $idField The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId) * @param mixed $extraWhereClause Additional where clause (see \IPS\Db::build for details) * @return static */ public static function load( $id, $idField=NULL, $extraWhereClause=NULL ) { try { if( $id === NULL OR $id === 0 OR $id === '' ) { $classname = get_called_class(); return new $classname; } else { $member = parent::load( $id, $idField, $extraWhereClause ); if ( $member->restrict_post > 0 and $member->restrict_post <= time() ) { $member->restrict_post = 0; $member->save(); } return $member; } } catch ( \OutOfRangeException $e ) { $classname = get_called_class(); return new $classname; } } /** * Load record based on a URL * * @param \IPS\Http\Url $url URL to load from * @return static * @throws \InvalidArgumentException * @throws \OutOfRangeException */ public static function loadFromUrl( \IPS\Http\Url $url ) { $member = parent::loadFromUrl( $url ); if ( !$member->member_id ) { throw new \OutOfRangeException; } return $member; } /** * Set Default Values * * @return void */ public function setDefaultValues() { /* If we're in the installer - don't do this */ if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation === 'setup' ) { return; } $this->member_group_id = \IPS\Settings::i()->guest_group; $this->mgroup_others = ''; $this->joined = time(); $this->ip_address = \IPS\Request::i()->ipAddress(); $this->timezone = 'UTC'; $this->allow_admin_mails = 1; $this->pp_photo_type = ''; $this->member_posts = 0; $this->_data['pp_main_photo'] = NULL; $this->_data['pp_thumb_photo'] = NULL; $this->_data['failed_logins'] = NULL; $this->_data['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(); /* Login handler callback */ foreach ( \IPS\Login::handlers( TRUE ) as $k => $handler ) { try { $handler->deleteAccount( $this ); } catch( \Exception $e ){} } /* Reset statistics */ \IPS\Widget::deleteCaches(); } /** * [ActiveRecord] Save Changed Columns * * @return void * @note We have to be careful when upgrading in case we are coming from an older version */ public function save() { if ( $this->member_id AND ( !\IPS\Dispatcher::hasInstance() OR \IPS\Dispatcher::i()->controllerLocation != 'setup' ) ) { $this->checkGroupPromotion(); } $new = $this->_new; $changes = $this->changed; parent::save(); if ( $new ) { $this->memberSync( 'onCreateAccount' ); /* Login handler callback */ foreach ( \IPS\Login::handlers( TRUE ) as $k => $handler ) { try { $handler->createAccount( $this ); } catch( \Exception $e ){} } /* 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 ) ) ); } } /* Check Changes for Member History */ $this->logChanges( $changes, $new ); /* 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(); } } /** * Log changes from the save method * * @param array $changes * @return void */ protected function logChanges( $changes, $new=FALSE ) { $logTypes = array( 'member_group_id', 'mgroup_others', 'name', 'linkedin_id', 'fb_uid', 'google_id', 'live_id', 'twitter_id', 'ipsconnect_id' ); foreach( $changes as $field => $value ) { if( !in_array( $field, $logTypes ) ) { continue; } /* Don't log anything if it's a new account, except if it's a social service connection */ if( $new and !in_array( $field, array( 'linkedin_id', 'fb_uid', 'google_id', 'live_id', 'twitter_id', 'ipsconnect_id' ) ) ) { continue; } switch( $field ) { case 'member_group_id': if( $this->_groupPromotion === NULL ) { $this->logHistory( 'core', $field, array( 'new' => $value ) ); } else { $this->logHistory( 'core', 'group_promotion', array( 'reason' => $this->_groupPromotion, 'new' => $value ) ); } break; case 'mgroup_others': if( $this->_groupPromotion === NULL ) { $this->logHistory( 'core', $field, array( 'new' => $value ) ); } else { $this->logHistory( 'core', 'group_promotion_o', array( 'reason' => $this->_groupPromotion, 'new' => $value ) ); } break; case 'name': $this->logHistory( 'core', 'display_name', array( 'old' => $this->previousName, 'new' => $value ) ); break; case 'linkedin_id': case 'fb_uid': case 'google_id': case 'live_id': case 'twitter_id': case 'ipsconnect_id': /* expand the name so it can be used later for language lookups */ if( $field == 'fb_uid' ) { $field = 'facebook'; } $data = array( 'service' => str_replace( array( '_uid', '_id' ), '', $field ), 'linked' => ( (bool) $value ) ); if( $value and $new ) { $data['registered'] = TRUE; } $this->logHistory( 'core', 'social_account', $data ); break; default: $this->logHistory( 'core', $field, array( 'new' => $value ) ); break; } } } /* !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->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 ) ) ) ) { \IPS\Task::queue( 'core', 'MemberContent', array_merge( array( 'initiated_by_member_id' => \IPS\Member::loggedIn()->member_id, 'member_id' => $this->member_id, 'name' => $this->name, 'class' => $class, 'action' => $action ), $extra ), 1 ); } } /* And private messages */ \IPS\Task::queue( 'core', 'MemberContent', array_merge( array( 'initiated_by_member_id' => \IPS\Member::loggedIn()->member_id, 'member_id' => $this->member_id, 'name' => $this->name, 'class' => 'IPS\\core\\Messenger\\Conversation', 'action' => $action ), $extra ), 1 ); } /** * Unflag as spammer * * @return void */ public function unflagAsSpammer() { if ( $this->members_bitoptions['bw_is_spammer'] ) { /* Save */ $this->members_bitoptions['bw_is_spammer'] = FALSE; $this->save(); /* Remove any pending hide or delete content queued tasks */ foreach( \IPS\Db::i()->select( '*', 'core_queue', array( '`key`=?', 'MemberContent' ) ) as $task ) { $data = json_decode( $task['data'], true ); if( $data['member_id'] == $this->member_id ) { \IPS\Db::i()->delete( 'core_queue', array( 'id=?', $task['id'] ) ); } } /* Report back to spam service */ if ( \IPS\Settings::i()->spam_service_enabled and \IPS\Settings::i()->spam_service_send_to_ips ) { $this->spamService( 'notspam' ); } /* 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 ( count( $this->_groups ) ) { 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 * * @return array */ public function socialGroups() { 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 ) ) ->setKeyField( 'group_id' ) ->setValueField( 'group_id' ) ); } } return $this->_socialGroups; } /** * Clubs */ protected $_clubs = NULL; /** * Clubs * * @return array */ public function clubs() { 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 ) ) ); } } return $this->_clubs; } /** * Permission Array * * @param bool $refresh If TRUE, will rebuild value * @return array */ public function permissionArray( $refresh = TRUE ) { $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->permission_array === NULL ) { $this->rebuildPermissionArray(); } $return = array_merge( $return, explode( ',', $this->permission_array ) ); } return $return; } /** * Permission Array * * @return void */ public function rebuildPermissionArray() { $permissionArray = array(); foreach ( $this->socialGroups() 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() as $clubId ) { $permissionArray[] = "c{$clubId}"; } if ( $this->modPermission('can_access_all_clubs') ) { $permissionArray[] = 'cm'; } } $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->localeDate(); } else if( $this->_data['bday_month'] ) { $date = new \IPS\DateTime( $this->_data['bday_month'] . '/' . $this->_data['bday_day'] ); return $date->dayAndMonth(); } else { return NULL; } } 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'] ) { /* 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 = "url . "' alt=''>"; } else { $image = str_repeat( "", 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 ) { try { $this->sessionData = \IPS\Db::i()->select( '*', 'core_sessions', array( 'member_id=?', $this->member_id ), 'running_time DESC' )->first(); } catch ( \UnderflowException $e ) {} } return ( $this->sessionData ) ? \IPS\Session::i()->getLocation( $this->sessionData ) : NULL; } /** * Get validating description * * @param null $validatingRow * @return string */ public function validatingDescription( $validatingRow=NULL ) { try { $validatingRow = ( $validatingRow ) ?: \IPS\Db::i()->select( '*', 'core_validating', array( 'member_id=?', $this->member_id ) )->first(); } catch( \UnderflowException $ex ) { return ''; } $validatingDescription = ''; if ( $validatingRow['new_reg'] ) { if ( $validatingRow['user_verified'] ) { $validatingDescription = \IPS\Member::loggedIn()->language()->addToStack('members_validating_admin'); } else { $validatingDescription = \IPS\Member::loggedIn()->language()->addToStack('members_validating_user'); } if ( $validatingRow['coppa_user'] ) { $validatingDescription .= \IPS\Member::loggedIn()->language()->addToStack('members_validating_coppa'); } if ( $validatingRow['spam_flag'] ) { $validatingDescription .= \IPS\Member::loggedIn()->language()->addToStack('members_validating_spam'); } } elseif ( $validatingRow['email_chg'] ) { $validatingDescription .= \IPS\Member::loggedIn()->language()->addToStack('members_validating_email_chg'); } return $validatingDescription; } /** * Followers * * @param int $privacy static::FOLLOW_PUBLIC + static::FOLLOW_ANONYMOUS * @param array $frequencyTypes array( 'immediate', 'daily', 'weekly' ) * @param \IPS\DateTime|int|NULL $date Only users who started following before this date will be returned. NULL for no restriction * @param int|array $limit LIMIT clause * @param string $order Column to order by * @param int $flags Flags to pass to select (e.g. \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS) * @param int * @return \IPS\Db\Select|NULL * @throws \BadMethodCallException */ public function followers( $privacy=3, $frequencyTypes=array( 'immediate', 'daily', 'weekly' ), $date=NULL, $limit=array( 0, 25 ), $order=NULL, $flags=\IPS\Db::SELECT_SQL_CALC_FOUND_ROWS ) { if( $this->members_bitoptions['pp_setting_moderate_followers'] ) { return NULL; } return static::_followers( 'member', $this->member_id, $privacy, $frequencyTypes, $date, $limit, $order, $flags ); } /** * Set failed login details * * @param array $data Data */ public function set_failed_logins( $data ) { $this->_data['failed_logins'] = json_encode( $data ); $highest = 0; foreach ( $data as $ipAddress => $times ) { if ( $highest < count( $times ) ) { $highest = count( $times ); } } $this->failed_login_count = $highest; } /** * 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 ); } /* !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 ) { $url = NULL; /* All this only applies to members... */ if ( isset( $memberData['member_id'] ) and $memberData['member_id'] ) { /* Is Gravatar disabled for them? */ $gravatarDisabled = isset( $memberData['member_bitoptions'] ) AND is_object( $memberData['members_bitoptions'] ) ? $memberData['members_bitoptions']['bw_disable_gravatar'] : $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' ) ) ) { try { return (string) \IPS\File::get( 'core_Profile', ( $thumb and $memberData['pp_thumb_photo'] ) ? $memberData['pp_thumb_photo'] : $memberData['pp_main_photo'] )->url; } catch ( \InvalidArgumentException $e ) { } } /* Gravatar */ elseif ( \IPS\Settings::i()->allow_gravatars and ( $memberData['pp_photo_type'] === 'gravatar' or ( !$memberData['pp_photo_type'] and !$gravatarDisabled ) ) ) { /* Default URL we send to Gravatar for it to fallback for. */ $defaultUrl = \IPS\Theme::i()->resource( 'default_photo.png', 'core', 'global' )->setScheme( \IPS\Request::i()->isSecure() ? 'https' : 'http' ); if( empty( $memberData['pp_main_photo'] ) ) { if( $photo = static::generateLetterPhoto( $memberData, TRUE ) ) { $defaultUrl = $photo; } } else { $defaultUrl = (string) \IPS\File::get( 'core_Profile', ( $thumb and $memberData['pp_thumb_photo'] ) ? $memberData['pp_thumb_photo'] : $memberData['pp_main_photo'] )->url; } /* 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).*/ $url = \IPS\Http\Url::external( "https://secure.gravatar.com/avatar/" . md5( trim( mb_strtolower( $memberData['pp_gravatar'] ?: $memberData['email'] ) ) ) )->setQueryString( array( 'd' => \IPS\IN_DEV ? '' : (string) $defaultUrl ) ); /* If we're in the ACP, munge because this is an external resource */ if ( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'admin' ) { $url = $url->makeSafeForAcp( TRUE ); } /* Return */ return (string) $url; } /* Other - This allows an app (such as Gallery) to set the pp_photo_type to a storage container to support custom images without duplicating them */ elseif( $memberData['pp_photo_type'] and $memberData['pp_photo_type'] != 'none' and mb_strpos( $memberData['pp_photo_type'], '_' ) !== FALSE ) { try { return (string) \IPS\File::get( $memberData['pp_photo_type'], $memberData['pp_main_photo'] )->url; } catch ( \InvalidArgumentException $e ) { /* If there was an exception, clear these values out - most likely the image or storage container is no longer valid */ $member = \IPS\Member::load( $memberData['member_id'] ); $member->pp_photo_type = NULL; $member->pp_main_photo = NULL; $member->save(); } } } /* Still here? Return default photo */ if ( !$url 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 $photo; } return (string) \IPS\Theme::i()->resource( 'default_photo.png', 'core', 'global' ); } } return NULL; } /** * Generate Letter Photo * * @param array $memberData * @param string|bool $skipType Skip setting pp_photo_type * @return boolean */ public static function generateLetterPhoto( $memberData, $skipType=FALSE ) { /* This theme has letter-photos 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'] ); $letterPhoto = new \IPS\Member\LetterPhoto( $member->name ); $photoVars = static::getLetterPhotoDimensions( $member ); if( $newPhoto = $letterPhoto->create( $photoVars['width'], $photoVars['height'] ) ) { $photoUrl = $newPhoto->container . '/' . $newPhoto->filename; if( !$skipType ) { $member->pp_photo_type = 'letter'; } $member->pp_main_photo = $photoUrl; $member->save(); return $newPhoto->url; } } 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 ) { try { $this->sessionData = \IPS\Db::i()->select( '*', 'core_sessions', array( 'member_id = ?', $this->member_id ) )->first(); } catch ( \UnderflowException $e ) { $this->sessionData = FALSE; } } if( $this->sessionData === FALSE ) { return FALSE; } $diff = \IPS\DateTime::ts( $this->last_activity )->diff( \IPS\DateTime::create() ); if ( $diff->y or $diff->m or $diff->d or $diff->h or $diff->i > 15 ) { return FALSE; } return TRUE; } /** * Is banned? * If is banned until a certain time, returns an \IPS\DateTime object * * @return FALSE|\IPS\DateTime|TRUE */ public function isBanned() { if ( $this->temp_ban != 0 ) { if ( $this->temp_ban != -1 and time() >= $this->temp_ban ) { $this->temp_ban = 0; $this->save(); return FALSE; } elseif ( $this->temp_ban > 0 ) { return \IPS\DateTime::ts( $this->temp_ban ); } return TRUE; } if( !$this->group['g_view_board'] ) { return TRUE; } return FALSE; } /** * Is the member in a certain group (including secondary groups) * * @param int|\IPS\Member\Group|array $group The group, or array of groups * @param bool $permissionArray If TRUE, checks the permission array rather than the groups * @return bool */ public function inGroup( $group, $permissionArray=FALSE ) { $group = array_filter( is_array( $group ) ? $group : array( $group ) ); $check = array_filter( $permissionArray ? $this->permissionArray() : $this->groups ); foreach ( $group as $_group ) { $groupId = ( $_group instanceof \IPS\Member\Group ) ? $_group->g_id : $_group; if ( in_array( $groupId, $check ) ) { return TRUE; } } return FALSE; } /** * Store a reference to the language object */ protected $_lang = NULL; /** * Return the language object to use for this member - returns default if member has not selected a language * * @param bool $frontOnly If TRUE, will only look at the the langauge for the front-end, not the AdminCP * @return \IPS\Lang */ public function language( $frontOnly=FALSE ) { /* Did we already load the language object? */ if( $this->_lang !== NULL ) { return $this->_lang; } /* If in setup, create a "dummy" language */ if ( \IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'setup' AND \IPS\Dispatcher::i()->setupLocation === 'install' ) { $this->_lang = \IPS\Lang::setupLanguage(); return $this->_lang; } else if ( \IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'setup' AND \IPS\Dispatcher::i()->setupLocation === 'upgrade' ) { $this->_lang = \IPS\Lang::upgraderLanguage(); return $this->_lang; } /* Work out if we are getting the ACP language or the normal language */ $column = 'language'; if ( !$frontOnly and \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'admin' and $this->member_id and $this->member_id == static::loggedIn()->member_id ) { $column = 'acp_language'; } /* If the member has a language set, try that */ if( $this->calculatedLanguageId !== NULL or $this->$column ) { try { $this->_lang = \IPS\Lang::load( $this->calculatedLanguageId ?: $this->$column ); if( $this->_lang->enabled ) { return $this->_lang; } } catch ( \OutOfRangeException $e ) { } } /* Otherwise, if this is us, try looking at HTTP_ACCEPT_LANGUAGE */ if ( \IPS\Dispatcher::hasInstance() and $this->member_id == static::loggedIn()->member_id ) { /* Work out what's in HTTP_ACCEPT_LANGUAGE */ $preferredLanguage = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? \IPS\Lang::autoDetectLanguage( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) : NULL; /* If we worked one out, use that and save it on the account so it gets used for emails etc */ if ( $preferredLanguage ) { $this->calculatedLanguageId = $preferredLanguage; if ( $this->member_id ) { $this->$column = $preferredLanguage; $this->save(); } } /* Otherwise, just use the default */ else { $this->calculatedLanguageId = \IPS\Lang::defaultLanguage(); } } else { /* Just return the default language */ $this->calculatedLanguageId = \IPS\Lang::defaultLanguage(); } /* Set it */ $this->_lang = \IPS\Lang::load( $this->calculatedLanguageId ); /* Add upgrader language bits if appropriate */ if ( \IPS\Dispatcher::hasInstance() AND class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'setup' AND \IPS\Dispatcher::i()->setupLocation === 'upgrade' ) { $this->_lang->upgraderLanguage(); } /* Return */ return $this->_lang; } /** * @brief Cached URL */ protected $_url = NULL; /** * Get URL * * @return \IPS\Http\Url */ public function url() { if( $this->_url === NULL ) { $this->_url = \IPS\Http\Url::internal( "app=core&module=members&controller=profile&id={$this->member_id}", 'front', 'profile', $this->members_seo_name ); } return $this->_url; } /** * URL to ACP "Edit Member" * * @return \IPS\Http\Url */ public function acpUrl() { return \IPS\Http\Url::internal( "app=core&module=members&controller=members&do=edit&id={$this->member_id}", 'admin' ); } /** * HTML link to profile with hovercard * * @param string|NULL $warningRef The reference key for warnings * @param boolean $groupFormatting Apply the group prefix/suffix to the name? * @return string */ public function link( $warningRef=NULL, $groupFormatting=FALSE ) { if ( !\IPS\Settings::i()->warn_on ) { $warningRef = NULL; } return \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( $this, $warningRef, $groupFormatting ); } /** * Profile Fields * * @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(); } $profileFields = array(); $values = array(); try { $values = \IPS\Db::i()->select( '*', 'core_pfields_content', array( 'member_id = ?', $this->member_id ) )->first(); } catch ( \UnderflowException $e ) {} if( !empty( $values ) ) { foreach ( \IPS\core\ProfileFields\Field::values( $values, $location ) as $group => $fields ) { $profileFields[ 'core_pfieldgroups_' . $group ] = $fields; } } return $profileFields; } /** * Profile Fields shown next to users content */ public $profileFields; /** * Profile Fields shown next to users content * * @return array */ public function contentProfileFields() { if ( $this->profileFields === NULL ) { $this->profileFields = array(); if ( $this->member_id AND \IPS\core\ProfileFields\Field::fieldsForContentView() ) { $select = '*'; /* Can we view private fields? */ if( !( \IPS\Member::loggedIn()->isAdmin() OR \IPS\Member::loggedIn()->member_id === $this->member_id ) ) { $select = 'member_id'; $publicFields = \IPS\Db::i()->select( 'pf_id', 'core_pfields_data', array( 'pf_member_hide = ?', 0 ) ); foreach( $publicFields as $field) { $select .= ", field_{$field}"; } } try { $values = \IPS\Db::i()->select( $select, 'core_pfields_content', array( 'member_id = ?', $this->member_id ) )->first(); if ( is_array( $values ) ) { $this->setProfileFieldValuesInMemory( $values ); } } catch ( \UnderflowException $e ) {} } } return $this->profileFields; } /** * Store profile field values in memory * * @param array $values * @return void */ public function setProfileFieldValuesInMemory( array $values ) { $this->profileFields = array(); $values = array_filter( $values, function ( $val) { return ( $val !== '' AND $val !== NULL ); } ); if( !empty( $values ) ) { foreach ( \IPS\core\ProfileFields\Field::values( $values, \IPS\core\ProfileFields\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; } } /** * Encrypt a plaintext password * * @param string $password Password to encrypt * @return string Encrypted password * @todo [Future] When we increase minimum PHP version, adjust blowfish to $2y$ */ public function encryptedPassword( $password ) { /* New password style introduced in 4.0 using Blowfish */ if ( mb_strlen( $this->members_pass_salt ) === 22 ) { return crypt( $password, '$2a$13$' . $this->members_pass_salt ); } /* Old encryption style using md5 */ else { return md5( md5( $this->members_pass_salt ) . md5( \IPS\Request::legacyEscape( $password ) ) ); } } /** * Notifications Configuration * * @return array */ public function notificationsConfiguration() { $return = array(); foreach ( \IPS\Db::i()->select( 'd.*, p.preference', array( 'core_notification_defaults', 'd' ) )->join( array( 'core_notification_preferences', 'p' ), array( 'd.notification_key=p.notification_key AND p.member_id=?', $this->member_id ) ) as $row ) { if ( $row['preference'] === NULL or !$row['editable'] ) { $return[ $row['notification_key'] ] = explode( ',', $row['default'] ); } else { $return[ $row['notification_key'] ] = array_diff( explode( ',', $row['preference'] ), explode( ',', $row['disabled'] ) ); } } return $return; } /** * @brief Following? */ protected $_following = array(); /** * Following * * @param string $app Application key * @param string $area Area * @param int $id Item ID * @return bool */ public function following( $app, $area, $id ) { $_key = md5( $app . $area . $id ); if( isset( $this->_following[ $_key ] ) ) { return $this->_following[ $_key ]; } try { \IPS\Db::i()->select( 'follow_id', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_rel_id=? AND follow_member_id=?', $app, $area, $id, $this->member_id ) )->first(); $this->_following[ $_key ] = TRUE; } catch ( \UnderflowException $e ) { $this->_following[ $_key ] = FALSE; } return $this->_following[ $_key ]; } /** * Admin CP Restrictions * * @return array */ protected function acpRestrictions() { if ( !$this->member_id ) { return FALSE; } if ( $this->restrictions === NULL ) { if ( !isset( \IPS\Data\Store::i()->administrators ) ) { \IPS\Data\Store::i()->administrators = array( 'm' => iterator_to_array( \IPS\Db::i()->select( '*', 'core_admin_permission_rows', array( 'row_id_type=?', 'member' ) )->setKeyField( 'row_id' ) ), 'g' => iterator_to_array( \IPS\Db::i()->select( '*', 'core_admin_permission_rows', array( 'row_id_type=?', 'group' ) )->setKeyField( 'row_id' ) ), ); } $rows = array(); if ( isset( \IPS\Data\Store::i()->administrators['m'][ $this->member_id ] ) ) { $rows[] = \IPS\Data\Store::i()->administrators['m'][ $this->member_id ]; } foreach ( $this->groups as $id ) { if ( isset( \IPS\Data\Store::i()->administrators['g'][ $id ] ) ) { $rows[] = \IPS\Data\Store::i()->administrators['g'][ $id ]; } } $this->restrictions = FALSE; if ( count( $rows ) > 0 ) { $this->restrictions = array(); foreach ( $rows as $row ) { if ( $row['row_perm_cache'] === '*' ) { $this->restrictions = '*'; break; } $perms = json_decode( $row['row_perm_cache'], TRUE ); if ( $row['row_id_type'] === 'member' ) { $this->restrictions = $perms; break; } else if( is_array( $perms ) ) { 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 * * @return int */ public function reportCount() { if ( $this->reportCount === NULL ) { if ( \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'modcp' ) ) ) { if( !\IPS\Member::loggedIn()->members_bitoptions['no_report_count'] ) { $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 ) ) ) ); $this->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(); } } 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; 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 * * @return array * @apiresponse int id ID number * @apiresponse string name Username * @apiresponse string title Member title * @apiresponse string timezone Member timezone * @apiresponse string formattedName Username with group formatting * @apiresponse \IPS\Member\Group primaryGroup Primary group * @apiresponse [\IPS\Member\Group] secondaryGroups Secondary groups * @apiresponse string email Email address * @apiresponse datetime joined Registration date * @apiresponse string registrationIpAddress IP address when registered * @apiresponse string photoUrl URL to photo (which will be the site's default if they haven't set one) * @apiresponse string profileUrl URL to profile * @apiresponse 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 lastActivity Last activity date on the site * @apiresponse datetime lastVisit Last distinct visit date on the site * @apiresponse datetime 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 */ public function apiOutput() { $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(); } 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 ) { $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(); } } catch( \UnderflowException $e ) { } # Guests will not have any profile field information. return array( 'id' => $this->member_id, 'name' => $this->name, 'title' => $this->member_title, 'timeZone' => $this->timezone, 'formattedName' => $group->formatName( $this->name ), 'primaryGroup' => $group->apiOutput(), 'secondaryGroups' => $secondaryGroups, 'email' => $this->email, 'joined' => $this->joined->rfc3339(), 'registrationIpAddress' => $this->ip_address, 'warningPoints' => $this->warn_level, 'reputationPoints' => $this->pp_reputation_points, 'photoUrl' => (string) $this->get_photo( FALSE ), 'profileUrl' => (string) $this->url(), 'validating' => (bool) $this->members_bitoptions['validating'], 'posts' => $this->member_posts, 'lastActivity' => \IPS\DateTime::ts( $this->last_activity )->rfc3339(), 'lastVisit' => \IPS\DateTime::ts( $this->last_visit )->rfc3339(), 'lastPost' => \IPS\DateTime::ts( $this->member_last_post )->rfc3339(), 'profileViews' => $this->members_profile_views, 'birthday' => $this->bday_month ? ( $this->bday_month . '/' . $this->bday_day . ( $this->bday_year ? '/' . $this->bday_year : '' ) ) : NULL, 'customFields' => $fields ); } /** * 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(); } /* !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 */ /** * Generate a salt * * @return string */ public function generateSalt() { $salt = ''; for ( $i=0; $i<22; $i++ ) { do { $chr = rand( 48, 122 ); } while ( in_array( $chr, range( 58, 64 ) ) or in_array( $chr, range( 91, 96 ) ) ); $salt .= chr( $chr ); } return $salt; } /** * Can use module * * @param \IPS\Application\Module $module The module to test * @return bool * @throws \InvalidArgumentException */ public function canAccessModule( $module ) { if ( !( $module instanceof \IPS\Application\Module ) ) { throw new \InvalidArgumentException; } return \IPS\Application::load( $module->application )->canAccess( $this ) and ( $module->protected or $module->can( 'view', $this ) ); } /** * IPS Spam Monitoring Service * * @param string $type Request type * @return int|NULL Action code based on spam service response, or NULL for no action */ public function spamService( $type='register' ) { try { $response = \IPS\Http\Url::ips( 'spam/' . $type )->request()->login( \IPS\Settings::i()->ipb_reg_number, '' )->post( array( 'email' => $this->email, 'ip' => $this->ip_address, ) ); if ( $response->httpResponseCode !== 200 ) { $spamCode = intval( (string) $response ); } else { $spamCode = 0; } } catch ( \IPS\Http\Request\Exception $e ) { $spamCode = 0; } $action = NULL; if( $type == 'register' and $spamCode ) { /* Log Request */ \IPS\Db::i()->insert( 'core_spam_service_log', array( 'log_date' => time(), 'log_code' => $spamCode, 'log_msg' => '', // No value is returned unless it's a developer account making the call 'email_address' => $this->email, 'ip_address' => $this->ip_address ) ); /* Action to perform */ $key = "spam_service_action_{$spamCode}"; $action = \IPS\Settings::i()->$key; /* Perform Action */ switch( $action ) { /* Proceed with registration */ case 1: break; /* Flag for admin approval */ case 2: \IPS\Settings::i()->reg_auth_type = 'admin'; break; /* Approve the account, but ban it */ case 3: $this->temp_ban = -1; $this->members_bitoptions['bw_is_spammer'] = TRUE; break; /* Deny registration - we return the code and the controller is expected to show an error */ case 4: break; } } return $action; } /** * Member Sync * * @param string $method Method * @param array $params Additional parameters to pass * @return void */ public function memberSync( $method, $params=array() ) { /* Don't do this during an upgrade */ if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'setup' ) { return; } foreach ( \IPS\Application::allExtensions( 'core', 'MemberSync', FALSE ) as $class ) { if ( method_exists( $class, $method ) ) { call_user_func_array( array( $class, $method ), array_merge( array( $this ), $params ) ); } } } /** * Merge * * @param \IPS\Member $otherMember Member to merge with * @return void */ public function merge( \IPS\Member $otherMember ) { if ( $this == $otherMember ) { throw new \InvalidArgumentException( 'merge_self_error' ); } /* Merge content */ $otherMember->hideOrDeleteAllContent( 'merge', array( 'merge_with_id' => $this->member_id, 'merge_with_name' => $this->name ) ); /* Let apps do their stuff */ $this->memberSync( 'onMerge', array( $otherMember ) ); /* Login handler callback */ foreach ( \IPS\Login::handlers( TRUE ) as $k => $handler ) { try { $handler->mergeAccounts( $this, $otherMember ); } catch( \Exception $e ){} } } /** * Add profile visitor * * @param \IPS\Member $visitor Member that viewed profile * @return void */ public function addVisitor( $visitor ) { $visitors = json_decode( $this->pp_last_visitors, TRUE ); /* If this member is already in the visitor list remove the entry so we can add back in the correct order */ if( isset( $visitors[ $visitor->member_id ] ) ) { unset( $visitors[ $visitor->member_id ] ); } /* We want to limit to 5 members */ else if ( count( $visitors ) >= 5 ) { $visitors = array_reverse( $visitors, TRUE ); array_pop( $visitors ); $visitors = array_reverse( $visitors, TRUE ); } /* Add the new entry */ $visitors[ $visitor->member_id ] = time(); /* Encode and save*/ $this->pp_last_visitors = json_encode( $visitors ); $this->save(); } /** * @brief Posts Per Day Storage */ protected $_ppdLimit = NULL; /** * Check posts per day to see if this member can post. * * @return bool */ public function checkPostsPerDay() { /* Fetch our PPD limit - we should only need to do this once */ if ( $this->_ppdLimit === NULL ) { $this->_ppdLimit = $this->group['g_ppd_limit']; } /* We can't actually check guests as we can't store how often they have posted - simply counting content is not viable */ if ( ! $this->member_id ) { return TRUE; } /* Is there any limit at all? */ if ( ! $this->_ppdLimit ) { return TRUE; } $count = $this->members_day_posts[0]; $time = $this->members_day_posts[1]; /* Have we posted at all yet? */ if ( ! $count OR ! $time ) { return TRUE; } /* Are we beyond our 24 hours? */ if ( $time AND $time < \IPS\DateTime::create()->sub( new \DateInterval( 'P1D' ) )->getTimestamp() ) { /* Update member immediately */ $this->members_day_posts = array( 0, 0 ); $this->save(); return TRUE; } /* Still within 24 hours... have we hit the limit? */ if ( $count >= $this->_ppdLimit ) { 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; } /** * @brief Group Promotion Type */ protected $_groupPromotion = NULL; /** * 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; } if( $this->group['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->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'] ) { $this->_groupPromotion = $ruleToUse->id; $this->member_group_id = $action['primary_group']; } if( count( $action['secondary_group'] ) OR count( $action['secondary_remove'] ) ) { $secondaryGroups = array_filter( explode( ',', $this->_data['mgroup_others'] ) ); if( count( $action['secondary_group'] ) ) { if( array_diff( $action['secondary_group'], $secondaryGroups ) ) { $this->_groupPromotion = $ruleToUse->id; $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 ] ); $this->_groupPromotion = $ruleToUse->id; } } } $this->mgroup_others = implode( ',', array_unique( $secondaryGroups ) ); } } /** * Is the current user allowed to use the contact us form * * @return bool */ public function canUseContactUs() { try { $module = \IPS\Application\Module::get( 'core', 'contact' ); } 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() { $newKey = uniqid(); \IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => $newKey ), array( 'conf_key=?', 'member_menu_create_key' ) ); \IPS\Settings::i()->member_menu_create_key = $newKey; unset( \IPS\Data\Store::i()->settings ); } /** * 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 ) { $where = array( array( 'member_id=?', $this->member_id ) ); if ( is_string( $frontEndSessions ) ) { $where[] = array( 'id<>?', $frontEndSessions ); } \IPS\Db::i()->delete( 'core_sessions', $where ); } 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 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 $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(); } /* 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 * * @param array $record Row from core_validating * @return void */ public function validationComplete() { /* Send a success email */ try { \IPS\Email::buildFromTemplate( 'core', 'registration_complete', array( $this ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $this ); } catch( \ErrorException $e ) { } /* 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' ); /* Login handler callback */ foreach ( \IPS\Login::handlers( TRUE ) as $k => $handler ) { try { $handler->validateAccount( $this ); } catch( \Exception $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; } } /** * Linked profile sync services * * @return array */ public function connectedProfileSyncServices() { $services = array(); foreach( \IPS\core\ProfileSync\ProfileSyncAbstract::services() as $service ) { $obj = new $service( $this ); if ( $obj->connected() ) { $services[] = $obj::$icon; } } return $services; } /** * 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 ) { $by = ( !\IPS\Dispatcher::hasInstance() ) ? NULL : \IPS\Member::loggedIn(); } \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' ) ); $orderBy = $type . ' DESC'; return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', $where, $orderBy, $limit ), 'IPS\Member' ); } else { $storeKey = 'store_' . $type; if ( !isset( \IPS\Data\Store::i()->$storeKey ) or ( time() - \IPS\Data\Store::i()->$storeKey['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']; $memberIds = array(); foreach( \IPS\Db::i()->select( 'COUNT(*) as count, ' . $type::$databaseTable . '.' . $authorField, $type::$databaseTable, $contentWhere, 'count DESC', $limit, $authorField ) as $row ) { $memberIds[ $row[ $authorField ] ] = $row['count']; } \IPS\Data\Store::i()->$storeKey = array( 'time' => time(), 'memberIds' => $memberIds ); } $results = array_combine( array_keys( \IPS\Data\Store::i()->$storeKey['memberIds'] ), array_fill( 0, count( \IPS\Data\Store::i()->$storeKey['memberIds'] ), 0 ) ); foreach ( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', \IPS\Db::i()->in( 'member_id', array_keys( \IPS\Data\Store::i()->$storeKey['memberIds'] ) ), NULL, $limit ), 'IPS\Member' ) as $member ) { $member->_customCount = \IPS\Data\Store::i()->$storeKey['memberIds'][ $member->member_id ]; $results[ $member->member_id ] = $member; } 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' ); } 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; } }