Version 4.2.0
This commit is contained in:
1 parent
0dc2aee5ed
commit
96997ddd8e
2074 files changed
+125454
-55780
No files matched your search
@@ -0,0 +1,914 @@
|
||||
<?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
|
||||
{
|
||||
const TYPE_PUBLIC = 'public';
|
||||
const TYPE_OPEN = 'open';
|
||||
const TYPE_CLOSED = 'closed';
|
||||
const TYPE_PRIVATE = 'private';
|
||||
|
||||
const STATUS_MEMBER = 'member';
|
||||
const STATUS_INVITED = 'invited';
|
||||
const STATUS_REQUESTED = 'requested';
|
||||
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';
|
||||
|
||||
/* !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 $mineOnly Limit to clubs a particular member has joined (TRUE to use the same value as $member)
|
||||
* @param array $filters Custom field filters
|
||||
* @param mixed $extraWhere Additional WHERE clause
|
||||
* @return \IPS\Patterns\ActiveRecordIterator
|
||||
*/
|
||||
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::TYPE_PRIVATE );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( 'type<>?', static::TYPE_PRIVATE );
|
||||
}
|
||||
}
|
||||
|
||||
/* Restrict to clubs we have joined */
|
||||
if ( $mineOnly )
|
||||
{
|
||||
$mineOnly = ( $mineOnly === TRUE ) ? $member : $mineOnly;
|
||||
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 . "')" );
|
||||
}
|
||||
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 . "')" );
|
||||
}
|
||||
}
|
||||
|
||||
/* 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()->in( "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,
|
||||
) ) ) );
|
||||
|
||||
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 ) );
|
||||
|
||||
$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'];
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
$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();
|
||||
$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 ) { }
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location
|
||||
*
|
||||
* @return \IPS\GeoLocation|NULL
|
||||
*/
|
||||
public function location()
|
||||
{
|
||||
if ( $this->location_json )
|
||||
{
|
||||
return \IPS\GeoLocation::buildFromJson( $this->location_json );
|
||||
}
|
||||
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
|
||||
* @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 )
|
||||
{
|
||||
$columns .= ',' . implode( ',', array_map( function( $column ) {
|
||||
return 'core_members.' . $column;
|
||||
}, \IPS\Member::columnsForPhoto() ) );
|
||||
}
|
||||
if ( $returnType === 3 )
|
||||
{
|
||||
$columns .= ',added_by.name,invited_by.name';
|
||||
}
|
||||
}
|
||||
|
||||
$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 )
|
||||
{
|
||||
$select->join( 'core_members', 'core_members.member_id=core_clubs_memberships.member_id' );
|
||||
}
|
||||
if ( $returnType === 3 )
|
||||
{
|
||||
$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' );
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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=? )', $this->id, static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER ) );
|
||||
$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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Node names and URLs
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function nodes()
|
||||
{
|
||||
$return = array();
|
||||
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_clubs_node_map', array( 'club_id=?', $this->id ) ) as $row )
|
||||
{
|
||||
$class = $row['node_class'];
|
||||
|
||||
$return[ $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 $return;
|
||||
}
|
||||
|
||||
/* !Permissions */
|
||||
|
||||
/**
|
||||
* 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 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 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ) ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* If they are banned, they cannot join */
|
||||
if ( $memberStatus === static::STATUS_BANNED )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* If it's private, they have to be invited */
|
||||
if ( $this->type === static::TYPE_PRIVATE )
|
||||
{
|
||||
return $memberStatus === static::STATUS_INVITED;
|
||||
}
|
||||
|
||||
/* 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:
|
||||
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:
|
||||
$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:
|
||||
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 ];
|
||||
}
|
||||
|
||||
/* ! 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 ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Clubs Customer Field Node
|
||||
* @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
|
||||
* @subpackage Nexus
|
||||
* @since 3 Mar 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Member\Club;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clubs Customer Field Node
|
||||
*/
|
||||
class _CustomField extends \IPS\CustomField
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_clubs_fields';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'f_';
|
||||
|
||||
/**
|
||||
* @brief [Node] Order Database Column
|
||||
*/
|
||||
public static $databaseColumnOrder = 'position';
|
||||
|
||||
/**
|
||||
* @brief [CustomField] Title/Description lang prefix
|
||||
*/
|
||||
protected static $langKey = 'core_clubfield';
|
||||
|
||||
/**
|
||||
* @brief [CustomField] Content database table
|
||||
*/
|
||||
protected static $contentDatabaseTable = 'core_clubs_fieldvalues';
|
||||
|
||||
/**
|
||||
* @brief [Node] ACP Restrictions
|
||||
* @code
|
||||
array(
|
||||
'app' => 'core', // The application key which holds the restrictrions
|
||||
'module' => 'foo', // The module key which holds the restrictions
|
||||
'map' => array( // [Optional] The key for each restriction - can alternatively use "prefix"
|
||||
'add' => 'foo_add',
|
||||
'edit' => 'foo_edit',
|
||||
'permissions' => 'foo_perms',
|
||||
'delete' => 'foo_delete'
|
||||
),
|
||||
'all' => 'foo_manage', // [Optional] The key to use for any restriction not provided in the map (only needed if not providing all 4)
|
||||
'prefix' => 'foo_', // [Optional] Rather than specifying each key in the map, you can specify a prefix, and it will automatically look for restrictions with the key "[prefix]_add/edit/permissions/delete"
|
||||
* @endcode
|
||||
*/
|
||||
protected static $restrictions = array(
|
||||
'app' => 'core',
|
||||
'module' => 'clubs',
|
||||
'all' => 'fields_manage'
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief [Node] Node Title
|
||||
*/
|
||||
public static $nodeTitle = 'clubs_custom_fields';
|
||||
|
||||
/**
|
||||
* @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
|
||||
*/
|
||||
public static $titleLangPrefix = 'core_clubfield_';
|
||||
|
||||
/**
|
||||
* @brief [CustomField] Column Map
|
||||
*/
|
||||
public static $databaseColumnMap = array(
|
||||
'content' => 'extra',
|
||||
'not_null' => 'required',
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief [CustomField] Additional Field Toggles
|
||||
*/
|
||||
public static $additionalFieldToggles = array(
|
||||
'Checkbox' => array( 'f_filterable' ),
|
||||
'CheckboxSet' => array( 'f_filterable' ),
|
||||
'Radio' => array( 'f_filterable' ),
|
||||
'Select' => array( 'f_filterable' ),
|
||||
'YesNo' => array( 'f_filterable' ),
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief [CustomField] Editor Options
|
||||
*/
|
||||
public static $editorOptions = array( 'app' => 'core', 'key' => 'Clubs' );
|
||||
|
||||
/**
|
||||
* @brief [CustomField] Upload Storage Extension
|
||||
*/
|
||||
public static $uploadStorageExtension = 'core_';
|
||||
|
||||
/**
|
||||
* Get fields
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function fields()
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->clubFields ) )
|
||||
{
|
||||
$fields = array();
|
||||
$filterable = FALSE;
|
||||
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_clubs_fields', NULL, 'f_position' ) as $row )
|
||||
{
|
||||
$fields[ $row['f_id'] ] = $row;
|
||||
if ( $row['f_filterable'] and in_array( $row['f_type'], array( 'Checkbox', 'CheckboxSet', 'Radio', 'Select', 'YesNo' ) ) )
|
||||
{
|
||||
$filterable = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Data\Store::i()->clubFields = array( 'fields' => $fields, 'filterable' => $filterable );
|
||||
}
|
||||
|
||||
return new \IPS\Patterns\ActiveRecordIterator( new \ArrayIterator( \IPS\Data\Store::i()->clubFields['fields'] ), 'IPS\Member\Club\CustomField' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if there are any filterable fields
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function areFilterableFields()
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->clubFields ) )
|
||||
{
|
||||
static::fields();
|
||||
}
|
||||
return \IPS\Data\Store::i()->clubFields['filterable'];
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Add/Edit Form
|
||||
*
|
||||
* @param \IPS\Helpers\Form $form The form
|
||||
* @return void
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
parent::form( $form );
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'f_filterable', (bool) $this->filterable, FALSE, array(), NULL, NULL, NULL, 'f_filterable' ) );
|
||||
|
||||
unset( $form->elements[''][1] );
|
||||
unset( $form->elements['']['pf_max_input'] );
|
||||
unset( $form->elements['']['pf_input_format'] );
|
||||
unset( $form->elements[''][2] );
|
||||
unset( $form->elements['']['pf_search_type'] );
|
||||
unset( $form->elements['']['pf_search_type_on_off'] );
|
||||
unset( $form->elements['']['pf_format'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Save Record
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$return = parent::save();
|
||||
|
||||
unset( \IPS\Data\Store::i()->clubFields );
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Delete Record
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
$return = parent::delete();
|
||||
|
||||
unset( \IPS\Data\Store::i()->clubFields );
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Format form values from add/edit form for save
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
if( array_key_exists( 'pf_allow_attachments', $values ) )
|
||||
{
|
||||
$values[ 'allow_attachments' ] = $values[ 'pf_allow_attachments' ];
|
||||
unset( $values[ 'pf_allow_attachments' ] );
|
||||
}
|
||||
|
||||
return parent::formatFormValues( $values );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Known Member Device 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 10 Mar 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Known Member Device Model
|
||||
*/
|
||||
class _Device extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
const LOGIN_KEY_VALIDITY = 'P3M';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_members_known_devices';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] ID Database Column
|
||||
*/
|
||||
public static $databaseColumnId = 'device_key';
|
||||
|
||||
/**
|
||||
* @brief Is this a known device?
|
||||
*/
|
||||
public $known = TRUE;
|
||||
|
||||
/**
|
||||
* Load device for current request, or create one if there isn't one
|
||||
*
|
||||
* @param \IPS\Member $member The member being authenticated
|
||||
* @param bool $sendNewDeviceEmail If true, and the associated setting is enabled, and this is a new device, an email will be sent to the member. Only set to FALSE if the user is registering where the email would be redundant.
|
||||
* @return \IPS\Member\Device
|
||||
*/
|
||||
public static function loadOrCreate( \IPS\Member $member, $sendNewDeviceEmail=TRUE )
|
||||
{
|
||||
if ( isset( \IPS\Request::i()->cookie['device_key'] ) and mb_strlen( \IPS\Request::i()->cookie['device_key'] ) === 32 )
|
||||
{
|
||||
try
|
||||
{
|
||||
$device = static::loadAndAuthenticate( \IPS\Request::i()->cookie['device_key'], $member );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
$device = new static;
|
||||
$device->known = FALSE;
|
||||
$device->device_key = \IPS\Request::i()->cookie['device_key'];
|
||||
$device->member_id = $member->member_id;
|
||||
|
||||
if ( $sendNewDeviceEmail )
|
||||
{
|
||||
$device->sendNewDeviceEmail();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$device = static::createNew();
|
||||
$device->known = FALSE;
|
||||
$device->member_id = $member->member_id;
|
||||
|
||||
if ( $sendNewDeviceEmail )
|
||||
{
|
||||
$device->sendNewDeviceEmail();
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Request::i()->setCookie( 'device_key', $device->device_key, ( new \IPS\DateTime )->add( new \DateInterval( 'P1Y' ) ) );
|
||||
|
||||
return $device;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load, but only if it is valid for a particular member and optionally login key
|
||||
*
|
||||
* @param string $deviceId The device ID
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string|null $loginKey If you also want to authenticate by login key, the login key to check
|
||||
* @return \IPS\Member\Device
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public static function loadAndAuthenticate( $deviceId, \IPS\Member $member, $loginKey = NULL )
|
||||
{
|
||||
/* Load the device */
|
||||
$device = static::load( $deviceId, NULL, array( 'member_id=?', $member->member_id ) );
|
||||
|
||||
/* Check the login key is valid */
|
||||
if ( $loginKey !== NULL )
|
||||
{
|
||||
/* Login keys expire after 3 months - if it has been more than 3 months since it was generted, do not authenticate */
|
||||
if ( $device->last_seen < ( new \IPS\DateTime )->sub( new \DateInterval( static::LOGIN_KEY_VALIDITY ) )->getTimestamp() )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
/* Validate login key is valid */
|
||||
if ( !\IPS\Login::compareHashes( (string) $device->login_key, (string) $loginKey ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $device;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new device with unique key
|
||||
*
|
||||
* @return \IPS\Member\Device
|
||||
*/
|
||||
public static function createNew()
|
||||
{
|
||||
do
|
||||
{
|
||||
$deviceKey = \IPS\Login::generateRandomString();
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\Db::i()->select( 'device_key', 'core_members_known_devices', array( 'device_key=?', $deviceKey ) )->first();
|
||||
$generatedDeviceKeyInUse = TRUE;
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
$generatedDeviceKeyInUse = FALSE;
|
||||
}
|
||||
}
|
||||
while ( $generatedDeviceKeyInUse );
|
||||
|
||||
$object = new self;
|
||||
$object->device_key = $deviceKey;
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update after logging in / automatic authentication with current request's user agent / IP address, etc.
|
||||
*
|
||||
* @param bool|null $rememberMe Remember me? NULL can be provided if the login is being processed from somewhere that doesn't ask
|
||||
* @param string $loginHandler The login handler which processed the login, or NULL if updating an existing login. Can also be empty string for logins that weren't processed by any handler (such as after registration or using the lost password feature)
|
||||
* @param bool $refreshLoginKey If login key should be refreshed (FALSE for automatic logins which will need the same login key subsequently)
|
||||
* @return void
|
||||
*/
|
||||
public function updateAfterAuthentication( $rememberMe, $loginHandler=NULL, $refreshLoginKey=TRUE )
|
||||
{
|
||||
$this->user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : NULL;
|
||||
if ( $refreshLoginKey )
|
||||
{
|
||||
$this->login_key = $rememberMe ? \IPS\Login::generateRandomString() : NULL;
|
||||
}
|
||||
$this->last_seen = time();
|
||||
if ( $loginHandler !== NULL )
|
||||
{
|
||||
$this->login_handler = $loginHandler;
|
||||
}
|
||||
$this->save();
|
||||
|
||||
$this->logIpAddress( \IPS\Request::i()->ipAddress() );
|
||||
|
||||
$cookieExpiration = ( new \IPS\DateTime )->add( new \DateInterval( static::LOGIN_KEY_VALIDITY ) );
|
||||
if ( $rememberMe === NULL )
|
||||
{
|
||||
\IPS\Request::i()->setCookie( 'member_id', $this->member_id, $cookieExpiration );
|
||||
}
|
||||
elseif ( $rememberMe === TRUE )
|
||||
{
|
||||
\IPS\Request::i()->setCookie( 'member_id', $this->member_id, $cookieExpiration );
|
||||
\IPS\Request::i()->setCookie( 'login_key', $this->login_key, $cookieExpiration );
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Request::i()->setCookie( 'member_id', $this->member_id, NULL ); // Just tells the guest caching mechanism that we are logged in, so it can expire on the session end
|
||||
\IPS\Request::i()->setCookie( 'login_key', NULL ); // Clear it in case they previously had chosen "Remember Me"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an IP address as been having used by this devuce
|
||||
*
|
||||
* @param string $ipAddress The IP Address
|
||||
* @return void
|
||||
*/
|
||||
public function logIpAddress( $ipAddress )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_members_known_ip_addresses', array(
|
||||
'device_key' => $this->device_key,
|
||||
'member_id' => $this->member_id,
|
||||
'ip_address' => $ipAddress,
|
||||
'last_seen' => time()
|
||||
), TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user agent data
|
||||
*
|
||||
* @return \IPS\Http\UserAgent
|
||||
*/
|
||||
public function userAgent()
|
||||
{
|
||||
return \IPS\Http\UserAgent::parse( $this->user_agent );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email to user notifying them about the new device
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function sendNewDeviceEmail()
|
||||
{
|
||||
if ( \IPS\Settings::i()->new_device_email )
|
||||
{
|
||||
$member = \IPS\Member::load( $this->member_id );
|
||||
|
||||
try
|
||||
{
|
||||
$location = \IPS\GeoLocation::getByIp( \IPS\Request::i()->ipAddress() );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
$location = NULL;
|
||||
}
|
||||
|
||||
\IPS\Email::buildFromTemplate( 'core', 'new_device', array( $member, $this, $location ) )->send( $member );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WHERE clause for save()
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _whereClauseForSave()
|
||||
{
|
||||
return array( 'device_key=? AND member_id=?', $this->device_key, $this->member_id );
|
||||
}
|
||||
}
|
||||
+15
-8
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Group Model
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @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 Mar 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Member;
|
||||
@@ -51,7 +50,7 @@ class _Group extends \IPS\Patterns\ActiveRecord
|
||||
'gbw_promote_unit_type' => 16, // 16 is deprecated (previously gbw_promote_unit_type). Remove in 4.1.19.
|
||||
'gbw_no_status_update' => 32, // Can NOT post status updates
|
||||
// 64 is deprecated (previously gbw_soft_delete)
|
||||
'gbw_soft_delete_own' => 128, // Allow users of this group to hide their own submitted content
|
||||
// 128 is deprecated (previously gbw_soft_delete_own)
|
||||
// 256 is deprecated (previously gbw_soft_delete_own_topic)
|
||||
// 512 is deprecated (previously gbw_un_soft_delete)
|
||||
// 1024 is deprecated (previously gbw_soft_delete_see)
|
||||
@@ -72,14 +71,15 @@ class _Group extends \IPS\Patterns\ActiveRecord
|
||||
// 33554432 is deprecated (previously gbw_hide_leaders_page)
|
||||
'gbw_pm_unblockable' => 67108864, // Deprecated in favour of global unblockable setting
|
||||
'gbw_pm_override_inbox_full'=> 134217728, // 1 means this group can send other members PMs even when that member's inbox is full
|
||||
'gbw_no_report' => 268435456, // 1 means they CAN'T report content. 0 means they can.
|
||||
// 268435456 is deprecated (previously gbw_no_report)
|
||||
'gbw_cannot_be_ignored' => 536870912, // 1 means they cannot be ignored. 0 means they can
|
||||
'gbw_delete_attachments' => 1073741824, // 1 means they can delete attachments from the "My Attachments" screen
|
||||
),
|
||||
'g_bitoptions2' => array(
|
||||
'gbw_post_highlight' => 1, // 1 means on, 0 means off
|
||||
'gbw_hide_group' => 2, // Hide this group on the front end in search and similar areas: 1 is on, 0 is off
|
||||
'gbw_lock_unlock_own' => 4, // Allow users in this group to lock and unlock their own content
|
||||
// 4 is deprecated (previously gbw_lock_unlock_own)
|
||||
'gbw_promote' => 8 // Allow users in this group to promote to facebook/twitter
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -213,6 +213,7 @@ class _Group extends \IPS\Patterns\ActiveRecord
|
||||
/* Clear application and module store so that permissions are correctly rebuilt */
|
||||
unset( \IPS\Data\Store::i()->applications );
|
||||
unset( \IPS\Data\Store::i()->modules );
|
||||
unset( \IPS\Data\Store::i()->groups );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,7 +343,13 @@ class _Group extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
parent::delete();
|
||||
\IPS\Lang::deleteCustom( 'core', 'core_group_' . $this->g_id );
|
||||
|
||||
unset( \IPS\Data\Store::i()->groups );
|
||||
unset( \IPS\Data\Store::i()->moderators );
|
||||
unset( \IPS\Data\Store::i()->administrators );
|
||||
|
||||
/* Update Core Leaders */
|
||||
\IPS\core\StaffDirectory\User::updateEmptySetting();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Group promotion node 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 4 Apr 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group promotion node model
|
||||
*/
|
||||
class _GroupPromotion extends \IPS\Node\Model
|
||||
{
|
||||
/**
|
||||
* @brief Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_group_promotions';
|
||||
|
||||
/**
|
||||
* @brief Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'promote_';
|
||||
|
||||
/**
|
||||
* @brief Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] ID Database Column
|
||||
*/
|
||||
public static $databaseColumnId = 'id';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Map
|
||||
*/
|
||||
protected static $multitonMap = array();
|
||||
|
||||
/**
|
||||
* @brief [Node] Node Title
|
||||
*/
|
||||
public static $nodeTitle = 'grouppromotions';
|
||||
|
||||
/**
|
||||
* @brief [Node] Sortable
|
||||
*/
|
||||
public static $nodeSortable = TRUE;
|
||||
|
||||
/**
|
||||
* @brief [Node] Positon Column
|
||||
*/
|
||||
public static $databaseColumnOrder = 'position';
|
||||
|
||||
/**
|
||||
* @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
|
||||
*/
|
||||
public static $titleLangPrefix = 'g_promotion_';
|
||||
|
||||
/**
|
||||
* @brief [Node] Enabled/Disabled Column
|
||||
*/
|
||||
public static $databaseColumnEnabledDisabled = 'enabled';
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
* @param \IPS\Helpers\Form The form
|
||||
* @return void
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
$form->addHeader( 'generic_gp_details' );
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'promote_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? 'g_promotion_' . $this->id : NULL ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'promote_enabled', $this->id ? $this->enabled : 1, TRUE ) );
|
||||
|
||||
/* Loop over our member filters */
|
||||
$form->addHeader( 'generic_gp_filters' );
|
||||
$options = $this->id ? $this->_filters : array();
|
||||
|
||||
$lastApp = 'core';
|
||||
|
||||
/* We take an extra step with groups to disable invalid options */
|
||||
$options['core_Group']['disabled_groups'] = $this->getDisabledGroups();
|
||||
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'MemberFilter', TRUE, 'core' ) as $key => $extension )
|
||||
{
|
||||
if( method_exists( $extension, 'getSettingField' ) AND $extension->availableIn( 'group_promotions' ) )
|
||||
{
|
||||
/* See if we need a new form header - one per app */
|
||||
$_key = explode( '_', $key );
|
||||
|
||||
if( $_key[0] != $lastApp )
|
||||
{
|
||||
$lastApp = $_key[0];
|
||||
$form->addHeader( $lastApp . '_bm_filters' );
|
||||
}
|
||||
|
||||
/* Grab our fields and add to the form */
|
||||
$fields = $extension->getSettingField( !empty( $options[ $key ] ) ? $options[ $key ] : array() );
|
||||
|
||||
foreach( $fields as $field )
|
||||
{
|
||||
$form->add( $field );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$form->addHeader( 'generic_gp_actions' );
|
||||
$groups = array_combine( array_keys( \IPS\Member\Group::groups( TRUE, FALSE ) ), array_map( function( $_group ) { return (string) $_group; }, \IPS\Member\Group::groups( TRUE, FALSE ) ) );
|
||||
$primary = array( 0 => 'do_not_change_group' ) + $groups;
|
||||
|
||||
/* And then allow the admin to choose which groups to promote to */
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'promote_group_primary', $this->id ? $this->_actions['primary_group'] : 0, FALSE,
|
||||
array( 'options' => $primary ) ) );
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'promote_group_secondary', $this->id ? $this->_actions['secondary_group'] : array(), FALSE,
|
||||
array( 'options' => $groups, 'multiple' => true ) ) );
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'demote_group_secondary', $this->id ? $this->_actions['secondary_remove'] : array(), FALSE,
|
||||
array( 'options' => $groups, 'multiple' => true ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of groups that cannot be promoted
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDisabledGroups()
|
||||
{
|
||||
$return = array( \IPS\Settings::i()->guest_group );
|
||||
|
||||
foreach( \IPS\Member\Group::groups() as $group )
|
||||
{
|
||||
if( $group->g_promote_exclude )
|
||||
{
|
||||
$return[] = $group->g_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Format form values from add/edit form for save
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
if ( !$this->id )
|
||||
{
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/* Save the title */
|
||||
\IPS\Lang::saveCustom( 'core', 'g_promotion_' . $this->id, $values['promote_title'] );
|
||||
|
||||
/* Json-encode the rules */
|
||||
$_options = array();
|
||||
|
||||
/* Loop over bulk mail extensions to format the options */
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'MemberFilter', TRUE, 'core' ) as $key => $extension )
|
||||
{
|
||||
if( method_exists( $extension, 'save' ) AND $extension->availableIn( 'group_promotions' ) )
|
||||
{
|
||||
/* Grab our fields and add to the form */
|
||||
$_value = $extension->save( $values );
|
||||
|
||||
if( $_value )
|
||||
{
|
||||
$_options[ $key ] = $_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$values['promote_filters'] = json_encode( $_options );
|
||||
|
||||
/* Json-encode the actions */
|
||||
$values['promote_actions'] = json_encode( array(
|
||||
'primary_group' => $values['promote_group_primary'],
|
||||
'secondary_group' => $values['promote_group_secondary'],
|
||||
'secondary_remove' => $values['demote_group_secondary']
|
||||
) );
|
||||
|
||||
/* Now we have to remove any fields that aren't valid... */
|
||||
foreach( $values as $k => $v )
|
||||
{
|
||||
if( !in_array( $k, array( 'promote_enabled', 'promote_filters', 'promote_actions', 'promote_position' ) ) )
|
||||
{
|
||||
unset( $values[ $k ] );
|
||||
}
|
||||
}
|
||||
|
||||
return parent::formatFormValues( $values );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return our filters as an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get__filters()
|
||||
{
|
||||
return json_decode( $this->filters, TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return our actions as an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get__actions()
|
||||
{
|
||||
return json_decode( $this->actions, TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch All Root Nodes
|
||||
*
|
||||
* @param string|NULL $permissionCheck The permission key to check for or NULl to not check permissions
|
||||
* @param \IPS\Member|NULL $member The member to check permissions for or NULL for the currently logged in member
|
||||
* @param mixed $where Additional WHERE clause
|
||||
* @return array
|
||||
*/
|
||||
public static function roots( $permissionCheck='view', $member=NULL, $where=array() )
|
||||
{
|
||||
if ( !count( $where ) )
|
||||
{
|
||||
$return = array();
|
||||
foreach( static::getStore() AS $node )
|
||||
{
|
||||
$return[ $node['promote_id'] ] = static::constructFromData( $node );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::roots( $permissionCheck, $member, $where );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Enabled
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set__enabled( $enabled )
|
||||
{
|
||||
parent::set__enabled( $enabled );
|
||||
|
||||
unset( \IPS\Data\Store::i()->group_promotions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data store
|
||||
*
|
||||
* @return array
|
||||
* @note Note that all records are returned, even disabled promotion rules. Enable status needs to be checked in userland code when appropriate.
|
||||
*/
|
||||
public static function getStore()
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->group_promotions ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->group_promotions = iterator_to_array( \IPS\Db::i()->select( '*', static::$databaseTable, NULL, "promote_position ASC" )->setKeyField( 'promote_id' ) );
|
||||
}
|
||||
|
||||
return \IPS\Data\Store::i()->group_promotions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Record
|
||||
*
|
||||
* @see \IPS\Db::build
|
||||
* @param int|string $id ID
|
||||
* @param string $idField The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
|
||||
* @param mixed $extraWhereClause Additional where clause(s) (see \IPS\Db::build for details)
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
|
||||
{
|
||||
if ( ( $idField === NULL or $idField === 'promote_id' ) and $extraWhereClause === NULL )
|
||||
{
|
||||
$promotions = static::getStore();
|
||||
if ( isset( $promotions[ $id ] ) )
|
||||
{
|
||||
return static::constructFromData( $promotions[ $id ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
|
||||
return parent::load( $id, $idField, $extraWhereClause );
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Delete
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
parent::delete();
|
||||
|
||||
unset( \IPS\Data\Store::i()->group_promotions );
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Save Changed Columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
parent::save();
|
||||
|
||||
unset( \IPS\Data\Store::i()->group_promotions );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cache extensions so we only need to load them once
|
||||
*/
|
||||
protected static $extensions = NULL;
|
||||
|
||||
/**
|
||||
* Check if a member matches the rule
|
||||
*
|
||||
* @param \IPS\Member $member Member to check
|
||||
* @return bool
|
||||
*/
|
||||
public function matches( \IPS\Member $member )
|
||||
{
|
||||
if( static::$extensions === NULL )
|
||||
{
|
||||
static::$extensions = \IPS\Application::allExtensions( 'core', 'MemberFilter', TRUE, 'core' );
|
||||
}
|
||||
|
||||
/* Loop over the filters */
|
||||
foreach( $this->_filters as $key => $filter )
|
||||
{
|
||||
if( isset( static::$extensions[ $key ] ) )
|
||||
{
|
||||
/* Ask the extension if this member matches the defined rule...if not, just return FALSE now */
|
||||
if( !static::$extensions[ $key ]->matches( $member, $filter ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If we are still here, then the rule matched */
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Member History Table
|
||||
* @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
|
||||
* @subpackage Nexus
|
||||
* @since 7 Dec 2016
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Member History Model
|
||||
*/
|
||||
class _History extends \IPS\Helpers\Table\Db
|
||||
{
|
||||
/**
|
||||
* @brief Parser extensions
|
||||
*/
|
||||
static $extensions = NULL;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\Http\Url $url The URL the table will be displayed on
|
||||
* @param mixed $where WHERE clause
|
||||
* @param bool $showIp If the IP address column should be included
|
||||
* @param bool $showMember If the customer column should show
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\Http\Url $url, $where, $showIp=TRUE, $showMember=FALSE, $showApp=TRUE, $showType=FALSE )
|
||||
{
|
||||
parent::__construct( 'core_member_history', $url, $where );
|
||||
|
||||
$this->include = array();
|
||||
if( static::$extensions === NULL )
|
||||
{
|
||||
$apps = \IPS\Application::appsWithExtension( 'core', 'MemberHistory' );
|
||||
|
||||
foreach( $apps as $application )
|
||||
{
|
||||
$result = $application->extensions( 'core', 'MemberHistory' );
|
||||
|
||||
/* Since we're parsing, we only want to have one extension */
|
||||
static::$extensions[ $application->directory ] = array_pop( $result );
|
||||
}
|
||||
}
|
||||
|
||||
if( $showType )
|
||||
{
|
||||
$this->include[] = 'log_type';
|
||||
}
|
||||
|
||||
/* This is here specifically so that log_type is always shown first, if required */
|
||||
$this->include = array_merge( $this->include, array( 'log_date', 'log_data' ) );
|
||||
|
||||
if ( $showIp )
|
||||
{
|
||||
$this->include[] = 'log_ip_address';
|
||||
}
|
||||
if ( $showMember )
|
||||
{
|
||||
$this->include[] = 'log_member';
|
||||
}
|
||||
|
||||
$options = array();
|
||||
$extensions = static::$extensions;
|
||||
|
||||
foreach( $extensions as $extension )
|
||||
{
|
||||
if( method_exists( $extension, 'getTypes' ) )
|
||||
{
|
||||
foreach( $extension->getTypes() as $type )
|
||||
{
|
||||
$options[ $type ] = 'log_type_title_' . $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->advancedSearch = array(
|
||||
'log_ip_address' => \IPS\Helpers\Table\SEARCH_QUERY_TEXT,
|
||||
'log_date' => \IPS\Helpers\Table\SEARCH_DATE_RANGE,
|
||||
'log_type' => array( \IPS\Helpers\Table\SEARCH_SELECT, array( 'options' => $options, 'multiple' => TRUE ) )
|
||||
);
|
||||
|
||||
$this->sortBy = $this->sortBy ?: 'log_date';
|
||||
$this->noSort = array( 'log_type', 'log_ip_address', 'log_data' );
|
||||
$this->rowClasses = array( 'log_data' => array( 'ipsTable_wrap' ) );
|
||||
$this->parsers = array(
|
||||
'log_type' => function( $val, $row ) use ( $extensions )
|
||||
{
|
||||
if( method_exists( $extensions[ $row['log_app'] ], 'parseLogType' ) )
|
||||
{
|
||||
return $extensions[ $row['log_app'] ]->parseLogType( $val, $row );
|
||||
}
|
||||
|
||||
return $val;
|
||||
},
|
||||
'log_date' => function( $val )
|
||||
{
|
||||
return \IPS\DateTime::ts( $val );
|
||||
},
|
||||
'log_ip_address' => function( $val )
|
||||
{
|
||||
return "<a href='" . \IPS\Http\Url::internal( "app=core&module=members&controller=ip&ip={$val}" ) . "'>{$val}</a>";
|
||||
},
|
||||
'log_member'=> function( $val, $row ) use ( $extensions )
|
||||
{
|
||||
if( method_exists( $extensions[ $row['log_app'] ], 'parseLogMember' ) )
|
||||
{
|
||||
return $extensions[ $row['log_app'] ]->parseLogMember( $val, $row );
|
||||
}
|
||||
|
||||
$member = \IPS\Member::load( $val );
|
||||
return \IPS\Theme::i()->getTemplate( 'global', 'core' )->userPhoto( $member, 'tiny' ) . ' ' . $member->link();
|
||||
},
|
||||
'log_data' => function( $val, $row ) use ( $extensions )
|
||||
{
|
||||
if( method_exists( $extensions[ $row['log_app'] ], 'parseLogData' ) )
|
||||
{
|
||||
return $extensions[ $row['log_app'] ]->parseLogData( $val, $row );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Letter photo generator for member accounts
|
||||
* @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 3 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Letter photo generator for member accounts
|
||||
*/
|
||||
class _LetterPhoto
|
||||
{
|
||||
/**
|
||||
* @brief File storage class to use
|
||||
*/
|
||||
protected $fileStorageClass = 'core_Profile';
|
||||
|
||||
/**
|
||||
* @brief Member name
|
||||
*/
|
||||
protected $name = NULL;
|
||||
|
||||
/**
|
||||
* @brief Text size
|
||||
*/
|
||||
protected $textSize = 75;
|
||||
|
||||
/**
|
||||
* Create a new photo object
|
||||
*
|
||||
* @param string $name Member's name
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $name='' )
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the photo
|
||||
*
|
||||
* @param int $maxWidth Max width
|
||||
* @param int $maxHeight Max height
|
||||
* @param int $letterCount How many letters to use
|
||||
* @return \IPS\Http\Url|NULL
|
||||
*/
|
||||
public function create( $maxWidth, $maxHeight, $letterCount=1 )
|
||||
{
|
||||
/* If we don't have any sizes, we can't generate a proper image */
|
||||
if( !$maxWidth OR !$maxHeight )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Set the font size to 1/2 of the $maxWidth and $maxHeight (which should be the same value) */
|
||||
$this->textSize = ( ( $maxWidth / 2 ) / $letterCount );
|
||||
|
||||
/* If we don't have a name, there's nothing to create */
|
||||
if( !$this->name )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Get the first character of the name...if we can't detect it, there's again nothing to convert */
|
||||
$letters = $this->getLetters( $letterCount );
|
||||
|
||||
if( !$letters )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Now we have to generate the background color we will be using */
|
||||
$rgbColor = $this->generateColorCode();
|
||||
|
||||
/* Create the image */
|
||||
$image = \IPS\Image::newImageCanvas( $maxWidth, $maxHeight, $rgbColor );
|
||||
|
||||
/* Draw the text on the image */
|
||||
$image->write( $letters, $this->getFont( $letters ), $this->textSize );
|
||||
|
||||
/* Save the image and return it */
|
||||
try
|
||||
{
|
||||
return \IPS\File::create( $this->fileStorageClass, $letters . '.png', (string) $image, NULL, TRUE );
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::debug( $e, 'letter_photo' );
|
||||
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first X non-punctuation characters of the name
|
||||
*
|
||||
* @param int $letters Number of letters to return
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function getLetters( $letters=1 )
|
||||
{
|
||||
$name = str_replace( array( '<', '>', '=', '-', '+', '"', "'" ), '', $this->name );
|
||||
$name = preg_replace( "/(\pP)+/u", '', trim( $name ) );
|
||||
|
||||
return mb_strtoupper( mb_substr( $name, 0, $letters ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Color saturation to use for HSV colors
|
||||
*/
|
||||
protected $colorCodeSaturation = .45;
|
||||
|
||||
/**
|
||||
* @brief Color value to use for HSV colors
|
||||
*/
|
||||
protected $colorCodeValue = 0.575;
|
||||
|
||||
/**
|
||||
* @brief If we have just generated a hue on this page load, store it to better randomize the next color
|
||||
*/
|
||||
protected static $lastHue = NULL;
|
||||
|
||||
/**
|
||||
* Return a unique color code based on the name.
|
||||
*
|
||||
* @return array (RGB value)
|
||||
*/
|
||||
protected function generateColorCode()
|
||||
{
|
||||
/* If we've already generated a hue on this page load, use the golden ratio to find a number sufficiently distinct */
|
||||
if( static::$lastHue )
|
||||
{
|
||||
$hue = static::$lastHue + 0.618033988749895;
|
||||
|
||||
if( $hue > 1.0 )
|
||||
{
|
||||
$hue -= 1.0;
|
||||
}
|
||||
|
||||
static::$lastHue = $hue;
|
||||
|
||||
return $this->convertHsvToRgb( $hue, $this->colorCodeSaturation, $this->colorCodeValue );
|
||||
}
|
||||
|
||||
$hue = ( ( ord( \mb_strtolower( $this->name ) ) - 97 ) / 25.0 ) + 0.25;
|
||||
$hue *= 1.61803398875;
|
||||
$hue *= (float) ( '1.' . rand() ) / 1;
|
||||
|
||||
if( rand( 0, 1 ) )
|
||||
{
|
||||
$float = (float) ( "1." . ord( array_rand( range( 'a', 'z' ) ) ) );
|
||||
$hue = abs( $float - $hue );
|
||||
}
|
||||
|
||||
while( $hue > 1.0 )
|
||||
{
|
||||
$hue -= 1.0;
|
||||
}
|
||||
|
||||
static::$lastHue = $hue;
|
||||
|
||||
return $this->convertHsvToRgb( $hue, $this->colorCodeSaturation, $this->colorCodeValue );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the HSV value to RGB
|
||||
*
|
||||
* @param float $h Hue
|
||||
* @param float $s Saturation
|
||||
* @param float $v Value
|
||||
* @return array
|
||||
*/
|
||||
protected function convertHsvToRgb( $h, $s, $v )
|
||||
{
|
||||
$rgb = array( $v, $v, $v );
|
||||
$diff = ( $v <= 0.5 ) ? ( $v * ( 1.0 + $s ) ) : ( $v + $s - $v * $s );
|
||||
|
||||
if( $diff )
|
||||
{
|
||||
$m = $v + $v - $diff;
|
||||
$sv = ( $diff - $m ) / $diff;
|
||||
$h *= 6.0;
|
||||
$fract = $h - floor( $h );
|
||||
$vsf = $diff * $sv * $fract;
|
||||
$mid1 = $m + $vsf;
|
||||
$mid2 = $diff - $vsf;
|
||||
|
||||
switch( floor( $h ) )
|
||||
{
|
||||
case 0:
|
||||
$rgb = array( $diff, $mid1, $m );
|
||||
break;
|
||||
|
||||
case 1:
|
||||
$rgb = array( $mid2, $diff, $m );
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$rgb = array( $m, $diff, $mid1 );
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$rgb = array( $m, $mid2, $diff );
|
||||
break;
|
||||
|
||||
case 4:
|
||||
$rgb = array( $mid1, $m, $diff );
|
||||
break;
|
||||
|
||||
case 5:
|
||||
$rgb = array( $diff, $m, $mid2 );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array_map( function( $value ) { return $value * 256.0; }, $rgb );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a font file path to use
|
||||
*
|
||||
* @param string $string String we are going to write
|
||||
* @return string
|
||||
*/
|
||||
protected function getFont( $string='' )
|
||||
{
|
||||
$codePointMap = array(
|
||||
'NotoSansArmenian-Regular.ttf' => array( 0x0530, 0x058F ),
|
||||
'NotoSansHebrew-Regular.ttf' => array( 0x0590, 0x05FF ),
|
||||
'NotoNaskhArabic-Regular.ttf' => array( 0x0600, 0x06FF ),
|
||||
'NotoSansGeorgian-Regular.ttf' => array( 0x10A0, 0x10FF ),
|
||||
'NotoSansThai-Regular.ttf' => array( 0x0E00, 0x0E7F ),
|
||||
'NotoSansDevanagari-Regular.ttf' => array( 0x0900, 0x097F ),
|
||||
'BMDOHYEON.ttf' => array(
|
||||
array( 0xAC00, 0xD7A3 ),
|
||||
array( 0x1100, 0x11FF ),
|
||||
array( 0x3130, 0x318F ),
|
||||
array( 0xA960, 0xA97F ),
|
||||
array( 0xD7B0, 0xD7FF )
|
||||
),
|
||||
'HanaMinA.ttf' => array(
|
||||
array( 0x4E00, 0x9FFF ),
|
||||
array( 0x3400, 0x4DB5 ),
|
||||
array( 0x3040, 0x309F ),
|
||||
array( 0x30A0, 0x30FF ),
|
||||
array( 0xFF00, 0xFFEF ),
|
||||
array( 0xF900, 0xFAD9 ),
|
||||
array( 0x20000, 0x2A6D6 ),
|
||||
array( 0x2A700, 0x2B734 ),
|
||||
array( 0x2B740, 0x2B81D ),
|
||||
array( 0x2B820, 0x2CEA1 ),
|
||||
array( 0x2F800, 0x2FA1D )
|
||||
),
|
||||
);
|
||||
|
||||
/* Get all characters */
|
||||
$characters = preg_split( '//u', $string, null, PREG_SPLIT_NO_EMPTY );
|
||||
|
||||
/* Loop over all characters. If any character is in a range defined above, use that font.
|
||||
Technically you could have a Korean character and an Arabic character in the same string, but the probability of that occurring
|
||||
is sufficiently rare that we will use the first font defined above which support latin ranges already */
|
||||
foreach( $characters as $character )
|
||||
{
|
||||
$decValue = unpack( 'N', mb_convert_encoding( $character, 'UCS-4BE', 'UTF-8' ) );
|
||||
$decValue = $decValue[1];
|
||||
|
||||
foreach( $codePointMap as $fontFile => $range )
|
||||
{
|
||||
if( is_array( $range[0] ) )
|
||||
{
|
||||
foreach( $range as $subRange )
|
||||
{
|
||||
if( $decValue > ( $subRange[0] ) AND $decValue < ( $subRange[1] ) )
|
||||
{
|
||||
return \IPS\ROOT_PATH . '/system/3rd_party/Fonts/' . $fontFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( $decValue > ( $range[0] ) AND $decValue < ( $range[1] ) )
|
||||
{
|
||||
return \IPS\ROOT_PATH . '/system/3rd_party/Fonts/' . $fontFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for the majority of languages */
|
||||
return \IPS\ROOT_PATH . '/system/3rd_party/Fonts/NotoSans-Regular.ttf';
|
||||
}
|
||||
}
|
||||
+772
-98
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,594 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Profile Step 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 10 Mar 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile Step Model
|
||||
*/
|
||||
class _ProfileStep extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_profile_steps';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] ID Database Column
|
||||
*/
|
||||
public static $databaseColumnId = 'id';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'step_';
|
||||
|
||||
/**
|
||||
* Load Record
|
||||
*
|
||||
* @see \IPS\Db::build
|
||||
* @param int|string $id ID
|
||||
* @param string $idField The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
|
||||
* @param mixed $extraWhereClause Additional where clause(s) (see \IPS\Db::build for details) - if used will cause multiton store to be skipped and a query always ran
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
|
||||
{
|
||||
if ( $idField === NULL AND $extraWhereClause === NULL )
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->profileSteps ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->profileSteps = iterator_to_array( \IPS\Db::i()->select( '*', 'core_profile_steps' )->setKeyField( 'step_id' ) );
|
||||
}
|
||||
|
||||
if ( isset( \IPS\Data\Store::i()->profileSteps[ $id ] ) )
|
||||
{
|
||||
return static::constructFromData( \IPS\Data\Store::i()->profileSteps[ $id ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::load( $id, $idField, $extraWhereClause );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Changed Columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
parent::save();
|
||||
|
||||
unset( \IPS\Data\Store::i()->profileSteps );
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Delete Record
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ( method_exists( $this->extension, 'onDelete' ) )
|
||||
{
|
||||
$this->extension->onDelete( $this );
|
||||
}
|
||||
|
||||
\IPS\Lang::deleteCustom( 'core', 'profile_step_title_' . $this->id );
|
||||
\IPS\Lang::deleteCustom( 'core', 'profile_step_text_' . $this->id );
|
||||
|
||||
parent::delete();
|
||||
|
||||
unset( \IPS\Data\Store::i()->profileSteps );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "subcompletion_act" field
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_subcompletion_act()
|
||||
{
|
||||
$return = ! empty( $this->_data['subcompletion_act'] ) ? json_decode( $this->_data['subcompletion_act'], TRUE ) : array();
|
||||
if ( ! is_array( $return ) and ! empty( $this->_data['subcompletion_act'] ) )
|
||||
{
|
||||
$return = array( $this->_data['subcompletion_act'] );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "subcompletion_act" field
|
||||
*
|
||||
* @param string|array $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_subcompletion_act( $value )
|
||||
{
|
||||
$this->_data['subcompletion_act'] = ( is_array( $value ) ? json_encode( $value ) : $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Used sub actions by other items
|
||||
*
|
||||
* @return array array( 'key1', 'key2', 'key3' )
|
||||
*/
|
||||
public function usedSubActions()
|
||||
{
|
||||
$return = array();
|
||||
|
||||
foreach( static::loadAll() as $id => $object )
|
||||
{
|
||||
if ( $object->id !== $this->id and count( $object->subcompletion_act ) )
|
||||
{
|
||||
foreach( $object->subcompletion_act as $item )
|
||||
{
|
||||
$return[ $object->completion_act ][] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next step
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getNextStep()
|
||||
{
|
||||
$nextOneIsIt = false;
|
||||
$steps = array();
|
||||
foreach( \IPS\Application::allExtensions( 'core', 'ProfileSteps' ) AS $extension )
|
||||
{
|
||||
if ( method_exists( $extension, 'wizard') AND count( $extension::wizard() ) )
|
||||
{
|
||||
$steps = array_merge( $steps, $extension::wizard() );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( $steps as $key => $object )
|
||||
{
|
||||
if ( $nextOneIsIt )
|
||||
{
|
||||
return $key;
|
||||
}
|
||||
|
||||
if ( $key === $this->key )
|
||||
{
|
||||
$nextOneIsIt = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Still here? */
|
||||
return 'profile_done';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load All Steps
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function loadAll()
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->profileSteps ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->profileSteps = iterator_to_array( \IPS\Db::i()->select( '*', 'core_profile_steps' )->setKeyField( 'step_id' ) );
|
||||
}
|
||||
|
||||
$return = array();
|
||||
foreach( \IPS\Data\Store::i()->profileSteps AS $id => $data )
|
||||
{
|
||||
$return[ $id ] = static::constructFromData( $data );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Actions Cache
|
||||
*/
|
||||
protected static $_actions = NULL;
|
||||
|
||||
/**
|
||||
* @brief Sub actions cache
|
||||
*/
|
||||
protected static $_subActions = NULL;
|
||||
|
||||
/**
|
||||
* Available parent actions to complete steps
|
||||
*
|
||||
* @return array array( 'key' => 'lang_string' )
|
||||
*/
|
||||
public static function actions()
|
||||
{
|
||||
if ( static::$_actions === NULL )
|
||||
{
|
||||
static::$_actions = array();
|
||||
foreach( \IPS\Application::allExtensions( 'core', 'ProfileSteps' ) AS $key => $extension )
|
||||
{
|
||||
foreach( $extension::actions() AS $action => $lang )
|
||||
{
|
||||
static::$_actions[ $action ] = $lang;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return static::$_actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Available sub actions to complete steps
|
||||
*
|
||||
* @return array array( 'key' => 'lang_string' )
|
||||
*/
|
||||
public static function subActions()
|
||||
{
|
||||
if ( static::$_subActions === NULL )
|
||||
{
|
||||
static::$_subActions = array();
|
||||
foreach( \IPS\Application::allExtensions( 'core', 'ProfileSteps' ) AS $key => $extension )
|
||||
{
|
||||
if ( method_exists( $extension, 'subActions' ) )
|
||||
{
|
||||
foreach( $extension::subActions() AS $parent => $row )
|
||||
{
|
||||
foreach( $row as $action => $lang )
|
||||
{
|
||||
static::$_subActions[ $parent ][ $action ] = $lang;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return static::$_subActions;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Can the actions have multiple choices?
|
||||
*
|
||||
* @param string $action Action key (basic_profile, etc)
|
||||
* @return boolean
|
||||
*/
|
||||
public static function actionMultipleChoice( $action )
|
||||
{
|
||||
foreach( \IPS\Application::allExtensions( 'core', 'ProfileSteps' ) AS $key => $extension )
|
||||
{
|
||||
if ( method_exists( $extension, 'actionMultipleChoice' ) )
|
||||
{
|
||||
foreach( $extension::actions() AS $extensionAction => $lang )
|
||||
{
|
||||
if ( $action == $extensionAction )
|
||||
{
|
||||
return $extension::actionMultipleChoice( $action );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be set as required?
|
||||
*
|
||||
* @return array
|
||||
* @note This is intended for items which have their own independent settings and dedicated enable pages, such as MFA and Social Login integration
|
||||
*/
|
||||
public static function canBeRequired()
|
||||
{
|
||||
$return = array();
|
||||
foreach( \IPS\Application::allExtensions( 'core', 'ProfileSteps' ) AS $key => $extension )
|
||||
{
|
||||
foreach( $extension::canBeRequired() AS $action )
|
||||
{
|
||||
$return[] = $action;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the wizard step key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_key()
|
||||
{
|
||||
return "profile_step_title_" . $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get automated step title in the ACP
|
||||
* @note It needs to fetch the parsed langauge so it does not end up storing the word hashes on save.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_acpTitle()
|
||||
{
|
||||
if ( empty( $this->subcompletion_act ) )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->get( 'complete_profile_' . $this->completion_act );
|
||||
}
|
||||
|
||||
$extension = $this->extension;
|
||||
$subActions = $extension::subActions()[ $this->completion_act ];
|
||||
$lang = array();
|
||||
|
||||
foreach( $this->subcompletion_act as $item )
|
||||
{
|
||||
if ( isset( $subActions[ $item ] ) )
|
||||
{
|
||||
$lang[] = \IPS\Member::loggedIn()->language()->get( $subActions[ $item ] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $lang ) )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->get( 'complete_profile_' . $this->completion_act ) . ' (' . \IPS\Member::loggedIn()->language()->formatList( $lang ) . ')';
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->get( 'complete_profile_' . $this->completion_act );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Extension
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_extension()
|
||||
{
|
||||
list( $app, $extension ) = explode( '_', $this->_data['extension'] );
|
||||
$class = "\\IPS\\{$app}\\extensions\\core\\ProfileSteps\\{$extension}";
|
||||
return new $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has a specific step been completed?
|
||||
*
|
||||
* @param \IPS\Member|NULL The member to check, or NULL for currently logged in
|
||||
* @return bool
|
||||
*/
|
||||
public function completed( \IPS\Member $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return $this->extension->completed( $this, $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
* @param \IPS\Helpers\Form Form
|
||||
* @return void
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'profile_step_title', NULL, FALSE, array( 'app' => 'core', 'key' => ( $this->id ) ? "profile_step_title_{$this->id}" : NULL ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'profile_step_text', NULL, FALSE, array( 'app' => 'core', 'key' => ( $this->id ) ? "profile_step_text_{$this->id}" : NULL ) ) );
|
||||
|
||||
$actions = static::actions();
|
||||
$subActions = static::subActions();
|
||||
$toggles = array();
|
||||
$subActionFormElements = array();
|
||||
$usedSubActions = $this->usedSubActions();
|
||||
|
||||
foreach( $actions as $key => $lang )
|
||||
{
|
||||
if ( isset( $subActions[ $key ] ) )
|
||||
{
|
||||
$disabled = NULL;
|
||||
|
||||
if ( isset( $usedSubActions[ $key ] ) )
|
||||
{
|
||||
$disabled = $usedSubActions[ $key ];
|
||||
}
|
||||
|
||||
$formClass = static::actionMultipleChoice( $key ) ? '\IPS\Helpers\Form\CheckboxSet' : '\IPS\Helpers\Form\Select';
|
||||
$subActionFormElements[] = new $formClass( 'profile_step_subaction_' . $key, $this->subcompletion_act, TRUE, array( 'options' => $subActions[ $key ], 'disabled' => $disabled ), NULL, NULL, NULL, 'profile_step_subaction_' . $key );
|
||||
$toggles[ $key ][] = 'profile_step_subaction_' . $key;
|
||||
}
|
||||
}
|
||||
|
||||
foreach( static::canBeRequired() AS $action )
|
||||
{
|
||||
$toggles[ $action ][] = 'step_required';
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'profile_step_completion_act', $this->completion_act, FALSE, array( 'options' => $actions, 'toggles' => $toggles ) ) );
|
||||
|
||||
foreach( $subActionFormElements as $element )
|
||||
{
|
||||
$form->add( $element );
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'profile_step_required', $this->required, FALSE, array(), NULL, NULL, NULL, 'step_required' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Form Values
|
||||
* @param array Values
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( !$this->id )
|
||||
{
|
||||
$this->save();
|
||||
}
|
||||
|
||||
if ( isset( $values[ 'profile_step_subaction_' . $values['profile_step_completion_act'] ] ) )
|
||||
{
|
||||
$values['profile_step_subcompletion_act'] = $values[ 'profile_step_subaction_' . $values['profile_step_completion_act'] ];
|
||||
unset( $values[ 'profile_step_subaction_' . $values['profile_step_completion_act'] ] );
|
||||
}
|
||||
|
||||
foreach( $values AS $key => $value )
|
||||
{
|
||||
$return[ str_replace( 'profile_', '', $key ) ] = $value;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$ext = static::findExtensionFromAction( $return['step_completion_act'] );
|
||||
$ext = explode( '_', $ext );
|
||||
$class = "\\IPS\\{$ext[0]}\\extensions\\core\\ProfileSteps\\{$ext[1]}";
|
||||
$extension = new $class;
|
||||
}
|
||||
catch( \OutOfBoundsException $e )
|
||||
{
|
||||
throw new \InvalidArgumentException;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform actions after saving the form
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return void
|
||||
*/
|
||||
public function postSaveForm( $values )
|
||||
{
|
||||
$return = array();
|
||||
foreach( $values AS $key => $value )
|
||||
{
|
||||
$return[ str_replace( 'profile_', '', $key ) ] = $value;
|
||||
}
|
||||
|
||||
if ( array_key_exists( 'step_title', $return ) )
|
||||
{
|
||||
if ( is_array( $return['step_title'] ) )
|
||||
{
|
||||
foreach( $return['step_title'] AS $lang => $text )
|
||||
{
|
||||
if ( empty( $text ) )
|
||||
{
|
||||
$return['step_title'][ $lang ] = $this->acpTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$return['step_title'] = array();
|
||||
foreach( \IPS\Lang::languages() AS $lang )
|
||||
{
|
||||
$return['step_title'][ $lang->_id ] = $this->acpTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$return['step_title'] = array();
|
||||
foreach( \IPS\Lang::languages() AS $lang )
|
||||
{
|
||||
$return['step_title'][ $lang->_id ] = $this->acpTitle;
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Lang::saveCustom( 'core', "profile_step_title_{$this->id}", $return['step_title'] );
|
||||
\IPS\Lang::saveCustom( 'core', "profile_step_text_{$this->id}", $return['step_text'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find extension key from action key
|
||||
*
|
||||
* @param string The action key
|
||||
* @return string The extension key
|
||||
* @throws \OutOfBoundsException
|
||||
*/
|
||||
public static function findExtensionFromAction( $key )
|
||||
{
|
||||
foreach( \IPS\Application::allExtensions( 'core', 'ProfileSteps' ) AS $extkey => $extension )
|
||||
{
|
||||
foreach( $extension::actions() AS $action => $lang )
|
||||
{
|
||||
if ( $key == $action )
|
||||
{
|
||||
return $extkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resync
|
||||
* Triggered when something happens externally that may affect these fields (such as a field being deleted, etc)
|
||||
*
|
||||
* return @void
|
||||
*/
|
||||
public static function resync()
|
||||
{
|
||||
$iterator = new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_profile_steps' ), '\IPS\Member\ProfileStep' );
|
||||
|
||||
foreach( $iterator as $step )
|
||||
{
|
||||
$class = $step->extension;
|
||||
|
||||
if ( method_exists( $class, 'resync' ) )
|
||||
{
|
||||
$class->resync( $step );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete by application
|
||||
* Deletes all steps tied to an application
|
||||
*
|
||||
* @param \IPS\Application $app The application being deleted
|
||||
* @return void
|
||||
*/
|
||||
public function deleteByApplication( \IPS\Application $app )
|
||||
{
|
||||
$iterator = new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_profile_steps' ), '\IPS\Member\ProfileStep' );
|
||||
|
||||
foreach( $iterator as $step )
|
||||
{
|
||||
list( $application, $extension ) = explode( '_', $this->_data['extension'] );
|
||||
|
||||
if ( $application == $app->directory )
|
||||
{
|
||||
$step->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Dummy Member Model used by installer
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @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 1 Jul 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Member;
|
||||
|
||||
Reference in new issue
Block a user