Version 4.7.23

This commit is contained in:
Neo committed 2025-12-19 16:21:27 -08:00
1 parent 7124a02564
commit 25ddeb65d6
1791 files changed
+76990 -44452

No files matched your search

@@ -156,7 +156,10 @@ class _directory extends \IPS\Dispatcher\Controller
}
}
}
$baseUrl = $baseUrl->setQueryString( 'f' . $field->id, array_keys( $filters[ $field->id ] ) );
if( isset( $filters[ $field->id ] ) )
{
$baseUrl = $baseUrl->setQueryString( 'f' . $field->id, array_keys( $filters[ $field->id ] ) );
}
break;
}
@@ -249,7 +252,7 @@ class _directory extends \IPS\Dispatcher\Controller
}
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('module__core_clubs');
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate('clubs')->directory( $featuredClubs, $allClubs, $pagination, $baseUrl, $sortOption, $myClubsActivity, $mapMarkers, $view );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate('clubs')->directory( $featuredClubs, $allClubs, $pagination, $baseUrl, $sortOption, $myClubsActivity, $mapMarkers, $view, $mineOnly );
}
/**
@@ -1344,6 +1344,11 @@ class _view extends \IPS\Helpers\CoverPhoto\Controller
/* Find the purchase */
foreach ( \IPS\core\extensions\nexus\Item\ClubMembership::getPurchases( \IPS\nexus\Customer::loggedIn(), $this->club->id ) as $purchase )
{
if ( $invoice = $purchase->invoice_pending )
{
\IPS\Output::i()->redirect( $invoice->checkoutUrl() );
}
\IPS\Output::i()->redirect( $purchase->url()->setQueryString( array( 'do' => 'renew', 'cycles' => 1 ) )->csrf() );
}
@@ -59,6 +59,7 @@ class _contact extends \IPS\Dispatcher\Controller
\IPS\Output::i()->bodyClasses[] = 'ipsLayout_minimal';
$form = new \IPS\Helpers\Form( 'contact', 'send' );
$form->hiddenValues['contact_referrer'] = (string) \IPS\Request::i()->referrer();
$form->class = 'ipsForm_vertical';
$form->add( new \IPS\Helpers\Form\Editor( 'contact_text', NULL, TRUE, array(
@@ -71,7 +72,7 @@ class _contact extends \IPS\Dispatcher\Controller
{
$form->add( new \IPS\Helpers\Form\Text( 'contact_name', NULL, TRUE ) );
$form->add( new \IPS\Helpers\Form\Email( 'email_address', NULL, TRUE, array( 'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_ALL ) ) );
if ( \IPS\Settings::i()->bot_antispam_type !== 'none' and \IPS\Settings::i()->guest_captcha )
if ( \IPS\Settings::i()->bot_antispam_type !== 'none' )
{
$form->add( new \IPS\Helpers\Form\Captcha );
}
@@ -55,7 +55,7 @@ class _popular extends \IPS\Dispatcher\Controller
}
}
if ( \IPS\Application::appIsEnabled('cloud') and \IPS\cloud\Application::featureIsEnabled('trending') )
if ( \IPS\Application::appIsEnabled('cloud') and \IPS\cloud\Application::trendingIsEnabled( alwaysOn: FALSE ) )
{
$tabs[] = 'trending';
}
@@ -277,25 +277,22 @@ class _popular extends \IPS\Dispatcher\Controller
$areas[ $item::$application . '-' . $item::reactionType() ] = array( $item, \IPS\Member::loggedIn()->language()->addToStack( "{$item::$title}_pl" ) );
}
if ( isset( $item::$commentClass ) )
if ( $item::supportsComments( \IPS\Member::loggedIn() ) and $commentClass = $item::$commentClass and \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) )
{
$commentClass = $item::$commentClass;
if ( \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) )
$supportsComments = \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) and $item::supportsComments( \IPS\Member::loggedIn() );
if ( $supportsComments )
{
$areas[ $item::$application . '-' . $commentClass::reactionType() ] = array( $commentClass, \IPS\Member::loggedIn()->language()->addToStack( "{$commentClass::$title}_pl" ) );
}
}
if ( isset( $item::$reviewClass ) )
if ( $item::supportsReviews( \IPS\Member::loggedIn() ) and $reviewClass = $item::$reviewClass and \IPS\IPS::classUsesTrait( $reviewClass, 'IPS\Content\Reactable' ) )
{
$reviewClass = $item::$reviewClass;
if ( \IPS\IPS::classUsesTrait( $reviewClass, 'IPS\Content\Reactable' ) )
{
$areas[ $item::$application . '-' . $reviewClass::reactionType() ] = array( $reviewClass, \IPS\Member::loggedIn()->language()->addToStack( "{$reviewClass::$title}_pl" ) );
}
}
}
$form = new \IPS\Helpers\Form( 'popular_date', 'continue' );
$form->class = 'ipsForm_vertical';
$customStart = ( isset( \IPS\Request::i()->custom_date_start ) and \is_numeric( \IPS\Request::i()->custom_date_start ) ) ? (int) \IPS\Request::i()->custom_date_start : NULL;
@@ -1003,6 +1003,7 @@ class _streams extends \IPS\Dispatcher\Controller
protected function _rebuildStreams()
{
$default = \IPS\Member::loggedIn()->defaultStream;
/* @note SELECT_FROM_WRITE_SERVER: Avoid race conditions when the writer has the new stream in the table, but the reader may not */
\IPS\Member::loggedIn()->member_streams = json_encode( array( 'default' => $default, 'streams' => iterator_to_array( \IPS\Db::i()->select( 'id, title', 'core_streams', array( '`member`=?', \IPS\Member::loggedIn()->member_id ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->setKeyField('id')->setValueField('title') ) ) );
\IPS\Member::loggedIn()->save();
}
@@ -1,155 +0,0 @@
<?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, '' );
}
}
}
@@ -1,402 +0,0 @@
<?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();
}
}
@@ -11,6 +11,9 @@
namespace IPS\core\modules\front\members;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Member;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
@@ -57,12 +60,12 @@ class _profile extends \IPS\Helpers\CoverPhoto\Controller
if( !\IPS\Request::i()->isAjax() )
{
/* Don't index new empty profiles */
if( ! $this->member->member_posts )
{
\IPS\Output::i()->metaTags['robots'] = 'noindex, follow';
}
\IPS\Output::i()->linkTags['canonical'] = (string) $this->member->url();
if( ! $this->shouldBeIndexed() )
{
\IPS\Output::i()->metaTags['robots'] = 'noindex, follow';
}
\IPS\Output::i()->linkTags['canonical'] = (string) $this->member->url();
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_statuses.js', 'core' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_core.js', 'core', 'global' ) );
@@ -322,8 +325,11 @@ 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(), \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 )" );
/* Show all clubs this member belongs to if the viewer is a moderator with permissions to access all clubs or if the viewer is on his own profile */
$extraWhere = ( Member::loggedIn()->member_id === $this->member->member_id OR \IPS\Member::loggedIn()->modPermission( 'can_access_all_clubs' ) ) ? NULL : "( show_membertab = 'nonmember' OR show_membertab IS NULL )";
$clubsCount = \IPS\Member\Club::clubs( \IPS\Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), 'last_activity', $this->member, array(), $extraWhere, TRUE );
$allClubs = \IPS\Member\Club::clubs( \IPS\Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), 'last_activity', $this->member, array(), $extraWhere );
$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' ) );
@@ -354,7 +360,7 @@ class _profile extends \IPS\Helpers\CoverPhoto\Controller
}
/* If this is AJAX request to change the tab, just display that */
if ( \IPS\Request::i()->isAjax() and isset( \IPS\Request::i()->tab ) )
if ( \IPS\Request::i()->isAjax() and isset( \IPS\Request::i()->tab ) and !isset( \IPS\Request::i()->entireSection ) )
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( $tabContents ) );
}
@@ -428,6 +434,11 @@ class _profile extends \IPS\Helpers\CoverPhoto\Controller
'@type' => "ProfilePage",
'url' => (string) $this->member->url(),
'name' => $this->member->name,
'mainEntity'=> [
'@type' => 'Person',
'name' => $this->member->name,
'identifier' => $this->member->member_id,
],
'primaryImageOfPage' => array(
'@type' => "ImageObject",
'contentUrl' => (string) $this->member->get_photo( TRUE, TRUE ),
@@ -1988,7 +1999,6 @@ class _profile extends \IPS\Helpers\CoverPhoto\Controller
{
/* Get the different types */
$types = array();
$hasCallback = array();
foreach ( \IPS\Content::routedClasses( TRUE, FALSE, TRUE ) as $class )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'profile.css', $class::$application ) );
@@ -1999,6 +2009,11 @@ class _profile extends \IPS\Helpers\CoverPhoto\Controller
}
}
if( !count( $types ) )
{
\IPS\Output::i()->error( 'no_module_permission', '2C138/T', 403, '' );
}
/* What type are we looking at? */
$currentType = NULL;
if ( isset( \IPS\Request::i()->type ) AND array_key_exists( \IPS\Request::i()->type, $types ) )
@@ -2236,4 +2251,19 @@ class _profile extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Output::i()->redirect( $content->url(), 'recognize_removed_success' );
}
}
/**
* Should the member profile be indexed?
*
* @return bool
*/
protected function shouldBeIndexed(): bool
{
/* Don't index new empty profiles */
if( ! $this->member->member_posts )
{
return FALSE;
}
return TRUE;
}
}
@@ -45,12 +45,16 @@ class _search extends \IPS\Dispatcher\Controller
*/
protected function _checkCached()
{
/* Check whether a 304 Not modified response is appropriate */
if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) AND ( new \IPS\DateTime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )->getTimestamp() > ( time() - $this->_cacheTimeout ) )
try
{
header('HTTP/1.1 304 Not Modified' );
exit;
/* Check whether a 304 Not modified response is appropriate */
if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) AND ( new \IPS\DateTime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )->getTimestamp() > ( time() - $this->_cacheTimeout ) )
{
header('HTTP/1.1 304 Not Modified' );
exit;
}
}
catch( \Exception $e ){}
}
/**
@@ -1066,7 +1070,12 @@ class _search extends \IPS\Dispatcher\Controller
/* Fields */
foreach ( $fields as $id => $field )
{
/* Only show to non-staff if available to view by all. */
if ( \IPS\core\ProfileFields\Field::load( $id )->member_hide != 'all' AND ( !\IPS\Member::loggedIn()->isAdmin() OR !\IPS\Member::loggedIn()->modPermissions() ) )
{
continue;
}
/* Alias the lang keys */
$realLangKey = "core_pfield_{$id}";
+58 -46
View File
@@ -10,6 +10,9 @@
namespace IPS\core\modules\front\system;
use IPS\Http\Url;
use IPS\Output;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -103,58 +106,64 @@ class _ajax extends \IPS\Dispatcher\Controller
/* As you can insert "other media" from other apps (such as Downloads), we need to route the attachIDs appropriately. */
$attachmentIds = array();
foreach( array_keys( \IPS\Request::i()->attachIDs ) as $attachId )
{
if( (int) $attachId == $attachId AND mb_strlen( (int) $attachId ) == mb_strlen( $attachId ) )
{
$attachmentIds[] = $attachId;
}
else
{
try
{
$url = \IPS\Http\Url::createFromString( $attachId );
/* Get the "real" query string (whatever the query string is, plus what we can get from decoding the FURL) */
$qs = array_merge( $url->queryString, $url->hiddenQueryString );
/* We need an app, and it needs to not be an RSS link */
if ( !isset( $qs['app'] ) )
{
throw new \UnexpectedValueException;
}
/* Load the application */
$application = \IPS\Application::load( $qs['app'] );
/* Loop through our content classes and see if we can find one that matches */
foreach ( $application->extensions( 'core', 'ContentRouter' ) as $key => $extension )
{
$classes = $extension->classes;
/* So for each of those... */
foreach ( $classes as $class )
if( \IPS\Request::i()->attachIDs )
{
foreach( array_keys( \IPS\Request::i()->attachIDs ) as $attachId )
{
if( (int) $attachId == $attachId and mb_strlen( (int) $attachId ) == mb_strlen( $attachId ) )
{
$attachmentIds[] = $attachId;
}else
{
try
{
$url = \IPS\Http\Url::createFromString( $attachId );
/* Get the "real" query string (whatever the query string is, plus what we can get from decoding the FURL) */
$qs = array_merge( $url->queryString, $url->hiddenQueryString );
/* We need an app, and it needs to not be an RSS link */
if( !isset( $qs[ 'app' ] ) )
{
/* Try to load it */
try
throw new \UnexpectedValueException;
}
/* Load the application */
$application = \IPS\Application::load( $qs[ 'app' ] );
/* Loop through our content classes and see if we can find one that matches */
foreach( $application->extensions( 'core', 'ContentRouter' ) as $key => $extension )
{
$classes = $extension->classes;
/* So for each of those... */
foreach( $classes as $class )
{
$item = $class::loadFromURL( $url );
if( !$item->canView() )
/* Try to load it */
try
{
throw new \OutOfRangeException;
}
$item = $class::loadFromURL( $url );
/* If we're still here, we should be good. Any exceptions will have been caught by our general try/catch. */
$toReturn[ $attachId ] = $item->getAttachmentInfo();
break;
if( !$item->canView() )
{
throw new \OutOfRangeException;
}
/* If we're still here, we should be good. Any exceptions will have been caught by our general try/catch. */
$toReturn[ $attachId ] = $item->getAttachmentInfo();
break;
}
catch( \OutOfRangeException $e )
{
}
}
catch( \OutOfRangeException $e ){}
}
}
catch( \Exception $e )
{
}
}
catch( \Exception $e ){}
}
}
@@ -197,7 +206,7 @@ class _ajax extends \IPS\Dispatcher\Controller
{
try
{
if ( $loadedExtensions[ $map['location_key'] ]->attachmentPermissionCheck( $member, $map['id1'], $map['id2'], $map['id3'], $attachment ) )
if ( method_exists( $loadedExtensions[ $map['location_key'] ], 'attachmentPermissionCheck') AND $loadedExtensions[ $map['location_key'] ]->attachmentPermissionCheck( $member, $map['id1'], $map['id2'], $map['id3'], $attachment ) )
{
$permission = TRUE;
break;
@@ -685,7 +694,10 @@ class _ajax extends \IPS\Dispatcher\Controller
public function getCsrfKey()
{
/* Don't cache the CSRF key */
\IPS\Output::i()->pageCaching = FALSE;
\IPS\Output::setCacheTime( false );
/* Restrict endpoint to our origin JS */
Output::i()->httpHeaders['Access-Control-Allow-Origin'] = Url::internal('')->data[ Url::COMPONENT_SCHEME ] . '://' . Url::internal('')->data[ Url::COMPONENT_HOST ];
if ( isset( \IPS\Request::i()->path ) )
{
@@ -717,7 +729,7 @@ class _ajax extends \IPS\Dispatcher\Controller
}
}
\IPS\Output::i()->json( [ 'key' => \IPS\Session::i()->csrfKey ] );
\IPS\Output::i()->json( [ 'key' => \IPS\Session::i()->csrfKey, 'expiry' => time() + 500 ] );
}
/**
@@ -12,6 +12,9 @@
namespace IPS\core\modules\front\system;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\core\Alerts\Alert;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
@@ -47,7 +50,7 @@ class _alerts extends \IPS\Dispatcher\Controller
{
$alert = \IPS\core\Alerts\Alert::load( \IPS\Request::i()->id );
if( $alert->reply == 2 and \IPS\Member::loggedIn()->member_id and \IPS\Member::loggedIn()->canUseMessenger() )
if( $alert->reply == Alert::REPLY_REQUIRED and \IPS\Member::loggedIn()->member_id and \IPS\Member::loggedIn()->canUseMessenger() and \IPS\Member::load( $alert->member_id )->member_id )
{
\IPS\Output::i()->error( 'alert_cant_dismiss', '3C428/1', 403, '' );
}
@@ -43,9 +43,18 @@ class _content extends \IPS\Dispatcher\Controller
try
{
$commentClass = $class::$commentClass;
$item = $class::load( \IPS\Request::i()->content_id );
if( isset( $item::$archiveClass ) AND method_exists( $item, 'isArchived' ) AND $item->isArchived() )
{
$commentClass = $class::$archiveClass;
}
else
{
$commentClass = $class::$commentClass;
}
$comment = $commentClass::load( \IPS\Request::i()->content_commentid );
$item = $comment->item();
}
catch( \OutOfRangeException $ex )
{
@@ -30,7 +30,7 @@ class _cookies extends \IPS\Dispatcher\Controller
{
parent::execute();
\IPS\Output::i()->pageCaching = FALSE;
\IPS\Output::setCacheTime( false );
}
/**
@@ -76,5 +76,6 @@ class _cookies extends \IPS\Dispatcher\Controller
\IPS\Output::i()->redirect( $url );
}
}
\IPS\Output::i()->redirect( \IPS\Http\Url::internal(''));
}
}
@@ -62,13 +62,30 @@ class _editor extends \IPS\Dispatcher\Controller
protected function image()
{
$maxImageDims = \IPS\Settings::i()->attachment_image_size ? explode( 'x', \IPS\Settings::i()->attachment_image_size ) : array( 1000, 750 );
/* Let's do some casting here */
\IPS\Request::i()->width = (int) \IPS\Request::i()->width;
\IPS\Request::i()->height = (int) \IPS\Request::i()->height;
\IPS\Request::i()->actualWidth = (int) \IPS\Request::i()->actualWidth;
\IPS\Request::i()->actualHeight = (int) \IPS\Request::i()->actualHeight;
$maxImageDims = array_map('intval', $maxImageDims);
foreach( array( 'width', 'height', 'actualWidth', 'actualHeight' ) as $key )
{
if( \IPS\Request::i()->$key <= 0 )
{
\IPS\Output::i()->error( 'invalid_image_dimensions', '2C270/2', 403, '' );
}
}
$ratioH = round( \IPS\Request::i()->height / \IPS\Request::i()->width, 2 );
$ratioW = round( \IPS\Request::i()->width / \IPS\Request::i()->height, 2 );
if( \intval( $maxImageDims[0] ) === 0 && \intval( $maxImageDims[1] ) === 0 )
if( $maxImageDims[0] === 0 && $maxImageDims[1] === 0 )
{
$maxWidth = \IPS\Request::i()->actualWidth;
$maxHeight = \IPS\Request::i()->actualHeight;
$maxWidth = (int) \IPS\Request::i()->actualWidth;
$maxHeight = (int) \IPS\Request::i()->actualHeight;
}
else
{
@@ -353,7 +370,7 @@ class _editor extends \IPS\Dispatcher\Controller
try
{
$item = $class::load( \IPS\Request::i()->contentId );
foreach( $item->mostRecent( \IPS\Request::i()->input ) AS $row )
foreach( $item->mostRecent( \IPS\Request::i()->input, 10, FALSE ) AS $row )
{
$memberIds[] = $row->member_id;
$results .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->mentionRow( $row );
@@ -57,6 +57,10 @@ class _embed extends \IPS\Content\Controller
\IPS\Output::i()->js( 'js/commonEmbedHandler.js', 'core', 'interface' ),
\IPS\Output::i()->js( 'js/externalEmbedHandler.js', 'core', 'interface' )
);
/* We don't want search engines indexing this */
\IPS\Output::i()->metaTags['robots'] = 'noindex';
/* Intentionally replace the cssFiles array with a single file here, since we don't need the complete CSS framework in external embeds */
\IPS\Output::i()->cssFiles = \IPS\Theme::i()->css( 'styles/embeds.css', 'core', 'front' );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->embedExternal( $return, $js ), 200 );
@@ -34,7 +34,7 @@ class _login extends \IPS\Dispatcher\Controller
*/
protected function manage()
{
\IPS\Output::i()->pageCaching = FALSE;
\IPS\Output::setCacheTime( false );
/* Init login class */
$login = new \IPS\Login( \IPS\Http\Url::internal( "app=core&module=system&controller=login", 'front', 'login' ) );
@@ -250,7 +250,7 @@ class _login extends \IPS\Dispatcher\Controller
/* Otherwise show the reauthenticate form */
\IPS\Output::i()->bodyClasses[] = 'ipsLayout_minimal';
\IPS\Output::i()->sidebar['enabled'] = FALSE;
\IPS\Output::i()->pageCaching = FALSE;
\IPS\Output::setCacheTime( false );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('login');
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system' )->mergeSocialAccount( $handler, $member, $login, $error );
}
@@ -10,6 +10,8 @@
namespace IPS\core\modules\front\system;
use IPS\Text\Encrypt;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -152,27 +154,29 @@ class _lostpass extends \IPS\Dispatcher\Controller
}
else
{
\IPS\Db::i()->update( 'core_validating', array( 'email_sent' => time() ), array( 'vid=?', $vid ) );
$plainSecurityKey = \IPS\Login::generateRandomString();
\IPS\Db::i()->update( 'core_validating', [ 'email_sent' => time(), 'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag() ], [ 'vid=?', $vid ] );
}
}
catch ( \UnderflowException $e )
{
$vid = md5( $member->members_pass_hash . \IPS\Login::generateRandomString() );
\IPS\Db::i()->insert( 'core_validating', array(
'vid' => $vid,
'member_id' => $member->member_id,
'entry_date' => time(),
'lost_pass' => 1,
'ip_address' => $member->ip_address,
'email_sent' => time(),
) );
$plainSecurityKey = \IPS\Login::generateRandomString();
\IPS\Db::i()->insert( 'core_validating', [
'vid' => $vid,
'member_id' => $member->member_id,
'entry_date' => time(),
'lost_pass' => 1,
'ip_address' => $member->ip_address,
'email_sent' => time(),
'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag()
] );
}
/* Send email */
if ( $sendEmail )
{
\IPS\Email::buildFromTemplate( 'core', 'lost_password_init', array( $member, $vid ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member );
\IPS\Email::buildFromTemplate( 'core', 'lost_password_init', array( $member, $vid, $plainSecurityKey ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member );
$message = "lost_pass_confirm";
}
else
@@ -205,6 +209,12 @@ class _lostpass extends \IPS\Dispatcher\Controller
\IPS\Output::i()->error( 'no_validation_key', '2S151/1', 410, '' );
}
/* Check security key */
if( !\IPS\Login::compareHashes( Encrypt::fromTag( $record['security_key'] )->decrypt(), \IPS\Request::i()->security_key ) )
{
\IPS\Output::i()->error( 'lostpass_invalid_security_key', '2S151/5', 403, '' );
}
/* Show a nicer error message if their link has expired */
if( $record['entry_date'] < \IPS\DateTime::create()->sub( new \DateInterval( 'PT1H' ) )->getTimestamp() )
{
@@ -223,7 +233,8 @@ class _lostpass extends \IPS\Dispatcher\Controller
$member = \IPS\Member::load( $record['member_id'] );
/* Reset the failed logins storage - we don't need to save because the login handler will do that for us later */
$member->failed_logins = array();
\IPS\Db::i()->delete( 'core_login_failures', [ 'login_member_id=?', $member->member_id ] );
$member->failed_login_count = 0;
/* Now reset the member's password. If no handlers accept the change, create a local password */
if ( !$member->changePassword( $values['password'], 'lost' ) )
@@ -1,62 +0,0 @@
<?php
/**
* @brief marketplace
* @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 16 Jul 2020
*/
namespace IPS\core\modules\front\system;
/* 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;
}
/**
* marketplace
*/
class _marketplace extends \IPS\Dispatcher\Controller
{
/**
* ...
*
* @return void
*/
protected function manage()
{
if ( isset( \IPS\Request::i()->member ) )
{
try
{
$token = \IPS\Db::i()->select( 'token', 'core_marketplace_tokens', array( 'id=?', \IPS\Request::i()->member ) )->first();
if ( $token !== 'pending-' . \IPS\Request::i()->hash )
{
throw new \UnderflowException;
}
\IPS\Db::i()->update( 'core_marketplace_tokens', array( 'token' => \IPS\Request::i()->access_token, 'expires_at' => time() + \IPS\Request::i()->expires_in ), array( 'id=?', \IPS\Request::i()->member ) );
$response = 'OK';
$responseCode = 200;
}
catch ( \UnderflowException $e )
{
$response = 'BAD_HASH';
$responseCode = 403;
}
}
else
{
$response = 'NO_MEMBER';
$responseCode = 404;
}
\IPS\Output::i()->sendOutput( $response, $responseCode, 'text/plain' );
}
}
@@ -176,6 +176,11 @@ class _metatags extends \IPS\Dispatcher\Controller
{
$manifest = json_decode( \IPS\Settings::i()->manifest_details, TRUE );
if( !$manifest )
{
$manifest = [];
}
$output = array(
'scope' => rtrim( \IPS\Settings::i()->base_url, '/' ) . '/',
'name' => \IPS\Settings::i()->board_name,
@@ -205,7 +210,7 @@ class _metatags extends \IPS\Dispatcher\Controller
$file = \IPS\File::get( 'core_Icons', $v['url'] );
$output['icons'][] = array(
'src' => (string) $file->url,
'src' => (string) $file->url->setQueryString( 'v', $manifest['cache_key'] ),
'type' => \IPS\File::getMimeType( $file->originalFilename ),
'sizes' => $v['width'] . 'x' . $v['height'],
'purpose' => 'any'
@@ -225,7 +230,7 @@ class _metatags extends \IPS\Dispatcher\Controller
$file = \IPS\File::get( 'core_Icons', $v['url'] );
$output['icons'][] = array(
'src' => (string) $file->url,
'src' => (string) $file->url->setQueryString( 'v', $manifest['cache_key'] ),
'type' => \IPS\File::getMimeType( $file->originalFilename ),
'sizes' => $v['width'] . 'x' . $v['height'],
'purpose' => 'maskable'
@@ -11,6 +11,9 @@
namespace IPS\core\modules\front\system;
/* To prevent PHP errors (extending class does not exist) revealing path */
use function md5;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
@@ -827,7 +830,7 @@ class _notifications extends \IPS\Dispatcher\Controller
protected function unfollowFromEmail()
{
/* Logged in? */
if ( \IPS\Member::loggedIn()->member_id )
if ( \IPS\Member::loggedIn()->member_id and ! isset( \IPS\Request::i()->listunsubscribe ) )
{
/* Go to the normal page */
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=core&module=system&controller=notifications&do=follow&follow_app=". \IPS\Request::i()->follow_app ."&follow_area=". \IPS\Request::i()->follow_area ."&follow_id=" . \IPS\Request::i()->follow_id ) );
@@ -835,7 +838,7 @@ class _notifications extends \IPS\Dispatcher\Controller
if ( ! empty( \IPS\Request::i()->gkey ) )
{
list( $followKey, $memberKey ) = explode( '-', \IPS\Request::i()->gkey );
[ $followKey, $memberKey ] = explode( '-', \IPS\Request::i()->gkey );
/* Do we follow it? */
try
{
@@ -910,7 +913,29 @@ class _notifications extends \IPS\Dispatcher\Controller
/* Grab a count */
$count = \IPS\Db::i()->select( 'COUNT(*)', 'core_follow', array( 'follow_member_id=? and follow_notify_freq != ?', $member->member_id, 'none' ) )->first();
if ( isset( \IPS\Request::i()->listunsubscribe ) and \IPS\Request::i()->requestMethod() == 'POST' )
{
\IPS\Db::i()->update( 'core_follow', array( 'follow_notify_freq' => 'none' ), array( 'follow_id=? AND follow_member_id=?', $current['follow_id'], $member->member_id ) );
/* Unfollow club areas */
if ( $class == "IPS\Member\Club" )
{
foreach ( $thing->nodes() as $node )
{
$itemClass = $node['node_class']::$contentItemClass;
$followApp = $itemClass::$application;
$followArea = mb_strtolower( mb_substr( $node['node_class'], mb_strrpos( $node['node_class'], '\\' ) + 1 ) );
\IPS\Db::i()->update( 'core_follow', array( 'follow_notify_freq' => 'none' ), array( 'follow_id=? AND follow_member_id=?', md5( $followApp . ';' . $followArea . ';' . $node['node_id'] . ';' . $member->member_id ), $member->member_id ) );
}
}
/* Done */
\IPS\Output::i()->output = \IPS\Member::loggedIn()->language()->addToStack('unsubscribed');
return;
}
$form = new \IPS\Helpers\Form( 'unfollowFromEmail', 'update_follow' );
$form->class = 'ipsForm_vertical';
@@ -10,6 +10,8 @@
namespace IPS\core\modules\front\system;
use IPS\Text\Encrypt;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -61,7 +63,7 @@ class _register extends \IPS\Dispatcher\Controller
\IPS\Output::i()->bodyClasses[] = 'ipsLayout_minimalNoHome';
}
\IPS\Output::i()->sidebar['enabled'] = FALSE;
\IPS\Output::i()->pageCaching = FALSE;
\IPS\Output::setCacheTime( false );
\IPS\Output::i()->linkTags['canonical'] = (string) \IPS\Http\Url::internal( 'app=core&module=system&controller=register', 'front', 'register' );
}
@@ -430,14 +432,20 @@ class _register extends \IPS\Dispatcher\Controller
/* Set the form label */
\IPS\Member::loggedIn()->language()->words['q_and_a'] = \IPS\Member::loggedIn()->language()->addToStack( 'core_question_and_answer_' . $question['qa_id'], FALSE );
}
$captcha = new \IPS\Helpers\Form\Captcha;
/* If PBR request is from the last 5 minutes, don't ask for a captcha again */
if( $postBeforeRegister !== NULL AND \IPS\DateTime::ts( $postBeforeRegister['timestamp'] )->add( new \DateInterval('PT5M' ) )->getTimestamp() > time() )
{
$captcha = '';
}
if ( (string) $captcha !== '' )
{
$form->add( $captcha );
}
if ( $question OR (string) $captcha !== '' )
{
$form->addSeparator();
@@ -600,7 +608,7 @@ class _register extends \IPS\Dispatcher\Controller
$answers[ $v ] = array(
'answer_question_id' => $v,
'answer_member_id' => $member->member_id,
'answer_answer' => \IPS\Text\Encrypt::fromPlaintext( $values[ 'security_question_a_' . $matches[1] ] )->tag()
'answer_answer' => Encrypt::fromPlaintext( $values[ 'security_question_a_' . $matches[1] ] )->tag()
);
}
}
@@ -683,6 +691,13 @@ class _register extends \IPS\Dispatcher\Controller
}
catch ( \UnderflowException $e )
{
/* Reset the validation flag and redirect the member to the index page if we have no row */
if( \IPS\Member::loggedIn()->members_bitoptions['validating'] )
{
\IPS\Member::loggedIn()->members_bitoptions['validating'] = FALSE;
\IPS\Member::loggedIn()->save();
$this->_performRedirect( NULL, FALSE, 'validate_no_record' );
}
\IPS\Output::i()->error( 'validate_no_record', '2S129/4', 404, '' );
}
@@ -727,10 +742,12 @@ class _register extends \IPS\Dispatcher\Controller
{
\IPS\Output::i()->error( \IPS\Member::loggedIn()->language()->addToStack('validation_email_rate_limit', FALSE, array( 'sprintf' => array( \IPS\DateTime::ts( $reg['email_sent'] )->relative( \IPS\DateTime::RELATIVE_FORMAT_LOWER ) ) ) ), '1C223/4', 429, '', array( 'Retry-After' => \IPS\DateTime::ts( $reg['email_sent'] )->add( new \DateInterval( 'PT15M' ) )->format('r') ) );
}
/* Rotate security key */
$plainSecurityKey = \IPS\Login::generateRandomString();
\IPS\Db::i()->update( 'core_validating', [ 'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag(), 'email_sent' => time() ], [ 'vid=?', $reg['vid'] ] );
\IPS\Email::buildFromTemplate( 'core', $reg['email_chg'] ? 'email_change' : 'registration_validate', array( \IPS\Member::loggedIn(), $reg['vid'] ), \IPS\Email::TYPE_TRANSACTIONAL )->send( \IPS\Member::loggedIn() );
\IPS\Db::i()->update( 'core_validating', array( 'email_sent' => time() ), array( 'vid=?', $reg['vid'] ) );
\IPS\Email::buildFromTemplate( 'core', $reg['email_chg'] ? 'email_change' : 'registration_validate', array( \IPS\Member::loggedIn(), $reg['vid'], $plainSecurityKey ), \IPS\Email::TYPE_TRANSACTIONAL )->send( \IPS\Member::loggedIn() );
}
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=validating', 'front', 'register' ), 'reg_email_resent' );
@@ -758,6 +775,12 @@ class _register extends \IPS\Dispatcher\Controller
$this->_performRedirect( NULL, FALSE, 'validate_no_record' );
}
/* Check security key */
if( !\IPS\Login::compareHashes( Encrypt::fromTag( $record['security_key'] )->decrypt(), \IPS\Request::i()->security_key ) )
{
\IPS\Output::i()->error( 'validate_invalid_security_key', '2C223/I', 403, '' );
}
if ( isset( $record['ref'] ) )
{
\IPS\Request::i()->ref = base64_encode( $record['ref'] );
@@ -878,17 +901,21 @@ class _register extends \IPS\Dispatcher\Controller
{
if( isset( $values['username'] ) )
{
\IPS\Member::loggedIn()->logHistory( 'core', 'display_name', array( 'new' => $values['username'], 'old' => \IPS\Member::loggedIn()->language()->get('none'), 'by' => 'manual' ) );
\IPS\Member::loggedIn()->name = $values['username'];
}
$spamCode = NULL;
$spamAction = NULL;
$disposable = FALSE;
$geoBlock = FALSE;
if( isset( $values['email_address'] ) )
{
\IPS\Member::loggedIn()->logHistory( 'core', 'email_change', array( 'new' => $values['new_email'], 'old' => \IPS\Member::loggedIn()->language()->get('none'), 'by' => 'manual' ) );
\IPS\Member::loggedIn()->email = $values['email_address'];
if( \IPS\Settings::i()->spam_service_enabled )
{
$spamAction = \IPS\Member::loggedIn()->spamService( 'register', NULL, $spamCode );
$spamAction = \IPS\Member::loggedIn()->spamService( 'register', NULL, $spamCode, $disposable, $geoBlock );
if( $spamAction == 4 )
{
$action = \IPS\Settings::i()->spam_service_action_4;
@@ -905,6 +932,12 @@ class _register extends \IPS\Dispatcher\Controller
}
\IPS\Member::loggedIn()->members_bitoptions['must_reaccept_terms'] = FALSE;
\IPS\Member::loggedIn()->allow_admin_mails = $values['reg_admin_mails'];
/* We should run geolocation again, this may have been an account created via login handler that has since changed details - check for admin validation */
if( !$spamAction )
{
\IPS\Member::loggedIn()->geoSpamCheck( $geoBlock );
}
/* Save */
\IPS\Member::loggedIn()->save();
@@ -967,10 +1000,8 @@ class _register extends \IPS\Dispatcher\Controller
$pending = null;
}
/* If we're pending *admin* validation, don't let them change their email - otherwise doing so would allow them to bypass validation
@todo - this is kind of an unsatisfcatory solution as ideally it would put them back into admin approval, but this would require
significant reengineering to address - see commit notes on this code block */
if ( $pending and $pending['new_reg'] and $pending['user_verified'] )
/* If we're a new registration, no longer allow email addresses to be changed. */
if ( $pending and $pending['new_reg'] )
{
\IPS\Output::i()->error( 'no_module_permission', '2C223/6', 403, '' );
}
@@ -1033,8 +1064,9 @@ class _register extends \IPS\Dispatcher\Controller
}
$vid = \IPS\Login::generateRandomString();
$plainSecurityKey = \IPS\Login::generateRandomString();
\IPS\Db::i()->insert( 'core_validating', array(
\IPS\Db::i()->insert( 'core_validating', [
'vid' => $vid,
'member_id' => \IPS\Member::loggedIn()->member_id,
'entry_date' => time(),
@@ -1043,9 +1075,10 @@ class _register extends \IPS\Dispatcher\Controller
'user_verified' => ( \IPS\Settings::i()->reg_auth_type == 'admin' ) ?: FALSE,
'ip_address' => \IPS\Request::i()->ipAddress(),
'email_sent' => time(),
) );
'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag()
] );
\IPS\Email::buildFromTemplate( 'core', $pending['email_chg'] ? 'email_change' : 'registration_validate', array( \IPS\Member::loggedIn(), $vid ), \IPS\Email::TYPE_TRANSACTIONAL )->send( \IPS\Member::loggedIn() );
\IPS\Email::buildFromTemplate( 'core', $pending['email_chg'] ? 'email_change' : 'registration_validate', array( \IPS\Member::loggedIn(), $vid, $plainSecurityKey ), \IPS\Email::TYPE_TRANSACTIONAL )->send( \IPS\Member::loggedIn() );
}
else
{
@@ -1233,7 +1266,7 @@ class _register extends \IPS\Dispatcher\Controller
return $ref;
}
\IPS\Output::i()->redirect( $ref, $message );
\IPS\Output::i()->redirect( $ref, $message );
}
/**
@@ -11,6 +11,10 @@
namespace IPS\core\modules\front\system;
/* To prevent PHP errors (extending class does not exist) revealing path */
use function mb_strlen;
use const LIBXML_NOWARNING;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
@@ -30,7 +34,6 @@ class _serviceworker extends \IPS\Dispatcher\Controller
protected function manage()
{
$cachedUrls = array();
$cachedUrls[] = (string) \IPS\Http\Url::internal("app=core&module=system&controller=offline", "front", "user_offline");
$notificationIcon = NULL;
@@ -50,31 +53,55 @@ class _serviceworker extends \IPS\Dispatcher\Controller
}
/* VARIABLES TO PASS THROUGH TO JS */
$DEBUG = ( ( \IPS\IN_DEV and \IPS\DEV_DEBUG_JS ) or \IPS\DEBUG_JS ) ? 'true' : 'false'; // Weird casting is intentional.
$BASE_URL = \IPS\Settings::i()->base_url;
$CACHED_ASSETS = json_encode( $cachedUrls, JSON_UNESCAPED_SLASHES );
$OFFLINE_URL = (string) \IPS\Http\Url::internal("app=core&module=system&controller=offline", "front", "user_offline");
$CACHE_VERSION = \IPS\Theme::i()->cssCacheBustKey();
$NOTIFICATION_ICON = $notificationIcon ? "\"{$notificationIcon}\"" : 'null'; // Weird casting is intentional. 'null' will become literal null in JS file.
$DEFAULT_NOTIFICATION_TITLE = \IPS\Member::loggedIn()->language()->addToStack('default_notification_title');
$DEFAULT_NOTIFICATION_BODY = \IPS\Member::loggedIn()->language()->addToStack('default_notification_body');
$variables = [
"DEBUG" => boolval( ( \IPS\IN_DEV and \IPS\DEV_DEBUG_JS ) or \IPS\DEBUG_JS ),
"BASE_URL" => \IPS\Settings::i()->base_url,
"CACHED_ASSETS" => $cachedUrls,
"CACHE_NAME" => "invision-community-{$CACHE_VERSION}",
"CACHE_VERSION" => $CACHE_VERSION,
"NOTIFICATION_ICON" => $notificationIcon ?: null,
"DEFAULT_NOTIFICATION_TITLE" => \IPS\Member::loggedIn()->language()->addToStack('default_notification_title'),
"DEFAULT_NOTIFICATION_BODY" => \IPS\Member::loggedIn()->language()->addToStack('default_notification_body'),
"OFFLINE_PAGE" => $this->buildCollapsedOfflinePage(),
];
$output = <<<JAVASCRIPT
"use strict";
const DEBUG = {$DEBUG};
const BASE_URL = "{$BASE_URL}";
const CACHED_ASSETS = {$CACHED_ASSETS};
const CACHE_NAME = 'invision-community-{$CACHE_VERSION}';
const OFFLINE_URL = "{$OFFLINE_URL}";
const NOTIFICATION_ICON = {$NOTIFICATION_ICON};
const DEFAULT_NOTIFICATION_TITLE = "{$DEFAULT_NOTIFICATION_TITLE}";
const DEFAULT_NOTIFICATION_BODY = "{$DEFAULT_NOTIFICATION_BODY}";
$variables["OFFLINE_PAGE_SIZE"] = \strlen( $variables["OFFLINE_PAGE"] );
$output = "\"use strict;\"\n\n";
foreach ( $variables as $var => $value )
{
$toEncode = json_encode( $value, JSON_UNESCAPED_SLASHES );
$output .= <<<JAVASCRIPT
const {$var} = {$toEncode};
JAVASCRIPT;
}
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $output );
$output .= file_get_contents( \IPS\ROOT_PATH . '/applications/core/interface/js/serviceWorker.js' );
$cacheHeaders = \IPS\IN_DEV !== true ? \IPS\Output::getCacheHeaders(time(), 86400) : array();
\IPS\Output::i()->sendOutput($output, 200, 'text/javascript', $cacheHeaders);
}
/**
* Return template for offline page
*
* @return string
*/
protected function buildCollapsedOfflinePage() : string
{
$html = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->swOffline();
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $html );
$doc = new \IPS\Xml\DOMDocument( "2.0", "utf-8" );
$doc->preserveWhiteSpace = false;
$doc->loadHTML( $html, LIBXML_NOBLANKS );
$doc->formatOutput = true;
$compact = $doc->saveHTML();
// We don't need these segments of whitespace. HTML entities can be used if it's absolutely necessary
return preg_replace( "/\s+/", " ", $compact );
}
}
@@ -10,6 +10,9 @@
namespace IPS\core\modules\front\system;
use IPS\Member;
use IPS\Text\Encrypt;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -130,8 +133,6 @@ class _settings extends \IPS\Dispatcher\Controller
{
$canConfigureMfa = TRUE;
}
$canChangeSignature = (bool) \IPS\Member::loggedIn()->canEditSignature();
/* Add login handlers */
$loginMethods = \IPS\Login::methods();
@@ -139,7 +140,7 @@ 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 );
return \IPS\Theme::i()->getTemplate( 'system' )->settings( $area, $output, ( \IPS\Settings::i()->allow_email_changes != 'disabled' ), $canChangePassword, \IPS\Member::loggedIn()->group['g_dname_changes'], (bool) \IPS\Settings::i()->signatures_enabled, $loginMethods, $canConfigureMfa, $showApps );
}
/**
@@ -240,7 +241,7 @@ class _settings extends \IPS\Dispatcher\Controller
$form = new \IPS\Helpers\Form;
$form->class = 'ipsForm_collapseTablet';
$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->addDummy( 'current_email', $currentEmail );
$form->add( new \IPS\Helpers\Form\Email(
'new_email',
'',
@@ -301,8 +302,9 @@ class _settings extends \IPS\Dispatcher\Controller
unset( $_SESSION['newEmail'] );
$vid = \IPS\Login::generateRandomString();
$plainSecurityKey = \IPS\Login::generateRandomString();
\IPS\Db::i()->insert( 'core_validating', array(
\IPS\Db::i()->insert( 'core_validating', [
'vid' => $vid,
'member_id' => \IPS\Member::loggedIn()->member_id,
'entry_date' => time(),
@@ -310,12 +312,13 @@ class _settings extends \IPS\Dispatcher\Controller
'ip_address' => \IPS\Request::i()->ipAddress(),
'prev_email' => $oldEmail,
'email_sent' => time(),
) );
'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag()
] );
\IPS\Member::loggedIn()->members_bitoptions['validating'] = TRUE;
\IPS\Member::loggedIn()->save();
\IPS\Email::buildFromTemplate( 'core', 'email_change', array( \IPS\Member::loggedIn(), $vid ), \IPS\Email::TYPE_TRANSACTIONAL )->send( \IPS\Member::loggedIn() );
\IPS\Email::buildFromTemplate( 'core', 'email_change', array( \IPS\Member::loggedIn(), $vid, $plainSecurityKey ), \IPS\Email::TYPE_TRANSACTIONAL )->send( \IPS\Member::loggedIn() );
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( '' ) );
}
@@ -614,7 +617,7 @@ class _settings extends \IPS\Dispatcher\Controller
\IPS\Output::i()->bypassCsrfKeyCheck = true;
/* Validate password */
if ( !isset( $_SESSION['passwordValidatedForMfa'] ) )
if ( !isset( $_SESSION['passwordForMfa'] ) )
{
$login = new \IPS\Login( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ), \IPS\Login::LOGIN_REAUTHENTICATE );
$usernamePasswordMethods = $login->usernamePasswordMethods();
@@ -628,7 +631,7 @@ class _settings extends \IPS\Dispatcher\Controller
{
if ( $success = $login->authenticate() )
{
$_SESSION['passwordValidatedForMfa'] = TRUE;
$_SESSION['passwordForMfa'] = TRUE;
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ) );
}
}
@@ -661,6 +664,9 @@ class _settings extends \IPS\Dispatcher\Controller
}
return $output . $mfaOutput;
}
/* We got past the prompts */
$_SESSION['passwordValidatedForMfa'] = TRUE;
/* Do any enabling/disabling */
if ( isset( \IPS\Request::i()->act ) )
@@ -836,6 +842,13 @@ class _settings extends \IPS\Dispatcher\Controller
*/
protected function confirmAccountDeletion()
{
$mfaOutput = \IPS\MFA\MFAHandler::accessToArea( 'core', 'SecurityQuestions', \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&area=confirmAccountDeletion', 'front' )->setQueryString('vid', \IPS\Request::i()->vid ) );
if ( $mfaOutput )
{
\IPS\Output::i()->output = $mfaOutput;
return;
}
$key = \IPS\Request::i()->vid;
try
{
@@ -1000,27 +1013,30 @@ class _settings extends \IPS\Dispatcher\Controller
}
else
{
\IPS\Db::i()->update( 'core_validating', array( 'email_sent' => time() ), array( 'vid=?', $vid ) );
$plainSecurityKey = \IPS\Login::generateRandomString();
\IPS\Db::i()->update( 'core_validating', [ 'email_sent' => time(), 'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag() ], [ 'vid=?', $vid ] );
}
}
catch ( \UnderflowException $e )
{
$vid = md5( $member->members_pass_hash . \IPS\Login::generateRandomString() );
$plainSecurityKey = \IPS\Login::generateRandomString();
\IPS\Db::i()->insert( 'core_validating', array(
\IPS\Db::i()->insert( 'core_validating', [
'vid' => $vid,
'member_id' => $member->member_id,
'entry_date' => time(),
'forgot_security' => 1,
'ip_address' => \IPS\Request::i()->ipAddress(),
'email_sent' => time(),
) );
'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag()
] );
}
/* Send email */
if ( $sendEmail )
{
\IPS\Email::buildFromTemplate( 'core', 'mfaRecovery', array( $member, $vid ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member );
\IPS\Email::buildFromTemplate( 'core', 'mfaRecovery', array( $member, $vid, $plainSecurityKey ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member );
$message = "mfa_recovery_email_sent";
}
else
@@ -1051,6 +1067,12 @@ class _settings extends \IPS\Dispatcher\Controller
{
\IPS\Output::i()->error( 'mfa_recovery_no_validation_key', '2C122/K', 410, '' );
}
/* Check security key */
if( !\IPS\Login::compareHashes( Encrypt::fromTag( $record['security_key'] )->decrypt(), \IPS\Request::i()->security_key ) )
{
\IPS\Output::i()->error( 'mfavalidate_invalid_security_key', '3C122/16', 403, '' );
}
/* Remove all MFA */
$member = \IPS\Member::load( $record['member_id'] );
@@ -1226,7 +1248,7 @@ class _settings extends \IPS\Dispatcher\Controller
/* Check they have permission to change their signature */
$sigLimits = explode( ":", \IPS\Member::loggedIn()->group['g_signature_limits']);
if( !\IPS\Member::loggedIn()->canEditSignature() )
if( (bool) \IPS\Settings::i()->signatures_enabled === FALSE )
{
\IPS\Output::i()->error( 'signatures_disabled', '2C122/C', 403, '' );
}
@@ -1268,12 +1290,15 @@ class _settings extends \IPS\Dispatcher\Controller
$form = new \IPS\Helpers\Form;
$form->class = 'ipsForm_collapseTablet';
$form->add( new \IPS\Helpers\Form\YesNo( 'view_sigs', \IPS\Member::loggedIn()->members_bitoptions['view_sigs'], FALSE ) );
$form->add( new \IPS\Helpers\Form\Editor( 'signature', \IPS\Member::loggedIn()->signature, FALSE, array( 'app' => 'core', 'key' => 'Signatures', 'autoSaveKey' => "frontsig-" .\IPS\Member::loggedIn()->member_id, 'attachIds' => array( \IPS\Member::loggedIn()->member_id ) ) ) );
if( Member::loggedIn()->canEditSignature() )
{
$form->add( new \IPS\Helpers\Form\Editor( 'signature', \IPS\Member::loggedIn()->signature, FALSE, ['app' => 'core', 'key' => 'Signatures', 'autoSaveKey' => 'frontsig-'.\IPS\Member::loggedIn()->member_id, 'attachIds' => [\IPS\Member::loggedIn()->member_id]] ) );
}
/* Handle submissions */
if ( $values = $form->values() )
{
if( $values['signature'] )
if( isset( $values['signature'] ) and $values['signature'] )
{
/* Check Limits */
$signature = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' );
@@ -1357,6 +1382,7 @@ class _settings extends \IPS\Dispatcher\Controller
{
$errors[] = \IPS\Member::loggedIn()->language()->addToStack('sig_num_lines_exceeded');
}
\IPS\Member::loggedIn()->signature = $values['signature'];
}
if( !empty( $errors ) )
@@ -1367,7 +1393,6 @@ class _settings extends \IPS\Dispatcher\Controller
return \IPS\Theme::i()->getTemplate( 'system' )->settingsSignature( $form, $sigLimits );
}
\IPS\Member::loggedIn()->signature = $values['signature'];
\IPS\Member::loggedIn()->members_bitoptions['view_sigs'] = $values['view_sigs'];
\IPS\Member::loggedIn()->save();
@@ -1897,7 +1922,7 @@ class _settings extends \IPS\Dispatcher\Controller
*/
protected function togglePii()
{
if ( ! \IPS\Settings::i()->core_datalayer_member_pii_choice )
if ( ! \IPS\Settings::i()->core_datalayer_member_pii_choice or !isset( $_SESSION['passwordValidatedForMfa'] ) OR \IPS\Settings::i()->pii_type !== 'on' )
{
\IPS\Output::i()->error( 'page_not_found', '3T251/7', 404 );
}
@@ -1982,6 +2007,12 @@ class _settings extends \IPS\Dispatcher\Controller
{
\IPS\Session::i()->csrfCheck();
if ( $output = \IPS\MFA\MFAHandler::accessToArea( 'core', 'DeviceManagement', $this->url->setQuerystring('do','updateDeviceEmail')->csrf()) )
{
\IPS\Output::i()->output = $output;
return;
}
/* Update the bitwise flag */
\IPS\Member::loggedIn()->members_bitoptions['new_device_email'] = (bool) \IPS\Request::i()->value;
\IPS\Member::loggedIn()->save();
@@ -198,6 +198,7 @@ class _warnings extends \IPS\Content\Controller
/* Acknowledge it */
$warning->acknowledged = TRUE;
$warning->save();
/* @note SELECT_FROM_WRITE_SERVER: Avoid issue where warning may be in writer table, but not in reader */
$member->members_bitoptions['unacknowledged_warnings'] = (bool) \IPS\Db::i()->select( 'COUNT(*)', 'core_members_warn_logs', array( "wl_member=? AND wl_acknowledged=0", $member->member_id ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
$member->save();
@@ -369,7 +370,7 @@ class _warnings extends \IPS\Content\Controller
try
{
$action = \IPS\Db::i()->select( '*', 'core_members_warn_actions', array( 'wa_points<=?', ( $member->warn_level + \IPS\Request::i()->points ) ), 'wa_points DESC', 1 )->first();
$action = \IPS\Db::i()->select( '*', 'core_members_warn_actions', array( 'wa_points<=?', ( $member->warn_level + (int) \IPS\Request::i()->points ) ), 'wa_points DESC', 1 )->first();
foreach ( array( 'mq', 'rpa', 'suspend' ) as $k )
{
if ( $action[ 'wa_' . $k ] == -1 )