Version 5.0.0 beta 10

This commit is contained in:
Neo committed 2025-12-19 17:53:48 -08:00
1 parent 56979f8ee8
commit 264eeb97e0
325 files changed
+6442 -1849

No files matched your search

+4 -2
View File
@@ -11,6 +11,7 @@
namespace IPS\Api\GraphQL;
use IPS\Api\GraphQL\Queries\ModuleAccess;
use IPS\Application;
use function defined;
/* To prevent PHP errors (extending class does not exist) revealing path */
@@ -30,11 +31,12 @@ abstract class AppQueries
/**
* Get the supported query types in this app
*
* @param string $app
* @param string|Application $app
* @return array
*/
public static function queries( string $app ): array
public static function queries( string|Application $app ): array
{
$app = ( $app instanceof Application ) ? $app : Application::load( $app );
return [
'moduleAccess' => new ModuleAccess($app)
];
+1 -1
View File
@@ -39,7 +39,7 @@ class QueryType extends ObjectType
{
try
{
$globalAppQueries = AppQueries::queries($app->directory);
$globalAppQueries = AppQueries::queries( $app );
$appQueryClass = "\\IPS\\" . $app->directory . "\\api\\GraphQL\\Query";
if( !class_exists( $appQueryClass ) )
+10 -1
View File
@@ -2249,7 +2249,16 @@ EOF;
$report = ( isset( $setting['report'] ) and $setting['report'] != 'none' ) ? $setting['report'] : NULL;
if ( ! array_key_exists( $setting['key'], $currentData ) )
{
$insert[] = array( 'conf_key' => $setting['key'], 'conf_value' => $setting['default'], 'conf_default' => $setting['default'], 'conf_app' => $this->directory, 'conf_report' => $report );
/* Is this a legacy setting? */
try
{
$settingRow = Db::i()->select( '*', 'core_sys_conf_settings', [ 'conf_key=? and conf_app is null', $setting['key'] ] )->first();
$update[] = array( array( 'conf_default' => $setting['default'], 'conf_report' => $report, 'conf_app' => $this->directory ), array( 'conf_key=?', $setting['key'] ) );
}
catch( UnderflowException )
{
$insert[] = array( 'conf_key' => $setting['key'], 'conf_value' => $setting['default'], 'conf_default' => $setting['default'], 'conf_app' => $this->directory, 'conf_report' => $report );
}
}
else
{
+1 -1
View File
@@ -1878,7 +1878,7 @@ abstract class Comment extends Content
}
}
if ( ! Output::i()->reduceLinks() and $this->canShare() )
if ( ! Output::i()->reduceLinks() and IPS::classUsesTrait( $this, Shareable::class ) and $this->canShare() )
{
if ( $this->mapped( 'first' ) )
{
-2
View File
@@ -290,8 +290,6 @@ abstract class Content extends ActiveRecord
$obj = parent::constructFromData( $data, $updateMultitonStoreIfExists );
}
Event::fire( 'onLoad', $obj );
/* Return */
return $obj;
}
+3 -1
View File
@@ -16,6 +16,7 @@ namespace IPS\Events;
use IPS\Application;
use IPS\Data\Store;
use IPS\Events\ListenerType\ClubListenerType;
use IPS\Events\ListenerType\ContentListenerType;
use IPS\Events\ListenerType\InvoiceListenerType;
use IPS\Events\ListenerType\NodeListenerType;
@@ -134,7 +135,8 @@ class ListenerType
public static function listenerTypes() : array
{
return array(
'ContentListenerType' => ContentListenerType::class,
'ContentListenerType' => ContentListenerType::class,
'ClubListenerType' => ClubListenerType::class,
'InvoiceListenerType' => InvoiceListenerType::class,
'MemberListenerType' => MemberListenerType::class,
'NodeListenerType' => NodeListenerType::class,
@@ -0,0 +1,36 @@
<?php
/**
* @brief ClubType
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage
* @since 01/09/2024
*/
namespace IPS\Events\ListenerType;
use IPS\Events\ListenerType;
use IPS\Member\Club;
/**
* @method onCreate( Club $club ) : void
* @method onEdit( ?Club $club ) : void
* @method onDelete( Club $club ) : void
*/
class ClubListenerType extends ListenerType
{
/**
* Defines the classes that are supported by each Listener Type
* When a new Listener Type is created, we must specify which
* classes are valid (e.g. \IPS\Content, \IPS\Member).
*
* @var array
*/
protected static array $supportedBaseClasses = array(
Club::class
);
}
@@ -26,7 +26,6 @@ if (!defined('\IPS\SUITE_UNIQUE_KEY')) {
}
/**
* @method onLoad( ContentClass $object ) : void
* @method onBeforeCreateOrEdit( ContentClass $object, array $values, bool $new = FALSE ) : void
* @method onCreateOrEdit( ContentClass $object, array $values, bool $new = FALSE ) : void
* @method onDelete( ContentClass $object ) : void
@@ -23,7 +23,6 @@ if (!defined('\IPS\SUITE_UNIQUE_KEY')) {
}
/**
* @method onLoad( Model $node ) : void
* @method onDelete( Model $node ) : void
*/
class NodeListenerType extends ListenerType
+23
View File
@@ -43,6 +43,18 @@ abstract class GroupFormAbstract
*/
abstract public function save( array $values, Group $group ) : void;
/**
* Run when a group is copied
*
* @param Group $oldGroup
* @param Group $newGroup
* @return void
*/
public function cloneGroup( Group $oldGroup, Group $newGroup ) : void
{
}
/**
* Can this group be deleted?
*
@@ -53,4 +65,15 @@ abstract class GroupFormAbstract
{
return TRUE;
}
/**
* Additional actions to take when a group is deleted
*
* @param Group $group
* @return void
*/
public function delete( Group $group ) : void
{
}
}
@@ -61,6 +61,17 @@ abstract class ProfileStepsAbstract
*/
abstract public function completed( ProfileStep $step, ?Member $member = NULL ): bool;
/**
* Can the member complete this step?
*
* @param Member $member
* @return bool
*/
public function canComplete( Member $member ) : bool
{
return true;
}
/**
* Can be set as required?
*
@@ -24,7 +24,7 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class TypeRegistry
{
protected static ?GeoLocationType $geolocation;
protected static ?GeoLocationType $geolocation = null;
/**
* Constructor
+26 -1
View File
@@ -13,13 +13,16 @@ namespace IPS\Helpers\CoverPhoto;
/* To prevent PHP errors (extending class does not exist) revealing path */
use Exception;
use IPS\Content;
use IPS\Dispatcher;
use IPS\Dispatcher\Controller as DispatcherController;
use IPS\Helpers\CoverPhoto;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Upload;
use IPS\Http\Url;
use IPS\Node\Model;
use IPS\Output;
use IPS\Output\UI\UiExtension;
use IPS\Request;
use IPS\Session;
use IPS\Theme;
@@ -59,7 +62,16 @@ abstract class Controller extends DispatcherController
$photo->delete();
}
catch ( Exception $e ) { }
$this->_coverPhotoSet( new CoverPhoto( $values['cover_photo'], 0 ), 'new' );
$coverPhoto = new CoverPhoto( $values['cover_photo'], 0 );
$this->_coverPhotoSet( $coverPhoto, 'new' );
/* Call any UI Extensions that might be linked to this object */
if( $photo->object instanceof Content or $photo->object instanceof Model )
{
UiExtension::i()->run( $photo->object, 'formPostSave', [ [ 'cover_photo' => $coverPhoto ] ] );
}
Output::i()->redirect( $this->_coverPhotoReturnUrl()->setQueryString( array( '_position' => 1 ) ) );
}
@@ -94,6 +106,13 @@ abstract class Controller extends DispatcherController
catch ( Exception $e ) { }
$this->_coverPhotoSet( new CoverPhoto( NULL, 0 ), 'remove' );
/* Call any UI Extensions that might be linked to this object */
if( $photo->object instanceof Content or $photo->object instanceof Model )
{
UiExtension::i()->run( $photo->object, 'formPostSave', [ [ 'cover_photo' => null, 'cover_photo_offset' => 0 ] ] );
}
if ( Request::i()->isAjax() )
{
Output::i()->json( 'OK' );
@@ -120,6 +139,12 @@ abstract class Controller extends DispatcherController
$photo->offset = Request::i()->offset;
$this->_coverPhotoSet( $photo, 'reposition' );
/* Call any UI Extensions that might be linked to this object */
if( $photo->object instanceof Content or $photo->object instanceof Model )
{
UiExtension::i()->run( $photo->object, 'formPostSave', [ [ 'cover_photo_offset' => $photo->offset ] ] );
}
if ( Request::i()->isAjax() )
{
+5 -19
View File
@@ -315,33 +315,19 @@ class Node extends FormAbstract
{
foreach ( $this->value as $node )
{
$title = isset( $node::$titleLangPrefix ) ? Member::loggedIn()->language()->addToStack( $node::$titleLangPrefix . $node->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : str_replace( "&#039;", "&apos;", htmlspecialchars( $node->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
$title = str_replace( "&#039;", "&apos;", htmlspecialchars( $node->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
$selected[ !( $node instanceof $nodeClass ) ? "{$node->_id}.s" : $node->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
if( isset( $val::$titleLangPrefix ) )
{
return Member::loggedIn()->language()->addToStack( $val::$titleLangPrefix . $val->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) );
}
else
{
return str_replace( "&#039;", "&apos;", htmlspecialchars( $val->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
}
return str_replace( "&#039;", "&apos;", htmlspecialchars( $val->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
}, iterator_to_array( $node->parents() ) ) ) );
}
}
elseif ( $this->value instanceof Model )
{
$class = get_class( $this->value );
$title = isset( $nodeClass::$titleLangPrefix ) ? Member::loggedIn()->language()->addToStack( $class::$titleLangPrefix . $this->value->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : str_replace( "&#039;", "&apos;", htmlspecialchars( $this->value->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
$selected[ !( $this->value instanceof $nodeClass ) ? "{$this->value->_id}.s" : $this->value->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
if( isset( $val::$titleLangPrefix ) )
{
return Member::loggedIn()->language()->addToStack( $val::$titleLangPrefix . $val->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) );
}
else
{
return str_replace( "&#039;", "&apos;", htmlspecialchars( $val->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
}
$title = str_replace( "&#039;", "&apos;", htmlspecialchars( $this->value->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
$selected[ !( $this->value instanceof $nodeClass ) ? "{$this->value->_id}.s" : $this->value->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
return str_replace( "&#039;", "&apos;", htmlspecialchars( $val->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
}, iterator_to_array( $this->value->parents() ) ) ) );
}
+3 -2
View File
@@ -12,6 +12,7 @@ namespace IPS\Login\Handler;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\File;
use IPS\Http\Url;
use IPS\Login;
use IPS\Login\Exception;
@@ -67,9 +68,9 @@ trait ButtonHandler
/**
* Get the button icon
*
* @return string
* @return string|File
*/
abstract public function buttonIcon(): string;
abstract public function buttonIcon(): string|File;
/**
* Get button text
+2 -2
View File
@@ -127,9 +127,9 @@ class Apple extends OpenID
/**
* Get the button icon
*
* @return string
* @return string|File
*/
public function buttonIcon(): string
public function buttonIcon(): string|File
{
return 'apple';
}
+2 -2
View File
@@ -213,9 +213,9 @@ class Custom extends OAuth2
/**
* Get the button icon
*
* @return string
* @return string|File
*/
public function buttonIcon(): string
public function buttonIcon(): string|File
{
return ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? File::get( 'core_Login', $this->settings['button_icon'] ) : '';
}
+3 -2
View File
@@ -12,6 +12,7 @@ namespace IPS\Login\Handler\OAuth2;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DomainException;
use IPS\File;
use IPS\Helpers\Form\Radio;
use IPS\Http\Request\Exception;
use IPS\Http\Url;
@@ -109,9 +110,9 @@ class Facebook extends OAuth2
/**
* Get the button icon
*
* @return string
* @return string|File
*/
public function buttonIcon(): string
public function buttonIcon(): string|File
{
return 'facebook-f';
}
+3 -2
View File
@@ -14,6 +14,7 @@ namespace IPS\Login\Handler\OAuth2;
use DomainException;
use IPS\Db;
use IPS\File;
use IPS\Helpers\Form\Radio;
use IPS\Http\Url;
use IPS\Login;
@@ -91,9 +92,9 @@ class Google extends OAuth2
/**
* Get the button icon
*
* @return string
* @return string|File
*/
public function buttonIcon(): string
public function buttonIcon(): string|File
{
return 'google';
}
+2 -2
View File
@@ -171,9 +171,9 @@ class Invision extends OAuth2
/**
* Get the button icon
*
* @return string
* @return string|File
*/
public function buttonIcon(): string
public function buttonIcon(): string|File
{
return ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? File::get( 'core_Login', $this->settings['button_icon'] ) : '';
}
+3 -2
View File
@@ -13,6 +13,7 @@ namespace IPS\Login\Handler\OAuth2;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DomainException;
use IPS\File;
use IPS\Helpers\Form\Radio;
use IPS\Http\Url;
use IPS\Login;
@@ -93,9 +94,9 @@ class LinkedIn extends OAuth2
/**
* Get the button icon
*
* @return string
* @return string|File
*/
public function buttonIcon(): string
public function buttonIcon(): string|File
{
return 'linkedin';
}
+3 -2
View File
@@ -13,6 +13,7 @@ namespace IPS\Login\Handler\OAuth2;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DomainException;
use IPS\File;
use IPS\Helpers\Form\Radio;
use IPS\Http\Url;
use IPS\Login;
@@ -86,9 +87,9 @@ class Microsoft extends OAuth2
/**
* Get the button icon
*
* @return string
* @return string|File
*/
public function buttonIcon(): string
public function buttonIcon(): string|File
{
return 'windows';
}
+2 -2
View File
@@ -163,9 +163,9 @@ class Wordpress extends OAuth2
/**
* Get the button icon
*
* @return string
* @return string|File
*/
public function buttonIcon(): string
public function buttonIcon(): string|File
{
return 'wordpress';
}
+6 -1
View File
@@ -830,7 +830,12 @@ class Handler extends MFAHandler
if ( $method == 'get' )
{
$response = $url->setQueryString( $data )->request()->get();
if( $data !== null )
{
$url = $url->setQueryString( $data );
}
$response = $url->request()->get();
}
else
{
+14 -1
View File
@@ -825,12 +825,22 @@ class Club extends ActiveRecord implements Embeddable
if( $new and $this->approved )
{
Webhook::fire( 'club_created', $this );
}
else if( !$new AND isset( $this->changed['approved'] ) )
{
$this->onApprove();
}
if( $new )
{
Event::fire( 'onCreate', $this );
}
else
{
Event::fire( 'onEdit', $this );
}
return NULL;
}
@@ -1821,6 +1831,8 @@ class Club extends ActiveRecord implements Embeddable
}
$this->coverPhoto( FALSE )->delete();
Event::fire( 'onDelete', $this );
}
/**
@@ -1833,7 +1845,7 @@ class Club extends ActiveRecord implements Embeddable
{
foreach( Db::i()->select( 'node_class', 'core_clubs_node_map', NULL, NULL, NULL, 'node_class' ) as $class )
{
if ( isset( $class::$contentItemClass ) )
if ( class_exists( $class ) and isset( $class::$contentItemClass ) )
{
$contentItemClass = $class::$contentItemClass;
@@ -2296,6 +2308,7 @@ class Club extends ActiveRecord implements Embeddable
public function onApprove() : void
{
Webhook::fire( 'club_created', $this );
Event::fire( 'onCreate', $this );
$this->owner->achievementAction( 'core', 'NewClub', $this );
try
+3 -9
View File
@@ -254,10 +254,7 @@ class Group extends ActiveRecord
/* Process each extension */
foreach ( $extensions as $class )
{
if( method_exists( $class, 'cloneGroup' ) )
{
$class->cloneGroup( $oldGroup, $this );
}
$class->cloneGroup( $oldGroup, $this );
}
Lang::saveCustom( 'core', "core_group_{$this->g_id}", iterator_to_array( Db::i()->select( '*', 'core_sys_lang_words', array( 'word_key=?', "core_group_{$oldId}" ) )->setKeyField('lang_id')->setValueField('word_custom') ) );
@@ -413,10 +410,7 @@ class Group extends ActiveRecord
/* Process each extension */
foreach ( $extensions as $class )
{
if( method_exists( $class, 'delete' ) )
{
$class->delete( $this );
}
$class->delete( $this );
}
parent::delete();
@@ -440,7 +434,7 @@ class Group extends ActiveRecord
foreach ( $extensions as $class )
{
if( method_exists( $class, 'canDelete' ) AND !$class->canDelete( $this ) )
if( !$class->canDelete( $this ) )
{
return FALSE;
}
+5 -4
View File
@@ -6644,6 +6644,10 @@ class Member extends ActiveRecord
try
{
$lastLogin = Db::i()->select( 'MIN(upgrade_date)', 'core_upgrade_history', [ 'upgrade_app=? AND upgrade_version_id>=?', 'core', 106100 ] )->first();
if( !$lastLogin )
{
return false;
}
}
catch( \UnderflowException $e )
{
@@ -6778,10 +6782,7 @@ class Member extends ActiveRecord
foreach ( Application::allExtensions( 'core', 'IpAddresses', FALSE, 'core' ) as $key => $extension )
{
if( method_exists( $extension, 'pruneIpAddresses' ) )
{
$extension->pruneIpAddresses( $time );
}
$extension->pruneIpAddresses( $time );
}
}
+6 -11
View File
@@ -349,14 +349,11 @@ class ProfileStep extends Model
{
foreach( Application::allExtensions( 'core', 'ProfileSteps' ) AS $key => $extension )
{
if ( method_exists( $extension, 'actionMultipleChoice' ) )
foreach( $extension::actions() AS $extensionAction => $lang )
{
foreach( $extension::actions() AS $extensionAction => $lang )
if ( $action == $extensionAction )
{
if ( $action == $extensionAction )
{
return $extension::actionMultipleChoice( $action );
}
return $extension::actionMultipleChoice( $action );
}
}
}
@@ -491,12 +488,10 @@ class ProfileStep extends Model
public function canComplete( Member $member = NULL ) : bool
{
$member = $member ?: Member::loggedIn();
/* @var ProfileStepsAbstract $extension */
$extension = $this->extension;
if( method_exists( $extension, 'canComplete' ) )
{
return $extension->canComplete( $member );
}
return TRUE;
return $extension->canComplete( $member );
}
/**
-2
View File
@@ -2015,8 +2015,6 @@ abstract class Model extends ActiveRecord
$obj = parent::constructFromData( $data, $updateMultitonStoreIfExists );
}
Event::fire( 'onLoad', $obj );
return $obj;
}
+11 -2
View File
@@ -584,10 +584,19 @@ abstract class ActiveRecord
$photo = new CoverPhoto;
if( $file = $this->coverPhotoFile() )
{
$photoOffset = static::$databaseColumnMap[ 'cover_photo_offset' ];
$photo->file = $file;
$photo->offset = $this->$photoOffset ?: 0;
if( isset( static::$databaseColumnMap[ 'cover_photo_offset' ] ) )
{
$photoOffset = static::$databaseColumnMap[ 'cover_photo_offset' ];
$photo->offset = $this->$photoOffset ?: 0;
}
else
{
$photo->offset = 0;
}
}
$photo->editable = $this->canEdit();
$photo->object = $this;
return $photo;
+12 -1
View File
@@ -399,7 +399,7 @@ class Category extends Model
$return['cat_app'] = $values['themeeditor_cat_app']->directory;
}
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->key, $values['themeeditor_cat_name'] );
Lang::saveCustom( 'core', static::$titleLangPrefix . $values['themeeditor_cat_key'], $values['themeeditor_cat_name'] );
return $return;
}
@@ -527,4 +527,15 @@ class Category extends Model
}
return $return;
}
/**
* [ActiveRecord] Delete Record
*
* @return void
*/
public function delete(): void
{
parent::delete();
Lang::deleteCustom( 'core', static::$titleLangPrefix . $this->key );
}
}
+24 -3
View File
@@ -315,6 +315,14 @@ class Setting extends Model
static::$appKeys = array_keys( Application::enabledApplications() );
}
$themesToCheck = [ Theme::i()->id, Theme::$defaultFrontendThemeSet ];
/* If we are working with a specific theme, make sure we allow it */
if( isset( Request::i()->set_id ) )
{
$themesToCheck[] = Request::i()->set_id;
}
$return = [];
foreach( new ActiveRecordIterator(
Db::i()->select( '*', static::$databaseTable, array( 'setting_category_id=?', $category->id ), 'setting_position' ),
@@ -327,7 +335,7 @@ class Setting extends Model
continue;
}
if( $setting->set_id and !in_array( $setting->set_id, array( Theme::$defaultFrontendThemeSet, Theme::i()->id ) ) )
if( $setting->set_id and !in_array( $setting->set_id, $themesToCheck ) )
{
continue;
}
@@ -643,9 +651,22 @@ class Setting extends Model
$return['setting_app'] = $values['themeeditor_setting_app']->directory;
}
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->key, $values['themeeditor_setting_name'] );
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->key . '_desc', $values['themeeditor_setting_desc'] );
Lang::saveCustom( 'core', static::$titleLangPrefix . $values['themeeditor_setting_key'], $values['themeeditor_setting_name'] );
Lang::saveCustom( 'core', static::$titleLangPrefix . $values['themeeditor_setting_key'] . '_desc', $values['themeeditor_setting_desc'] );
return $return;
}
/**
* [ActiveRecord] Delete Record
*
* @return void
*/
public function delete(): void
{
parent::delete();
Lang::deleteCustom( 'core', static::$titleLangPrefix . $this->key );
Lang::deleteCustom( 'core', static::$titleLangPrefix . $this->key . '_desc' );
}
}
+11 -1
View File
@@ -1366,7 +1366,17 @@ abstract class Widget
}
$caches[ $cacheKey ] = time();
Db::i()->update( 'core_widgets', array( 'caches' => json_encode( $caches ) ), static::_buildWhere( $this->key, $this->app ) );
$keyForCacheStore = $this->key;
if( in_array( Polymorphic::class, class_implements( $this ) ) )
{
/* @var Polymorphic $widgetClass */
$widgetClass = get_class( $this );
$keyForCacheStore = $widgetClass::getBaseKey();
}
Db::i()->update( 'core_widgets', array( 'caches' => json_encode( $caches ) ), static::_buildWhere( $keyForCacheStore, $this->app ) );
}
/* Then use what the store has */