Version 4.7.13

This commit is contained in:
Neo committed 2025-12-19 16:14:56 -08:00
1 parent fd22d99e69
commit 7124a02564
954 files changed
+29277 -59308

No files matched your search

@@ -397,6 +397,13 @@ class _directory extends \IPS\Dispatcher\Controller
$club = new \IPS\Member\Club;
if ( $form = $club->form( FALSE, TRUE, $availableTypes ) )
{
if( $values = $form->values() )
{
$club->processForm( $values, FALSE, TRUE, $availableTypes );
\IPS\Output::i()->redirect( $club->url() );
}
$form->class = 'ipsForm_vertical';
}
else
+52 -8
View File
@@ -77,9 +77,10 @@ class _view extends \IPS\Helpers\CoverPhoto\Controller
array( \IPS\Http\Url::internal( 'app=core&module=clubs&controller=directory', 'front', 'clubs_list' ), \IPS\Member::loggedIn()->language()->addToStack('module__core_clubs') )
);
/* Add a "Search in this club" contextual search option */
/* Add a "Search in this club" contextual search option and set to default*/
\IPS\Output::i()->contextualSearchOptions[ \IPS\Member::loggedIn()->language()->addToStack( 'search_contextual_item_club' ) ] = array( 'type' => '', 'club' => "c{$this->club->id}" );
\IPS\Output::i()->defaultSearchOption = array( '', 'search_contextual_item_club' );
/* CSS */
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/clubs.css', 'core', 'front' ) );
if ( \IPS\Theme::i()->settings['responsive'] )
@@ -265,22 +266,42 @@ class _view extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Output::i()->error( 'no_module_permission', '2C350/A', 403, '' );
}
if ( $form = $this->club->form() )
$form = $this->club->form();
if( $values = $form->values() )
{
\IPS\Output::i()->title = $this->club->name;
if( \IPS\Request::i()->isAjax() )
$this->club->skipCloneDuplication = TRUE;
$old = clone $this->club;
$this->club->processForm( $values, FALSE, FALSE, NULL );
$changes = $this->club::renewalChanges( $old, $this->club );
if ( !empty( $changes ) )
{
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->decision( 'product_change_blurb', array(
'product_change_blurb_existing' => \IPS\Http\Url::internal( "app=core&module=clubs&controller=view&do=updateExisting&id={$this->club->id}" )->setQueryString( 'changes', json_encode( $changes ) )->csrf(),
'product_change_blurb_new' => $this->club->url(),
) );
return;
}
else
{
\IPS\Output::i()->output = $form;
\IPS\Output::i()->redirect( $this->club->url() );
}
}
\IPS\Output::i()->title = $this->club->name;
if( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
}
else
{
\IPS\Output::i()->redirect( $this->club->url() );
\IPS\Output::i()->output = $form;
}
}
/**
@@ -1776,4 +1797,27 @@ class _view extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Output::i()->metaTags['og:description'] = $this->club->about;
}
}
/**
* Update Existing Purchases
*
* @return void
*/
public function updateExisting()
{
\IPS\Session::i()->csrfCheck();
/* Make sure logged-in user has permission */
if ( !( $this->club->owner and $this->club->owner->member_id == \IPS\Member::loggedIn()->member_id ) and !\IPS\Member::loggedIn()->modPermission('can_access_all_clubs') )
{
\IPS\Output::i()->error( 'no_module_permission', '3C350/Q', 403, '' );
}
$changes = json_decode( \IPS\Request::i()->changes, TRUE );
\IPS\Task::queue( 'core', 'UpdateClubRenewals', array( 'changes' => $changes, 'club' => $this->club->id ), 5 );
/* Redirect */
\IPS\Output::i()->redirect( $this->club->url(), 'saved' );
}
}
@@ -42,6 +42,7 @@ class _contact extends \IPS\Dispatcher\Controller
/* Execute */
return parent::execute();
}
/**
* Method
*
@@ -82,6 +83,28 @@ class _contact extends \IPS\Dispatcher\Controller
if ( $values = $form->values() )
{
if ( !\IPS\Member::loggedIn()->member_id AND \IPS\Settings::i()->contact_email_verify )
{
$key = \IPS\Login::generateRandomString( 32 );
\IPS\Db::i()->insert( 'core_contact_verify', array(
'email_address' => $values['email_address'],
'contact_data' => json_encode( $values ),
'verify_key' => $key
), TRUE );
$email = \IPS\Email::buildFromTemplate( 'core', 'contact_verify', array( $values['email_address'], $key ), \IPS\Email::TYPE_TRANSACTIONAL );
$email->send( $values['email_address'] );
if ( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->json( 'OK' );
}
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'contact_verify' );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system' )->contactVerify();
return;
}
foreach ( $extensions as $k => $class )
{
if ( $handled = $class->handleForm( $values ) )
@@ -102,7 +125,63 @@ class _contact extends \IPS\Dispatcher\Controller
{
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'contact' );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system' )->contact( $form );
}
}
}
/**
* Confirm
*
* @return void
*/
protected function confirm()
{
/* Show interstitial page to prevent email clients from auto-verifying. */
\IPS\Output::i()->sidebar['enabled'] = FALSE;
\IPS\Output::i()->bodyClasses[] = 'ipsLayout_minimal';
$form = new \IPS\Helpers\Form( 'form', NULL );
$form->class = 'ipsForm_vertical';
$form->actionButtons[] = \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->button( 'contact_click_to_verify', 'submit', null, 'ipsButton ipsButton_primary ipsButton_fullWidth', array( 'tabindex' => '2', 'accesskey' => 's' ) );
$form->hiddenValues['email'] = \IPS\Request::i()->email;
$form->hiddenValues['key'] = \IPS\Request::i()->key;
if ( $values = $form->values() )
{
try
{
$verify = \IPS\Db::i()->select( '*', 'core_contact_verify', array( "email_address=?", $values['email'] ) )->first();
}
catch( \UnderflowException )
{
\IPS\Output::i()->error( 'node_error', '2C435/1', 404, '' );
}
if ( \IPS\Login::compareHashes( $verify['verify_key'], $values['key'] ) === FALSE )
{
\IPS\Output::i()->error( 'contact_verify_key_mismatch', '2C435/2', 403, '' );
}
/* Send it */
$extensions = \IPS\Application::allExtensions( 'core', 'ContactUs', FALSE, 'core', 'InternalEmail', TRUE );
foreach( $extensions AS $k => $extension )
{
if ( $extension->handleForm( json_decode( $verify['contact_data'], true ) ) )
{
break;
}
}
\IPS\Db::i()->delete( 'core_contact_verify', array( "email_address=?", $values['email'] ) );
\IPS\Output::i()->sidebar['enabled'] = FALSE;
\IPS\Output::i()->bodyClasses[] = 'ipsLayout_minimal';
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'message_sent' );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system' )->contactDone();
return;
}
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'contact_verify' );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system' )->contactConfirmVerify( $form );
}
}
@@ -0,0 +1,155 @@
<?php
/**
* @brief account
* @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 21 Aug 2023
*/
namespace IPS\core\modules\front\hive;
use IPS\core\Hive;
use IPS\Db;
use IPS\Http\Url;
use IPS\Member;
use IPS\Output;
use IPS\Session;
use IPS\Settings;
use IPS\Theme;
/* 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;
}
/**
* account
*/
class _account extends \IPS\Dispatcher\Controller
{
/**
* Execute
*
* @return void
*/
public function execute()
{
parent::execute();
}
/**
* ...
*
* @return void
*/
protected function manage()
{
Output::i()->error( 'node_error', '2HV100/2', 404, '' );
}
/**
* Follow Community
*
* @return void
*/
protected function follow()
{
/* Check Hive is Enabled */
if( empty( Settings::i()->hive_key ) )
{
Output::i()->error( 'node_error', '2HV100/3', 404, '' );
}
if( Member::loggedIn()->member_id )
{
$form = new \IPS\Helpers\Form( 'hive', 'hive_subscribe_button' );
$form->class = 'ipsForm_vertical ipsType_center ipsForm_noLabels';
$form->add( new \IPS\Helpers\Form\Email( 'hive_subscriber_email_address', Member::loggedIn()->email ?? '', TRUE, [ 'placeholder' => Member::loggedIn()->language()->addToStack('email_address')] ) );
if ( $values = $form->values() )
{
Session::i()->csrfCheck();
try
{
$response = Hive::api( 'subscribe', [
'site_member_id' => Member::loggedIn()->member_id ?? 0,
'group_hash' => Member::loggedin()->member_id ? Hive::groupHash( Member::loggedin() ) : 'guest',
'member_email' => $values['hive_subscriber_email_address'],
] );
/* Save subscribe */
if ( Member::loggedIn()->member_id )
{
try
{
Db::i()->insert( 'core_hive_subscribers', [ 'member_id' => Member::loggedIn()->member_id, 'subscribe_date' => time() ] );
}
catch ( Db\Exception $e ){}
}
if ( !empty( $response['redirect_url'] ) )
{
Output::i()->redirect( Url::external( $response['redirect_url'] ) );
}
}
catch ( \IPS\Http\Url\Exception|\LogicException $e )
{
$form->error = $e->getMessage();
}
}
}
else
{
$form = Theme::i()->getTemplate( 'hive', 'core', 'front' )->signin();
}
Output::i()->title = Member::loggedIn()->language()->addToStack('hive_subscribe_to_hive', NULL, [ 'sprintf' => Settings::i()->board_name ]);
Output::i()->output = Theme::i()->getTemplate( 'hive', 'core', 'front' )->follow( $form );
}
/**
* Anonymously follow community
*
* @return void
*/
protected function anonymousFollow()
{
Output::i()->pageCaching = FALSE;
/* Check Hive is Enabled */
if( empty( Settings::i()->hive_key ) )
{
Output::i()->error( 'node_error', '2HV100/4', 404, '' );
}
try
{
$response = Hive::api( 'subscribe', [
'site_member_id' => 0,
'group_hash' => 'guest',
'member_email' => false,
] );
/* Save subscribe */
if ( !empty( $response['redirect_url'] ) )
{
Output::i()->redirect( Url::external( $response['redirect_url'] ) );
}
else
{
throw new \LogicException( 'unknown_error' );
}
}
catch ( \IPS\Http\Url\Exception|\LogicException $e )
{
Output::i()->error( $e->getMessage(), '2HV100/1', 403, '' );
}
}
}
@@ -0,0 +1,402 @@
<?php
/**
* @brief content
* @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 21 Aug 2023
*/
namespace IPS\core\modules\front\hive;
use IPS\core\Hive;
use IPS\Content\Item;
use IPS\core\Stream;
use IPS\Db;
use IPS\File;
use IPS\File\Exception;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Member;
use IPS\Output;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Request;
use IPS\Settings;
use Jose\Component\Checker\AudienceChecker;
use Jose\Component\Checker\ClaimCheckerManager;
use Jose\Component\Checker\ExpirationTimeChecker;
use Jose\Component\Checker\InvalidClaimException;
use Jose\Component\Checker\IssuedAtChecker;
use Jose\Component\Checker\IssuerChecker;
use Jose\Component\Checker\MissingMandatoryClaimException;
use Jose\Component\Checker\NotBeforeChecker;
use Jose\Component\Signature\Serializer\CompactSerializer;
/* 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;
}
/**
* content
*/
class _content extends \IPS\Dispatcher\Controller
{
public function __construct( $url = NULL )
{
parent::__construct( $url );
IPS::$PSR0Namespaces['Jose'] = \IPS\ROOT_PATH . '/system/3rd_party/JwtFramework/src';
IPS::$PSR0Namespaces['Base64Url'] = \IPS\ROOT_PATH .'/system/3rd_party/Base64Url';
\IPS\Output::i()->pageCaching = FALSE;
}
/**
* Execute
*
* @return void
*/
public function execute()
{
if( isset( Request::i()->click ) )
{
$this->_doClick();
}
elseif( isset( Request::i()->follow ) )
{
$this->_doFollowCommunity();
}
else
{
/* Don't feed any content if disabled */
if( !Settings::i()->hive_enabled )
{
Output::i()->json( [ 'error' => 'Community Hive not enabled' ], 404 );
}
$this->_verifyJwt();
}
parent::execute();
}
/**
* ...
*
* @return void
*/
protected function manage()
{
try
{
match ( $this->payload['request_type'] )
{
'content' => $this->_getContent(),
'sync' => $this->_syncMembers(),
'unfollow' => $this->_unfollow()
};
}
catch( \UnhandledMatchError $e )
{
Output::i()->json( array( 'error' => 'Invalid request type' ), 400 );
}
}
/**
* Process Hive click and redirect
*
* @return void
*/
protected function _doClick()
{
try
{
$string = base64_decode( Request::i()->click );
$result = [];
parse_str( $string, $result );
if( empty( $result['key2' ] ) )
{
throw new \UnexpectedValueException( 'key2 missing' );
}
$item = explode( '/', $result['key2'] );
if( \count( $item ) !== 2 )
{
throw new \UnexpectedValueException( 'key2 format unexpected' );
}
if( !class_exists( $item[0], TRUE ) )
{
throw new \UnexpectedValueException( 'key2 data unexpected' );
}
$object = $item[0]::load( (int) $item[1] );
if( !( $object instanceof \IPS\Content ) )
{
throw new \UnexpectedValueException( 'key2 data unexpected 2' );
}
Output::i()->redirect( $object->url() );
}
catch( \UnexpectedValueException | \OutOfRangeException $e )
{
Output::i()->redirect( Url::internal( '' ) );
}
}
/**
* Redirect to Hive follow page
*
* @return void
*/
protected function _doFollowCommunity()
{
Output::i()->redirect( Url::internal( 'app=core&module=hive&controller=account&do=follow', 'front', 'hive_follow' ) );
}
/**
* Get content for Hive
*
* @return void
*/
protected function _getContent()
{
/* Make sure we have a valid payload */
if( ! isset ( $this->payload['group_hash'] ) )
{
Output::i()->json( [ 'error' => 'Missing Group Hash' ], 400 );
}
/* Use new member object for permissions */
$member = new Member;
/* Set secondary groups */
$member->member_group_id = Settings::i()->guest_group;
if( $this->payload['group_hash'] !== 'guest' )
{
$member->mgroup_others = $this->payload['group_hash'];
}
$stream = Stream::allActivityStream();
$stream->date_relative_days = 365;
$stream->id = 0;
$stream->include_comments = TRUE;
$stream->baseUrl = Url::internal( "app=core&module=discover&controller=streams", 'front', 'discover_all' );
/* Content Limitations */
$settings = json_decode( Settings::i()->hive_content, TRUE );
if( $settings['content_classes'] !== '*' )
{
$stream->classes = $settings['content_classes'];
}
else
{
$stream->classes = '';
foreach ( \IPS\Content::routedClasses( TRUE, FALSE, TRUE ) as $class )
{
if ( is_subclass_of( $class, 'IPS\Content\Searchable' ) and isset( $class::$databaseColumnMap['date'] ) )
{
$stream->classes .= ',' . $class;
}
}
}
/* Container - Stream wants JSON and we have an array... */
$stream->containers = json_encode( $settings['content_containers'] );
$query = $stream->query( $member )->setLimit( 10 );
$results = $query->search();
$return = [];
foreach( $results as $comment )
{
$commentData = $comment->asArray();
$commentClass = $commentData['indexData']['index_class'];
$itemClass = $commentClass::$itemClass ?? $commentClass;
$item = $itemClass::load( $commentData['indexData']['index_item_id'] );
/* Attachments */
$attachment = $fileObj = NULL;
try
{
if( !empty( $commentData['itemData']['attachedImages'] ) )
{
$fileObj = File::get( $commentData['itemData']['attachedImages'][0]['extension'], $commentData['itemData']['attachedImages'][0]['thumb_location'] ?: $commentData['itemData']['attachedImages'][0]['location'] );
}
elseif( is_subclass_of( $commentData['indexData']['index_class'], 'IPS\Content\Item' ) AND $contentImage = $item->contentImages( 1, TRUE ) )
{
$attachType = key( $contentImage[0] );
$fileObj = File::get( $attachType, $contentImage[0][ $attachType ] );
}
if( $fileObj !== NULL )
{
if ( $fileObj->isImage() )
{
$fileContents = $fileObj->contents();
if ( \strlen( $fileContents ) > 1000000 )
{
throw new \OutOfRangeException;
}
$attachment = [
'name' => $fileObj->originalFilename,
'file' => base64_encode( $fileContents )
];
}
}
}
catch( Exception | \UnderflowException | \OutOfRangeException $e ) { }
/* Truncate content */
if( mb_strlen( $commentData['indexData']['index_content'] ) > 500 )
{
$commentData['indexData']['index_content'] = trim( mb_substr( $commentData['indexData']['index_content'], 0, 500 ) ) . '...';
}
$classObj = $commentClass::load( $commentData['indexData']['index_object_id'] );
$return[] = array(
'title' => $item->mapped('title'),
'content' => $commentData['indexData']['index_content'],
'date' => $commentData['indexData']['index_date_commented'],
'author' => $classObj->isAnonymous() ? \IPS\Member::loggedIn()->language()->get( "post_anonymously_placename" ) : $commentData['authorData']['name'],
'key1' => $itemClass . '/' . $commentData['indexData']['index_item_id'],
'key2' => $commentClass . '/' . $commentData['indexData']['index_object_id'],
'replies' => ( $item instanceof Item ) ? $item->commentCount() : NULL,
'reactions' => IPS::classUsesTrait( $itemClass, 'IPS\Content\Reactable' ) ? $item->reactionCount() : NULL,
'image' => $attachment
);
}
Output::i()->json([
'results' => $return
]);
}
/**
* Sync Hive Membership data
*
* @return void
*/
protected function _syncMembers()
{
if( !isset( $this->payload['sync_data'] ) OR !\count( $this->payload['sync_data'] ) )
{
Output::i()->json( [ 'error' => 'Missing sync data' ], 400 );
}
$memberIds = array_keys( $this->payload['sync_data'] );
$memberData = new ActiveRecordIterator( Db::i()->select( '*', 'core_members', [ Db::i()->in( 'member_id', $memberIds ) ] ), 'IPS\Member' );
$respond = [];
$seen = [];
foreach( $memberData as $member )
{
$generatedHash = Hive::groupHash( $member );
if( $generatedHash !== $this->payload['sync_data'][ $member->member_id ] )
{
$respond[ $member->member_id ] = $generatedHash;
}
$seen[] = $member->member_id;
}
/* Record subscribe confirm */
Db::i()->update( 'core_hive_subscribers', [ 'subscribe_confirmed' => 1 ], [ [ 'subscribe_confirmed=?', 0 ], [ Db::i()->in( 'member_id', $memberIds ) ] ] );
/* Those that haven't been seen, thus deleted */
$deleted = array_diff( $memberIds, $seen );
foreach( $deleted as $del )
{
$respond[ $del ] = 'guest';
}
Output::i()->json( $respond );
}
/**
* Unfollow
*
* @return void
*/
protected function _unfollow()
{
Db::i()->delete( 'core_hive_subscribers', [ 'member_id=?', (int) $this->payload['member_id'] ] );
Output::i()->json( 'ok' );
}
/**
* Verify JWT
*
* @return void
*/
protected function _verifyJwt()
{
/* Make sure the request is PUT */
if( ! isset( $_SERVER['REQUEST_METHOD'] ) OR mb_strtoupper( $_SERVER['REQUEST_METHOD'] ) !== 'POST' )
{
Output::i()->json( array( 'error' => 'Invalid HTTP Method' ), 400 );
}
/* Make sure we have the hive key (activated) */
if( ! Settings::i()->hive_enabled )
{
Output::i()->json( array( 'error' => 'Community Hive not enabled' ), 404 );
}
/* Do we have the JWT */
$token = file_get_contents('php://input');
if( empty( $token ) )
{
Output::i()->json( array( 'error' => 'Missing JWT' ), 401 );
}
$jwk = new \Jose\Component\Core\JWK([
'kty' => 'oct',
'k' => base64_encode( Settings::i()->hive_key )
]);
$jwsVerifier = new \Jose\Component\Signature\JWSVerifier(
new \Jose\Component\Core\AlgorithmManager([ new \Jose\Component\Signature\Algorithm\HS256() ])
);
$jwsCompactSerializer = new CompactSerializer();
$data = $jwsCompactSerializer->unserialize( $token );
if( !$jwsVerifier->verifyWithKey( $data, $jwk, 0 ) )
{
Output::i()->json( array( 'error' => 'Invalid JWT' ), 401 );
}
$this->payload = json_decode( $data->getPayload(), true );
$claimCheckerManager = new ClaimCheckerManager(
[
new IssuerChecker( ['communityhive'] ),
new AudienceChecker( rtrim( Settings::i()->base_url, '/' ) ),
new IssuedAtChecker( 1000 ),
new NotBeforeChecker( 1000 ),
new ExpirationTimeChecker( 1000 )
]
);
try
{
$claimCheckerManager->check( $this->payload, [ 'iss', 'sub', 'exp', 'aud', 'nbf', 'iat' ] );
}
catch( MissingMandatoryClaimException | InvalidClaimException $e )
{
Output::i()->json( array( 'message' => $e->getMessage() ), 400 );
}
parent::execute();
}
}
@@ -322,8 +322,8 @@ class _profile extends \IPS\Helpers\CoverPhoto\Controller
$page = 1;
}
$clubsCount = \IPS\Member\Club::clubs( \IPS\Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), 'last_activity', $this->member, array(), NULL, TRUE );
$allClubs = \IPS\Member\Club::clubs( \IPS\Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), 'last_activity', $this->member );
$clubsCount = \IPS\Member\Club::clubs( \IPS\Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), 'last_activity', $this->member, array(), \IPS\Member::loggedIn()->modPermission( 'can_access_all_clubs' ) ? NULL : "( show_membertab = 'nonmember' OR show_membertab IS NULL )", TRUE );
$allClubs = \IPS\Member\Club::clubs( \IPS\Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), 'last_activity', $this->member, array(), \IPS\Member::loggedIn()->modPermission( 'can_access_all_clubs' ) ? NULL : "( show_membertab = 'nonmember' OR show_membertab IS NULL )" );
$pagination = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->pagination( $baseUrl, ( ceil( $clubsCount / $perPage ) ), $page, $perPage );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/clubs.css', 'core', 'front' ) );
@@ -609,9 +609,9 @@ class _profile extends \IPS\Helpers\CoverPhoto\Controller
{
$where[] = $currentClass::commentWhere();
}
$baseUrl = \IPS\Http\Url::internal( "app=core&module=members&controller=profile&id={$this->member->member_id}&do=content", 'front', 'profile_content', $this->member->members_seo_name );
$output = new \IPS\Helpers\Table\Content( $currentClass, $baseUrl->setQueryString( array( 'type' => $currentType ) ), $where, NULL, NULL, 'read', FALSE );
$output = new \IPS\Helpers\Table\Content( $currentClass, $baseUrl->setQueryString( array( 'type' => $currentType ) ), $where, NULL, \IPS\Content\Hideable::FILTER_AUTOMATIC, 'read', FALSE );
}
if ( $currentType == 'core_statuses_status' )
@@ -535,7 +535,7 @@ class _messenger extends \IPS\Content\Controller
} );
$form = new \IPS\Helpers\Form;
$form->add( new \IPS\Helpers\Form\Select( 'move_message_to', NULL, FALSE, array( 'options' => array_merge( array( 'myconvo' => \IPS\Member::loggedIn()->language()->addToStack('inbox') ), $folders ), 'parse' => 'normal' ) ) );
$form->add( new \IPS\Helpers\Form\Select( 'move_message_to', NULL, FALSE, array( 'options' => array( 'myconvo' => \IPS\Member::loggedIn()->language()->addToStack('inbox') ) + $folders, 'parse' => 'normal' ) ) );
if ( $values = $form->values() )
{
@@ -316,12 +316,12 @@ class _search extends \IPS\Dispatcher\Controller
case 'IPS\Helpers\Form\Email':
case 'IPS\Helpers\Form\TextArea':
case 'IPS\Helpers\Form\Url':
case 'IPS\Helpers\Form\Date':
case 'IPS\Helpers\Form\Number':
$fieldName = 'core_pfield_' . $id;
if( isset( \IPS\Request::i()->$fieldName ) )
{
if( \IPS\Data\Store::i()->profileFields['fields'][$group][$id]['pf_search_type'] == 'loose' )
{
$where[] = array( 'LOWER(core_pfields_content.field_' . $id . ') LIKE ?', '%' . mb_strtolower( \IPS\Request::i()->$fieldName ) . '%' );
@@ -333,6 +333,21 @@ class _search extends \IPS\Dispatcher\Controller
$baseUrl = $baseUrl->setQueryString( $fieldName, \IPS\Request::i()->$fieldName );
}
break;
case 'IPS\Helpers\Form\Date':
$fieldName = 'core_pfield_' . $id;
if ( is_array( \IPS\Request::i()->$fieldName ) AND \IPS\Request::i()->$fieldName['start'] )
{
$start = new \IPS\DateTime( \IPS\Request::i()->$fieldName['start'] );
$where[] = "core_pfields_content.field_{$id}>" . $start->getTimestamp();
}
if ( is_array( \IPS\Request::i()->$fieldName ) AND \IPS\Request::i()->$fieldName['end'] )
{
$end = new \IPS\DateTime( \IPS\Request::i()->$fieldName['end'] );
$where[] = "core_pfields_content.field_{$id}<" . $end->getTimestamp();
}
break;
case 'IPS\Helpers\Form\Select':
case 'IPS\Helpers\Form\CheckboxSet':
case 'IPS\Helpers\Form\Radio':
@@ -498,27 +513,27 @@ class _search extends \IPS\Dispatcher\Controller
if ( isset( \IPS\Request::i()->search_min_comments ) )
{
$filter->minimumComments( \IPS\Request::i()->search_min_comments );
$baseUrl = $baseUrl->setQueryString( 'search_min_comments', \IPS\Request::i()->search_min_comments );
$titleConditions[] = \IPS\Member::loggedIn()->language()->addToStack( 'search_blurb_min_comments', FALSE, array( 'sprintf' => array( \IPS\Request::i()->search_min_comments ) ) );
$filter->minimumComments( (int) \IPS\Request::i()->search_min_comments );
$baseUrl = $baseUrl->setQueryString( 'search_min_comments', (int) \IPS\Request::i()->search_min_comments );
$titleConditions[] = \IPS\Member::loggedIn()->language()->addToStack( 'search_blurb_min_comments', FALSE, [ 'sprintf' => [ (int) \IPS\Request::i()->search_min_comments ] ] );
}
if ( isset( \IPS\Request::i()->search_min_replies ) AND isset( $class::$commentClass ) )
{
$filter->minimumComments( \IPS\Request::i()->search_min_replies + 1 );
$baseUrl = $baseUrl->setQueryString( 'search_min_replies', \IPS\Request::i()->search_min_replies );
$titleConditions[] = \IPS\Member::loggedIn()->language()->addToStack( 'search_blurb_min_replies', FALSE, array( 'sprintf' => array( \IPS\Request::i()->search_min_replies ) ) );
$filter->minimumComments( (int) \IPS\Request::i()->search_min_replies + 1 );
$baseUrl = $baseUrl->setQueryString( 'search_min_replies', (int) \IPS\Request::i()->search_min_replies );
$titleConditions[] = \IPS\Member::loggedIn()->language()->addToStack( 'search_blurb_min_replies', FALSE, [ 'sprintf' => [ (int) \IPS\Request::i()->search_min_replies ] ] );
}
if ( isset( \IPS\Request::i()->search_min_reviews ) AND isset( $class::$reviewClass ) )
{
$filter->minimumReviews( \IPS\Request::i()->search_min_reviews );
$baseUrl = $baseUrl->setQueryString( 'search_min_reviews', \IPS\Request::i()->search_min_reviews );
$titleConditions[] = \IPS\Member::loggedIn()->language()->addToStack( 'search_blurb_min_reviews', FALSE, array( 'sprintf' => array( \IPS\Request::i()->search_min_reviews ) ) );
$filter->minimumReviews( (int) \IPS\Request::i()->search_min_reviews );
$baseUrl = $baseUrl->setQueryString( 'search_min_reviews', (int) \IPS\Request::i()->search_min_reviews );
$titleConditions[] = \IPS\Member::loggedIn()->language()->addToStack( 'search_blurb_min_reviews', FALSE, [ 'sprintf' => [ (int) \IPS\Request::i()->search_min_reviews ] ] );
}
if ( isset( \IPS\Request::i()->search_min_views ) AND isset( $class::$databaseColumnMap['views'] ) AND \IPS\Content\Search\Index::i()->supportViewFiltering() )
{
$filter->minimumViews( \IPS\Request::i()->search_min_views );
$baseUrl = $baseUrl->setQueryString( 'search_min_views', \IPS\Request::i()->search_min_views );
$titleConditions[] = \IPS\Member::loggedIn()->language()->addToStack( 'search_blurb_min_views', FALSE, array( 'sprintf' => array( \IPS\Request::i()->search_min_views ) ) );
$filter->minimumViews( (int) \IPS\Request::i()->search_min_views );
$baseUrl = $baseUrl->setQueryString( 'search_min_views', (int) \IPS\Request::i()->search_min_views );
$titleConditions[] = \IPS\Member::loggedIn()->language()->addToStack( 'search_blurb_min_views', FALSE, [ 'sprintf' => [ (int) \IPS\Request::i()->search_min_views ] ] );
}
$query->filterByContent( array( $filter ) );
@@ -314,7 +314,7 @@ class _ajax extends \IPS\Dispatcher\Controller
public function topContributors()
{
/* How many? */
$limit = \intval( ( isset( \IPS\Request::i()->limit ) and \IPS\Request::i()->limit <= 25 ) ? \IPS\Request::i()->limit : 5 );
$limit = \intval( ( isset( \IPS\Request::i()->limit ) and \IPS\Request::i()->limit <= 25 and \IPS\Request::i()->limit > 0 ) ? \IPS\Request::i()->limit : 5 );
/* What timeframe? */
$where = array( array( 'member_received > 0' ) );
@@ -11,6 +11,10 @@
namespace IPS\core\modules\front\system;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Helpers\Form\YesNo;
use IPS\Output;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
@@ -18,12 +22,19 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
}
/**
* Cookie Policy
* Cookie Information Page
*/
class _cookies extends \IPS\Dispatcher\Controller
{
public function execute()
{
parent::execute();
\IPS\Output::i()->pageCaching = FALSE;
}
/**
* Privacy Policy
* Cookie Information Page
*
* @return void
*/
@@ -35,7 +46,35 @@ class _cookies extends \IPS\Dispatcher\Controller
\IPS\Output::i()->breadcrumb[] = array( NULL, \IPS\Member::loggedIn()->language()->addToStack('cookies_about') );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('cookies_about');
\IPS\Output::i()->sidebar['enabled'] = FALSE;
\IPS\Output::i()->metaTags['robots'] = 'noindex';
\IPS\Output::i()->bodyClasses[] = 'ipsLayout_minimal';
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system' )->cookies();
}
/**
* Opt out of optional cookies
*
* @return void
*/
protected function cookieConsentToggle()
{
\IPS\Session::i()->csrfCheck();
\IPS\Member::loggedIn()->setAllowOptionalCookies( (bool) \IPS\Request::i()->status );
if ( isset( \IPS\Request::i()->ref ) )
{
try
{
$url = \IPS\Http\Url::createFromString( base64_decode( \IPS\Request::i()->ref ) );
}
catch( \IPS\Http\Url\Exception $e )
{
$url = NULL;
}
if ( $url instanceof \IPS\Http\Url\Internal and !$url->openRedirect() )
{
\IPS\Output::i()->redirect( $url );
}
}
}
}
@@ -394,7 +394,7 @@ class _register extends \IPS\Dispatcher\Controller
throw new \DomainException( 'q_and_a_incorrect' );
}
$answers = json_decode( $question['qa_answers'] );
$answers = json_decode( $question['qa_answers'], true );
if( $answers )
{
@@ -551,9 +551,11 @@ class _register extends \IPS\Dispatcher\Controller
/* Query spam service */
$spamCode = NULL;
$spamAction = NULL;
$disposable = FALSE;
$geoBlock = FALSE;
if( \IPS\Settings::i()->spam_service_enabled )
{
$spamAction = $member->spamService( 'register', NULL, $spamCode );
$spamAction = $member->spamService( 'register', NULL, $spamCode, $disposable, $geoBlock );
if( $spamAction == 4 )
{
\IPS\Output::i()->error( 'spam_denied_account', '2S129/1', 403, '' );
@@ -575,7 +577,7 @@ class _register extends \IPS\Dispatcher\Controller
/* Save anything the profile extensions did */
$member->save();
$member->logHistory( 'core', 'account', array( 'type' => 'register', 'spamCode' => $spamCode, 'spamAction' => $spamAction ), FALSE );
$member->logHistory( 'core', 'account', array( 'type' => 'register', 'spamCode' => $spamCode, 'spamAction' => $spamAction, 'disposable' => $disposable, 'geoBlock' => $geoBlock ), FALSE );
/* Security Questions */
if ( \IPS\Settings::i()->security_questions_enabled and \in_array( \IPS\Settings::i()->security_questions_prompt, array( 'register', 'optional' ) ) )
@@ -834,6 +836,11 @@ class _register extends \IPS\Dispatcher\Controller
}
elseif ( \IPS\Member::loggedIn()->real_name and \IPS\Member::loggedIn()->email )
{
/* If we somehow came here from the oauth authorization prompt but the member, redirect back there */
if ( isset( \IPS\Request::i()->oauth ) and $ref = static::_refUrl() and $ref->base === 'none' )
{
\IPS\Output::i()->redirect( $ref );
}
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( '' ) );
}
@@ -138,7 +138,6 @@ class _settings extends \IPS\Dispatcher\Controller
/* Show our own oauth clients? */
$showApps = (bool) \IPS\Db::i()->select( 'COUNT(*)', 'core_oauth_clients', array( array( 'oauth_enabled=1 AND oauth_ucp=1' ) ) )->first();
/* Return */
return \IPS\Theme::i()->getTemplate( 'system' )->settings( $area, $output, ( \IPS\Settings::i()->allow_email_changes != 'disabled' ), $canChangePassword, \IPS\Member::loggedIn()->group['g_dname_changes'], $canChangeSignature, $loginMethods, $canConfigureMfa, $showApps );
}
@@ -240,8 +239,14 @@ class _settings extends \IPS\Dispatcher\Controller
/* Build the form */
$form = new \IPS\Helpers\Form;
$form->class = 'ipsForm_collapseTablet';
$form->addDummy( 'current_email', htmlspecialchars( \IPS\Member::loggedIn()->email, ENT_DISALLOWED, 'UTF-8', FALSE ) );
$form->add( new \IPS\Helpers\Form\Email( 'new_email', '', TRUE, array( 'accountEmail' => \IPS\Member::loggedIn() ) ) );
$currentEmail = htmlspecialchars( \IPS\Member::loggedIn()->email, ENT_DISALLOWED, 'UTF-8', FALSE );
$form->addDummy( 'current_email', \IPS\Member::loggedIn()->members_bitoptions["email_messages_bounce"] ? \IPS\Theme::i()->getTemplate( 'global', 'core' )->memberEmailBlockedMessage( $currentEmail ) : $currentEmail );
$form->add( new \IPS\Helpers\Form\Email(
'new_email',
'',
TRUE,
array( 'accountEmail' => \IPS\Member::loggedIn() )
) );
/* Handle submissions */
$values = NULL;
@@ -634,7 +639,7 @@ class _settings extends \IPS\Dispatcher\Controller
return \IPS\Theme::i()->getTemplate( 'system' )->settingsMfaPassword( $login, $error );
}
}
/* Get our handlers and the output, even if it's just for a backdrop */
$handlers = array();
foreach ( \IPS\MFA\MFAHandler::handlers() as $key => $handler )
@@ -683,6 +688,9 @@ class _settings extends \IPS\Dispatcher\Controller
\IPS\Member::loggedIn()->members_bitoptions['security_questions_opt_out'] = FALSE;
\IPS\Member::loggedIn()->save();
/* Invalidate other sessions */
\IPS\Member::loggedIn()->invalidateSessionsAndLogins( \IPS\Session::i()->id );
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ) );
}
@@ -720,10 +728,128 @@ class _settings extends \IPS\Dispatcher\Controller
return;
}
}
$output.= \IPS\Theme::i()->getTemplate( 'system' )->settingsPrivacy();
/* If we're still here, just show the screen */
return $output;
}
/**
* Request personal identifiable information
*
* @return void
*/
protected function requestPiiData()
{
if( !isset( $_SESSION['passwordValidatedForMfa'] ) OR !\IPS\Member\PrivacyAction::canRequestPiiData() OR \IPS\Settings::i()->pii_type !== 'on' )
{
\IPS\Output::i()->error( 'node_error', '1C122/10', 403, '' );
}
\IPS\Member\PrivacyAction::requestPiiData();
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front')->setFragment('piiDataRequest'), 'pii_requested' );
}
/**
* Download personal identifiable information
*
* @return void
* @throws \Exception
*/
protected function downloadPiiData()
{
\IPS\Session::i()->csrfCheck();
if( !isset( $_SESSION['passwordValidatedForMfa'] ) OR !\IPS\Member\PrivacyAction::canDownloadPiiData() OR \IPS\Settings::i()->pii_type !== 'on' )
{
\IPS\Output::i()->error( 'node_error', '1C122/11', 403, '' );
}
$xml = \IPS\Member::loggedIn()->getPiiData();
\IPS\Db::i()->delete( 'core_member_privacy_actions', array( 'member_id=? AND action=?', \IPS\Member::loggedIn()->member_id, \IPS\Member\PrivacyAction::TYPE_REQUEST_PII ) );
\IPS\Db::i()->delete( 'core_notifications', array( 'member=? AND notification_key=?', \IPS\Member::loggedIn()->member_id, 'pii_data' ) );
\IPS\Member::loggedIn()->logHistory( 'core', 'privacy', array( 'type' => 'pii_download' ) );
\IPS\Output::i()->sendOutput( $xml->asXML(), 200, 'application/xml', [ 'Content-Disposition' => \IPS\Output::getContentDisposition( 'attachment', \IPS\Member::loggedIn()->name.'_personal_information.xml' ) ], FALSE, FALSE, FALSE );
}
/**
* Request account deletion
*
* @return void
*/
protected function requestAccountDeletion()
{
\IPS\Session::i()->csrfCheck();
if( !isset( $_SESSION['passwordValidatedForMfa'] ) OR !\IPS\Member::loggedIn()->canUseAccountDeletion() OR \IPS\Settings::i()->right_to_be_forgotten_type !== 'on' )
{
\IPS\Output::i()->error( 'node_error', '2C122/13', 403, '' );
}
if( !\IPS\Member\PrivacyAction::canDeleteAccount() )
{
\IPS\Output::i()->error( 'node_error', '1C122/12', 403, '' );
}
if( \IPS\Request::i()->vkey )
{
\IPS\Member\PrivacyAction::requestAccountDeletion( NULL, FALSE );
}
else
{
\IPS\Member\PrivacyAction::requestAccountDeletion(NULL, TRUE );
}
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front')->setFragment('requestAccountDeletion'), 'account_deletion_requested' );
}
/**
* Cancel account deletion
*
* @return void
*/
protected function cancelAccountDeletion()
{
\IPS\Session::i()->csrfCheck();
try
{
$where = [];
$where[] = ['member_id=?', \IPS\Member::loggedIn()->member_id];
$where[] = [ \IPS\Db::i()->in( 'action',[\IPS\Member\PrivacyAction::TYPE_REQUEST_DELETE, \IPS\Member\PrivacyAction::TYPE_REQUEST_DELETE_VALIDATION ] ) ];
$row = \IPS\Db::i()->select( '*', \IPS\Member\PrivacyAction::$databaseTable, $where )->first();
\IPS\Member\PrivacyAction::constructFromData( $row )->delete();
\IPS\Member::loggedIn()->logHistory( 'core', 'privacy', [ 'type' => 'account_deletion_cancelled' ] );
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front'), 'account_deletion_cancelled' );
}
catch( \UnderflowException $e )
{
\IPS\Output::i()->error( 'node_error', '2C122/Y', 404, '' );
}
}
/**
* Confirm account deletion
*
* @return void
*/
protected function confirmAccountDeletion()
{
$key = \IPS\Request::i()->vid;
try
{
$request = \IPS\Member\PrivacyAction::getDeletionRequestByMemberAndKey( \IPS\Member::loggedIn(), $key );
$request->confirmAccountDeletion();
\IPS\Output::i()->redirect( $this->url->setQueryString( 'area','mfa'), 'account_deletion_confirmed' );
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2C122/Z', 404, '' );
}
}
/**
* Initial MFA Setup
@@ -1225,8 +1351,9 @@ class _settings extends \IPS\Dispatcher\Controller
$content = nl2br( trim( $pre->nodeValue ) );
$preBreaks += \count( explode( "<br />", $content ) );
}
if ( \is_numeric( $sigLimits[5] ) and ( $signature->getElementsByTagName('p')->length + $signature->getElementsByTagName('br')->length + $preBreaks ) > $sigLimits[5] )
/* Line limit with a sensible length restriction to prevent broken html */
if ( ( \is_numeric( $sigLimits[5] ) and ( $signature->getElementsByTagName('p')->length + $signature->getElementsByTagName('br')->length + $preBreaks ) > $sigLimits[5] ) or \strlen( $values['signature'] ) > 20000 )
{
$errors[] = \IPS\Member::loggedIn()->language()->addToStack('sig_num_lines_exceeded');
}
@@ -1729,6 +1856,12 @@ class _settings extends \IPS\Dispatcher\Controller
{
\IPS\Session::i()->csrfCheck();
/* Check validation */
if( !isset( $_SESSION['passwordValidatedForMfa'] ) )
{
\IPS\Output::i()->error( 'node_error', '1C122/14', 403, '' );
}
/* Check this value can be toggled */
if( \IPS\Member::loggedIn()->group['g_hide_online_list'] >= 1 )
{