Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 13 Feb 2017 */ namespace IPS\Member; /* 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; } /** * Club Model */ class _Club extends \IPS\Patterns\ActiveRecord implements \IPS\Content\Embeddable { const TYPE_PUBLIC = 'public'; const TYPE_OPEN = 'open'; const TYPE_CLOSED = 'closed'; const TYPE_PRIVATE = 'private'; const TYPE_READONLY = 'readonly'; const STATUS_MEMBER = 'member'; const STATUS_INVITED = 'invited'; const STATUS_INVITED_BYPASSING_PAYMENT = 'invited_bypassing_payment'; const STATUS_REQUESTED = 'requested'; const STATUS_WAITING_PAYMENT = 'payment_pending'; const STATUS_EXPIRED = 'expired'; const STATUS_EXPIRED_MODERATOR = 'expired_moderator'; const STATUS_DECLINED = 'declined'; const STATUS_BANNED = 'banned'; const STATUS_MODERATOR = 'moderator'; const STATUS_LEADER = 'leader'; /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'core_clubs'; /** * @brief Use a default cover photo */ public static $coverPhotoDefault = true; /* !Fetch Clubs */ /** * Construct ActiveRecord from database row * * @param array $data Row from database table * @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there? * @return static */ public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE ) { $return = parent::constructFromData( $data, $updateMultitonStoreIfExists ); if ( isset( $data['member_id'] ) and isset( $data['status'] ) ) { $return->_memberStatuses[ $data['member_id'] ] = $data['status']; } return $return; } /** * Get all clubs a member can see * * @param \IPS\Member $member The member to base permission off or NULL for all clubs * @param int $limit Number to get * @param string $sortOption The sort option ('last_activity', 'members', 'content' or 'created') * @param bool|\IPS\Member|array $mineOnly Limit to clubs a particular member has joined (TRUE to use the same value as $member). Can also provide an array as array( 'member' => \IPS\Member, 'statuses' => array( STATUS_MEMBER... ) ) to limit to certain member statuses * @param array $filters Custom field filters * @param mixed $extraWhere Additional WHERE clause * @return \IPS\Patterns\ActiveRecordIterator|array */ public static function clubs( \IPS\Member $member = NULL, $limit, $sortOption, $mineOnly=FALSE, $filters=array(), $extraWhere=NULL ) { $where = array(); $joins = array(); /* Restrict to clubs we can see */ if ( $member and !$member->modPermission('can_access_all_clubs') ) { /* Exclude clubs which are pending approval, unless we are the owner */ if ( \IPS\Settings::i()->clubs_require_approval ) { $where[] = array( '( approved=1 OR owner=? )', $member->member_id ); } /* Specify our memberships */ if ( $member->member_id ) { $joins['membership'] = array( array( 'core_clubs_memberships', 'membership' ), array( 'membership.club_id=core_clubs.id AND membership.member_id=?', $member->member_id ) ); $where[] = array( "( type<>? OR membership.status IN('" . static::STATUS_MEMBER . "','" . static::STATUS_MODERATOR . "','" . static::STATUS_LEADER . "','" . static::STATUS_EXPIRED . "','" . static::STATUS_EXPIRED_MODERATOR . "') )", static::TYPE_PRIVATE ); } else { $where[] = array( 'type<>?', static::TYPE_PRIVATE ); } } /* Restrict to clubs we have joined */ if ( $mineOnly ) { if ( is_array( $mineOnly ) ) { $statuses = $mineOnly['statuses']; $mineOnly = $mineOnly['member']; } else { $mineOnly = ( $mineOnly === TRUE ) ? $member : $mineOnly; $statuses = array( static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER, static::STATUS_EXPIRED, static::STATUS_EXPIRED_MODERATOR ); } if ( !$mineOnly->member_id ) { return array(); } if ( $member and $mineOnly->member_id === $member->member_id and isset( $joins['membership'] ) ) { $where[] = array( "membership.status IN('" . static::STATUS_MEMBER . "','" . static::STATUS_MODERATOR . "','" . static::STATUS_LEADER . "','" . static::STATUS_EXPIRED . "','" . static::STATUS_EXPIRED_MODERATOR . "')" ); } else { $joins['others_membership'] = array( array( 'core_clubs_memberships', 'others_membership' ), array( 'others_membership.club_id=core_clubs.id AND others_membership.member_id=?', $mineOnly->member_id ) ); $where[] = array( "others_membership.status IN('" . static::STATUS_MEMBER . "','" . static::STATUS_MODERATOR . "','" . static::STATUS_LEADER . "','" . static::STATUS_EXPIRED . "','" . static::STATUS_EXPIRED_MODERATOR . "')" ); } } /* Other filters */ if ( $filters ) { $joins['core_clubs_fieldvalues'] = array( 'core_clubs_fieldvalues', array( 'core_clubs_fieldvalues.club_id=core_clubs.id' ) ); foreach ( $filters as $k => $v ) { if ( is_array( $v ) ) { $where[] = array( \IPS\Db::i()->findInSet( "field_{$k}", $v ) ); } else { $where[] = array( "field_{$k}=?", $v ); } } } /* Additional where clause */ if ( $extraWhere ) { if ( is_array( $extraWhere ) ) { $where = array_merge( $where, $extraWhere ); } else { $where[] = array( $extraWhere ); } } /* Query */ $select = \IPS\Db::i()->select( '*', 'core_clubs', $where, ( $sortOption === 'name' ? "{$sortOption} ASC" : "{$sortOption} DESC" ), $limit, NULL, NULL, \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS ); foreach ( $joins as $join ) { $select->join( $join[0], $join[1] ); } $select->setKeyField( 'id' ); /* Return */ return new \IPS\Patterns\ActiveRecordIterator( $select, 'IPS\Member\Club' ); } /** * Get number clubs a member is leader of * * @param \IPS\Member $member The member * @return int */ public static function numberOfClubsMemberIsLeaderOf( \IPS\Member $member ) { return \IPS\Db::i()->select( 'COUNT(*)', 'core_clubs_memberships', array( 'member_id=? AND status=?', $member->member_id, static::STATUS_LEADER ) ); } /* !ActiveRecord */ /** * Set Default Values * * @return void */ public function setDefaultValues() { $this->type = static::TYPE_OPEN; $this->created = new \IPS\DateTime; $this->last_activity = time(); $this->members = 1; $this->owner = NULL; $this->approved = \IPS\Settings::i()->clubs_require_approval ? 0 : 1; } /** * Get owner * * @return \IPS\Member|NULL */ public function get_owner() { try { $owner = \IPS\Member::load( $this->_data['owner'] ); return $owner->member_id ? $owner : NULL; } catch( \OutOfRangeException $e ) { return NULL; } } /** * Set member * * @param \IPS\Member * @return void */ public function set_owner( \IPS\Member $owner = NULL ) { $this->_data['owner'] = $owner ? ( (int) $owner->member_id ) : NULL; } /** * Get created date * * @return \IPS\DateTime */ public function get_created() { return \IPS\DateTime::ts( $this->_data['created'] ); } /** * Set created date * * @param \IPS\DateTime $date The invoice date * @return void */ public function set_created( \IPS\DateTime $date ) { $this->_data['created'] = $date->getTimestamp(); } /** * Get club URL * * @return \IPS\Http\Url */ public function url() { return \IPS\Http\Url::internal( "app=core&module=clubs&controller=view&id={$this->id}", 'front', 'clubs_view', \IPS\Http\Url\Friendly::seoTitle( $this->name ) ); } /** * Columns needed to query for search result / stream view * * @return array */ public static function basicDataColumns() { return array( 'id', 'name' ); } /** * Edit Club Form * * @param bool $acp TRUE if editing in the ACP * @param bool $new TRUE if creating new * @param array $availableTypes If creating new, the available types * @return \IPS\Helpers\Form|NULL */ public function form( $acp=FALSE, $new=FALSE, $availableTypes=NULL ) { $form = new \IPS\Helpers\Form; $form->add( new \IPS\Helpers\Form\Text( 'club_name', $this->name, TRUE, array( 'maxLength' => 255 ) ) ); if ( $acp or ( $new and count( $availableTypes ) > 1 ) ) { $form->add( new \IPS\Helpers\Form\Radio( 'club_type', $this->type, TRUE, array( 'options' => $new ? $availableTypes : array( \IPS\Member\Club::TYPE_PUBLIC => 'club_type_' . \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_OPEN => 'club_type_' . \IPS\Member\Club::TYPE_OPEN, \IPS\Member\Club::TYPE_CLOSED => 'club_type_' . \IPS\Member\Club::TYPE_CLOSED, \IPS\Member\Club::TYPE_PRIVATE => 'club_type_' . \IPS\Member\Club::TYPE_PRIVATE, \IPS\Member\Club::TYPE_READONLY => 'club_type_' . \IPS\Member\Club::TYPE_READONLY, ), 'toggles' => array( \IPS\Member\Club::TYPE_OPEN => array( 'club_membership_fee' ), \IPS\Member\Club::TYPE_CLOSED => array( 'club_membership_fee' ), \IPS\Member\Club::TYPE_PRIVATE => array( 'club_membership_fee' ), \IPS\Member\Club::TYPE_READONLY => array( 'club_membership_fee' ), ) ) ) ); if ( $acp ) { $form->add( new \IPS\Helpers\Form\Member( 'club_owner', $this->owner, TRUE ) ); } } $form->add( new \IPS\Helpers\Form\TextArea( 'club_about', $this->about ) ); if ( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on and \IPS\Member::loggedIn()->group['gbw_paid_clubs'] ) { $form->add( new \IPS\Helpers\Form\Radio( 'club_membership_fee', ( $this->id and $this->fee ) ? 'paid' : 'free', TRUE, array( 'options' => array( 'free' => 'club_membership_free', 'paid' => 'club_membership_paid' ), 'toggles' => array( 'paid' => array( 'club_fee', 'club_renewals' ) ) ), NULL, NULL, NULL, 'club_membership_fee' ) ); $commissionBlurb = NULL; $fees = NULL; if ( $_fees = \IPS\Settings::i()->clubs_paid_transfee ) { $fees = array(); foreach ( $_fees as $fee ) { $fees[] = (string) ( new \IPS\nexus\Money( $fee['amount'], $fee['currency'] ) ); } $fees = \IPS\Member::loggedIn()->language()->formatList( $fees, \IPS\Member::loggedIn()->language()->get('or_list_format') ); } if ( \IPS\Settings::i()->clubs_paid_commission and $fees ) { $commissionBlurb = \IPS\Member::loggedIn()->language()->addToStack( 'club_fee_desc_both', FALSE, array( 'sprintf' => array( \IPS\Settings::i()->clubs_paid_commission, $fees ) ) ); } elseif ( \IPS\Settings::i()->clubs_paid_commission ) { $commissionBlurb = \IPS\Member::loggedIn()->language()->addToStack('club_fee_desc_percent', FALSE, array( 'sprintf' => \IPS\Settings::i()->clubs_paid_commission ) ); } elseif ( $fees ) { $commissionBlurb = \IPS\Member::loggedIn()->language()->addToStack('club_fee_desc_fee', FALSE, array( 'sprintf' => $fees ) ); } \IPS\Member::loggedIn()->language()->words['club_fee_desc'] = $commissionBlurb; $form->add( new \IPS\nexus\Form\Money( 'club_fee', $this->id ? json_decode( $this->fee, TRUE ) : array(), NULL, array(), NULL, NULL, NULL, 'club_fee' ) ); $form->add( new \IPS\Helpers\Form\Radio( 'club_renewals', $this->id ? ( $this->renewal_term ? 1 : 0 ) : 0, TRUE, array( 'options' => array( 0 => 'club_renewals_off', 1 => 'club_renewals_on' ), 'toggles' => array( 1 => array( 'club_renewal_term' ) ) ), NULL, NULL, NULL, 'club_renewals' ) ); \IPS\Member::loggedIn()->language()->words['club_renewal_term_desc'] = $commissionBlurb; $renewTermForEdit = NULL; if ( $this->id and $this->renewal_term ) { $renewPrices = array(); foreach ( json_decode( $this->renewal_price, TRUE ) as $currency => $data ) { $renewPrices[ $currency ] = new \IPS\nexus\Money( $data['amount'], $currency ); } $renewTermForEdit = new \IPS\nexus\Purchase\RenewalTerm( $renewPrices, new \DateInterval( 'P' . $this->renewal_term . mb_strtoupper( $this->renewal_units ) ) ); } $form->add( new \IPS\nexus\Form\RenewalTerm( 'club_renewal_term', $renewTermForEdit, NULL, array( 'allCurrencies' => TRUE ), NULL, NULL, NULL, 'club_renewal_term' ) ); } $form->add( new \IPS\Helpers\Form\Upload( 'club_profile_photo', $this->profile_photo ? \IPS\File::get( 'core_Clubs', $this->profile_photo ) : NULL, FALSE, array( 'storageExtension' => 'core_Clubs', 'image' => array( 'maxWidth' => 200, 'maxHeight' => 200 ) ) ) ); if ( \IPS\Settings::i()->clubs_locations ) { $form->add( new \IPS\Helpers\Form\Address( 'club_location', $this->location_json ? \IPS\GeoLocation::buildFromJson( $this->location_json ) : NULL, FALSE, array( 'requireFullAddress' => FALSE ) ) ); } $fieldValues = $this->fieldValues(); foreach ( \IPS\Member\Club\CustomField::roots() as $field ) { $form->add( $field->buildHelper( isset( $fieldValues["field_{$field->id}"] ) ? $fieldValues["field_{$field->id}"] : NULL ) ); } if ( $values = $form->values() ) { $this->name = $values['club_name']; /* If there is only one type available, set it. */ if( count( $availableTypes ) == 1 ) { $values['club_type'] = key( $availableTypes ); } $needToUpdatePermissions = FALSE; if ( $acp ) { if ( $this->type != $values['club_type'] ) { $this->type = $values['club_type']; $needToUpdatePermissions = TRUE; } if ( $this->owner != $values['club_owner'] ) { $this->owner = $values['club_owner']; $this->addMember( $values['club_owner'], \IPS\Member\Club::STATUS_LEADER, TRUE ); } } elseif ( $new ) { $this->type = $values['club_type']; $this->owner = \IPS\Member::loggedIn(); } $this->about = $values['club_about']; $this->profile_photo = (string) $values['club_profile_photo']; if ( isset( $values['club_location'] ) ) { $this->location_json = json_encode( $values['club_location'] ); if ( $values['club_location']->lat and $values['club_location']->long ) { $this->location_lat = $values['club_location']->lat; $this->location_long = $values['club_location']->long; } else { $this->location_lat = NULL; $this->location_long = NULL; } } else { $this->location_json = NULL; $this->location_lat = NULL; $this->location_long = NULL; } if ( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on and \IPS\Member::loggedIn()->group['gbw_paid_clubs'] ) { switch ( $values['club_membership_fee'] ) { case 'free': $this->fee = NULL; $this->renewal_term = 0; $this->renewal_units = NULL; $this->renewal_price = NULL; break; case 'paid': $this->fee = json_encode( $values['club_fee'] ); if ( $values['club_renewals'] and $values['club_renewal_term'] ) { $term = $values['club_renewal_term']->getTerm(); $this->renewal_term = $term['term']; $this->renewal_units = $term['unit']; $this->renewal_price = json_encode( $values['club_renewal_term']->cost ); } else { $this->renewal_term = 0; $this->renewal_units = NULL; $this->renewal_price = NULL; } break; } } $this->save(); if ( $new ) { $this->addMember( \IPS\Member::loggedIn(), \IPS\Member\Club::STATUS_LEADER ); } $this->recountMembers(); $customFieldValues = array(); foreach ( \IPS\Member\Club\CustomField::roots() as $field ) { if ( isset( $values["core_clubfield_{$field->id}"] ) ) { $helper = $field->buildHelper(); if ( $helper instanceof \IPS\Helpers\Form\Upload ) { $customFieldValues[ "field_{$field->id}" ] = (string) $values["core_clubfield_{$field->id}"]; } else { $customFieldValues[ "field_{$field->id}" ] = $helper::stringValue( $values["core_clubfield_{$field->id}"] ); } if ( $field->type === 'Editor' ) { $field->claimAttachments( $this->id ); } } } if ( count( $customFieldValues ) ) { $customFieldValues['club_id'] = $this->id; \IPS\Db::i()->insert( 'core_clubs_fieldvalues', $customFieldValues, TRUE ); } if ( $needToUpdatePermissions ) { foreach ( $this->nodes() as $node ) { try { $nodeClass = $node['node_class']; $node = $nodeClass::load( $node['node_id'] ); $node->setPermissionsToClub( $this ); } catch ( \Exception $e ) { } } } if( $new and \IPS\Settings::i()->clubs_require_approval and !$this->approved ) { /* Send notification to mods */ $moderators = array( 'm' => array(), 'g' => array() ); foreach ( \IPS\Db::i()->select( '*', 'core_moderators' ) as $mod ) { $canView = FALSE; if ( $mod['perms'] == '*' ) { $canView = TRUE; } if ( $canView === FALSE ) { $perms = json_decode( $mod['perms'], TRUE ); if ( isset( $perms['can_access_all_clubs'] ) AND $perms['can_access_all_clubs'] === TRUE ) { $canView = TRUE; } } if ( $canView === TRUE ) { $moderators[ $mod['type'] ][] = $mod['id']; } } $notification = new \IPS\Notification( \IPS\Application::load('core'), 'unapproved_club', $this, array( $this ) ); foreach ( \IPS\Db::i()->select( '*', 'core_members', ( count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $member ) { if( $member['member_id'] != \IPS\Member::loggedIn()->member_id ) { $notification->recipients->attach( \IPS\Member::constructFromData( $member ) ); } } if( count( $notification->recipients ) ) { $notification->send(); } } return NULL; } return $form; } /** * Custom Field Values * * @return array */ public function fieldValues() { try { return \IPS\Db::i()->select( '*', 'core_clubs_fieldvalues', array( 'club_id=?', $this->id ) )->first(); } catch ( \UnderflowException $e ) { return array(); } } /** * Cover Photo * * @param bool $getOverlay If FALSE, will not set the overlay, which saves queries if it will not be used (such as in clubCard) * @return \IPS\Helpers\CoverPhoto */ public function coverPhoto( $getOverlay=TRUE, $position='full' ) { $photo = new \IPS\Helpers\CoverPhoto; if ( $this->cover_photo ) { $photo->file = \IPS\File::get( 'core_Clubs', $this->cover_photo ); $photo->offset = $this->cover_offset; } if ( $getOverlay ) { $photo->overlay = \IPS\Theme::i()->getTemplate( 'clubs', 'core', 'front' )->coverPhotoOverlay( $this, $position ); } $photo->editable = $this->isLeader(); $photo->object = $this; return $photo; } /** * Produce a random hex color for a background * * @return string */ public function coverPhotoBackgroundColor() { return $this->staticCoverPhotoBackgroundColor( $this->name ); } /** * Location * * @return \IPS\GeoLocation|NULL */ public function location() { if ( $this->location_json ) { return \IPS\GeoLocation::buildFromJson( $this->location_json ); } return NULL; } /** * Is paid? * * @return string|NULL */ public function isPaid() { if ( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on and $this->fee ) { return TRUE; } return FALSE; } /** * Message to explain paid club joining process * * @return string */ public function memberFeeMessage() { if ( $this->type === static::TYPE_CLOSED ) { return \IPS\Member::loggedIn()->language()->addToStack( 'club_closed_join_fee', FALSE, array( 'sprintf' => array( $this->priceBlurb() ) ) ); } else { return \IPS\Member::loggedIn()->language()->addToStack( 'club_open_join_fee', FALSE, array( 'sprintf' => array( $this->priceBlurb() ) ) ); } } /** * Joining fee * * @param string|NULL $currency Desired currency, or NULL to choose based on member's chosen currency * @return \IPS\nexus\Money|NULL */ public function joiningFee( $currency = NULL ) { if ( $this->isPaid() ) { if ( !$currency ) { $currency = ( isset( $_SESSION['currency'] ) and in_array( $_SESSION['currency'], \IPS\nexus\Money::currencies() ) ) ? $_SESSION['currency'] : \IPS\nexus\Customer::loggedIn()->defaultCurrency(); } $costs = json_decode( $this->fee, TRUE ); if ( is_array( $costs ) and isset( $costs[ $currency ]['amount'] ) and $costs[ $currency ]['amount'] ) { return new \IPS\nexus\Money( $costs[ $currency ]['amount'], $currency ); } } return NULL; } /** * Joining fee * * @param string|NULL $currency Desired currency, or NULL to choose based on member's chosen currency * @return \IPS\nexus\Money|NULL * @throws \OutOfRangeException */ public function renewalTerm( $currency = NULL ) { if ( $this->renewal_price and $renewalPrices = json_decode( $this->renewal_price, TRUE ) ) { if ( !$currency ) { $currency = ( isset( $_SESSION['currency'] ) and in_array( $_SESSION['currency'], \IPS\nexus\Money::currencies() ) ) ? $_SESSION['currency'] : \IPS\nexus\Customer::loggedIn()->defaultCurrency(); } if ( isset( $renewalPrices[ $currency ] ) ) { return new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $renewalPrices[ $currency ]['amount'], $currency ), new \DateInterval( 'P' . $this->renewal_term . mb_strtoupper( $this->renewal_units ) ) ); } else { throw new \OutOfRangeException; } } return NULL; } /** * Price Blurb * * @return string|NULL */ public function priceBlurb() { if ( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on ) { if ( $this->isPaid() ) { if ( $fee = $this->joiningFee() ) { try { $renewalTerm = $this->renewalTerm( $fee->currency ); if ( $renewalTerm->cost->amount == $fee->amount ) { return $renewalTerm; } else { return \IPS\Member::loggedIn()->language()->addToStack( 'club_fee_plus_renewal', FALSE, array( 'sprintf' => array( $fee, $renewalTerm ) ) ); } } catch ( \OutOfRangeException $e ) { return \IPS\Member::loggedIn()->language()->addToStack('club_paid_unavailable'); } } else { return \IPS\Member::loggedIn()->language()->addToStack('club_paid_unavailable'); } } else { return \IPS\Member::loggedIn()->language()->addToStack('club_membership_free'); } } return NULL; } /* !Manage Memberships */ /** * Get members * * @param array $statuses The membership statuses to get * @param int $limit Number to get * @param string $order ORDER BY clause * @param int $returnType 0 = core_clubs_memberships rows, 1 = core_clubs_memberships plus \IPS\Member::columnsForPhoto(), 2 = full core_members rows, 3 = same as 1 but also getting name of adder/invitee, 4 = count only, 5 = same as 3 but also getting expire date * @return \IPS\Db\Select|int */ public function members( $statuses = array( 'member', 'moderator', 'leader' ), $limit = 25, $order = 'core_clubs_memberships.joined ASC', $returnType = 1 ) { if ( $returnType === 4 ) { return \IPS\Db::i()->select( 'COUNT(*)', 'core_clubs_memberships', array( array( 'club_id=?', $this->id ), array( \IPS\Db::i()->in( 'status', $statuses ) ) ) )->first(); } else { if ( $returnType === 2 ) { $columns = 'core_members.*'; } else { $columns = 'core_clubs_memberships.member_id,core_clubs_memberships.joined,core_clubs_memberships.status,core_clubs_memberships.added_by,core_clubs_memberships.invited_by'; if ( $returnType === 1 or $returnType === 3 or $returnType === 5 ) { $columns .= ',' . implode( ',', array_map( function( $column ) { return 'core_members.' . $column; }, \IPS\Member::columnsForPhoto() ) ); } if ( $returnType === 3 or $returnType === 5 ) { $columns .= ',added_by.name,invited_by.name'; if ( $returnType === 5 and \IPS\Application::appIsEnabled('nexus') and \IPS\Settings::i()->clubs_paid_on and $this->isPaid() and $this->renewal_price ) { $columns .= ',nexus_purchases.ps_active,nexus_purchases.ps_expire'; } } } $select = \IPS\Db::i()->select( $columns, 'core_clubs_memberships', array( array( 'club_id=?', $this->id ), array( \IPS\Db::i()->in( 'status', $statuses ) ) ), $order, $limit, NULL, NULL, \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS + \IPS\Db::SELECT_MULTIDIMENSIONAL_JOINS ); } if ( $returnType === 1 or $returnType === 2 or $returnType === 3 or $returnType === 5 ) { $select->join( 'core_members', 'core_members.member_id=core_clubs_memberships.member_id' ); } if ( $returnType === 3 or $returnType === 5 ) { $select->join( array( 'core_members', 'added_by' ), 'added_by.member_id=core_clubs_memberships.added_by' ); $select->join( array( 'core_members', 'invited_by' ), 'invited_by.member_id=core_clubs_memberships.invited_by' ); if ( $returnType === 5 and \IPS\Application::appIsEnabled('nexus') and \IPS\Settings::i()->clubs_paid_on and $this->isPaid() and $this->renewal_price ) { $select->join( 'nexus_purchases', array( 'ps_app=? AND ps_type=? AND ps_member=core_clubs_memberships.member_id AND ps_item_id=? AND ps_cancelled=0', 'core', 'club', $this->id ) ); } } return $select; } /** * @brief Cache of randomTenMembers() */ protected $_randomTenMembers = NULL; /** * Get basic data of a random ten members in the club (for cards) * * @return array */ public function randomTenMembers() { if ( !isset( $this->_randomTenMembers ) ) { $this->_randomTenMembers = iterator_to_array( $this->members( array( 'leader', 'moderator', 'member' ), 10, 'RAND()' ) ); } return $this->_randomTenMembers; } /** * Add a member * * @param \IPS\Member $member The member * @param bool $status Status * @param bool $update Update membership if already a member? * @param \IPS\Member|NULL $addedBy The leader who added them, or NULL if joining themselves * @param \IPS\Member|NULL $invitedBy The member who invited them, or NULL if joining themselves * @param bool $updateJoinedDate Whether to update the joined date or not (FALSE by default, set to TRUE when an invited member accepts) * @return void * @throws \OverflowException Member is already in the club and $update was FALSE */ public function addMember( \IPS\Member $member, $status = 'member', $update = FALSE, \IPS\Member $addedBy = NULL, \IPS\Member $invitedBy = NULL, $updateJoinedDate = FALSE ) { try { \IPS\Db::i()->insert( 'core_clubs_memberships', array( 'club_id' => $this->id, 'member_id' => $member->member_id, 'joined' => time(), 'status' => $status, 'added_by' => $addedBy ? $addedBy->member_id : NULL, 'invited_by'=> $invitedBy ? $invitedBy->member_id : NULL ) ); $member->rebuildPermissionArray(); if ( \IPS\Settings::i()->club_nodes_in_apps ) { $member->create_menu = NULL; $member->save(); } } catch ( \IPS\Db\Exception $e ) { if ( $e->getCode() === 1062 ) { if ( $update ) { $save = array( 'status' => $status ); if ( $addedBy ) { $save['added_by'] = $addedBy->member_id; } if( $updateJoinedDate === TRUE ) { $save['joined'] = time(); } \IPS\Db::i()->update( 'core_clubs_memberships', $save, array( 'club_id=? AND member_id=?', $this->id, $member->member_id ) ); $member->rebuildPermissionArray(); if ( \IPS\Settings::i()->club_nodes_in_apps ) { $member->create_menu = NULL; $member->save(); } } else { throw new \OverflowException; } } else { throw $e; } } } /** * Remove a member * * @param \IPS\Member $member The member * @return void */ public function removeMember( \IPS\Member $member ) { \IPS\Db::i()->delete( 'core_clubs_memberships', array( 'club_id=? AND member_id=?', $this->id, $member->member_id ) ); $member->rebuildPermissionArray(); if ( \IPS\Settings::i()->club_nodes_in_apps ) { $member->create_menu = NULL; $member->save(); } } /** * Recount members * * @return void */ public function recountMembers() { $this->members = \IPS\Db::i()->select( 'COUNT(*)', 'core_clubs_memberships', array( 'club_id=? AND ( status=? OR status=? OR status=? OR status=? OR status=? )', $this->id, static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER, static::STATUS_EXPIRED, static::STATUS_EXPIRED_MODERATOR ) ); $this->save(); } /* !Manage Nodes */ /** * Get available features * * @param \IPS\Member|NULL If a member object is provided, will opnly get the types that member can create * @return array */ public static function availableNodeTypes( \IPS\Member $member = NULL ) { $return = array(); foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter' ) as $contentRouter ) { foreach ( $contentRouter->classes as $class ) { if ( isset( $class::$containerNodeClass ) and \IPS\IPS::classUsesTrait( $class::$containerNodeClass, 'IPS\Content\ClubContainer' ) ) { if ( $member === NULL or $member->group['g_club_allowed_nodes'] === '*' or in_array( $class::$containerNodeClass, explode( ',', $member->group['g_club_allowed_nodes'] ) ) ) { $return[] = $class::$containerNodeClass; } } } } return array_unique( $return ); } /** * @brief Cached nodes */ protected $cachedNodes = NULL; /** * Get Node names and URLs * * @return array */ public function nodes() { if( $this->cachedNodes === NULL ) { $this->cachedNodes = array(); foreach ( \IPS\Db::i()->select( '*', 'core_clubs_node_map', array( 'club_id=?', $this->id ) ) as $row ) { $class = $row['node_class']; $classBits = explode( '\\', $class ); if( !\IPS\Application::load( $classBits[1] )->_enabled ) { continue; } $this->cachedNodes[ $row['id'] ] = array( 'name' => $row['name'], 'url' => \IPS\Http\Url::internal( $class::$urlBase . $row['node_id'], 'front', $class::$urlTemplate, array( \IPS\Http\Url\Friendly::seoTitle( $row['name'] ) ) ), 'node_class' => $row['node_class'], 'node_id' => $row['node_id'], ); } } return $this->cachedNodes; } /* !Permissions */ /** * Load and check permissions * * @param mixed $id ID * @return static * @throws \OutOfRangeException */ public static function loadAndCheckPerms( $id ) { $obj = static::load( $id ); if ( !$obj->canView() ) { throw new \OutOfRangeException; } return $obj; } /** * Can a member see this club and who's in it? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canView( \IPS\Member $member = NULL ) { $member = $member ?: \IPS\Member::loggedIn(); /* If we can't access the module, stop here */ if ( !$member->canAccessModule( \IPS\Application\Module::get( 'core', 'clubs', 'front' ) ) ) { return FALSE; } /* If it's not approved, only moderators and the person who created it can see it */ if ( \IPS\Settings::i()->clubs_require_approval and !$this->approved ) { return ( $member->modPermission('can_access_all_clubs') or ( $this->owner AND $member->member_id == $this->owner->member_id ) ); } /* Unless it's private, everyone can see it exists */ if ( $this->type !== static::TYPE_PRIVATE ) { return TRUE; } /* Moderators can see everything */ if ( $member->modPermission('can_access_all_clubs') ) { return TRUE; } /* Otherwise, only if they're a member or have been invited */ return in_array( $this->memberStatus( $member ), array( static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER, static::STATUS_INVITED, static::STATUS_INVITED_BYPASSING_PAYMENT, static::STATUS_EXPIRED, static::STATUS_EXPIRED_MODERATOR ) ); } /** * Can a member join (or ask to join) this club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canJoin( \IPS\Member $member = NULL ) { /* If it's not approved, nobody can join it */ if ( \IPS\Settings::i()->clubs_require_approval and !$this->approved ) { return FALSE; } /* Nobody can join public clubs */ if ( $this->type === static::TYPE_PUBLIC ) { return FALSE; } /* Guests cannot join clubs */ $member = $member ?: \IPS\Member::loggedIn(); if ( !$member->member_id ) { return FALSE; } /* If they're already a member, or have aleready asked to join, they can't join again */ $memberStatus = $this->memberStatus( $member ); if ( in_array( $memberStatus, array( static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER, static::STATUS_REQUESTED, static::STATUS_DECLINED, static::STATUS_EXPIRED, static::STATUS_EXPIRED_MODERATOR ) ) ) { return FALSE; } /* If they are banned, they cannot join */ if ( $memberStatus === static::STATUS_BANNED ) { return FALSE; } /* If it's private or read-only, they have to be invited */ if ( $this->type === static::TYPE_PRIVATE or $this->type === static::TYPE_READONLY ) { return in_array( $memberStatus, array( static::STATUS_INVITED, static::STATUS_INVITED_BYPASSING_PAYMENT ) ); } /* Otherwise they can join */ return TRUE; } /** * Can a member see the posts in this club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canRead( \IPS\Member $member = NULL ) { switch ( $this->type ) { case static::TYPE_PUBLIC: case static::TYPE_OPEN: case static::TYPE_READONLY: return TRUE; case static::TYPE_CLOSED: case static::TYPE_PRIVATE: $member = $member ?: \IPS\Member::loggedIn(); return $member->modPermission('can_access_all_clubs') or in_array( $this->memberStatus( $member ), array( static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER ) ); } } /** * Can a member participate this club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canPost( \IPS\Member $member = NULL ) { switch ( $this->type ) { case static::TYPE_PUBLIC: return TRUE; case static::TYPE_OPEN: case static::TYPE_CLOSED: case static::TYPE_PRIVATE: case static::TYPE_READONLY: $member = $member ?: \IPS\Member::loggedIn(); return $member->modPermission('can_access_all_clubs') or in_array( $this->memberStatus( $member ), array( static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER ) ); } } /** * Can a member invite other members * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canInvite( \IPS\Member $member = NULL ) { if ( \IPS\Settings::i()->clubs_require_approval and !$this->approved ) { return FALSE; } switch ( $this->type ) { case static::TYPE_PUBLIC: return FALSE; case static::TYPE_OPEN: $member = $member ?: \IPS\Member::loggedIn(); return $member->modPermission('can_access_all_clubs') or in_array( $this->memberStatus( $member ), array( static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER ) ); case static::TYPE_CLOSED: case static::TYPE_PRIVATE: case static::TYPE_READONLY: return $this->isLeader( $member ); } } /** * Does this user have leader permissions in the club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function isLeader( \IPS\Member $member = NULL ) { $member = $member ?: \IPS\Member::loggedIn(); return $member->modPermission('can_access_all_clubs') or $this->memberStatus( $member ) === static::STATUS_LEADER; } /** * Does this user have moderator permissions in the club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function isModerator( \IPS\Member $member = NULL ) { $member = $member ?: \IPS\Member::loggedIn(); return $member->modPermission('can_access_all_clubs') or in_array( $this->memberStatus( $member ), array( static::STATUS_MODERATOR, static::STATUS_LEADER ) ); } /** * @brief Membership status cache */ protected $_memberStatuses = array(); /** * Get status of a particular member * * @param \IPS\Member $member The member * @param int $returnType 1 will return a string with the type or NULL if not applicable. 2 will return array with status, joined, accepted_by, invited_by * @return mixed */ public function memberStatus( \IPS\Member $member, $returnType = 1 ) { if ( !$member->member_id ) { return NULL; } if ( !isset( $this->_memberStatuses[ $member->member_id ] ) or $returnType === 2 ) { try { $val = \IPS\Db::i()->select( $returnType === 2 ? '*' : array( 'status' ), 'core_clubs_memberships', array( 'club_id=? AND member_id=?', $this->id, $member->member_id ) )->first(); if ( $returnType === 2 ) { return $val; } else { $this->_memberStatuses[ $member->member_id ] = $val; } } catch ( \UnderflowException $e ) { $this->_memberStatuses[ $member->member_id ] = NULL; } } return $this->_memberStatuses[ $member->member_id ]; } /* ! Following */ const FOLLOW_PUBLIC = 1; const FOLLOW_ANONYMOUS = 2; /** * @brief Cache for current follow data, used on "My Followed Content" screen */ public $_followData; /** * @brief Application */ public static $application = 'core'; /** * Container Follower Count * * @return int */ public function followerCount() { return \IPS\Db::i()->select( 'COUNT(*)', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', 'core', 'club', $this->id ) )->first(); } /** * 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 ) { return static::_followers( 'club', $this->id, $privacy, $frequencyTypes, $date, $limit, $order, $flags ); } /* ! Utility */ /** * Remove nodes that are owned by a specific application. Used when uninstalling an app * * @param \IPS\Application $app The application being deleted * @return void */ public static function deleteByApplication( \IPS\Application $app ) { foreach( \IPS\Db::i()->select( 'node_class', 'core_clubs_node_map', NULL, NULL, NULL, 'node_class' ) as $class ) { if ( isset( $class::$contentItemClass ) ) { $contentItemClass = $class::$contentItemClass; if ( $contentItemClass::$application == $app->directory ) { \IPS\Db::i()->delete( 'core_clubs_node_map', array( 'node_class=?', $class ) ); } } } } }