<?php
/**
* @brief Club Model
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 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
{
/**
* @brief Club: public
*/
const TYPE_PUBLIC = 'public';
/**
* @brief Club: open
*/
const TYPE_OPEN = 'open';
/**
* @brief Club: closed
*/
const TYPE_CLOSED = 'closed';
/**
* @brief Club: private
*/
const TYPE_PRIVATE = 'private';
/**
* @brief Club: read-only
*/
const TYPE_READONLY = 'readonly';
/**
* @brief Status: member
*/
const STATUS_MEMBER = 'member';
/**
* @brief Status: invited
*/
const STATUS_INVITED = 'invited';
/**
* @brief Status: invited (bypassing payment)
*/
const STATUS_INVITED_BYPASSING_PAYMENT = 'invited_bypassing_payment';
/**
* @brief Status: requested
*/
const STATUS_REQUESTED = 'requested';
/**
* @brief Status: awaiting payment
*/
const STATUS_WAITING_PAYMENT = 'payment_pending';
/**
* @brief Status: expired
*/
const STATUS_EXPIRED = 'expired';
/**
* @brief Status: expired moderator
*/
const STATUS_EXPIRED_MODERATOR = 'expired_moderator';
/**
* @brief Status: declined
*/
const STATUS_DECLINED = 'declined';
/**
* @brief Status: banned
*/
const STATUS_BANNED = 'banned';
/**
* @brief Status: moderator
*/
const STATUS_MODERATOR = 'moderator';
/**
* @brief Status: leader
*/
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
* @param bool $countOnly Only return a count, instead of an iterator
* @return \IPS\Patterns\ActiveRecordIterator|array|int
*/
public static function clubs( ?\IPS\Member $member, $limit, $sortOption, $mineOnly=FALSE, $filters=array(), $extraWhere=NULL, $countOnly=FALSE )
{
$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 */
if( $countOnly )
{
$select = \IPS\Db::i()->select( 'COUNT(*)', 'core_clubs', $where );
}
else
{
$select = \IPS\Db::i()->select( '*', 'core_clubs', $where, ( $sortOption === 'name' ? "{$sortOption} ASC" : "{$sortOption} DESC" ), $limit );
}
foreach ( $joins as $join )
{
$select->join( $join[0], $join[1] );
}
if( $countOnly )
{
return $select->first();
}
$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 $owner The owner
* @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 creation 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', 'club_show_membertab' ),
\IPS\Member\Club::TYPE_CLOSED => array( 'club_membership_fee', 'club_show_membertab' ),
\IPS\Member\Club::TYPE_PRIVATE => array( 'club_membership_fee', 'club_show_membertab' ),
\IPS\Member\Club::TYPE_READONLY => array( 'club_membership_fee', 'club_show_membertab' ),
)
) ) );
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 ) );
$memberTabFieldPosition = '';
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(), function( $value ){
if ( \count( $value ) == 0 )
{
throw new \DomainException( 'form_required' );
}
foreach( $value as $currency => $fee )
{
if( !$fee->amount->isGreaterThanZero() )
{
throw new \DomainException( 'form_required' );
}
}
}, 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' ) );
$memberTabFieldPosition = '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', 'allowStockPhotos' => TRUE, '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, 'minimize' => ( $this->location_json ) ? FALSE : TRUE, 'preselectCountry' => FALSE ) ) );
}
$fieldValues = $this->fieldValues();
foreach ( \IPS\Member\Club\CustomField::roots() as $field )
{
if ( $field->type === 'Editor' )
{
if ( $field->allow_attachments AND !$new )
{
$field::$editorOptions = array_merge( $field::$editorOptions, array( 'attachIds' => array( $this->id, $field->id, NULL ), 'autoSaveKey' => "clubs-field{$field->id}-{$this->id}" ) );
}
}
$helper = $field->buildHelper( isset( $fieldValues["field_{$field->id}"] ) ? $fieldValues["field_{$field->id}"] : NULL );
if ( $field->type === 'Editor' )
{
if ( $field->allow_attachments AND $new )
{
$field::$editorOptions = array_merge( $field::$editorOptions, array( 'attachIds' => array( NULL ), 'autoSaveKey' => "clubs-field{$field->id}-new" ) );
}
}
$form->add( $helper );
}
if ( $acp or ( $new and \count( $availableTypes ) > 1 ) )
{
$form->add( new \IPS\Helpers\Form\Radio( 'club_show_membertab', ( $this AND $this->show_membertab ) ? $this->show_membertab : 'nonmember', TRUE, array( 'options' => array(
'nonmember' => 'club_membertab_everyone',
'member' => 'club_membertab_members',
'moderator' => 'club_membertab_moderators'
) ), NULL, NULL, NULL, 'club_show_membertab' ),$memberTabFieldPosition );
}
/* We want to show the member page configuration also while editing the club */
else if ( !$new )
{
$form->add( new \IPS\Helpers\Form\Radio( 'club_show_membertab', ( $this AND $this->show_membertab ) ? $this->show_membertab : 'nonmember', TRUE, array( 'options' => array(
'nonmember' => 'club_membertab_everyone',
'member' => 'club_membertab_members',
'moderator' => 'club_membertab_moderators'
) ), NULL, NULL, NULL, 'club_show_membertab' ), $memberTabFieldPosition );
}
$form->add( new \IPS\Helpers\Form\YesNo( 'show_rules', ( !$new AND $this->rules ) ? TRUE : FALSE, FALSE, array(
'togglesOn' => array(
'club_rules',
'club_rules_required'
)
) ) );
$form->add( new \IPS\Helpers\Form\Editor( 'club_rules', ( $this->rules ) ? $this->rules : NULL, FALSE, array(
'app' => 'core',
'key' => 'ClubRules',
'attachIds' => ( $new ) ? array( NULL, NULL, NULL ) : array( $this->id, NULL, 'rules' ),
'autoSaveKey' => ( $new ) ? "club-rules-new" : "club-rules-{$this->id}"
), NULL, NULL, NULL, 'club_rules' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'club_rules_required', $this->rules_required, FALSE, array( 'togglesOn' => array( 'club_rules_reacknowledge' ) ), NULL, NULL, NULL, 'club_rules_required' ) );
/* Only show this if we're editing a club */
if ( !$new )
{
$form->add( new \IPS\Helpers\Form\YesNo( 'club_rules_reacknowledge', FALSE, FALSE, [], NULL, NULL, NULL, 'club_rules_reacknowledge' ) );
}
if ( $values = $form->values() )
{
$this->name = $values['club_name'];
/* If there is only one type available, set it. */
if( \is_array( $availableTypes ) AND \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( $values['club_profile_photo'] )
{
$this->profile_photo_uncropped = (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;
}
}
if ( array_key_exists( 'club_show_membertab', $values ) )
{
$this->show_membertab = $values['club_show_membertab'];
}
else
{
/* Default is "Everybody" */
$this->show_membertab = "nonmember";
}
$this->save();
if ( $values['show_rules'] )
{
$this->rules = $values['club_rules'];
\IPS\File::claimAttachments( ( $new ) ? "club-rules-new" : "club-rules-{$this->id}", $this->id, NULL, 'rules' );
$this->rules_required = (bool) $values['club_rules_required'];
/* Do we need to reset the acknowledge flags? */
if ( !$new AND \array_key_exists( 'club_rules_reacknowledge', $values ) )
{
if ( $values['club_rules_reacknowledge'] )
{
\IPS\Db::i()->update( 'core_clubs_memberships', array( "rules_acknowledged" => FALSE ), array( "club_id=?", $this->id ) );
}
}
}
else
{
$this->rules = NULL;
$this->rules_required = FALSE;
if ( !$new )
{
/* If this isn't a new club, update all memberships in case they decide to re-add rules later on */
\IPS\Db::i()->update( "core_clubs_memberships", array( 'rules_acknowledged' => FALSE ), array( "club_id=?", $this->id ) );
}
}
if ( $new )
{
$this->addMember( \IPS\Member::loggedIn(), \IPS\Member\Club::STATUS_LEADER );
$this->acknowledgeRules( \IPS\Member::loggedIn() );
}
$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 )
{
$this->sendModeratorApprovalNotification();
}
if( $new and $this->approved )
{
\IPS\Api\Webhook::fire( 'club_created', $this );
}
else if( !$new AND isset( $this->changed['approved'] ) )
{
$this->onApprove();
}
return NULL;
}
return $form;
}
/**
* Send moderator notice of new club pending approval
*
* @param \IPS\Member|NULL $savingMember Member saving the club or NULL for currently logged in member
* @return void
*/
public function sendModeratorApprovalNotification( $savingMember = NULL )
{
$savingMember = $savingMember ?? \IPS\Member::loggedIn();
/* 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'] != $savingMember->member_id )
{
$notification->recipients->attach( \IPS\Member::constructFromData( $member ) );
}
}
if( \count( $notification->recipients ) )
{
$notification->send();
}
}
/**
* 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)
* @param string $position Position of cover photo
* @return \IPS\Helpers\CoverPhoto
*/
public function coverPhoto( $getOverlay=TRUE, $position='full' )
{
$photo = new \IPS\Helpers\CoverPhoto;
$photo->maxSize = \IPS\Settings::i()->club_max_cover;
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 bool
*/
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( \IPS\Request::i()->cookie['currency'] ) and \in_array( \IPS\Request::i()->cookie['currency'], \IPS\nexus\Money::currencies() ) ) ? \IPS\Request::i()->cookie['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;
}
/**
* Renewal 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( \IPS\Request::i()->cookie['currency'] ) and \in_array( \IPS\Request::i()->cookie['currency'], \IPS\nexus\Money::currencies() ) ) ? \IPS\Request::i()->cookie['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() )
{
/* Include tax? */
$taxRate = NULL;
if ( \IPS\Settings::i()->nexus_show_tax and \IPS\Settings::i()->clubs_paid_tax )
{
try
{
$taxRate = new \IPS\Math\Number( \IPS\nexus\Tax::load( \IPS\Settings::i()->clubs_paid_tax )->rate( \IPS\nexus\Customer::loggedIn()->estimatedLocation() ) );
}
catch ( \OutOfRangeException $e ) {}
}
if ( $taxRate )
{
$fee->amount = $fee->amount->add( $fee->amount->multiply( $taxRate ) );
}
try
{
$renewalTerm = $this->renewalTerm( $fee->currency );
if ( $renewalTerm and $taxRate )
{
$renewalTerm->cost->amount = $renewalTerm->cost->amount->add( $renewalTerm->cost->amount->multiply( $taxRate ) );
}
if ( !$renewalTerm )
{
return $fee;
}
else if ( $renewalTerm AND $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;
}
/**
* Generate invoice for a member
*
* @param \IPS\nexus\Customer|null $member Member to generate the invoice for
* @return \IPS\Http\Url
*/
public function generateInvoice( \IPS\nexus\Customer $member = NULL )
{
$member = $member ?: \IPS\nexus\Customer::loggedIn();
$fee = $this->joiningFee();
/* Create the item */
$item = new \IPS\core\extensions\nexus\Item\ClubMembership( $this->name, $fee );
$item->id = $this->id;
try
{
$item->tax = \IPS\Settings::i()->clubs_paid_tax ? \IPS\nexus\Tax::load( \IPS\Settings::i()->clubs_paid_tax ) : NULL;
}
catch ( \OutOfRangeException $e ) { }
if ( \IPS\Settings::i()->clubs_paid_gateways )
{
$item->paymentMethodIds = explode( ',', \IPS\Settings::i()->clubs_paid_gateways );
}
$item->renewalTerm = $this->renewalTerm( $fee->currency );
$item->payTo = $this->owner;
$item->commission = \IPS\Settings::i()->clubs_paid_commission;
if ( $fees = \IPS\Settings::i()->clubs_paid_transfee and isset( $fees[ $fee->currency ] ) )
{
$item->fee = new \IPS\nexus\Money( $fees[ $fee->currency ]['amount'], $fee->currency );
}
/* Generate the invoice */
$invoice = new \IPS\nexus\Invoice;
$invoice->currency = $fee->currency;
$invoice->member = $member;
$invoice->addItem( $item );
$invoice->return_uri = "app=core&module=clubs&controller=view&id={$this->id}";
$invoice->save();
return $invoice->checkoutUrl();
}
/* !Manage Memberships */
/**
* Get members
*
* @param array $statuses The membership statuses to get
* @param array|int $limit Rows to fetch or array( offset, limit )
* @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_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 string $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();
}
$this->memberStatuses[ $member->member_id ] = $status;
}
catch ( \IPS\Db\Exception $e )
{
if ( $e->getCode() === 1062 )
{
if ( $update )
{
$save = array( 'status' => $status );
if ( $addedBy )
{
$save['added_by'] = $addedBy->member_id;
}
if ( $invitedBy )
{
$save['invited_by'] = $invitedBy->member_id;
}
if( $updateJoinedDate === TRUE )
{
$save['joined'] = time();
}
/* Log to Member History */
if ( \in_array( $status, array( \IPS\Member\Club::STATUS_MEMBER, \IPS\Member\Club::STATUS_BANNED ) ) )
{
$memberStatus = $this->memberStatus( $member, 2 );
/* Joining by invite */
if ( $memberStatus['status'] == \IPS\Member\Club::STATUS_INVITED )
{
$addedBy = \IPS\Member::load( $memberStatus['invited_by'] );
}
$member->logHistory( 'core', 'club_membership', array('club_id' => $this->id, 'type' => $status ), $addedBy );
}
\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;
}
}
$params = [
'club' => $this,
'member' => $member,
'status' => $status
];
\IPS\Api\Webhook::fire( 'club_member_added', $params );
/* Achievements */
if( \in_array( $status, array( static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER, static::STATUS_EXPIRED, static::STATUS_EXPIRED_MODERATOR ) ) )
{
$member->achievementAction( 'core', 'JoinClub', $this );
}
}
/**
* Send an invitation to a member
*
* @param \IPS\Member $inviter Person doing the inviting
* @param array $members Array of members being invited
* @return void
*/
public function sendInvitation( \IPS\Member $inviter, $members )
{
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'club_invitation', $this, array( $this, $inviter ), array( 'invitedBy' => $inviter->member_id ) );
foreach ( $members as $member )
{
if ( $member instanceof \IPS\Member )
{
$memberStatus = $this->memberStatus( $member );
if ( !$memberStatus or \in_array( $memberStatus, array( \IPS\Member\Club::STATUS_INVITED, \IPS\Member\Club::STATUS_REQUESTED, \IPS\Member\Club::STATUS_DECLINED, \IPS\Member\Club::STATUS_BANNED ) ) )
{
$notification->recipients->attach( $member );
}
}
}
$notification->send();
}
/**
* 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 ) );
\IPS\Api\Webhook::fire( 'club_member_removed', ['club' => $this, 'member' => $member] );
$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 ) )->first();
$this->save();
}
/* !Manage Nodes */
/**
* Get available features
*
* @param \IPS\Member|NULL $member If a member object is provided, will only 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 );
}
/**
* Get Pages
*
* @return array
*/
public function pages(): array
{
$return = array();
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_club_pages', array( "page_club=?", $this->id ) ), 'IPS\Member\Club\Page' ) AS $row )
{
$return['page-' . $row->id] = $row;
}
return $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;
}
try
{
$this->cachedNodes[ $row['id'] ] = array(
'name' => $row['name'],
'url' => $row['node_class']::load( $row['node_id'] )->url(),
'node_class' => $row['node_class'],
'node_id' => $row['node_id'],
'public' => $row['public']
);
}
catch( \OutOfRangeException $e )
{
\IPS\Log::log( 'Missing club node ' . $row['node_class'] . ' ' . $row['node_id'] . " is being loaded.", 'club_nodes');
}
}
}
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 permissions to manage the navigation
*
* @param \IPS\Member $member The member (NULL for currently logged in member)
* @return bool
*/
public function canManageNavigation( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
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
*/
public $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 ( !array_key_exists( $member->member_id, $this->memberStatuses ) 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 */
/**
* @brief Following publicly
*/
const FOLLOW_PUBLIC = 1;
/**
* @brief Following anonymously
*/
const FOLLOW_ANONYMOUS = 2;
/**
* @brief Cache for current follow data, used on "My Followed Content" screen
*/
public $_followData;
/**
* @brief Application
*/
public static $application = 'core';
/**
* Followers Count
*
* @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
* @return int
*/
public function followersCount( $privacy=3, $frequencyTypes=array( 'none', 'immediate', 'daily', 'weekly' ), $date=NULL )
{
/* Return the count */
return static::_followersCount( 'club', $this->id, $privacy, $frequencyTypes, $date );
}
/**
* 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
* @return \IPS\Db\Select|NULL
* @throws \BadMethodCallException
*/
public function followers( $privacy=3, $frequencyTypes=array( 'none', 'immediate', 'daily', 'weekly' ), $date=NULL, $limit=array( 0, 25 ), $order=NULL )
{
return static::_followers( 'club', $this->id, $privacy, $frequencyTypes, $date, $limit, $order );
}
/* ! Utility */
/**
* [ActiveRecord] Delete Record
*
* @return void
*/
public function delete()
{
parent::delete();
\IPS\Api\Webhook::fire( 'club_deleted', $this );
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_club_pages', array( 'page_club=?', $this->id ) ), 'IPS\Member\Club\Page' ) as $page )
{
$page->delete( FALSE );
}
$this->coverPhoto( FALSE )->delete();
}
/**
* 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 ) );
}
}
}
}
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse int id ID number
* @apiresponse string name Name
* @apiresponse string url URL to the club
* @apiresponse string type Type of club (public, open, closed, private, readonly)
* @clientapiresponse bool approved Whether the club is approved or not
* @apiresponse datetime created Datetime the club was created
* @apiresponse int memberCount Number of members in the club
* @apiresponse \IPS\Member owner Club owner
* @apiresponse string|null photo URL to the club's profile photo
* @apiresponse bool paid Whether the club is paid or not
* @apiresponse bool featured Whether the club is featured or not
* @apiresponse \IPS\GeoLocation|NULL location Geolocation object representing the club's location, or NULL if no location is available
* @apiresponse string about Club 'about' information supplied by owner
* @apiresponse datetime lastActivity Datetime of last activity within the club
* @apiresponse int contentCount Count of all content items + comments in the club
* @apiresponse string|NULL coverPhotoUrl URL to the club's cover photo, or NULL if no cover photo is available
* @apiresponse string coverOffset Cover photo offset
* @apiresponse string coverPhotoColor Cover photo overlay background color
* @apiresponse [\IPS\Member] members Club members
* @apiresponse [\IPS\Member] leaders Club leaders
* @apiresponse [\IPS\Member] moderators Club moderators
* @apiresponse [\IPS\core\ProfileFields\Api\Field] fieldValues Club's custom field values
* @apiresponse [\IPS\Node\Model] nodes Nodes created for this club
* @apiresponse \IPS\nexus\Money|null joiningFee Cost to join the club, or null if there is no cost
* @apiresponse \IPS\nexus\Purchase\RenewalTerm|null renewalTerm Renewal term for the club, or null if there are no renewals
* @note When trying to determine all users who can access the club, the owner object should be combined with all leaders, moderators and members. Only up to 250 members will be returned (sorted by most recently joining the club) but the full member count can be seen with the memberCount property.
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
$coverPhoto = NULL;
if ( $this->cover_photo )
{
$coverPhoto = (string) \IPS\File::get( 'core_Clubs', $this->cover_photo )->url;
}
$members = array();
$leaders = array();
$moderators = array();
foreach( $this->members( array( 'member', 'moderator', 'leader' ), 250, 'core_clubs_memberships.joined DESC', 2 ) as $member )
{
$member = \IPS\Member::constructFromData( $member );
if( $this->owner != $member )
{
if( $this->isLeader( $member ) )
{
$leaders[] = $member->apiOutput();
}
elseif( $this->isModerator( $member ) )
{
$moderators[] = $member->apiOutput();
}
else
{
$members[] = $member->apiOutput();
}
}
}
$customFields = array();
$fieldValues = $this->fieldValues();
if( \IPS\Member\Club\CustomField::roots() )
{
foreach( \IPS\Member\Club\CustomField::roots() as $field )
{
if( isset( $fieldValues['field_' . $field->id ] ) )
{
$fieldObject = new \IPS\core\ProfileFields\Api\Field( \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->get( 'core_clubfield_' . $field->id ), $fieldValues[ 'field_' . $field->id ] );
$customFields[] = $fieldObject->apiOutput( $authorizedMember );
}
}
}
$return = array(
'id' => $this->id,
'name' => $this->name,
'url' => (string) $this->url(),
'type' => $this->type,
'created' => $this->created->rfc3339(),
'memberCount' => $this->members,
'owner' => $this->owner ? $this->owner->apiOutput() : NULL,
'photo' => $this->profile_photo ? (string) \IPS\File::get( 'core_Clubs', $this->profile_photo )->url : NULL,
'featured' => (bool) $this->featured,
'paid' => (bool) $this->isPaid(),
'location' => ( $location = $this->location() ) ? $location->apiOutput() : NULL,
'about' => $this->about,
'lastActivity' => \IPS\DateTime::ts( $this->last_activity )->rfc3339(),
'contentCount' => $this->content,
'coverPhotoUrl' => $coverPhoto,
'coverOffset' => $this->cover_offset,
'coverPhotoColor' => $this->coverPhotoBackgroundColor(),
'members' => $members,
'leaders' => $leaders,
'moderators' => $moderators,
'fieldValues' => $customFields,
'nodes' => array_map( function( $node ){
$node['url'] = (string) $node['url'];
$node['id'] = $node['node_id'];
$node['class'] = $node['node_class'];
$node['public'] = $node['public'];
unset( $node['node_id'], $node['node_class'] );
return $node;
}, $this->nodes() ),
);
if( !$authorizedMember )
{
$return['approved'] = (bool) $this->approved;
}
if ( \IPS\Application::appIsEnabled( 'nexus' ) )
{
$defaultCurrency = $authorizedMember ? \IPS\nexus\Customer::load( $authorizedMember->member_id )->defaultCurrency() : ( new \IPS\nexus\Customer )->defaultCurrency();
$return['joiningFee'] = $this->joiningFee( $defaultCurrency ) ? $this->joiningFee( $defaultCurrency )->apiOutput() : NULL;
try
{
$return['renewalTerm'] = $this->renewalTerm( $defaultCurrency ) ? $this->renewalTerm( $defaultCurrency )->apiOutput() : NULL;
}
catch( \OutOfRangeException $e )
{
$return['renewalTerm'] = NULL;
}
}
else
{
$return['joiningFee'] = NULL;
$return['renewalTerm'] = NULL;
}
return $return;
}
/**
* @brief Cached tabs
*/
static $tabs = NULL;
/**
* Get the club navbar tabs
*
* @param \IPS\Node\Model|NULL $container Container
* @return array
*/
public function tabs( \IPS\Node\Model $container = NULL )
{
if ( !static::$tabs )
{
$tabs = array();
$tabs[ 'club_home' ] = array( 'href' => $this->url()->setQueryString('do', 'overview'), 'title' => \IPS\Member::loggedIn()->language()->addToStack( 'club_home' ), 'isActive' => ( \IPS\Request::i()->module == 'clubs' AND \IPS\Request::i()->do == 'overview' ) ? TRUE : FALSE );
if ( $this->canViewMembers() )
{
$tabs['club_members'] = array( 'href' => $this->url()->setQueryString('do', 'members'), 'title' => \IPS\Member::loggedIn()->language()->addToStack( 'club_members' ), 'isActive' => ( \IPS\Request::i()->module == 'clubs' AND \IPS\Request::i()->do == 'members' ) ? TRUE : FALSE );
}
foreach( $this->nodes() as $nodeID => $node )
{
if ( $this->canRead() or $node['public'] )
{
$tabs[$nodeID] = array( 'href' => $node['url'] , 'title' => $node['name'], 'isActive' => ( isset( $container ) AND \get_class( $container ) === $node['node_class'] and $container->_id == $node['node_id'] ) ? TRUE : FALSE );
}
}
foreach( $this->pages() AS $pageId => $page )
{
if ( $page->canView() )
{
$tabs[$pageId] = array( 'href' => $page->url(), 'title' => $page->title, 'isActive' => ( \IPS\Request::i()->module == 'clubs' AND \IPS\Request::i()->controller == 'page' AND \IPS\Request::i()->id == $page->id ) );
}
}
$tabs = $this->_tabs( $tabs, $container );
$changed = FALSE;
if ( $this->menu_tabs AND $this->menu_tabs != "" )
{
$order = array_values( json_decode( $this->menu_tabs , TRUE ) );
uksort( $tabs, function( $a, $b ) use ( $order, &$changed ) {
if ( \in_array( $a, $order ) and \in_array( $b, $order ) )
{
return ( array_search( $a, $order ) > array_search( $b, $order ) ? 1 : -1 );
}
elseif ( !\in_array( $b, $order) )
{
/* A new node was added, attach it to the end */
$changed = TRUE;
return -1;
}
else
{
return 0;
}
} );
}
/* If none of the tabs are active, set the first one as active */
$hasActive = FALSE;
foreach( $tabs as $tab )
{
if( $tab['isActive'] )
{
$hasActive = TRUE;
break;
}
}
if( !$hasActive )
{
reset( $tabs );
$first = key( $tabs );
$tabs[ $first ]['isActive'] = TRUE;
}
if ( $changed )
{
$this->menu_tabs = json_encode( array_keys( $tabs ) );
$this->save();
}
static::$tabs = $tabs;
}
return static::$tabs;
}
/**
* Can a member view the members page
*
* @param \IPS\Member|null $member The member (NULL for currently logged in member)
* @return bool
*/
public function canViewMembers( \IPS\Member $member = NULL ): bool
{
$member = $member ?: \IPS\Member::loggedIn();
/* Public Clubs have no member list */
if ( $this->type == \IPS\Member\Club::TYPE_PUBLIC )
{
return FALSE;
}
/* If NULL, everyone can view */
if ( $this->show_membertab === NULL )
{
return TRUE;
}
/* Leader can see it always*/
if ( $this->memberStatus( $member ) === \IPS\Member\Club::STATUS_LEADER )
{
return TRUE;
}
/* Moderator */
if ( $this->show_membertab == 'moderator' AND ( $this->memberStatus( $member ) === \IPS\Member\Club::STATUS_MODERATOR ) )
{
return TRUE;
}
/* Members */
if ( $this->show_membertab == 'member' AND \in_array( $this->memberStatus( $member ), array( \IPS\Member\Club::STATUS_MEMBER, \IPS\Member\Club::STATUS_INVITED, \IPS\Member\Club::STATUS_INVITED_BYPASSING_PAYMENT, \IPS\Member\Club::STATUS_EXPIRED, \IPS\Member\Club::STATUS_EXPIRED_MODERATOR ) ) )
{
return TRUE;
}
if ( $this->show_membertab == 'nonmember' )
{
return TRUE;
}
return FALSE;
}
/**
* Can be used by 3rd parties to add own club navigation tabs before they get sorted
*
* @param array $tabs Tabs
* @param \IPS\Node\Model|NULL $container Container
* @return array
*/
protected function _tabs( array $tabs, \IPS\Node\Model $container = NULL ) : array
{
return $tabs;
}
/**
* Get the first tab for the club page
*
* @return mixed
*/
public function firstTab()
{
$tabs = $this->tabs();
reset( $tabs );
$first = key( $tabs );
return array( $first => $tabs[ $first ] );
}
/**
* Number of members to show per page
*
* @return int
*/
public function membersPerPage()
{
return 24;
}
/**
* Set navigational breadcrumbs
*
* @param \IPS\Node\Model $node The node we are viewing
* @return void
*/
public function setBreadcrumbs( $node )
{
\IPS\core\FrontNavigation::$clubTabActive = TRUE;
\IPS\Output::i()->breadcrumb = array();
\IPS\Output::i()->breadcrumb[] = array( \IPS\Http\Url::internal( 'app=core&module=clubs&controller=directory', 'front', 'clubs_list' ), \IPS\Member::loggedIn()->language()->addToStack('module__core_clubs') );
/* We have to prime the cache to ensure the correct club tab is selected */
$this->tabs( $node );
if( !( $firstTab = $this->firstTab() AND $firstTab = array_pop( $firstTab ) ) OR (string) $firstTab['href'] != (string) $node->url() )
{
\IPS\Output::i()->breadcrumb[] = array( $this->url(), $this->name );
\IPS\Output::i()->breadcrumb[] = array( NULL, $node->_title );
}
else
{
\IPS\Output::i()->breadcrumb[] = array( NULL, $this->name );
}
if ( \IPS\Settings::i()->clubs_header == 'sidebar' )
{
\IPS\Output::i()->sidebar['contextual'] = \IPS\Theme::i()->getTemplate( 'clubs', 'core' )->header( $this, $node, 'sidebar' );
}
/* CSS */
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/clubs.css', 'core', 'front' ) );
if ( \IPS\Theme::i()->settings['responsive'] )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/clubs_responsive.css', 'core', 'front' ) );
}
/* JS */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_clubs.js', 'core', 'front' ) );
}
/**
* Helper method to determine if the member is in a club
*
* @return bool
*/
public static function userIsInClub() : bool
{
return \IPS\core\FrontNavigation::$clubTabActive or ( \IPS\Dispatcher::i()->application->directory === 'core' and \IPS\Dispatcher::i()->module->key === 'clubs' );
}
/* ! Rules */
/**
* Rules have been acknowledged
*
* @param \IPS\Member|NULL $member Member to check, or NULL for currently logged in member.
* @return bool
*/
public function rulesAcknowledged( ?\IPS\Member $member = NULL ): bool
{
/* Rules must be acknowledged? */
if ( !$this->rules_required )
{
return TRUE;
}
$member = $member ?: \IPS\Member::loggedIn();
/* Owners are exempt. */
if ( $this->owner == $member )
{
return TRUE;
}
/* Leaders and Moderators are exempt. */
if ( \in_array( $this->memberStatus( $member ), array( static::STATUS_LEADER, static::STATUS_MODERATOR, static::STATUS_EXPIRED_MODERATOR ) ) )
{
return TRUE;
}
try
{
return (bool) \IPS\Db::i()->select( 'rules_acknowledged', 'core_clubs_memberships', array( "club_id=? AND member_id=?", $this->id, $member->member_id ) )->first();
}
catch( \UnderflowException $e )
{
/* If we can join the club return FALSE so we see the acknowledgement form. If we can't join, just return TRUE so the rules are returned but the user is not prompted to accept them. */
return !$this->canJoin( $member );
}
}
/**
* Acknowledge the rules
*
* @param \IPS\Member|NULL $member Member to set, or NULL for currently logged in member.
* @return void
* @throws \InvalidArgumentException
*/
public function acknowledgeRules( ?\IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
if ( $this->memberStatus( $member ) === NULL )
{
throw new \InvalidArgumentException;
}
\IPS\Db::i()->update( 'core_clubs_memberships', array( 'rules_acknowledged' => TRUE ), array( "club_id=? AND member_id=?", $this->id, $member->member_id ) );
}
/**
* Called when a club requiring moderation gets approved
*
* @return void
*/
public function onApprove()
{
\IPS\Api\Webhook::fire( 'club_created', $this );
$this->owner->achievementAction( 'core', 'NewClub', $this );
}
}