Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

File diff suppressed because it is too large. Load diff
+9 -5
View File
@@ -5,13 +5,13 @@
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 8 Aug 2013␍
* @since 8 Aug 2013
*/
namespace IPS\Application;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -22,14 +22,18 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class _BuilderFilter extends \RecursiveFilterIterator
{
/**
* Accept the member
*
* @return bool
*/
public function accept()
{
return !( $this->isDir() && in_array( $this->getFilename(), $this->getDirectoriesToIgnore() ) );
return !( $this->isDir() && \in_array( $this->getFilename(), $this->getDirectoriesToIgnore() ) );
}
/**
* returns the skipped directories
* Returns the skipped directories
*
* @return array
*/
+1 -1
View File
@@ -11,7 +11,7 @@
namespace IPS\Application;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
+8 -8
View File
@@ -11,7 +11,7 @@
namespace IPS\Application;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -22,15 +22,15 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class _Developer extends \IPS\Application
{
/* [Brief] Array of directories that should always be present inside /dev */
/**
* @brief Array of directories that should always be present inside /dev
*/
protected static $devDirs = array( 'css', 'email', 'html', 'img', 'js' );
/**
* Returns NULL or a list of missing IN_DEV required directories
*
* @return NULL|Array Null if all directories are present, or an array of directory names ('dev', 'dev/css', 'dev/email',... )
* @return NULL|array Null if all directories are present, or an array of directory names ('dev', 'dev/css', 'dev/email',... )
*/
public function getMissingDirectories()
{
@@ -50,7 +50,7 @@ class _Developer extends \IPS\Application
}
}
return ( count( $missing ) ) ? $missing : null;
return ( \count( $missing ) ) ? $missing : null;
}
/**
@@ -76,6 +76,6 @@ class _Developer extends \IPS\Application
}
}
return ( count( $unwriteable ) ) ? $unwriteable : null;
return ( \count( $unwriteable ) ) ? $unwriteable : null;
}
}
+391
View File
@@ -0,0 +1,391 @@
<?php
/**
* @brief Application Feature Highlight Class
* @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 15 Nov 2013
*/
namespace IPS\Application;
/* 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;
}
class _Feature extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
{
/**
* @brief [ActiveRecord] Database Prefix
*/
public static $databasePrefix = 'feature_';
/**
* @brief [ActiveRecord] Database table
* @note This MUST be over-ridden
*/
public static $databaseTable = 'core_members_features';
/**
* @brief [ActiveRecord] Multiton Store
* @note This needs to be declared in any child classes as well, only declaring here for editor code-complete/error-check functionality
*/
protected static $multitons = array();
/**
* @brief [ActiveRecord] Database ID Fields
* @note If using this, declare a static $multitonMap = array(); in the child class to prevent duplicate loading queries
*/
protected static $databaseIdFields = array( 'feature_unique_key' );
/**
* @brief [ActiveRecord] Multiton Map
*/
protected static $multitonMap = array();
/**
* Developer Management Table
*
* @param \IPS\Http\Url $url URL of the page the table is shown on.
* @param string $appKay Application Key
* @return string
*/
public static function devTable( \IPS\Http\Url $url, string $appKey ): string
{
$application = \IPS\Application::load( $appKey );
if ( isset( \IPS\Request::i()->featureAction ) )
{
switch( \IPS\Request::i()->featureAction )
{
case 'form':
$current = NULL;
if ( isset( \IPS\Request::i()->uniqueKey ) )
{
try
{
$current = static::load( \IPS\Request::i()->uniqueKey, 'feature_unique_key' );
}
catch( \OutOfRangeException $e ) {}
}
$form = new \IPS\Helpers\Form;
static::form( $form, $application, $current, isset( \IPS\Request::i()->version ) ? \IPS\Request::i()->version : NULL );
if ( $values = $form->values() )
{
if ( $current === NULL )
{
$current = new static;
}
$current->formatFormValues( $values );
$current->save();
\IPS\Output::i()->redirect( $url, 'saved' );
}
return $form;
break;
case 'delete':
$feature = static::load( \IPS\Request::i()->uniqueKey, 'feature_unique_key' );
$feature->delete();
\IPS\Output::i()->redirect( $url, 'deleted' );
break;
}
}
$tree = new \IPS\Helpers\Tree\Tree(
$url,
'dev_features',
function() use ( $application, $url ) {
$return = array();
$return[] = static::_devTableRow( -1, FALSE, $application, $url );
$versions = $application->getAllVersions();
krsort( $versions );
foreach( $versions AS $long => $human )
{
$return[] = static::_devTableRow( $long, FALSE, $application, $url );
}
return $return;
},
function( $long, $root=FALSE ) use ( $application, $url ) {
return static::_devTableRow( $long, $root, $application, $url );
},
function( $long ) {
return NULL;
},
function( $long ) use ( $application, $url ) {
$return = array();
$file = \IPS\ROOT_PATH . "/applications/{$application->directory}/setup/upg_" . ( $long == -1 ? 'working' : $long ) . "/whatsnew.json";
if ( file_exists( $file ) )
{
if ( $data = @json_decode( \file_get_contents( $file ), TRUE ) )
{
foreach( $data AS $key => $row )
{
$return[] = \IPS\Theme::i()->getTemplate( 'trees', 'core' )->row( $url, $long . '.' . $key, $row['feature_title'], FALSE, array(
'edit' => array(
'icon' => 'pencil',
'title' => 'edit',
'link' => $url->setQueryString( array( 'featureAction' => 'form', 'uniqueKey' => $row['feature_unique_key'] ) ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('edit') )
),
'delete' => array(
'icon' => 'times',
'title' => 'delete',
'link' => $url->setQueryString( array( 'featureAction' => 'delete', 'uniqueKey' => $row['feature_unique_key'] ) ),
'data' => array( 'confirm' => '' )
)
), NULL, NULL, NULL, FALSE );
}
}
}
return $return;
}
);
return $tree;
}
public static function _devTableRow( $long, $root, $application, $url )
{
$file = \IPS\ROOT_PATH . "/applications/{$application->directory}/setup/upg_" . ( $long == -1 ? 'working' : $long ) . "/whatsnew.json";
$hasChildren = FALSE;
if ( file_exists( $file ) )
{
if ( $data = @json_decode( \file_get_contents( $file ), TRUE ) )
{
if ( \count( $data ) )
{
$hasChildren = TRUE;
}
}
}
if ( $root === FALSE )
{
$buttons = array(
'add' => array(
'icon' => 'plus-circle',
'title' => 'add_feature_highlight',
'link' => $url->setQueryString( array( 'featureAction' => 'form', 'version' => $long ) ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('add_feature_highlight') )
)
);
}
else
{
$buttons = array();
}
if ( $long === -1 )
{
$name = \IPS\Member::loggedIn()->language()->addToStack( 'versions_working' );
}
else
{
$name = $application->getAllVersions()[ $long ];
}
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->row( $url, $long, $name, $hasChildren, $buttons, ( $long === -1 ) ? NULL : $long, NULL, NULL, $root );
}
/**
* Form
*
* @param \IPS\Helpers\Form $form The form, passed by reference
* @param \IPS\Application $application Current application
* @param NULL|\IPS\Application\Feature $current Currently editing, or NULL for new.
* @return void
*/
public static function form( &$form, \IPS\Application $application, ?\IPS\Application\Feature $current, ?int $version ): void
{
$form->hiddenValues['feature_app'] = $application->directory;
$form->hiddenValues['feature_version_added'] = ( $current ) ? $current->version_added : $version;
$form->add( new \IPS\Helpers\Form\Text( 'feature_title', ( $current ) ? $current->title : NULL, TRUE ) );
$form->add( new \IPS\Helpers\Form\TextArea( 'feature_text', ( $current ) ? $current->text : NULL, TRUE, array( 'maxLength' => 300 ) ) );
$form->add( new \IPS\Helpers\Form\Upload( 'feature_image', NULL, FALSE, array( 'image' => TRUE, 'temporary' => TRUE ) ) );
$form->add( new \IPS\Helpers\Form\Url( 'feature_url', ( $current ) ? $current->url : NULL, FALSE ) );
}
/**
* Save
*
* @return void
*/
public function save()
{
parent::save();
\IPS\Db::i()->insert( 'core_members_feature_seen', array( 'member_id' => \IPS\Member::loggedIn()->member_id, 'feature_id' => $this->id ), FALSE, TRUE );
static::writeJsonFile( $this->app, $this->version_added );
}
/**
* Delete
*
* @return void
*/
public function delete()
{
if ( $this->image )
{
@\unlink( \IPS\ROOT_PATH . "/applications/{$this->app}/setup/upg_" . ( $this->version_added == -1 ? 'working' : $this->version_added ) . "/features/{$this->image}" );
}
parent::delete();
/* If there's no more features for this version, make sure features folder is cleaned up */
if ( \is_dir( \IPS\ROOT_PATH . "/applications/{$this->app}/setup/upg_" . ( $this->version_added == -1 ? 'working' : $this->version_added ) . "/features/" ) AND !\IPS\Db::i()->select( 'COUNT(*)', 'core_members_features', array( "feature_version_added=? AND feature_app=?", $this->version_added, $this->app ) )->first() )
{
@\rmdir( \IPS\ROOT_PATH . "/applications/{$this->app}/setup/upg_" . ( $this->version_added == -1 ? 'working' : $this->version_added ) . "/features/" );
}
static::writeJsonFile( $this->app, $this->version_added );
}
/**
* Get Application
*
* @return \IPS\Application
*/
public function get__app(): \IPS\Application
{
return \IPS\Application::load( $this->_data['app'] );
}
/**
* Format Form Values
*
* @param array $values Form Values
* @return void
*/
public function formatFormValues( array $values ): void
{
$this->title = $values['feature_title'];
$this->text = $values['feature_text'];
$this->url = (string) $values['feature_url'];
$this->version_added = ( isset( $values['feature_version_added'] ) AND $values['feature_version_added'] ) ? $values['feature_version_added'] : -1;
$this->app = $values['feature_app'];
if ( \array_key_exists( 'feature_image', $values ) AND $values['feature_image'] )
{
/* Make a friendly file name */
$filename = \IPS\Http\Url\Friendly::seoTitle( $values['feature_title'] ) . image_type_to_extension( exif_imagetype( $values['feature_image'] ) );
if ( !\is_dir( \IPS\ROOT_PATH . "/applications/{$values['feature_app']}/setup/upg_" . ( $values['feature_version_added'] == -1 ? "working" : $values['feature_version_added'] ) . "/" ) )
{
mkdir( \IPS\ROOT_PATH . "/applications/{$values['feature_app']}/setup/upg_" . ( $values['feature_version_added'] == -1 ? "working" : $values['feature_version_added'] ) . "/" );
chmod( \IPS\ROOT_PATH . "/applications/{$values['feature_app']}/setup/upg_" . ( $values['feature_version_added'] == -1 ? "working" : $values['feature_version_added'] ) . "/", \IPS\IPS_FOLDER_PERMISSION );
}
$dir = \IPS\ROOT_PATH . "/applications/{$values['feature_app']}/setup/upg_" . ( $values['feature_version_added'] == -1 ? "working" : $values['feature_version_added'] ) . "/features/";
if ( !\is_dir( $dir ) )
{
mkdir( $dir );
chmod( $dir, \IPS\IPS_FOLDER_PERMISSION );
}
/* If there is an existing file, remove it */
if ( $this->image AND @\file_exists( $dir . '/' . $this->image ) )
{
@\unlink( $dir . '/' . $this->image );
}
\move_uploaded_file( $values['feature_image'], $dir . '/' . $filename );
$this->image = $filename;
}
if ( !$this->unique_key )
{
$this->unique_key = md5( $values['feature_app'] . $values['feature_title'] . mt_rand( 0, 1000 ) );
}
}
/**
* Mark feature as read
*
* @param \IPS\Member|NULL $member The member, or NULL for currently logged in member.
* @param \IPS\Member\Feature $features Variable length amount of features to mark as read.
* @return void
*/
public static function markRead( ?\IPS\Member $member, \IPS\Application\Feature ...$features ): void
{
$member = $member ?: \IPS\Member::loggedIn();
$insert = array();
foreach( $features AS $feature )
{
$insert[] = array( 'member_id' => $member->member_id, 'feature_id' => $feature->id );
}
if ( \count( $insert ) )
{
\IPS\Db::i()->insert( 'core_members_feature_seen', $insert );
}
}
/**
* JSON Serialize
*
* @return array
*/
public function jsonSerialize(): array
{
$versions = $this->_app->getAllVersions();
return array(
'id' => $this->id,
'version' => ( $this->version_added AND isset( $versions[ $this->version_added ] ) ) ? $versions[$this->version_added] : $this->_app->version,
'title' => $this->title,
'description' => $this->text,
'more_info' => $this->url,
'image' => $this->image ? (string) \IPS\Http\Url::internal( "/applications/{$this->app}/setup/upg_" . ( $this->version_added == -1 ? 'working' : $this->version_added ) . "/features/{$this->image}", 'none' ) : NULL
);
}
/**
* Write Json File
*
* @param string $appKey Application key to write.
* @param int $versionId Long version
* @return void
*/
public static function writeJsonFile( string $appKey, int $versionId ): void
{
$path = \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_" . ( $versionId == -1 ? 'working' : $versionId ) . "/whatsnew.json";
@unlink( $path );
if ( !\is_dir( \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_" . ( $versionId == -1 ? 'working' : $versionId ) . "/" ) )
{
mkdir( \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_" . ( $versionId == -1 ? 'working' : $versionId ) . "/" );
chmod( \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_" . ( $versionId == -1 ? 'working' : $versionId ) . "/", \IPS\IPS_FOLDER_PERMISSION );
}
$data = array();
foreach( \IPS\Db::i()->select( '*', 'core_members_features', array( "feature_app=? AND feature_version_added=?", $appKey, $versionId ), "feature_version_added DESC" ) AS $row )
{
$data[ $row['feature_unique_key'] ] = $row;
}
if ( \count( $data ) )
{
\file_put_contents( $path, json_encode( $data, JSON_PRETTY_PRINT ) );
}
else
{
@unlink( $path );
}
}
}
+47 -29
View File
@@ -11,7 +11,7 @@
namespace IPS\Application;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -111,18 +111,8 @@ class _Module extends \IPS\Node\Model implements \IPS\Node\Permissions
{
if( static::$modules === NULL )
{
if ( isset( \IPS\Data\Store::i()->modules ) )
{
$rows = \IPS\Data\Store::i()->modules;
}
else
{
$rows = iterator_to_array( \IPS\Db::i()->select( '*', 'core_modules', NULL, 'sys_module_position' )->join( 'core_permission_index', array( "core_permission_index.app=? AND core_permission_index.perm_type=? AND core_permission_index.perm_type_id=core_modules.sys_module_id", 'core', 'module' ) ) );
\IPS\Data\Store::i()->modules = $rows;
}
static::$modules = array();
foreach ( $rows as $row )
foreach ( static::getStore() as $row )
{
static::$modules[ $row['sys_module_application'] ][ $row['sys_module_area'] ][ $row['sys_module_key'] ] = static::constructFromData( $row );
}
@@ -130,6 +120,22 @@ class _Module extends \IPS\Node\Model implements \IPS\Node\Permissions
return static::$modules;
}
/**
* Get data store
*
* @return array
* @note Note that all records are returned, even disabled report rules. Enable status needs to be checked in userland code when appropriate.
*/
public static function getStore()
{
if ( !isset( \IPS\Data\Store::i()->modules ) )
{
\IPS\Data\Store::i()->modules = iterator_to_array( \IPS\Db::i()->select( '*', 'core_modules', NULL, 'sys_module_position' )->join( 'core_permission_index', array( "core_permission_index.app=? AND core_permission_index.perm_type=? AND core_permission_index.perm_type_id=core_modules.sys_module_id", 'core', 'module' ) ) );
}
return \IPS\Data\Store::i()->modules;
}
/**
* Get a module
@@ -193,7 +199,7 @@ class _Module extends \IPS\Node\Model implements \IPS\Node\Permissions
{
try
{
$module = static::load( $matches[2], 'sys_module_key', count( $where ) ? array_merge( array( array( 'sys_module_application=? and sys_module_area=?', $matches[1], 'front' ) ), array( $where ) ) : array( array( 'sys_module_application=?', $matches[1] ) ) );
$module = static::load( $matches[2], 'sys_module_key', \count( $where ) ? array_merge( array( array( 'sys_module_application=? and sys_module_area=?', $matches[1], 'front' ) ), array( $where ) ) : array( array( 'sys_module_application=?', $matches[1] ) ) );
$return[ $module->_id ] = $module;
}
catch ( \OutOfRangeException $e ) { }
@@ -243,7 +249,7 @@ class _Module extends \IPS\Node\Model implements \IPS\Node\Permissions
$buttons['default'] = array(
'icon' => $this->default ? 'star' : 'star-o',
'title' => 'make_default_module',
'link' => $url . "&do=setDefaultModule&id={$this->_id}&default=1",
'link' => $url->csrf() . "&do=setDefaultModule&id={$this->_id}&default=1",
);
return $buttons;
@@ -370,8 +376,12 @@ class _Module extends \IPS\Node\Model implements \IPS\Node\Permissions
public function save( $skipMember=FALSE )
{
$new = $this->_new;
$this->_skipClearingMenuCache = $skipMember;
parent::save();
$this->_skipClearingMenuCache = FALSE;
if ( $new )
{
@@ -384,24 +394,32 @@ class _Module extends \IPS\Node\Model implements \IPS\Node\Permissions
'perm_view' => '*',
) );
}
}
/* Clear caches */
if( !$skipMember )
/**
* @brief [ActiveRecord] Caches
* @note Defined cache keys will be cleared automatically as needed
*/
protected $caches = array( 'modules' );
/**
* @brief Skip clearing create menu cache
*/
protected $_skipClearingMenuCache = FALSE;
/**
* Clear any defined caches
*
* @param bool $removeMultiton Should the multiton record also be removed?
* @return void
*/
public function clearCaches( $removeMultiton=FALSE )
{
parent::clearCaches( $removeMultiton );
if( $this->_skipClearingMenuCache === FALSE )
{
\IPS\Member::clearCreateMenu();
}
unset( \IPS\Data\Store::i()->modules );
}
/**
* [Node] Delete the theme set
*
* @return void
*/
public function delete()
{
parent::delete();
\IPS\Member::clearCreateMenu();
unset( \IPS\Data\Store::i()->modules );
}
}