Version 4.2.0

This commit is contained in:
Neo committed 2025-12-19 05:44:59 -08:00
1 parent 0dc2aee5ed
commit 96997ddd8e
2074 files changed
+125454 -55780

No files matched your search

+126 -82
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Application Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 18 Feb 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS;
@@ -249,12 +248,12 @@ class _Application extends \IPS\Node\Model
/**
* Get all extensions
*
* @param \IPS\Application|string $app The app key of the application which owns the extension
* @param string $extension Extension Type
* @param \IPS\Member|bool $checkAccess Check access permission for application against supplied member (or logged in member, if TRUE) before including extension
* @param string|NULL $firstApp If specified, the application with this key will be returned first
* @param string|NULL $firstExtensionKey If specified, the extension with this key will be returned first
* @param bool $construct Should an object be returned? (If false, just the classname will be returned)
* @param \IPS\Application|string $app The app key of the application which owns the extension
* @param string $extension Extension Type
* @param \IPS\Member|\IPS\Member\Group|bool $checkAccess Check access permission for application against supplied member/group (or logged in member, if TRUE) before including extension
* @param string|NULL $firstApp If specified, the application with this key will be returned first
* @param string|NULL $firstExtensionKey If specified, the extension with this key will be returned first
* @param bool $construct Should an object be returned? (If false, just the classname will be returned)
* @return array
*/
public static function allExtensions( $app, $extension, $checkAccess=TRUE, $firstApp=NULL, $firstExtensionKey=NULL, $construct=TRUE )
@@ -512,58 +511,35 @@ class _Application extends \IPS\Node\Model
$correctMd5s = \IPS\Http\Url::ips( 'md5' )->request()->get()->decodeJson();
/* And return whichever ones don't match */
return static::_md5sumCheckerIterator( \IPS\ROOT_PATH, $correctMd5s, 1 );
}
/**
* MD5 check directory
*
* @param string $directory Directory to look through
* @param array $correctMd5s The correct md5 hashes
* @param int $depth How deep into the folder structure we are
* @return array
*/
public static function _md5sumCheckerIterator( $directory, $correctMd5s, $depth = 1 )
{
$return = array();
foreach( new \DirectoryIterator( $directory ) as $file )
foreach ( $correctMd5s as $file => $md5Hash )
{
if ( mb_substr( $file, 0, 1 ) === '.' or mb_substr( $file, 0, 1 ) === '_' or $file == 'index.html' )
/* Fix the admin directory */
$file = preg_replace( '/^\/admin\//', '/' . \IPS\CP_DIRECTORY . '/', $file );
/* If this is an application directory but the application doesn't exist or has been disabled then we shouldn't check it */
preg_match( '/^\/applications\/(.+?)\//', $file, $matches );
if ( $matches )
{
if( in_array( $matches[1], static::$ipsApps ) AND !static::appIsEnabled( $matches[1] ) )
{
continue;
}
}
/* Ignore init.php (which always changes on build) and conf_global.dist.php (which doesn't exist after install) */
if ( in_array( $file, array( '/init.php', '/conf_global.dist.php' ) ) )
{
continue;
}
if ( $file->isDir() )
/* MD5 check */
if ( !file_exists( \IPS\ROOT_PATH . $file ) or !\IPS\Login::compareHashes( md5( preg_replace( '#\s#', '', utf8_decode( file_get_contents( \IPS\ROOT_PATH . $file ) ) ) ), $md5Hash ) ) // We strip whitespace since FTP in ASCII mode will change the whitespace characters
{
/* We only want to check directories which are ours (not other random directories they have on the server) and we can ignore datastore, plugins and uploads */
if ( $depth === 1 and !in_array( $file->getFilename(), array( \IPS\CP_DIRECTORY, 'api', 'applications', 'system' ) ) )
{
continue;
}
/* If this is an application directory but the application has been disabled then we shouldn't check it */
if( mb_strpos( $file->getPathname(), \IPS\ROOT_PATH . DIRECTORY_SEPARATOR . 'applications' ) !== FALSE AND $file->getFilename() != 'applications' )
{
/* If the directory name is a valid application key and the application is not enabled, don't check it */
if( in_array( $file->getFilename(), static::$ipsApps ) AND !static::appIsEnabled( $file->getFilename() ) )
{
continue;
}
}
$return = array_merge( $return, static::_md5sumCheckerIterator( $file->getPathname(), $correctMd5s, $depth + 1 ) );
}
elseif ( mb_substr( $file, -4 ) === '.php' )
{
$fullPath = $file->getPathname();
$shortPath = str_replace( '\\', '/', mb_substr( $fullPath, mb_strlen( \IPS\ROOT_PATH ) ) );
if ( $shortPath != '/init.php' and isset( $correctMd5s[ $shortPath ] ) and !\IPS\Login::compareHashes( md5( preg_replace( '#\s#', '', utf8_decode( file_get_contents( $fullPath ) ) ) ), $correctMd5s[ $shortPath ] ) ) // We strip whitespace since FTP in ASCII mode will change the whitespace characters
{
$return[] = $fullPath;
}
$return[] = \IPS\ROOT_PATH . $file;
}
}
return $return;
}
@@ -727,10 +703,10 @@ EOF;
/**
* Get Extensions
*
* @param \IPS\Application|string $app The app key of the application which owns the extension
* @param string $extension Extension Type
* @param bool $construct Should an object be returned? (If false, just the classname will be returned)
* @param \IPS\Member|bool $checkAccess Check access permission for extension against supplied member (or logged in member, if TRUE)
* @param \IPS\Application|string $app The app key of the application which owns the extension
* @param string $extension Extension Type
* @param bool $construct Should an object be returned? (If false, just the classname will be returned)
* @param \IPS\Member|\IPS\Member\Group|bool $checkAccess Check access permission for extension against supplied member/group (or logged in member, if TRUE)
* @return array
*/
public function extensions( $app, $extension, $construct=TRUE, $checkAccess=FALSE )
@@ -750,6 +726,19 @@ EOF;
if ( !$file->isDir() and !$file->isDot() and mb_substr( $file, -4 ) === '.php' AND mb_substr( $file, 0, 2 ) != '._' )
{
$classname = 'IPS\\' . $this->directory . '\extensions\\' . $app . '\\' . $extension . '\\' . mb_substr( $file, 0, -4 );
/* Check if class exists - sometimes we have to use blank files to wipe out old extensions */
try
{
if( !class_exists( $classname ) )
{
continue;
}
}
catch( \ErrorException $e )
{
continue;
}
if ( method_exists( $classname, 'generate' ) )
{
@@ -1510,7 +1499,7 @@ EOF;
}
catch( \IPS\Db\Exception $e )
{
\IPS\Log::log( "Error: " . $instruction['params']['method'] . ' ' . json_encode( $instruction['params']['params'] ), 'upgrade_error' );
\IPS\Log::log( "Error (" . $e->getCode() . ") " . $e->getMessage() . ": " . $instruction['params']['method'] . ' ' . json_encode( $instruction['params']['params'] ), 'upgrade_error' );
/* If the issue is with a create table other than exists, we should just throw it */
if ( $instruction['params']['method'] == 'createTable' and ! in_array( $e->getCode(), array( 1007, 1050 ) ) )
@@ -2603,14 +2592,21 @@ EOF;
$tableChanges = array();
$needIgnore = false;
$innoDbFullTextIndexes = array();
/* Get our local definition of this table */
try
{
$localDefinition = \IPS\Db::i()->getTableDefinition( $tableName );
$originalDefinition = $localDefinition; #Store this before it is normalised and engine stripped
$localDefinition = \IPS\Db::i()->normalizeDefinition( $localDefinition );
$compareDefinition = \IPS\Db::i()->normalizeDefinition( $tableDefinition );
$tableDefinition = \IPS\Db::i()->updateDefinitionIndexLengths( $tableDefinition );
/* Ensure that we use the proper engine, not whatever is in the schema.json as this will confuse index sub_part lengths */
if ( isset( $originalDefinition['engine'] ) )
{
$tableDefinition['engine'] = $originalDefinition['engine'];
}
if ( $compareDefinition != $localDefinition )
{
@@ -2787,6 +2783,17 @@ EOF;
{
if ( $indexName != 'PRIMARY' and !isset( $compareDefinition['indexes'][ $indexName ] ) )
{
/* If the index is on a column which we don't recognise (which may happen on tables which we add columns to like the ones that
store custom fields, or very naughty third parties adding columns on tables they don't own), don't drop it */
foreach ( $indexData['columns'] as $indexedColumn )
{
if ( !isset( $compareDefinition['columns'][ $indexedColumn ] ) )
{
continue 2;
}
}
/* Still here? Go ahead */
$dropIndexQuery = "DROP INDEX `{$db->escape_string( $indexName )}`";
if ( !in_array( $dropIndexQuery, $tableChanges ) ) // We skip the primary key as it can cause errors related to auto-increment
{
@@ -2892,24 +2899,32 @@ EOF;
static::writeJson( $queriesJsonFile, $write );
}
/* Add the actual version number in upgrade.php */
$upgradeFile = $workingDir . "/upgrade.php";
if ( file_exists( $upgradeFile ) )
/* Add the actual version number in upgrade.php & options.php */
$versionReplacement = function( $file ) use ( $human, $long )
{
$contents = file_get_contents( $upgradeFile );
$contents = str_replace(
array(
'{version_human}',
'upg_working'
),
array(
$human,
"upg_{$long}"
),
$contents
);
\file_put_contents( $upgradeFile, $contents );
}
if ( file_exists( $file ) )
{
$contents = file_get_contents( $file );
$contents = str_replace(
array(
'{version_human}',
'upg_working',
'{version_long}'
),
array(
$human,
"upg_{$long}",
$long
),
$contents
);
\file_put_contents( $file, $contents );
}
};
/* Make the replacement */
$versionReplacement( $workingDir . "/upgrade.php" );
$versionReplacement( $workingDir . "/options.php" );
/* Rename the directory */
rename( $workingDir, $setupDir . "/upg_{$long}" );
@@ -3013,7 +3028,7 @@ EOF;
$xml->text( "Invision Power Services, Inc" );
$xml->endAttribute();
$xml->startAttribute('author_url');
$xml->text( "http://www.invisionpower.com" );
$xml->text( "https://www.invisioncommunity.com" );
$xml->endAttribute();
/* Skin settings */
@@ -3612,10 +3627,10 @@ EOF;
/**
* Can the user access this application?
*
* @param \IPS\Member|NULL $member Member we are checking against or NULL for currently logged on user
* @param \IPS\Member|\IPS\Member\Group|NULL $memberOrGroup Member/group we are checking against or NULL for currently logged on user
* @return bool
*/
public function canAccess( $member=NULL )
public function canAccess( $memberOrGroup=NULL )
{
/* If it's not enabled, we can't */
if( !$this->enabled )
@@ -3636,8 +3651,15 @@ EOF;
}
/* Check member */
$member = ( $member === NULL ) ? \IPS\Member::loggedIn() : $member;
$memberGroups = array_merge( array( $member->member_group_id ), array_filter( explode( ',', $member->mgroup_others ) ) );
if ( $memberOrGroup instanceof \IPS\Member\Group )
{
$memberGroups = array( $memberOrGroup->g_id );
}
else
{
$member = ( $memberGroups === NULL ) ? \IPS\Member::loggedIn() : $memberGroups;
$memberGroups = array_merge( array( $member->member_group_id ), array_filter( explode( ',', $member->mgroup_others ) ) );
}
$accessGroups = explode( ',', $this->disabled_groups );
/* Are we in an allowed group? */
@@ -3672,6 +3694,18 @@ EOF;
public function save( $skipMember=FALSE )
{
parent::save();
static::postToggleEnable( $skipMember );
}
/**
* Cleanup after saving
*
* @param bool $skipMember Skip clearing member cache clearing
* @return void
* @note This is abstracted so it can be called externally, i.e. by the support tool
*/
public static function postToggleEnable( $skipMember=FALSE )
{
unset( \IPS\Data\Store::i()->applications );
unset( \IPS\Data\Store::i()->frontNavigation );
@@ -3724,9 +3758,15 @@ EOF;
$templatesToRecompile[ $class ] = $class;
}
/* Delete profile steps */
\IPS\Member\ProfileStep::deleteByApplication( $this );
/* Delete menu items */
\IPS\core\FrontNavigation::deleteByApplication( $this );
/* Delete club node maps */
\IPS\Member\Club::deleteByApplication( $this );
/* Delete data from shared tables */
\IPS\Content\Search\Index::i()->removeApplicationContent( $this );
\IPS\Db::i()->delete( 'core_permission_index', array( 'app=? AND perm_type=? AND perm_type_id IN(?)', 'core', 'module', \IPS\Db::i()->select( 'sys_module_id', 'core_modules', array( 'sys_module_application=?', $this->directory ) ) ) );
@@ -3742,6 +3782,7 @@ EOF;
\IPS\Db::i()->delete( 'core_queue', array( 'app=?', $this->directory ) );
\IPS\Db::i()->delete( 'core_follow', array( 'follow_app=?', $this->directory ) );
\IPS\Db::i()->delete( 'core_view_updates', array( "classname LIKE CONCAT( ?, '%' )", "IPS\\\\{$this->directory}" ) );
\IPS\Db::i()->delete( 'core_moderator_logs', array( 'appcomponent=?', $this->directory ) );
$classes = array();
foreach( $this->extensions( 'core', 'ContentRouter' ) AS $contentRouter )
@@ -3778,6 +3819,9 @@ EOF;
}
\IPS\Db::i()->delete( 'core_notifications', \IPS\Db::i()->in( 'item_class', $classes ) );
/* Delete Deletion Log Records */
\IPS\Db::i()->delete( 'core_deletion_log', \IPS\Db::i()->in( 'dellog_content_class', $classes ) );
}
unset( \IPS\Data\Store::i()->settings );