5389 lines
167 KiB
PHP
5389 lines
167 KiB
PHP
<?php
|
|
/**
|
|
* @brief Application 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 18 Feb 2013
|
|
*/
|
|
|
|
namespace IPS;
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/**
|
|
* @brief Abstract class that applications extend and use to handle application data
|
|
*/
|
|
class _Application extends \IPS\Node\Model
|
|
{
|
|
/**
|
|
* @brief IPS Applications
|
|
* @deprecated - see \IPS\IPS::$ipsApps
|
|
*/
|
|
public static $ipsApps = array(
|
|
'blog',
|
|
'calendar',
|
|
'cms',
|
|
'core',
|
|
'downloads',
|
|
'forums',
|
|
'gallery',
|
|
'nexus',
|
|
'convert'
|
|
);
|
|
|
|
/**
|
|
* @brief [ActiveRecord] Multiton Store
|
|
*/
|
|
protected static $multitons;
|
|
|
|
/**
|
|
* @brief Have fetched all?
|
|
*/
|
|
protected static $gotAll = FALSE;
|
|
|
|
/**
|
|
* @brief Defined versions
|
|
*/
|
|
protected $definedVersions = NULL;
|
|
|
|
/**
|
|
* @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
|
|
*/
|
|
public static $titleLangPrefix = '__app_';
|
|
|
|
/**
|
|
* @brief Defined theme locations for the theme system
|
|
*/
|
|
public $themeLocations = array('admin', 'front', 'global');
|
|
|
|
/**
|
|
* @brief [ActiveRecord] Caches
|
|
* @note Defined cache keys will be cleared automatically as needed
|
|
*/
|
|
protected $caches = array( 'updatecount_applications' );
|
|
|
|
/**
|
|
* Set default
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setAsDefault()
|
|
{
|
|
/* Update any FURL customizations */
|
|
if ( \IPS\Settings::i()->furl_configuration )
|
|
{
|
|
$furlCustomizations = json_decode( \IPS\Settings::i()->furl_configuration, TRUE );
|
|
|
|
try
|
|
{
|
|
/* Add the top-level directory to all the FURLs for the old default app */
|
|
$previousDefaultApp = static::constructFromData( \IPS\Db::i()->select( '*', 'core_applications', 'app_default=1' )->first() );
|
|
if( file_exists( $previousDefaultApp->getApplicationPath() . "/data/furl.json" ) )
|
|
{
|
|
$oldDefaultAppDefinition = json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( $previousDefaultApp->getApplicationPath() . "/data/furl.json" ) ), TRUE );
|
|
if ( $oldDefaultAppDefinition['topLevel'] )
|
|
{
|
|
foreach ( $oldDefaultAppDefinition['pages'] as $k => $data )
|
|
{
|
|
if ( isset( $furlCustomizations[ $k ] ) )
|
|
{
|
|
$furlCustomizations[ $k ] = \IPS\Http\Url\Friendly::buildFurlDefinition( $furlCustomizations[ $k ]['friendly'], $furlCustomizations[ $k ]['real'], $oldDefaultAppDefinition['topLevel'], FALSE, isset( $furlCustomizations[ $k ]['alias'] ) ? $furlCustomizations[ $k ]['alias'] : NULL, isset( $furlCustomizations[ $k ]['custom'] ) ? $furlCustomizations[ $k ]['custom'] : FALSE, isset( $furlCustomizations[ $k ]['verify'] ) ? $furlCustomizations[ $k ]['verify'] : NULL );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch ( \UnderflowException $e ){}
|
|
|
|
|
|
/* And remove it from the new */
|
|
if( file_exists( $this->getApplicationPath() . "/data/furl.json" ) )
|
|
{
|
|
$newDefaultAppDefinition = json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( $this->getApplicationPath() . "/data/furl.json" ) ), TRUE );
|
|
if ( $newDefaultAppDefinition['topLevel'] )
|
|
{
|
|
foreach ( $newDefaultAppDefinition['pages'] as $k => $data )
|
|
{
|
|
if ( isset( $furlCustomizations[ $k ] ) )
|
|
{
|
|
$furlCustomizations[ $k ] = \IPS\Http\Url\Friendly::buildFurlDefinition( rtrim( preg_replace( '/^' . preg_quote( $newDefaultAppDefinition['topLevel'], '/' ) . '\/?/', '', $furlCustomizations[ $k ]['friendly'] ), '/' ), $furlCustomizations[ $k ]['real'], $newDefaultAppDefinition['topLevel'], TRUE, isset( $furlCustomizations[ $k ]['alias'] ) ? $furlCustomizations[ $k ]['alias'] : NULL, isset( $furlCustomizations[ $k ]['custom'] ) ? $furlCustomizations[ $k ]['custom'] : FALSE, isset( $furlCustomizations[ $k ]['verify'] ) ? $furlCustomizations[ $k ]['verify'] : NULL );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Save the new FURL customisation */
|
|
\IPS\Settings::i()->changeValues( array( 'furl_configuration' => json_encode( $furlCustomizations ) ) );
|
|
}
|
|
|
|
foreach( \IPS\Application::applications() as $directory => $application )
|
|
{
|
|
if( $application->default )
|
|
{
|
|
$defaultApplication = $directory;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static::removeMetaPrefix( \IPS\Application::load( $defaultApplication ) );
|
|
static::addMetaPrefix( $this );
|
|
|
|
/* Actually update the database */
|
|
\IPS\Db::i()->update( 'core_applications', array( 'app_default' => 0 ) );
|
|
\IPS\Db::i()->update( 'core_applications', array( 'app_default' => 1 ), array( 'app_id=?', $this->id ) );
|
|
|
|
/* Clear cached data */
|
|
unset( \IPS\Data\Store::i()->applications );
|
|
unset( \IPS\Data\Store::i()->furl_configuration );
|
|
\IPS\Member::clearCreateMenu();
|
|
|
|
/* Clear guest page caches */
|
|
\IPS\Data\Cache::i()->clearAll();
|
|
}
|
|
|
|
/**
|
|
* Get Applications
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function applications()
|
|
{
|
|
if( static::$gotAll === FALSE )
|
|
{
|
|
static::$multitons = array();
|
|
|
|
foreach ( static::getStore() as $row )
|
|
{
|
|
try
|
|
{
|
|
static::$multitons[ $row['app_directory'] ] = static::constructFromData( $row );
|
|
}
|
|
catch( \UnexpectedValueException $e )
|
|
{
|
|
if ( mb_stristr( $e->getMessage(), 'Missing:' ) )
|
|
{
|
|
/* Ignore this, the app is in the table, but not 4.0 compatible */
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
static::$gotAll = TRUE;
|
|
}
|
|
|
|
return static::$multitons;
|
|
}
|
|
|
|
/**
|
|
* Get data store
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function getStore()
|
|
{
|
|
if ( !isset( \IPS\Data\Store::i()->applications ) )
|
|
{
|
|
\IPS\Data\Store::i()->applications = iterator_to_array( \IPS\Db::i()->select( '*', 'core_applications', NULL, 'app_position' ) );
|
|
}
|
|
|
|
return \IPS\Data\Store::i()->applications;
|
|
}
|
|
|
|
/**
|
|
* Get enabled applications
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function enabledApplications()
|
|
{
|
|
$applications = static::applications();
|
|
$enabled = array();
|
|
|
|
foreach( $applications as $key => $application )
|
|
{
|
|
if( $application->enabled )
|
|
{
|
|
$enabled[ $key ] = $application;
|
|
}
|
|
}
|
|
|
|
return $enabled;
|
|
}
|
|
|
|
/**
|
|
* Does an application exist and is it enabled? Note: does not check if offline for a particular member
|
|
*
|
|
* @see \IPS\Application::canAccess()
|
|
* @param string $key Application key
|
|
* @return bool
|
|
*/
|
|
public static function appIsEnabled( $key )
|
|
{
|
|
$applications = static::applications();
|
|
|
|
if ( !array_key_exists( $key, $applications ) )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
return $applications[ $key ]->enabled;
|
|
}
|
|
|
|
/**
|
|
* Load Record
|
|
*
|
|
* @see \IPS\Db::build
|
|
* @param int|string $id ID
|
|
* @param string $idField The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
|
|
* @param mixed $extraWhereClause Additional where clause(s) (see \IPS\Db::build for details)
|
|
* @return static
|
|
* @throws \InvalidArgumentException
|
|
* @throws \OutOfRangeException
|
|
*/
|
|
public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
|
|
{
|
|
static::applications(); // Load all applications so we can grab the data from the cache
|
|
return parent::load( $id, $idField, $extraWhereClause );
|
|
}
|
|
|
|
/**
|
|
* Fetch All Root Nodes
|
|
*
|
|
* @param string|NULL $permissionCheck The permission key to check for or NULl to not check permissions
|
|
* @param \IPS\Member|NULL $member The member to check permissions for or NULL for the currently logged in member
|
|
* @param mixed $where Additional WHERE clause
|
|
* @param array|NULL $limit Limit/offset to use, or NULL for no limit (default)
|
|
* @note This is overridden to prevent UnexpectedValue exceptions when there is an old application record in core_applications without an Application.php file
|
|
* @return array
|
|
*/
|
|
public static function roots( $permissionCheck='view', $member=NULL, $where=array(), $limit=NULL )
|
|
{
|
|
return static::applications();
|
|
}
|
|
|
|
/**
|
|
* 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|\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 )
|
|
{
|
|
$extensions = array();
|
|
|
|
/* Get applications */
|
|
$apps = static::applications();
|
|
|
|
if ( $firstApp !== NULL )
|
|
{
|
|
$apps = static::$multitons;
|
|
|
|
usort( $apps, function( $a, $b ) use ( $firstApp )
|
|
{
|
|
if ( $a->directory === $firstApp )
|
|
{
|
|
return -1;
|
|
}
|
|
if ( $b->directory === $firstApp )
|
|
{
|
|
return 1;
|
|
}
|
|
return 0;
|
|
} );
|
|
}
|
|
|
|
/* Get extensions */
|
|
foreach ( $apps as $application )
|
|
{
|
|
if ( !static::appIsEnabled( $application->directory ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if( $checkAccess !== FALSE )
|
|
{
|
|
if( !$application->canAccess( $checkAccess === TRUE ? NULL : $checkAccess ) )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$_extensions = array();
|
|
|
|
foreach ( $application->extensions( $app, $extension, $construct, $checkAccess ) as $key => $class )
|
|
{
|
|
$_extensions[ $application->directory . '_' . $key ] = $class;
|
|
}
|
|
|
|
if ( $firstExtensionKey !== NULL AND array_key_exists( $application->directory . '_' . $firstExtensionKey, $_extensions ) )
|
|
{
|
|
uksort( $_extensions, function( $a, $b ) use ( $application, $firstExtensionKey )
|
|
{
|
|
if ( $a === $application->directory . '_' . $firstExtensionKey )
|
|
{
|
|
return -1;
|
|
}
|
|
if ( $b === $application->directory . '_' . $firstExtensionKey )
|
|
{
|
|
return 1;
|
|
}
|
|
return 0;
|
|
} );
|
|
}
|
|
|
|
$extensions = array_merge( $extensions, $_extensions );
|
|
}
|
|
|
|
/* Return */
|
|
return $extensions;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a list of applications that contain a specific type of extension
|
|
*
|
|
* @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
|
|
* @return array
|
|
*/
|
|
public static function appsWithExtension( $app, $extension, $checkAccess=TRUE )
|
|
{
|
|
$_apps = array();
|
|
|
|
foreach( static::applications() as $application )
|
|
{
|
|
if ( static::appIsEnabled( $application->directory ) )
|
|
{
|
|
/* If $checkAccess is false we don't verify access to the app */
|
|
if( $checkAccess !== FALSE )
|
|
{
|
|
/* If we passed true, we want to check current member, otherwise pass the member in directly */
|
|
if( $application->canAccess( ( $checkAccess === TRUE ) ? NULL : $checkAccess ) !== TRUE )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if( \count( $application->extensions( $app, $extension ) ) )
|
|
{
|
|
$_apps[ $application->directory ] = $application;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $_apps;
|
|
}
|
|
|
|
/**
|
|
* Get available version for an application
|
|
* Used by the installer/upgrader
|
|
*
|
|
* @param string $appKey The application key
|
|
* @param bool $human Return the human-readable version instead
|
|
* @return int|null
|
|
*/
|
|
public static function getAvailableVersion( $appKey, $human=FALSE )
|
|
{
|
|
$versionsJson = static::getRootPath( $appKey ) . "/applications/{$appKey}/data/versions.json";
|
|
|
|
$_versions = $human ? array_values( json_decode( file_get_contents( $versionsJson ), TRUE ) ) : array_keys( json_decode( file_get_contents( $versionsJson ), TRUE ) );
|
|
if ( file_exists( $versionsJson ) and $versionsJson = $_versions )
|
|
{
|
|
return array_pop( $versionsJson );
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Get all defined versions for an application
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getAllVersions()
|
|
{
|
|
if( $this->definedVersions !== NULL )
|
|
{
|
|
return $this->definedVersions;
|
|
}
|
|
|
|
$this->definedVersions = array();
|
|
|
|
$versionsJson = $this->getApplicationPath() . "/data/versions.json";
|
|
|
|
if ( file_exists( $versionsJson ) )
|
|
{
|
|
$this->definedVersions = json_decode( file_get_contents( $versionsJson ), TRUE );
|
|
}
|
|
|
|
return $this->definedVersions;
|
|
}
|
|
|
|
/**
|
|
* Return the human version of an INT long version
|
|
*
|
|
* @param int $longVersion Long version (10001)
|
|
* @return string|false Long Version (1.1.1 Beta 1)
|
|
*/
|
|
public function getHumanVersion( $longVersion )
|
|
{
|
|
$this->getAllVersions();
|
|
|
|
if ( isset( $this->definedVersions[ $longVersion ] ) )
|
|
{
|
|
return $this->definedVersions[ (int) $longVersion ];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* The available version we can upgrade to
|
|
*
|
|
* @param bool $latestOnly If TRUE, will return the latest version only
|
|
* @param bool $skipSameHumanVersion If TRUE, will not include any versions with the same "human" version number as the current version
|
|
* @return array
|
|
*/
|
|
public function availableUpgrade( $latestOnly=FALSE, $skipSameHumanVersion=TRUE )
|
|
{
|
|
$update = array();
|
|
|
|
if( $this->update_version )
|
|
{
|
|
$versions = json_decode( $this->update_version, TRUE );
|
|
if ( \is_array( $versions ) and !isset( $versions[0] ) and isset( $versions['longversion'] ) )
|
|
{
|
|
$versions = array( $versions );
|
|
}
|
|
|
|
$update = array();
|
|
foreach ( $versions as $data )
|
|
{
|
|
if( !empty( $data['longversion'] ) and $data['longversion'] > $this->long_version and ( !$skipSameHumanVersion or $data['version'] != $this->version ) )
|
|
{
|
|
if( $data['released'] AND ( (int) $data['released'] != $data['released'] OR \strlen($data['released']) != 10 ) )
|
|
{
|
|
$data['released'] = strtotime( $data['released'] );
|
|
}
|
|
|
|
$update[] = $data;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !empty( $update ) and $latestOnly )
|
|
{
|
|
$update = array_pop( $update );
|
|
}
|
|
|
|
return $update;
|
|
}
|
|
|
|
/**
|
|
* The latest new feature ID
|
|
*
|
|
* @return int|null
|
|
*/
|
|
public function newFeature()
|
|
{
|
|
if( $this->update_version )
|
|
{
|
|
$versions = json_decode( $this->update_version, TRUE );
|
|
if ( \is_array( $versions ) and !isset( $versions[0] ) and isset( $versions['longversion'] ) )
|
|
{
|
|
$versions = array( $versions );
|
|
}
|
|
|
|
$latestVersion = NULL;
|
|
|
|
foreach ( $versions as $data )
|
|
{
|
|
if( isset( $data['latestNewFeature'] ) AND $data['latestNewFeature'] AND $data['latestNewFeature'] > $latestVersion )
|
|
{
|
|
$latestVersion = $data['latestNewFeature'];
|
|
}
|
|
}
|
|
|
|
return $latestVersion;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Is the application up to date with security patches?
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function missingSecurityPatches()
|
|
{
|
|
$updates = $this->availableUpgrade();
|
|
if( !empty( $updates ) )
|
|
{
|
|
foreach( $updates as $update )
|
|
{
|
|
if( $update['security'] )
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* MD5 check (returns path to files which do not match)
|
|
*
|
|
* @param int|NULL $version Version to check against
|
|
* @return array
|
|
* @throws \IPS\Http\Request\Exception
|
|
*/
|
|
public static function md5Check( $version = NULL )
|
|
{
|
|
/* For Community in the Cloud customers we cannot do this because they have encoded files
|
|
and the encoder produces different output each time it runs, so every Cloud customer
|
|
has different files, even for the same version */
|
|
$key = \IPS\IPS::licenseKey();
|
|
if ( \IPS\CIC OR $key['cloud'] )
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/* For everyone else, get the correct md5 sums for each file... */
|
|
$url = \IPS\Http\Url::ips( 'md5' );
|
|
if ( $version !== NULL )
|
|
{
|
|
$url = $url->setQueryString( 'version', $version );
|
|
}
|
|
$correctMd5s = $url->request()->get()->decodeJson();
|
|
|
|
/* And return whichever ones don't match */
|
|
$return = array();
|
|
foreach ( $correctMd5s as $file => $md5Hash )
|
|
{
|
|
/* 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], \IPS\IPS::$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 the file doesn't exist at all, flag it */
|
|
if ( !file_exists( \IPS\ROOT_PATH . $file ) )
|
|
{
|
|
$return[] = \IPS\ROOT_PATH . $file;
|
|
}
|
|
/* Otherwise, compare the md5 hashes... */
|
|
else
|
|
{
|
|
/* Get the contents. If you can't get the contents, it may be that the file permissions are set wrong. Try to fix. */
|
|
$fileContents = @file_get_contents( \IPS\ROOT_PATH . $file );
|
|
if ( !$fileContents )
|
|
{
|
|
@chmod( \IPS\ROOT_PATH . $file, \IPS\FILE_PERMISSION_NO_WRITE );
|
|
}
|
|
|
|
/* If we got the file contents... */
|
|
if ( $fileContents )
|
|
{
|
|
/* Strip whitespace since FTP in ASCII mode will change the whitespace characters */
|
|
$fileContents = preg_replace( '#\s#', '', utf8_decode( file_get_contents( \IPS\ROOT_PATH . $file ) ) );
|
|
|
|
/* Compare */
|
|
if ( md5( $fileContents ) != $md5Hash )
|
|
{
|
|
$return[] = \IPS\ROOT_PATH . $file;
|
|
}
|
|
}
|
|
|
|
/* Otherwise, flag it */
|
|
else
|
|
{
|
|
$return[] = \IPS\ROOT_PATH . $file;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* @brief [ActiveRecord] Database Table
|
|
*/
|
|
public static $databaseTable = 'core_applications';
|
|
|
|
/**
|
|
* @brief [ActiveRecord] Database Prefix
|
|
*/
|
|
public static $databasePrefix = 'app_';
|
|
|
|
/**
|
|
* @brief [ActiveRecord] ID Database Column
|
|
*/
|
|
public static $databaseColumnId = 'directory';
|
|
|
|
/**
|
|
* @brief [ActiveRecord] Database ID Fields
|
|
*/
|
|
protected static $databaseIdFields = array( 'app_id', 'app_marketplace_id' );
|
|
|
|
/**
|
|
* @brief [ActiveRecord] Multiton Map
|
|
*/
|
|
protected static $multitonMap = array();
|
|
|
|
/**
|
|
* @brief [Node] Subnode class
|
|
*/
|
|
public static $subnodeClass = 'IPS\Application\Module';
|
|
|
|
/**
|
|
* @brief [Node] Node Title
|
|
*/
|
|
public static $nodeTitle = 'applications_and_modules';
|
|
|
|
/**
|
|
* @brief [Node] Order Database Column
|
|
*/
|
|
public static $databaseColumnOrder = 'position';
|
|
|
|
/**
|
|
* @brief [Node] ACP Restrictions
|
|
*/
|
|
protected static $restrictions = array( 'app' => 'core', 'module' => 'applications', 'prefix' => 'app_' );
|
|
|
|
/**
|
|
* Construct ActiveRecord from database row
|
|
*
|
|
* @param array $data Row from database table
|
|
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
|
|
* @return static
|
|
*/
|
|
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
|
|
{
|
|
/* Load class */
|
|
if( !file_exists( static::getRootPath( $data['app_directory'] ) . '/applications/' . $data['app_directory'] . '/Application.php' ) )
|
|
{
|
|
/* If you are upgrading and you have an application "123flashchat" this causes a PHP error, so just die out now */
|
|
if( !\in_array( mb_strtolower( mb_substr( $data['app_directory'], 0, 1 ) ), range( 'a', 'z' ) ) )
|
|
{
|
|
throw new \UnexpectedValueException( "Missing: " . '/applications/' . $data['app_directory'] . '/Application.php' );
|
|
}
|
|
|
|
if( !\IPS\Dispatcher::hasInstance() OR \IPS\Dispatcher::i()->controllerLocation !== 'setup' )
|
|
{
|
|
throw new \UnexpectedValueException( "Missing: " . '/applications/' . $data['app_directory'] . '/Application.php' );
|
|
}
|
|
else
|
|
{
|
|
$className = "\\IPS\\{$data['app_directory']}\\Application";
|
|
|
|
if( !class_exists( $className ) )
|
|
{
|
|
$code = <<<EOF
|
|
namespace IPS\\{$data['app_directory']};
|
|
class Application extends \\IPS\\Application{}
|
|
EOF;
|
|
eval( $code );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
require_once static::getRootPath( $data['app_directory'] ) . '/applications/' . $data['app_directory'] . '/Application.php';
|
|
}
|
|
|
|
/* Initiate an object */
|
|
$classname = 'IPS\\' . $data['app_directory'] . '\\Application';
|
|
$obj = new $classname;
|
|
$obj->_new = FALSE;
|
|
|
|
/* Import data */
|
|
if ( static::$databasePrefix )
|
|
{
|
|
$databasePrefixLength = \strlen( static::$databasePrefix );
|
|
}
|
|
foreach ( $data as $k => $v )
|
|
{
|
|
if( static::$databasePrefix )
|
|
{
|
|
$k = \substr( $k, $databasePrefixLength );
|
|
}
|
|
|
|
$obj->_data[ $k ] = $v;
|
|
}
|
|
$obj->changed = array();
|
|
|
|
/* Return */
|
|
return $obj;
|
|
}
|
|
|
|
/**
|
|
* @brief Modules Store
|
|
*/
|
|
protected $modules = NULL;
|
|
|
|
/**
|
|
* Get Modules
|
|
*
|
|
* @see static::$modules
|
|
* @param string $location Location (e.g. "admin" or "front")
|
|
* @return array
|
|
*/
|
|
public function modules( $location=NULL )
|
|
{
|
|
/* Don't have an instance? */
|
|
if( $this->modules === NULL )
|
|
{
|
|
$modules = \IPS\Application\Module::modules();
|
|
$this->modules = array_key_exists( $this->directory, $modules ) ? $modules[ $this->directory ] : array();
|
|
}
|
|
|
|
/* Return */
|
|
return isset( $this->modules[ $location ] ) ? $this->modules[ $location ] : array();
|
|
}
|
|
|
|
/**
|
|
* Returns the ACP Menu JSON for this application.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function acpMenu()
|
|
{
|
|
return json_decode( file_get_contents( $this->getApplicationPath() . "/data/acpmenu.json" ), TRUE );
|
|
}
|
|
|
|
/**
|
|
* ACP Menu Numbers
|
|
*
|
|
* @param array $queryString Query String
|
|
* @return int
|
|
*/
|
|
public function acpMenuNumber( $queryString )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* 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|\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 )
|
|
{
|
|
$app = ( \is_string( $app ) ? $app : $app->directory );
|
|
|
|
$classes = array();
|
|
$jsonFile = $this->getApplicationPath() . "/data/extensions.json";
|
|
$directory = $this->getApplicationPath() . "/extensions/{$app}/{$extension}";
|
|
|
|
/* New extensions.json based approach */
|
|
if ( file_exists( $jsonFile ) and $json = @json_decode( \file_get_contents( $jsonFile ), TRUE ) )
|
|
{
|
|
if ( isset( $json[ $app ] ) and isset( $json[ $app ][ $extension ] ) )
|
|
{
|
|
foreach ( $json[ $app ][ $extension ] as $name => $classname )
|
|
{
|
|
if ( method_exists( $classname, 'generate' ) )
|
|
{
|
|
$classes = array_merge( $classes, $classname::generate() );
|
|
}
|
|
elseif ( !$construct )
|
|
{
|
|
$classes[ $name ] = $classname;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
$classes[ $name ] = new $classname( $checkAccess === TRUE ? \IPS\Member::loggedIn() : ( $checkAccess === FALSE ? NULL : $checkAccess ) );
|
|
}
|
|
catch( \RuntimeException $e ){}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Legacy DirectoryIterator approach */
|
|
elseif ( is_dir( $directory ) )
|
|
{
|
|
$dir = new \DirectoryIterator( $directory );
|
|
|
|
foreach ( $dir as $file )
|
|
{
|
|
/* Macs create copies of files with "._" prefix which breaks when we just load up all files in a dir, ignore those */
|
|
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;
|
|
}
|
|
|
|
if ( method_exists( $classname, 'deprecated' ) )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
catch( \ErrorException $e )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( method_exists( $classname, 'generate' ) )
|
|
{
|
|
$classes = array_merge( $classes, $classname::generate() );
|
|
}
|
|
elseif ( !$construct )
|
|
{
|
|
$classes[ mb_substr( $file, 0, -4 ) ] = $classname;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
$classes[ mb_substr( $file, 0, -4 ) ] = new $classname( $checkAccess === TRUE ? \IPS\Member::loggedIn() : ( $checkAccess === FALSE ? NULL : $checkAccess ) );
|
|
}
|
|
catch( \RuntimeException $e ){}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $classes;
|
|
}
|
|
|
|
/**
|
|
* [Node] Get Node Title
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get__title()
|
|
{
|
|
$key = "__app_{$this->directory}";
|
|
return \IPS\Member::loggedIn()->language()->addToStack( $key );
|
|
}
|
|
|
|
/**
|
|
* [Node] Get Node Icon
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get__icon()
|
|
{
|
|
return 'cubes';
|
|
}
|
|
|
|
/**
|
|
* [Node] Does this node have children?
|
|
*
|
|
* @param string|NULL $permissionCheck The permission key to check for or NULl to not check permissions
|
|
* @param \IPS\Member|NULL $member The member to check permissions for or NULL for the currently logged in member
|
|
* @param bool $subnodes Include subnodes?
|
|
* @param array $_where Additional WHERE clause
|
|
* @return bool
|
|
*/
|
|
public function hasChildren( $permissionCheck='view', $member=NULL, $subnodes=TRUE, $_where=array() )
|
|
{
|
|
return $subnodes;
|
|
}
|
|
|
|
/**
|
|
* [Node] Does the currently logged in user have permission to delete this node?
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function canDelete()
|
|
{
|
|
if( \IPS\NO_WRITES or !static::restrictionCheck( 'delete' ) )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if( $this->_data['protected'] )
|
|
{
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Cached URL
|
|
*/
|
|
protected $_url = NULL;
|
|
|
|
/**
|
|
* Get URL
|
|
*
|
|
* @return \IPS\Http\Url
|
|
*/
|
|
public function url()
|
|
{
|
|
if( $this->_url === NULL )
|
|
{
|
|
$this->_url = \IPS\Http\Url::internal( "app={$this->directory}" );
|
|
}
|
|
|
|
return $this->_url;
|
|
}
|
|
|
|
/**
|
|
* [Node] Get buttons to display in tree
|
|
* Example code explains return value
|
|
*
|
|
* @code
|
|
array(
|
|
array(
|
|
'icon' => array(
|
|
'icon.png' // Path to icon
|
|
'core' // Application icon belongs to
|
|
),
|
|
'title' => 'foo', // Language key to use for button's title parameter
|
|
'link' => \IPS\Http\Url::internal( 'app=foo...' ) // URI to link to
|
|
'class' => 'modalLink' // CSS Class to use on link (Optional)
|
|
),
|
|
... // Additional buttons
|
|
);
|
|
* @endcode
|
|
* @param string $url Base URL
|
|
* @param bool $subnode Is this a subnode?
|
|
* @return array
|
|
*/
|
|
public function getButtons( $url, $subnode=FALSE )
|
|
{
|
|
/* Get normal buttons */
|
|
$buttons = parent::getButtons( $url );
|
|
$edit = NULL;
|
|
$uninstall = NULL;
|
|
if( \IPS\IN_DEV and isset( $buttons['edit'] ) )
|
|
{
|
|
$edit = $buttons['edit'];
|
|
}
|
|
unset( $buttons['edit'] );
|
|
unset( $buttons['copy'] );
|
|
if( isset( $buttons['delete'] ) )
|
|
{
|
|
$buttons['delete']['title'] = 'uninstall';
|
|
$buttons['delete']['data'] = array( 'delete' => '', 'delete-warning' => \IPS\Member::loggedIn()->language()->addToStack( \IPS\IN_DEV ? 'app_files_indev_uninstall' : 'app_files_delete_uninstall') );
|
|
|
|
$uninstall = $buttons['delete'];
|
|
unset( $buttons['delete'] );
|
|
}
|
|
|
|
/* Default */
|
|
if( $this->enabled AND \count( $this->modules( 'front' ) ) )
|
|
{
|
|
$buttons['default'] = array(
|
|
'icon' => $this->default ? 'star' : 'star-o',
|
|
'title' => 'make_default_app',
|
|
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=setAsDefault" )->csrf(),
|
|
);
|
|
}
|
|
|
|
/* Online/offline */
|
|
if( !$this->protected )
|
|
{
|
|
$buttons['offline'] = array(
|
|
'icon' => 'lock',
|
|
'title' => 'permissions',
|
|
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&id={$this->_id}&do=permissions" ),
|
|
'data' => array( 'ipsDialog' => '', 'ipsDialog-forceReload' => 'true', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('permissions') )
|
|
);
|
|
}
|
|
|
|
/* View Details */
|
|
$buttons['details'] = array(
|
|
'icon' => 'search',
|
|
'title' => 'app_view_details',
|
|
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&do=details&id={$this->_id}" ),
|
|
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('app_view_details') )
|
|
);
|
|
|
|
/* Upgrade */
|
|
if( !$this->protected AND !\IPS\DEMO_MODE AND $this->marketplace_id === NULL )
|
|
{
|
|
$buttons['upgrade'] = array(
|
|
'icon' => 'upload',
|
|
'title' => 'upload_new_version',
|
|
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=upload" ),
|
|
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('upload_new_version') )
|
|
);
|
|
}
|
|
|
|
/* Uninstall */
|
|
if ( $uninstall )
|
|
{
|
|
$buttons['delete'] = $uninstall;
|
|
$buttons['delete']['link'] = $buttons['delete']['link']->csrf();
|
|
|
|
if ( $this->default )
|
|
{
|
|
$buttons['delete']['data'] = array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('uninstall') );
|
|
}
|
|
|
|
if ( !isset( $buttons['delete']['data'] ) )
|
|
{
|
|
$buttons['delete']['data'] = array();
|
|
}
|
|
$buttons['delete']['data'] = $buttons['delete']['data'] + array( 'noajax' => '' );
|
|
}
|
|
|
|
/* Developer */
|
|
if( \IPS\IN_DEV )
|
|
{
|
|
if ( $edit )
|
|
{
|
|
$buttons['edit'] = $edit;
|
|
}
|
|
|
|
$buttons['compilejs'] = array(
|
|
'icon' => 'cog',
|
|
'title' => 'app_compile_js',
|
|
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=compilejs" )->csrf()
|
|
);
|
|
|
|
$buttons['build'] = array(
|
|
'icon' => 'cog',
|
|
'title' => 'app_build',
|
|
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=build" ),
|
|
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('app_build') )
|
|
);
|
|
|
|
$buttons['export'] = array(
|
|
'icon' => 'download',
|
|
'title' => 'download',
|
|
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=download" ),
|
|
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('download'), 'ipsDialog-remoteVerify' => 'false' )
|
|
);
|
|
|
|
$buttons['developer'] = array(
|
|
'icon' => 'cogs',
|
|
'title' => 'developer_mode',
|
|
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=developer&appKey={$this->_id}" ),
|
|
);
|
|
}
|
|
|
|
return $buttons;
|
|
}
|
|
|
|
/**
|
|
* [Node] Get whether or not this node is enabled
|
|
*
|
|
* @note Return value NULL indicates the node cannot be enabled/disabled
|
|
* @return bool|null
|
|
*/
|
|
protected function get__enabled()
|
|
{
|
|
if ( $this->directory == 'core' )
|
|
{
|
|
return TRUE;
|
|
}
|
|
return $this->enabled and ( !\in_array( $this->directory, \IPS\IPS::$ipsApps ) or $this->version == \IPS\Application::load('core')->version );
|
|
}
|
|
|
|
/**
|
|
* [Node] Set whether or not this node is enabled
|
|
*
|
|
* @param bool|int $enabled Whether to set it enabled or disabled
|
|
* @return void
|
|
*/
|
|
protected function set__enabled( $enabled )
|
|
{
|
|
if ( \IPS\NO_WRITES )
|
|
{
|
|
throw new \RuntimeException;
|
|
}
|
|
|
|
$this->enabled = $enabled;
|
|
$this->save();
|
|
\IPS\Plugin\Hook::writeDataFile();
|
|
|
|
/* Clear templates to rebuild automatically */
|
|
\IPS\Theme::deleteCompiledTemplate();
|
|
|
|
/* Invalidate disk templates */
|
|
\IPS\Theme::resetAllCacheKeys();
|
|
|
|
/* Enable queue task in case there are pending items */
|
|
if( $this->enabled )
|
|
{
|
|
$queueTask = \IPS\Task::load( 'queue', 'key' );
|
|
$queueTask->enabled = TRUE;
|
|
$queueTask->save();
|
|
}
|
|
|
|
/* Update other app specific task statuses */
|
|
\IPS\Db::i()->update( 'core_tasks', array( 'enabled' => (int) $this->enabled ), array( 'app=?', $this->directory ) );
|
|
}
|
|
|
|
/**
|
|
* [Node] Get whether or not this node is locked to current enabled/disabled status
|
|
*
|
|
* @note Return value NULL indicates the node cannot be enabled/disabled
|
|
* @return bool|null
|
|
*/
|
|
protected function get__locked()
|
|
{
|
|
if ( $this->directory == 'core' )
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
if ( !$this->_enabled and \in_array( $this->directory, \IPS\IPS::$ipsApps ) and $this->version != \IPS\Application::load('core')->version )
|
|
{
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* [Node] Get Node Description
|
|
*
|
|
* @return string|null
|
|
*/
|
|
protected function get__description()
|
|
{
|
|
return \IPS\Theme::i()->getTemplate( 'applications', 'core' )->appRowDescription( $this );
|
|
}
|
|
|
|
/**
|
|
* Get the Application State Description ( Offline , Offline for specific groups or all )
|
|
* @return mixed
|
|
*/
|
|
public function get__disabledMessage()
|
|
{
|
|
if ( $this->_locked and $this->directory != 'core' )
|
|
{
|
|
return \IPS\Member::loggedIn()->language()->addToStack('app_force_disabled');
|
|
}
|
|
elseif ( $this->disabled_groups )
|
|
{
|
|
$groups = array();
|
|
if ( $this->disabled_groups != '*' )
|
|
{
|
|
foreach ( explode( ',', $this->disabled_groups ) as $groupId )
|
|
{
|
|
try
|
|
{
|
|
$groups[] = \IPS\Member\Group::load( $groupId )->name;
|
|
}
|
|
catch ( \OutOfRangeException $e ) { }
|
|
}
|
|
}
|
|
|
|
if ( empty( $groups ) )
|
|
{
|
|
return \IPS\Member::loggedIn()->language()->addToStack('app_offline_to_all');
|
|
}
|
|
else
|
|
{
|
|
return \IPS\Member::loggedIn()->language()->addToStack( 'app_offline_to_groups', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->formatList( $groups ) ) ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return an array of specific links to add to the sitemap
|
|
*
|
|
* @return array
|
|
*/
|
|
public function sitemapLinks()
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Get the authors website
|
|
*
|
|
* @return \IPS\Http\Url|null
|
|
*/
|
|
public function website()
|
|
{
|
|
if ( $this->_data['website'] )
|
|
{
|
|
return \IPS\Http\Url::createFromString( $this->_data['website'] );
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Return the custom badge for each row
|
|
*
|
|
* @return NULL|array Null for no badge, or an array of badge data (0 => CSS class type, 1 => language string, 2 => optional raw HTML to show instead of language string)
|
|
*/
|
|
public function get__badge()
|
|
{
|
|
if ( $availableUpgrade = $this->availableUpgrade( TRUE ) )
|
|
{
|
|
return array(
|
|
0 => 'new',
|
|
1 => '',
|
|
2 => \IPS\Theme::i()->getTemplate( 'global', 'core' )->updatebadge( $availableUpgrade['version'], isset( $availableUpgrade['updateurl'] ) ? $availableUpgrade['updateurl'] : '', (string) \IPS\DateTime::ts( $availableUpgrade['released'] )->localeDate() )
|
|
);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* [Node] Does the currently logged in user have permission to add a child node?
|
|
*
|
|
* @return bool
|
|
* @note Modules are added via the developer center and should not be added by a regular admin via the standard node controller
|
|
*/
|
|
public function canAdd()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* [Node] Does the currently logged in user have permission to add aa root node?
|
|
*
|
|
* @return bool
|
|
* @note If IN_DEV is on, the admin can create a new application
|
|
*/
|
|
public static function canAddRoot()
|
|
{
|
|
return ( \IPS\IN_DEV ) ? true : false;
|
|
}
|
|
|
|
/**
|
|
* [Node] Does the currently logged in user have permission to edit permissions for this node?
|
|
*
|
|
* @return bool
|
|
* @note We don't allow permissions to be set for applications - they are handled by modules and by the enabled/disabled mode
|
|
*/
|
|
public function canManagePermissions()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add or edit an application
|
|
*
|
|
* @param \IPS\Helpers\Form $form Form object we can add our fields to
|
|
* @return void
|
|
*/
|
|
public function form( &$form )
|
|
{
|
|
if ( !$this->directory )
|
|
{
|
|
$form->add( new \IPS\Helpers\Form\Text( 'app_title', NULL, FALSE, array( 'app' => 'core', 'key' => ( !$this->directory ) ? NULL : "__app_{$this->directory}" ) ) );
|
|
}
|
|
|
|
$form->add( new \IPS\Helpers\Form\Text( 'app_directory', $this->directory, TRUE, array( 'disabled' => $this->id ? TRUE : FALSE, 'regex' => '/^[a-zA-Z][a-zA-Z0-9]+$/', 'maxLength' => 80 ) ) );
|
|
$form->add( new \IPS\Helpers\Form\Text( 'app_author', $this->author ) );
|
|
$form->add( new \IPS\Helpers\Form\Url( 'app_website', $this->website ) );
|
|
$form->add( new \IPS\Helpers\Form\Url( 'app_update_check', $this->update_check ) );
|
|
$form->add( new \IPS\Helpers\Form\YesNo( 'app_protected', $this->protected, FALSE ) );
|
|
}
|
|
|
|
/**
|
|
* [Node] Format form values from add/edit form for save
|
|
*
|
|
* @param array $values Values from the form
|
|
* @return array
|
|
*/
|
|
public function formatFormValues( $values )
|
|
{
|
|
/* New application stuff */
|
|
if ( !$this->id )
|
|
{
|
|
/* Check dir is writable */
|
|
if( !is_writable( \IPS\ROOT_PATH . '/applications/' ) )
|
|
{
|
|
\IPS\Output::i()->error( 'app_dir_not_write', '4S134/2', 403, '' );
|
|
}
|
|
|
|
/* Check key isn't in use */
|
|
$values['app_directory'] = mb_strtolower( $values['app_directory'] );
|
|
try
|
|
{
|
|
$test = \IPS\Application::load( $values['app_directory'] );
|
|
\IPS\Output::i()->error( 'app_error_key_used', '1S134/1', 403, '' );
|
|
}
|
|
catch ( \OutOfRangeException $e ) { }
|
|
|
|
/* Attempt to create the basic directory structure for the developer */
|
|
if( is_writable( \IPS\ROOT_PATH . '/applications/' ) )
|
|
{
|
|
/* If we can make the root dir, we can create the subfolders */
|
|
if( @mkdir( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] ) )
|
|
{
|
|
@chmod( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'], \IPS\FOLDER_PERMISSION_NO_WRITE );
|
|
|
|
/* Create directories */
|
|
foreach ( array( 'data', 'dev', 'dev/css', 'dev/email', 'dev/html', 'dev/resources', 'dev/js', 'extensions', 'extensions/core', 'hooks', 'interface', 'modules', 'modules/admin', 'modules/front', 'setup', '/setup/upg_working', 'sources', 'tasks' ) as $f )
|
|
{
|
|
@mkdir( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/' . $f );
|
|
@chmod( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/' . $f, \IPS\FOLDER_PERMISSION_NO_WRITE );
|
|
\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/' . $f . '/index.html', '' );
|
|
}
|
|
|
|
/* Create files */
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/schema.json', '[]' );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/settings.json', '[]' );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/tasks.json', '[]' );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/themesettings.json', '[]' );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/acpmenu.json', '[]' );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/modules.json', '[]' );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/widgets.json', '[]' );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/acpsearch.json', '{}' );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/hooks.json', '[]' );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/versions.json', json_encode( array() ) );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/dev/lang.php', '<?' . "php\n\n\$lang = array(\n\t'__app_{$values['app_directory']}'\t=> \"{$values['app_title']}\"\n);\n" );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/dev/jslang.php', '<?' . "php\n\n\$lang = array(\n\n);\n" );
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/Application.php', str_replace(
|
|
array(
|
|
'{app}',
|
|
'{website}',
|
|
'{author}',
|
|
'{year}',
|
|
'{subpackage}',
|
|
'{date}'
|
|
),
|
|
array(
|
|
$values['app_directory'],
|
|
$values['app_website'],
|
|
$values['app_author'],
|
|
date('Y'),
|
|
$values['app_title'],
|
|
date( 'd M Y' ),
|
|
),
|
|
file_get_contents( \IPS\ROOT_PATH . "/applications/core/data/defaults/Application.txt" )
|
|
) );
|
|
|
|
@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/application.json', json_encode( array(
|
|
'application_title' => $values['app_title'],
|
|
'app_author' => $values['app_author'],
|
|
'app_directory' => $values['app_directory'],
|
|
'app_protected' => $values['app_protected'],
|
|
'app_website' => $values['app_website'],
|
|
'app_update_check' => $values['app_update_check'],
|
|
) ) );
|
|
}
|
|
}
|
|
|
|
/* Enable it */
|
|
$values['enabled'] = TRUE;
|
|
$values['app_added'] = time();
|
|
}
|
|
|
|
if( isset( $values['app_title'] ) )
|
|
{
|
|
unset( $values['app_title'] );
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* [Node] Perform actions after saving the form
|
|
*
|
|
* @param array $values Values from the form
|
|
* @return void
|
|
*/
|
|
public function postSaveForm( $values )
|
|
{
|
|
/* Clear out member's cached "Create Menu" contents */
|
|
\IPS\Member::clearCreateMenu();
|
|
unset( \IPS\Data\Store::i()->applications );
|
|
\IPS\Settings::i()->clearCache();
|
|
}
|
|
|
|
/**
|
|
* Install database changes from the schema.json file
|
|
*
|
|
* @param bool $skipInserts Skip inserts
|
|
* @throws \Exception
|
|
*/
|
|
public function installDatabaseSchema( $skipInserts=FALSE )
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/schema.json" ) )
|
|
{
|
|
$schema = json_decode( file_get_contents( $this->getApplicationPath() . "/data/schema.json" ), TRUE );
|
|
|
|
foreach( $schema as $table => $definition )
|
|
{
|
|
/* Look for missing tables first */
|
|
if( !\IPS\Db::i()->checkForTable( $table ) )
|
|
{
|
|
\IPS\Db::i()->createTable( $definition );
|
|
}
|
|
else
|
|
{
|
|
/* If the table exists, look for missing columns */
|
|
if( \is_array( $definition['columns'] ) AND \count( $definition['columns'] ) )
|
|
{
|
|
/* Get the table definition first */
|
|
$tableDefinition = \IPS\Db::i()->getTableDefinition( $table );
|
|
|
|
foreach( $definition['columns'] as $column )
|
|
{
|
|
/* Column does not exist in the table definition? Add it then. */
|
|
if( empty($tableDefinition['columns'][ $column['name'] ]) )
|
|
{
|
|
\IPS\Db::i()->addColumn( $table, $column );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( isset( $definition['inserts'] ) AND !$skipInserts )
|
|
{
|
|
foreach ( $definition['inserts'] as $insertData )
|
|
{
|
|
$adminName = \IPS\Member::loggedIn()->name;
|
|
try
|
|
{
|
|
\IPS\Db::i()->insert( $definition['name'], array_map( function( $column ) use( $adminName ) {
|
|
if( !\is_string( $column ) )
|
|
{
|
|
return $column;
|
|
}
|
|
|
|
$column = str_replace( '<%TIME%>', time(), $column );
|
|
$column = str_replace( '<%ADMIN_NAME%>', $adminName, $column );
|
|
$column = str_replace( '<%IP_ADDRESS%>', $_SERVER['REMOTE_ADDR'], $column );
|
|
return $column;
|
|
}, $insertData ) );
|
|
}
|
|
catch( \IPS\Db\Exception $e )
|
|
{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if( file_exists( $this->getApplicationPath() . "/setup/install/queries.json" ) )
|
|
{
|
|
$schema = json_decode( file_get_contents( $this->getApplicationPath() . "/setup/install/queries.json" ), TRUE );
|
|
|
|
ksort($schema);
|
|
|
|
foreach( $schema as $instruction )
|
|
{
|
|
if ( $instruction['method'] === 'addColumn' )
|
|
{
|
|
/* Check to see if it exists first */
|
|
$tableDefinition = \IPS\Db::i()->getTableDefinition( $instruction['params'][0] );
|
|
|
|
if ( ! empty( $tableDefinition['columns'][ $instruction['params'][1]['name'] ] ) )
|
|
{
|
|
/* Run an alter instead */
|
|
\IPS\Db::i()->changeColumn( $instruction['params'][0], $instruction['params'][1]['name'], $instruction['params'][1] );
|
|
continue;
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
if( isset( $instruction['params'][1] ) and \is_array( $instruction['params'][1] ) )
|
|
{
|
|
$groups = array_filter( iterator_to_array( \IPS\Db::i()->select( 'g_id', 'core_groups' ) ), function( $groupId ) {
|
|
if( $groupId == 2 )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
});
|
|
|
|
foreach( $instruction['params'][1] as $column => $value )
|
|
{
|
|
if( $value === "<%NO_GUESTS%>" )
|
|
{
|
|
$instruction['params'][1][ $column ] = implode( ",", $groups );
|
|
}
|
|
}
|
|
}
|
|
|
|
$method = $instruction['method'];
|
|
$params = $instruction['params'];
|
|
\IPS\Db::i()->$method( ...$params );
|
|
}
|
|
catch( \Exception $e )
|
|
{
|
|
if( $instruction['method'] == 'insert' )
|
|
{
|
|
return;
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install database changes from an upgrade schema file
|
|
*
|
|
* @param int $version Version to execute database updates from
|
|
* @param int $lastJsonIndex JSON index to begin from
|
|
* @param int $limit Limit updates
|
|
* @param bool $return Check table size first and return queries for larger tables instead of running automatically
|
|
* @return array Returns an array: ( count: count of queries run, queriesToRun: array of queries to run)
|
|
* @note We ignore some database errors that shouldn't prevent us from continuing.
|
|
* @li 1007: Can't create database because it already exists
|
|
* @li 1008: Can't drop database because it does not exist
|
|
* @li 1050: Can't rename a table as it already exists
|
|
* @li 1051: Can't drop a table because it doesn't exist
|
|
* @li 1060: Can't add a column as it already exists
|
|
* @li 1062: Can't add an index as index already exists
|
|
* @li 1062: Can't add a row as PKEY already exists
|
|
* @li 1091: Can't drop key or column because it does not exist
|
|
*/
|
|
public function installDatabaseUpdates( $version=0, $lastJsonIndex=0, $limit=50, $return=FALSE )
|
|
{
|
|
$toReturn = array();
|
|
$count = 0;
|
|
|
|
/* Try to prevent timeouts to the extent possible */
|
|
$cutOff = null;
|
|
|
|
if( $maxExecution = @ini_get( 'max_execution_time' ) )
|
|
{
|
|
/* If max_execution_time is set to "no limit" we should add a hard limit to prevent browser timeouts */
|
|
if ( $maxExecution == -1 )
|
|
{
|
|
$maxExecution = 30;
|
|
}
|
|
$cutOff = time() + ( $maxExecution * .5 );
|
|
}
|
|
|
|
if( file_exists( $this->getApplicationPath() . "/setup/upg_{$version}/queries.json" ) )
|
|
{
|
|
$schema = json_decode( file_get_contents( $this->getApplicationPath() . "/setup/upg_{$version}/queries.json" ), TRUE );
|
|
|
|
ksort($schema, SORT_NUMERIC);
|
|
|
|
foreach( $schema as $jsonIndex => $instruction['params'] )
|
|
{
|
|
if ( $lastJsonIndex AND ( $jsonIndex <= $lastJsonIndex ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( $count >= $limit )
|
|
{
|
|
return array( 'count' => $count, 'queriesToRun' => $toReturn );
|
|
}
|
|
else if( $cutOff !== null AND time() >= $cutOff )
|
|
{
|
|
return array( 'count' => $count, 'queriesToRun' => $toReturn );
|
|
}
|
|
|
|
$_SESSION['lastJsonIndex'] = $jsonIndex;
|
|
|
|
$count++;
|
|
|
|
/* Get the table name, we need it */
|
|
$_table = $instruction['params']['params'][0];
|
|
|
|
if ( !\is_string( $_table ) )
|
|
{
|
|
$_table = $instruction['params']['params'][0]['name'];
|
|
}
|
|
|
|
/* Check table size first and store query if requested */
|
|
if( $return === TRUE )
|
|
{
|
|
if(
|
|
/* Only run manually if we need to */
|
|
\IPS\Db::i()->recommendManualQuery( $_table ) AND
|
|
/* And if it's not a drop table, insert or rename table query */
|
|
!\in_array( $instruction['params']['method'], array( 'dropTable', 'insert', 'renameTable' ) ) AND
|
|
/* ANNNNNDDD only if the method is not delete or there's a where clause, i.e. a truncate table statement does not run manually */
|
|
( $instruction['params']['method'] != 'delete' OR isset( $instructions['params']['params'][1] ) )
|
|
)
|
|
{
|
|
\IPS\Log::debug( "Big table " . $_table . ", storing query to run manually", 'upgrade' );
|
|
|
|
\IPS\Db::i()->returnQuery = TRUE;
|
|
|
|
$method = $instruction['params']['method'];
|
|
$params = $instruction['params']['params'];
|
|
$query = \IPS\Db::i()->$method( ...$params );
|
|
|
|
if( $query )
|
|
{
|
|
$toReturn[] = $query;
|
|
|
|
if ( $instruction['params']['method'] == 'renameTable' )
|
|
{
|
|
\IPS\Db::i()->cachedTableData[ $instruction['params']['params'][1] ] = \IPS\Db::i()->cachedTableData[ $_table ];
|
|
|
|
foreach( $toReturn as $k => $v )
|
|
{
|
|
$toReturn[ $k ] = preg_replace( "/\`" . \IPS\Db::i()->prefix . $_table . "\`/", "`" . \IPS\Db::i()->prefix . $instruction['params']['params'][1] . "`", $v );
|
|
}
|
|
}
|
|
|
|
return array( 'count' => $count, 'queriesToRun' => $toReturn );
|
|
}
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
$method = $instruction['params']['method'];
|
|
$params = $instruction['params']['params'];
|
|
\IPS\Db::i()->$method( ...$params );
|
|
}
|
|
catch( \IPS\Db\Exception $e )
|
|
{
|
|
\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 ) ) )
|
|
{
|
|
throw $e;
|
|
}
|
|
|
|
/* Can't change a column as it doesn't exist */
|
|
if ( $e->getCode() == 1054 )
|
|
{
|
|
if ( $instruction['params']['method'] == 'changeColumn' )
|
|
{
|
|
if ( \IPS\Db::i()->checkForTable( $instruction['params']['params'][0] ) )
|
|
{
|
|
/* Does the column exist already? */
|
|
if ( \IPS\Db::i()->checkForColumn( $instruction['params']['params'][0], $instruction['params']['params'][2]['name'] ) )
|
|
{
|
|
/* Just make sure it's up to date */
|
|
\IPS\Db::i()->changeColumn( $instruction['params']['params'][0], $instruction['params']['params'][2]['name'], $instruction['params']['params'][2] );
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
/* The table exists, so lets just add the column */
|
|
\IPS\Db::i()->addColumn( $instruction['params']['params'][0], $instruction['params']['params'][2] );
|
|
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
/* Can't rename a table as it doesn't exist */
|
|
else if ( $e->getCode() == 1017 )
|
|
{
|
|
if ( $instruction['params']['method'] == 'renameTable' )
|
|
{
|
|
if ( \IPS\Db::i()->checkForTable( $instruction['params']['params'][1] ) )
|
|
{
|
|
/* The table we are renaming to *does* exist */
|
|
continue;
|
|
}
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
/* Possibly trying to change a column to not null that has NULL values */
|
|
else if ( $e->getCode() == 1138 )
|
|
{
|
|
if ( $instruction['params']['method'] == 'changeColumn' and ! $instruction['params']['params'][2]['allow_null'] )
|
|
{
|
|
$currentDefintion = \IPS\Db::i()->getTableDefinition( $instruction['params']['params'][0] );
|
|
$column = $instruction['params']['params'][2]['name'];
|
|
|
|
if ( isset( $currentDefintion['columns'][ $column ] ) AND $currentDefintion['columns'][ $column ]['allow_null'] )
|
|
{
|
|
\IPS\Db::i()->update( $instruction['params']['params'][0], array( $column => '' ), array( $column . ' IS NULL' ) );
|
|
|
|
/* Just make sure it's up to date */
|
|
\IPS\Db::i()->changeColumn( $instruction['params']['params'][0], $instruction['params']['params'][1], $instruction['params']['params'][2] );
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
/* If the error isn't important we should ignore it */
|
|
else if( !\in_array( $e->getCode(), array( 1007, 1008, 1050, 1060, 1061, 1062, 1091, 1051 ) ) )
|
|
{
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return array( 'count' => $count, 'queriesToRun' => $toReturn );
|
|
}
|
|
|
|
/**
|
|
* Rebuild common data during an install or upgrade. This is a shortcut method which
|
|
* * Installs module data from JSON file
|
|
* * Installs task data from JSON file
|
|
* * Installs setting data from JSON file
|
|
* * Installs ACP live search keywords from JSON file
|
|
* * Installs hooks from JSON file
|
|
* * Updates latest version in the database
|
|
*
|
|
* @param bool $skipMember Skip clearing member cache clearing
|
|
* @return void
|
|
*/
|
|
public function installJsonData( $skipMember=FALSE )
|
|
{
|
|
/* Rebuild modules */
|
|
$this->installModules();
|
|
|
|
/* Rebuild tasks */
|
|
$this->installTasks();
|
|
|
|
/* Rebuild settings */
|
|
$this->installSettings();
|
|
|
|
/* Rebuild sidebar widgets */
|
|
$this->installWidgets();
|
|
|
|
/* Rebuild search keywords */
|
|
$this->installSearchKeywords();
|
|
|
|
/* Rebuild hooks */
|
|
$this->installHooks();
|
|
|
|
/* Update app version data */
|
|
$versions = $this->getAllVersions();
|
|
$longVersions = array_keys( $versions );
|
|
$humanVersions = array_values( $versions );
|
|
|
|
if( \count($versions) )
|
|
{
|
|
$latestLVersion = array_pop( $longVersions );
|
|
$latestHVersion = array_pop( $humanVersions );
|
|
|
|
\IPS\Db::i()->update( 'core_applications', array( 'app_version' => $latestHVersion, 'app_long_version' => $latestLVersion ), array( 'app_directory=?', $this->directory ) );
|
|
}
|
|
|
|
unset( \IPS\Data\Store::i()->applications );
|
|
|
|
if( !$skipMember )
|
|
{
|
|
\IPS\Member::clearCreateMenu();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the application's modules
|
|
*
|
|
* @note A module's "default" status will not be adjusted during upgrades - if there is already a module flagged as default, it will remain the default.
|
|
* @return void
|
|
*/
|
|
public function installModules()
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/modules.json" ) )
|
|
{
|
|
$currentModules = array();
|
|
$moduleStore = array();
|
|
$hasDefault = FALSE;
|
|
|
|
foreach ( \IPS\Db::i()->select( '*', 'core_modules', array( 'sys_module_application=?', $this->directory ) ) as $row )
|
|
{
|
|
if( $row['sys_module_default'] )
|
|
{
|
|
$hasDefault = TRUE;
|
|
}
|
|
|
|
$currentModules[ $row['sys_module_area'] ][ $row['sys_module_key'] ] = array(
|
|
'default_controller' => $row['sys_module_default_controller'],
|
|
'protected' => $row['sys_module_protected'],
|
|
'default' => $row['sys_module_default']
|
|
);
|
|
$moduleStore[ $row['sys_module_area'] ][ $row['sys_module_key'] ] = $row;
|
|
}
|
|
|
|
$insert = array();
|
|
$update = array();
|
|
|
|
$position = 0;
|
|
foreach( json_decode( file_get_contents( $this->getApplicationPath() . "/data/modules.json" ), TRUE ) as $area => $modules )
|
|
{
|
|
foreach ( $modules as $key => $data )
|
|
{
|
|
$position++;
|
|
|
|
if ( !isset( $currentModules[ $area ][ $key ] ) )
|
|
{
|
|
$module = new \IPS\Application\Module;
|
|
}
|
|
elseif ( $currentModules[ $area ][ $key ] != $data )
|
|
{
|
|
$module = \IPS\Application\Module::constructFromData( $moduleStore[ $area ][ $key ] );
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$module->application = $this->directory;
|
|
$module->key = $key;
|
|
$module->protected = \intval( $data['protected'] );
|
|
$module->visible = TRUE;
|
|
$module->position = $position;
|
|
$module->area = $area;
|
|
$module->default_controller = $data['default_controller'];
|
|
|
|
/* We don't set/change default status if a module is already flagged as the default. An administrator may legitimately wish to change which module is the default, and we wouldn't want to reset that. */
|
|
if( !$hasDefault )
|
|
{
|
|
$module->default = ( isset( $data['default'] ) and $data['default'] );
|
|
}
|
|
|
|
$module->save( TRUE );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the application's tasks
|
|
*
|
|
* @return void
|
|
*/
|
|
public function installTasks()
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/tasks.json" ) )
|
|
{
|
|
foreach ( json_decode( file_get_contents( $this->getApplicationPath() . "/data/tasks.json" ), TRUE ) as $key => $frequency )
|
|
{
|
|
\IPS\Db::i()->replace( 'core_tasks', array(
|
|
'app' => $this->directory,
|
|
'key' => $key,
|
|
'frequency' => $frequency,
|
|
'next_run' => \IPS\DateTime::create()->add( new \DateInterval( $frequency ) )->getTimestamp()
|
|
) );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the application's extension data where required
|
|
*
|
|
* @param bool $newInstall TRUE if the community is being installed for the first time (opposed to an app being added)
|
|
* @return void
|
|
*/
|
|
public function installExtensions( $newInstall=FALSE )
|
|
{
|
|
/* File storage */
|
|
$settings = json_decode( \IPS\Settings::i()->upload_settings, TRUE );
|
|
|
|
try
|
|
{
|
|
/* Only check for Amazon when installing an app via the Admin CP on Community in the Cloud. The CiC Installer will handle brand new installs. */
|
|
if ( \IPS\CIC AND !$newInstall )
|
|
{
|
|
$fileSystem = \IPS\Db::i()->select( '*', 'core_file_storage', array( 'method=?', 'Amazon' ), 'id ASC' )->first();
|
|
}
|
|
else
|
|
{
|
|
$fileSystem = \IPS\Db::i()->select( '*', 'core_file_storage', array( 'method=?', 'FileSystem' ), 'id ASC' )->first();
|
|
}
|
|
}
|
|
catch( \UnderflowException $ex )
|
|
{
|
|
$fileSystem = \IPS\Db::i()->select( '*', 'core_file_storage', NULL, 'id ASC' )->first();
|
|
}
|
|
|
|
foreach( $this->extensions( 'core', 'FileStorage' ) as $key => $path )
|
|
{
|
|
$settings[ 'filestorage__' . $this->directory . '_' . $key ] = $fileSystem['id'];
|
|
}
|
|
|
|
\IPS\Settings::i()->changeValues( array( 'upload_settings' => json_encode( $settings ) ) );
|
|
|
|
$inserts = array();
|
|
foreach( $this->extensions( 'core', 'Notifications' ) as $key => $class )
|
|
{
|
|
if ( method_exists( $class, 'getConfiguration' ) )
|
|
{
|
|
$defaults = $class->getConfiguration( NULL );
|
|
|
|
foreach( $defaults AS $k => $config )
|
|
{
|
|
$inserts[] = array(
|
|
'notification_key' => $k,
|
|
'default' => implode( ',', $config['default'] ),
|
|
'disabled' => implode( ',', $config['disabled'] ),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if( \count( $inserts ) )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_notification_defaults', $inserts );
|
|
}
|
|
|
|
/* Install Menu items */
|
|
if ( !$newInstall )
|
|
{
|
|
$defaultNavigation = $this->defaultFrontNavigation();
|
|
foreach ( $defaultNavigation as $type => $tabs )
|
|
{
|
|
foreach ( $tabs as $config )
|
|
{
|
|
$config['real_app'] = $this->directory;
|
|
if ( !isset( $config['app'] ) )
|
|
{
|
|
$config['app'] = $this->directory;
|
|
}
|
|
|
|
\IPS\core\FrontNavigation::insertMenuItem( NULL, $config, \IPS\Db::i()->select( 'MAX(position)', 'core_menu' )->first() );
|
|
}
|
|
}
|
|
unset( \IPS\Data\Store::i()->frontNavigation );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the application's settings
|
|
*
|
|
* @return void
|
|
*/
|
|
public function installSettings()
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/settings.json" ) )
|
|
{
|
|
$currentData = iterator_to_array( \IPS\Db::i()->select( array( 'conf_key', 'conf_default', 'conf_report' ), 'core_sys_conf_settings' )->setKeyField('conf_key') );
|
|
|
|
$insert = array();
|
|
$update = array();
|
|
|
|
foreach ( json_decode( file_get_contents( $this->getApplicationPath() . "/data/settings.json" ), TRUE ) as $setting )
|
|
{
|
|
$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 );
|
|
}
|
|
elseif ( $currentData[ $setting['key'] ]['conf_default'] != $setting['default'] or $currentData[ $setting['key'] ]['conf_report'] != $report )
|
|
{
|
|
$update[] = array( array( 'conf_default' => $setting['default'], 'conf_report' => $report ), array( 'conf_key=?', $setting['key'] ) );
|
|
}
|
|
}
|
|
|
|
if ( !empty( $insert ) )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_sys_conf_settings', $insert, TRUE );
|
|
}
|
|
|
|
foreach ( $update as $data )
|
|
{
|
|
\IPS\Db::i()->update( 'core_sys_conf_settings', $data[0], $data[1] );
|
|
}
|
|
|
|
\IPS\Settings::i()->clearCache();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the application's language strings
|
|
*
|
|
* @param int|null $offset Offset to begin import from
|
|
* @param int|null $limit Number of rows to import
|
|
* @return int Rows inserted
|
|
*/
|
|
public function installLanguages( $offset=null, $limit=null )
|
|
{
|
|
$languages = array_keys( \IPS\Lang::languages() );
|
|
$inserted = 0;
|
|
|
|
$current = array();
|
|
foreach( $languages as $languageId )
|
|
{
|
|
foreach( iterator_to_array( \IPS\Db::i()->select( 'word_key, word_default, word_js', 'core_sys_lang_words', array( 'word_app=? AND lang_id=?', $this->directory, $languageId ) ) ) as $word )
|
|
{
|
|
$current[ $languageId ][ $word['word_key'] . '-.-' . $word['word_js'] ] = $word['word_default'];
|
|
}
|
|
}
|
|
|
|
if ( !$offset and file_exists( $this->getApplicationPath() . "/data/installLang.json" ) )
|
|
{
|
|
$inserts = array();
|
|
foreach ( json_decode( file_get_contents( $this->getApplicationPath() . "/data/installLang.json" ), TRUE ) as $key => $default )
|
|
{
|
|
foreach( $languages as $languageId )
|
|
{
|
|
if ( !isset( $current[ $languageId ][ $key . '-.-0' ] ) )
|
|
{
|
|
$inserts[] = array(
|
|
'word_app' => $this->directory,
|
|
'word_key' => $key,
|
|
'lang_id' => $languageId,
|
|
'word_default' => $default,
|
|
'word_custom' => $default,
|
|
'word_default_version' => $this->long_version,
|
|
'word_custom_version' => $this->long_version,
|
|
'word_js' => 0,
|
|
'word_export' => 0,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( \count( $inserts ) )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_sys_lang_words', $inserts, TRUE );
|
|
}
|
|
}
|
|
|
|
if( file_exists( $this->getApplicationPath() . "/data/lang.xml" ) )
|
|
{
|
|
/* Open XML file */
|
|
$xml = new \IPS\Xml\XMLReader;
|
|
$xml->open( $this->getApplicationPath() . "/data/lang.xml" );
|
|
$xml->read();
|
|
|
|
/* Get the version */
|
|
$xml->read();
|
|
$xml->read();
|
|
$version = $xml->getAttribute('version');
|
|
|
|
/* Get all installed languages */
|
|
$inserts = array();
|
|
$batchSize = 25;
|
|
$batchesDone = 0;
|
|
$i = 0;
|
|
|
|
/* Try to prevent timeouts to the extent possible */
|
|
$cutOff = null;
|
|
|
|
if( $maxExecution = @ini_get( 'max_execution_time' ) )
|
|
{
|
|
/* If max_execution_time is set to "no limit" we should add a hard limit to prevent browser timeouts */
|
|
if ( $maxExecution == -1 )
|
|
{
|
|
$maxExecution = 30;
|
|
}
|
|
|
|
$cutOff = time() + ( $maxExecution * .5 );
|
|
}
|
|
|
|
/* Start looping through each word */
|
|
while ( $xml->read() )
|
|
{
|
|
if( $xml->name != 'word' OR $xml->nodeType != \XMLReader::ELEMENT )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if( $cutOff !== null AND time() >= $cutOff )
|
|
{
|
|
return $inserted;
|
|
}
|
|
|
|
$i++;
|
|
|
|
if ( $offset !== null )
|
|
{
|
|
if ( $i - 1 < $offset )
|
|
{
|
|
$xml->next();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$inserted++;
|
|
|
|
$key = $xml->getAttribute('key');
|
|
$value = $xml->readString();
|
|
foreach( $languages as $languageId )
|
|
{
|
|
if ( !isset( $current[ $languageId ][ $key . '-.-' . (int) $xml->getAttribute('js') ] ) or $current[ $languageId ][ $key . '-.-' . (int) $xml->getAttribute('js') ] != $value )
|
|
{
|
|
$inserts[] = array(
|
|
'word_app' => $this->directory,
|
|
'word_key' => $key,
|
|
'lang_id' => $languageId,
|
|
'word_default' => $value,
|
|
'word_default_version' => $version,
|
|
'word_js' => (int) $xml->getAttribute('js'),
|
|
'word_export' => 1,
|
|
);
|
|
}
|
|
}
|
|
|
|
$done = ( $limit !== null AND $i === ( $limit + $offset ) );
|
|
|
|
if ( $done OR $i % $batchSize === 0 )
|
|
{
|
|
if ( \count( $inserts ) )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_sys_lang_words', $inserts, TRUE );
|
|
$inserts = array();
|
|
}
|
|
$batchesDone++;
|
|
}
|
|
|
|
if ( $done )
|
|
{
|
|
break;
|
|
}
|
|
|
|
$xml->next();
|
|
}
|
|
|
|
if ( \count( $inserts ) )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_sys_lang_words', $inserts, TRUE );
|
|
}
|
|
}
|
|
|
|
return $inserted;
|
|
}
|
|
|
|
/**
|
|
* Install the application's email templates
|
|
*
|
|
* @return void
|
|
*/
|
|
public function installEmailTemplates()
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/emails.xml" ) )
|
|
{
|
|
/* First, delete any existing non-customized email templates for this app */
|
|
\IPS\Db::i()->delete( 'core_email_templates', array( 'template_app=? AND template_parent=0', $this->directory ) );
|
|
|
|
/* Open XML file */
|
|
$xml = new \IPS\Xml\XMLReader;
|
|
$xml->open( $this->getApplicationPath() . "/data/emails.xml" );
|
|
$xml->read();
|
|
|
|
/* Start looping through each word */
|
|
while ( $xml->read() and $xml->name == 'template' )
|
|
{
|
|
if( $xml->nodeType != \XMLReader::ELEMENT )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$insert = array(
|
|
'template_parent' => 0,
|
|
'template_app' => $this->directory,
|
|
'template_edited' => 0,
|
|
'template_pinned' => 0
|
|
);
|
|
|
|
while ( $xml->read() and $xml->name != 'template' )
|
|
{
|
|
if( $xml->nodeType != \XMLReader::ELEMENT )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
switch( $xml->name )
|
|
{
|
|
case 'template_name':
|
|
$insert['template_name'] = $xml->readString();
|
|
$insert['template_key'] = md5( $this->directory . ';' . $insert['template_name'] );
|
|
break;
|
|
|
|
case 'template_data':
|
|
$insert['template_data'] = $xml->readString();
|
|
break;
|
|
|
|
case 'template_content_html':
|
|
$insert['template_content_html'] = $xml->readString();
|
|
break;
|
|
|
|
case 'template_content_plaintext':
|
|
$insert['template_content_plaintext'] = $xml->readString();
|
|
break;
|
|
|
|
case 'template_pinned':
|
|
$insert['template_pinned'] = $xml->readString();
|
|
break;
|
|
}
|
|
}
|
|
|
|
\IPS\Db::i()->replace( 'core_email_templates', $insert );
|
|
}
|
|
|
|
/* Now re-associate customized email templates */
|
|
foreach( \IPS\Db::i()->select( '*', 'core_email_templates', array( 'template_app=? AND template_parent>0', $this->directory ) ) as $template )
|
|
{
|
|
/* Find the real parent now */
|
|
try
|
|
{
|
|
$parent = \IPS\Db::i()->select( '*', 'core_email_templates', array( 'template_app=? and template_name=? and template_parent=0', $template['template_app'], $template['template_name'] ) )->first();
|
|
|
|
/* And now update this template */
|
|
\IPS\Db::i()->update( 'core_email_templates', array( 'template_parent' => $parent['template_id'] ), array( 'template_id=?', $template['template_id'] ) );
|
|
\IPS\Db::i()->update( 'core_email_templates', array( 'template_edited' => 1 ), array( 'template_id=?', $parent['template_id'] ) );
|
|
}
|
|
catch( \UnderflowException $ex ) { }
|
|
}
|
|
|
|
\IPS\Data\Cache::i()->clearAll();
|
|
\IPS\Data\Store::i()->clearAll();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the application's skin templates, CSS files and resources
|
|
*
|
|
* @param bool $update If set to true, do not overwrite current theme setting values
|
|
* @return void
|
|
*/
|
|
public function installSkins( $update=FALSE )
|
|
{
|
|
/* Clear old caches */
|
|
\IPS\Data\Cache::i()->clearAll();
|
|
\IPS\Data\Store::i()->clearAll();
|
|
|
|
/* Install the stuff */
|
|
$this->installThemeSettings( $update );
|
|
$this->clearTemplates();
|
|
$this->installTemplates( $update );
|
|
}
|
|
|
|
/**
|
|
* Install the application's theme settings
|
|
*
|
|
* @param bool $update If set to true, do not overwrite current theme setting values
|
|
* @return void
|
|
*/
|
|
public function installThemeSettings( $update=FALSE )
|
|
{
|
|
if ( file_exists( $this->getApplicationPath() . "/data/themesettings.json" ) )
|
|
{
|
|
unset( \IPS\Data\Store::i()->themes );
|
|
try
|
|
{
|
|
$defaultThemeId = \IPS\Theme::load('default', 'set_key')->id;
|
|
}
|
|
catch( \Exception $ex )
|
|
{
|
|
$defaultThemeId = \IPS\Theme::defaultTheme();
|
|
}
|
|
|
|
$currentSettings = iterator_to_array( \IPS\Db::i()->select( '*', 'core_theme_settings_fields', array( 'sc_set_id=? AND sc_app=?', $defaultThemeId, $this->directory ) )->setKeyField('sc_key') );
|
|
$json = json_decode( file_get_contents( $this->getApplicationPath() . "/data/themesettings.json" ), TRUE );
|
|
|
|
/* Add */
|
|
foreach( $json as $key => $data)
|
|
{
|
|
$insertedSetting = FALSE;
|
|
|
|
if ( ! isset( $currentSettings[ $data['sc_key'] ] ) )
|
|
{
|
|
$insertedSetting = TRUE;
|
|
|
|
$currentId = \IPS\Db::i()->insert( 'core_theme_settings_fields', array(
|
|
'sc_set_id' => $defaultThemeId,
|
|
'sc_key' => $data['sc_key'],
|
|
'sc_tab_key' => $data['sc_tab_key'],
|
|
'sc_type' => $data['sc_type'],
|
|
'sc_multiple' => $data['sc_multiple'],
|
|
'sc_default' => $data['sc_default'],
|
|
'sc_content' => $data['sc_content'],
|
|
'sc_show_in_vse' => ( isset( $data['sc_show_in_vse'] ) ) ? $data['sc_show_in_vse'] : 0,
|
|
'sc_updated' => time(),
|
|
'sc_app' => $this->directory,
|
|
'sc_title' => $data['sc_title'],
|
|
'sc_order' => $data['sc_order'],
|
|
'sc_condition' => $data['sc_condition'],
|
|
) );
|
|
|
|
$currentSettings[ $data['sc_key'] ] = $data;
|
|
}
|
|
else
|
|
{
|
|
/* Update */
|
|
\IPS\Db::i()->update( 'core_theme_settings_fields', array(
|
|
'sc_tab_key' => $data['sc_tab_key'],
|
|
'sc_type' => $data['sc_type'],
|
|
'sc_multiple' => $data['sc_multiple'],
|
|
'sc_default' => $data['sc_default'],
|
|
'sc_show_in_vse' => ( isset( $data['sc_show_in_vse'] ) ) ? $data['sc_show_in_vse'] : 0,
|
|
'sc_content' => $data['sc_content'],
|
|
'sc_title' => $data['sc_title'],
|
|
'sc_order' => $data['sc_order'],
|
|
'sc_condition' => $data['sc_condition'],
|
|
), array( 'sc_set_id=? AND sc_key=? AND sc_app=?', $defaultThemeId, $data['sc_key'], $this->directory ) );
|
|
|
|
$currentId = $currentSettings[ $data['sc_key'] ]['sc_id'];
|
|
}
|
|
|
|
/* Are we updating the value? */
|
|
if( $update === FALSE OR $insertedSetting === TRUE )
|
|
{
|
|
\IPS\Db::i()->delete('core_theme_settings_values', array('sv_id=?', $currentId ) );
|
|
\IPS\Db::i()->insert('core_theme_settings_values', array( 'sv_id' => $currentId, 'sv_value' => (string)$data['sc_default'] ) );
|
|
}
|
|
}
|
|
|
|
if ( $update )
|
|
{
|
|
$defaultCurrentSettings = $currentSettings;
|
|
foreach( \IPS\Theme::themes() as $theme )
|
|
{
|
|
/* If we are using the stock default theme, then use the setting values from the JSON as the base */
|
|
if ( $theme->id == $defaultThemeId )
|
|
{
|
|
$currentSettings = $defaultCurrentSettings;
|
|
}
|
|
else
|
|
{
|
|
$currentSettings = iterator_to_array( \IPS\Db::i()->select( '*', 'core_theme_settings_fields', array( 'sc_set_id=?', $theme->id ) )->setKeyField('sc_key') );
|
|
}
|
|
|
|
$added = FALSE;
|
|
$save = json_decode( $theme->template_settings, TRUE );
|
|
|
|
/* Add */
|
|
foreach( $json as $key => $data )
|
|
{
|
|
if ( ! isset( $currentSettings[ $data['sc_key'] ] ) )
|
|
{
|
|
$added = TRUE;
|
|
$save[ $data['sc_key'] ] = $data['sc_default'];
|
|
|
|
\IPS\Db::i()->insert( 'core_theme_settings_fields', array(
|
|
'sc_set_id' => $theme->id,
|
|
'sc_key' => $data['sc_key'],
|
|
'sc_tab_key' => $data['sc_tab_key'],
|
|
'sc_type' => $data['sc_type'],
|
|
'sc_multiple' => $data['sc_multiple'],
|
|
'sc_default' => $data['sc_default'],
|
|
'sc_content' => $data['sc_content'],
|
|
'sc_show_in_vse' => ( isset( $data['sc_show_in_vse'] ) ) ? $data['sc_show_in_vse'] : 0,
|
|
'sc_updated' => time(),
|
|
'sc_app' => $this->directory,
|
|
'sc_title' => $data['sc_title'],
|
|
'sc_order' => $data['sc_order'],
|
|
'sc_condition' => $data['sc_condition'],
|
|
) );
|
|
}
|
|
else
|
|
{
|
|
/* Update */
|
|
\IPS\Db::i()->update( 'core_theme_settings_fields', array(
|
|
'sc_type' => $data['sc_type'],
|
|
'sc_multiple' => $data['sc_multiple'],
|
|
'sc_default' => $data['sc_default'],
|
|
'sc_show_in_vse' => ( isset( $data['sc_show_in_vse'] ) ) ? $data['sc_show_in_vse'] : 0,
|
|
'sc_content' => $data['sc_content'],
|
|
'sc_title' => $data['sc_title'],
|
|
'sc_condition' => $data['sc_condition'],
|
|
), array( 'sc_set_id=? AND sc_key=?', $theme->id, $data['sc_key'] ) );
|
|
|
|
$currentId = $currentSettings[ $data['sc_key'] ]['sc_id'];
|
|
|
|
try
|
|
{
|
|
$currentValue = \IPS\Db::i()->select( 'sv_value', 'core_theme_settings_values', array( array( 'sv_id=?', $currentId ) ) )->first();
|
|
}
|
|
catch( \UnderFlowException $ex )
|
|
{
|
|
$currentValue = $currentSettings[ $data['sc_key'] ]['sc_default'];
|
|
}
|
|
|
|
/* Are we using the existing default? If so, update it */
|
|
if ( ( $data['sc_default'] != $currentSettings[ $data['sc_key'] ]['sc_default'] ) and ( $currentValue == $defaultCurrentSettings[ $data['sc_key'] ]['sc_default'] ) )
|
|
{
|
|
$added = TRUE;
|
|
$save[ $data['sc_key'] ] = $data['sc_default'];
|
|
|
|
\IPS\Db::i()->delete('core_theme_settings_values', array('sv_id=?', $currentId ) );
|
|
\IPS\Db::i()->insert('core_theme_settings_values', array( 'sv_id' => $currentId, 'sv_value' => (string)$data['sc_default'] ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $added )
|
|
{
|
|
$theme->template_settings = json_encode( $save );
|
|
$theme->save();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear out existing templates before installing new ones
|
|
*
|
|
* @return void
|
|
*/
|
|
public function clearTemplates()
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/theme.xml" ) )
|
|
{
|
|
unset( \IPS\Data\Store::i()->themes );
|
|
\IPS\Theme::removeTemplates( $this->directory );
|
|
\IPS\Theme::removeCss( $this->directory );
|
|
\IPS\Theme::clearFiles( \IPS\Theme::CSS );
|
|
\IPS\Theme::removeResources( $this->directory );
|
|
\IPS\Theme::resetAllCacheKeys();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the application's templates
|
|
* Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
|
|
*
|
|
* @param bool $update If set to true, do not overwrite current theme setting values
|
|
* @param int|null $offset Offset to begin import from
|
|
* @param int|null $limit Number of rows to import
|
|
* @return int Rows inserted
|
|
*/
|
|
public function installTemplates( $update=FALSE, $offset=null, $limit=null )
|
|
{
|
|
$i = 0;
|
|
$inserted = 0;
|
|
|
|
if ( \IPS\Dispatcher::hasInstance() AND class_exists( '\IPS\Dispatcher', FALSE ) and \IPS\Dispatcher::i()->controllerLocation === 'setup' )
|
|
{
|
|
$class = '\IPS\Theme';
|
|
}
|
|
else
|
|
{
|
|
$class = ( \IPS\Theme::designersModeEnabled() ) ? '\IPS\Theme\Advanced\Theme' : '\IPS\Theme';
|
|
}
|
|
|
|
if( file_exists( $this->getApplicationPath() . "/data/theme.xml" ) )
|
|
{
|
|
unset( \IPS\Data\Store::i()->themes );
|
|
|
|
/* Try to prevent timeouts to the extent possible */
|
|
$cutOff = null;
|
|
|
|
if( $maxExecution = @ini_get( 'max_execution_time' ) )
|
|
{
|
|
/* If max_execution_time is set to "no limit" we should add a hard limit to prevent browser timeouts */
|
|
if ( $maxExecution == -1 )
|
|
{
|
|
$maxExecution = 30;
|
|
}
|
|
|
|
$cutOff = time() + ( $maxExecution * .5 );
|
|
}
|
|
|
|
/* Open XML file */
|
|
$xml = new \IPS\Xml\XMLReader;
|
|
$xml->open( $this->getApplicationPath() . "/data/theme.xml" );
|
|
$xml->read();
|
|
|
|
while( $xml->read() )
|
|
{
|
|
if( $xml->nodeType != \XMLReader::ELEMENT )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if( $cutOff !== null AND time() >= $cutOff )
|
|
{
|
|
break;
|
|
}
|
|
|
|
$i++;
|
|
|
|
if ( $offset !== null )
|
|
{
|
|
if ( $i - 1 < $offset )
|
|
{
|
|
$xml->next();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$inserted++;
|
|
|
|
if( $xml->name == 'template' )
|
|
{
|
|
$template = array(
|
|
'app' => $this->directory,
|
|
'group' => $xml->getAttribute('template_group'),
|
|
'name' => $xml->getAttribute('template_name'),
|
|
'variables' => $xml->getAttribute('template_data'),
|
|
'content' => $xml->readString(),
|
|
'location' => $xml->getAttribute('template_location'),
|
|
'_default_template' => true
|
|
);
|
|
|
|
try
|
|
{
|
|
$class::addTemplate( $template );
|
|
}
|
|
catch( \OverflowException $e )
|
|
{
|
|
if ( ! $update )
|
|
{
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
else if( $xml->name == 'css' )
|
|
{
|
|
$css = array(
|
|
'app' => $this->directory,
|
|
'location' => $xml->getAttribute('css_location'),
|
|
'path' => $xml->getAttribute('css_path'),
|
|
'name' => $xml->getAttribute('css_name'),
|
|
'content' => $xml->readString(),
|
|
'_default_template' => true
|
|
);
|
|
|
|
try
|
|
{
|
|
$class::addCss( $css );
|
|
}
|
|
catch( \OverflowException $e )
|
|
{
|
|
if( ! $update )
|
|
{
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
else if( $xml->name == 'resource' )
|
|
{
|
|
$resource = array(
|
|
'app' => $this->directory,
|
|
'location' => $xml->getAttribute('location'),
|
|
'path' => $xml->getAttribute('path'),
|
|
'name' => $xml->getAttribute('name'),
|
|
'content' => base64_decode( $xml->readString() ),
|
|
);
|
|
|
|
$class::addResource( $resource, TRUE );
|
|
}
|
|
|
|
if( $limit !== null AND $i === ( $limit + $offset ) )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $inserted;
|
|
}
|
|
|
|
/**
|
|
* Install the application's javascript
|
|
*
|
|
* @param int|null $offset Offset to begin import from
|
|
* @param int|null $limit Number of rows to import
|
|
* @return int Rows inserted
|
|
*/
|
|
public function installJavascript( $offset=null, $limit=null )
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/javascript.xml" ) )
|
|
{
|
|
return \IPS\Output\Javascript::importXml( $this->getApplicationPath() . "/data/javascript.xml", $offset, $limit );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the application's ACP search keywords
|
|
*
|
|
* @return void
|
|
*/
|
|
public function installSearchKeywords()
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/acpsearch.json" ) )
|
|
{
|
|
\IPS\Db::i()->delete( 'core_acp_search_index', array( 'app=?', $this->directory ) );
|
|
|
|
$inserts = array();
|
|
$maxInserts = 50;
|
|
|
|
foreach( json_decode( file_get_contents( $this->getApplicationPath() . "/data/acpsearch.json" ), TRUE ) as $url => $data )
|
|
{
|
|
foreach ( $data['keywords'] as $word )
|
|
{
|
|
$inserts[] = array(
|
|
'url' => $url,
|
|
'keyword' => $word,
|
|
'app' => $this->directory,
|
|
'lang_key' => $data['lang_key'],
|
|
'restriction' => $data['restriction'] ?: NULL
|
|
);
|
|
|
|
if( \count( $inserts ) >= $maxInserts )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_acp_search_index', $inserts );
|
|
$inserts = array();
|
|
}
|
|
}
|
|
}
|
|
|
|
if( \count( $inserts ) )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_acp_search_index', $inserts );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install hooks
|
|
*
|
|
* @return void
|
|
*/
|
|
public function installHooks()
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/hooks.json" ) )
|
|
{
|
|
\IPS\Db::i()->delete( 'core_hooks', array( 'app=?', $this->directory ) );
|
|
|
|
$inserts = array();
|
|
$templatesToRecompile = array();
|
|
foreach( json_decode( file_get_contents( $this->getApplicationPath() . "/data/hooks.json" ), TRUE ) as $filename => $data )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_hooks', array(
|
|
'app' => $this->directory,
|
|
'type' => $data['type'],
|
|
'class' => $data['class'],
|
|
'filename' => $filename
|
|
) );
|
|
|
|
if ( $data['type'] === 'S' )
|
|
{
|
|
$templatesToRecompile[ $data['class'] ] = $data['class'];
|
|
}
|
|
}
|
|
|
|
\IPS\Plugin\Hook::writeDataFile();
|
|
|
|
foreach ( $templatesToRecompile as $k )
|
|
{
|
|
$exploded = explode( '_', $k );
|
|
\IPS\Theme::deleteCompiledTemplate( $exploded[1], $exploded[2], $exploded[3] );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the application's widgets
|
|
*
|
|
* @return void
|
|
*/
|
|
public function installWidgets()
|
|
{
|
|
if( file_exists( $this->getApplicationPath() . "/data/widgets.json" ) )
|
|
{
|
|
$currentWidgets = iterator_to_array( \IPS\Db::i()->select( '`key`', 'core_widgets', array( 'app=?', $this->directory ) ) );
|
|
|
|
$inserts = array();
|
|
foreach ( json_decode( file_get_contents( $this->getApplicationPath() . "/data/widgets.json" ), TRUE ) as $key => $json )
|
|
{
|
|
if ( ! \in_array( $key, $currentWidgets ) )
|
|
{
|
|
$inserts[] = array(
|
|
'app' => $this->directory,
|
|
'key' => $key,
|
|
'class' => $json['class'],
|
|
'restrict' => json_encode( $json['restrict'] ),
|
|
'default_area' => ( isset( $json['default_area'] ) ? $json['default_area'] : NULL ),
|
|
'allow_reuse' => ( isset( $json['allow_reuse'] ) ? $json['allow_reuse'] : 0 ),
|
|
'menu_style' => ( isset( $json['menu_style'] ) ? $json['menu_style'] : 'menu' ),
|
|
'embeddable' => ( isset( $json['embeddable'] ) ? $json['embeddable'] : 0 ),
|
|
);
|
|
}
|
|
}
|
|
|
|
if( \count( $inserts ) )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_widgets', $inserts, TRUE );
|
|
unset( \IPS\Data\Store::i()->widgets );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install 'other' items. Left blank here so that application classes can override for app
|
|
* specific installation needs. Always run as the last step.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function installOther()
|
|
{
|
|
|
|
}
|
|
|
|
/**
|
|
* Default front navigation
|
|
*
|
|
* @code
|
|
|
|
// Each item...
|
|
array(
|
|
'key' => 'Example', // The extension key
|
|
'app' => 'core', // [Optional] The extension application. If ommitted, uses this application
|
|
'config' => array(...), // [Optional] The configuration for the menu item
|
|
'title' => 'SomeLangKey', // [Optional] If provided, the value of this language key will be copied to menu_item_X
|
|
'children' => array(...), // [Optional] Array of child menu items for this item. Each has the same format.
|
|
)
|
|
|
|
return array(
|
|
'rootTabs' => array(), // These go in the top row
|
|
'browseTabs' => array(), // These go under the Browse tab on a new install or when restoring the default configuraiton; or in the top row if installing the app later (when the Browse tab may not exist)
|
|
'browseTabsEnd' => array(), // These go under the Browse tab after all other items on a new install or when restoring the default configuraiton; or in the top row if installing the app later (when the Browse tab may not exist)
|
|
'activityTabs' => array(), // These go under the Activity tab on a new install or when restoring the default configuraiton; or in the top row if installing the app later (when the Activity tab may not exist)
|
|
)
|
|
* @endcode
|
|
* @return array
|
|
*/
|
|
public function defaultFrontNavigation()
|
|
{
|
|
return array(
|
|
'rootTabs' => array(),
|
|
'browseTabs' => array(),
|
|
'browseTabsEnd' => array(),
|
|
'activityTabs' => array()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Database check
|
|
*
|
|
* @return array Queries needed to correct database in the following format ( table => x, query = x );
|
|
*/
|
|
public function databaseCheck()
|
|
{
|
|
$db = \IPS\Db::i();
|
|
$changesToMake = array();
|
|
|
|
/* Loop the tables in the schema */
|
|
foreach( json_decode( file_get_contents( $this->getApplicationPath() . "/data/schema.json" ), TRUE ) as $tableName => $tableDefinition )
|
|
{
|
|
$tableChanges = array();
|
|
$needIgnore = false;
|
|
$innoDbFullTextIndexes = array();
|
|
|
|
/* Get our local definition of this table */
|
|
try
|
|
{
|
|
$localDefinition = \IPS\Db::i()->getTableDefinition( $tableName, FALSE, TRUE );
|
|
$originalDefinition = $localDefinition; #Store this before it is normalised and engine stripped
|
|
$localDefinition = \IPS\Db::i()->normalizeDefinition( $localDefinition );
|
|
|
|
if( isset( $tableDefinition['reporting'] ) )
|
|
{
|
|
unset( $tableDefinition['reporting'] );
|
|
}
|
|
|
|
if( isset( $tableDefinition['inserts'] ) )
|
|
{
|
|
unset( $tableDefinition['inserts'] );
|
|
}
|
|
|
|
/* Now we have to add the correct colation for text columns to our compare definition to flag any columns that don't have the correct charset/collation */
|
|
$tableDefinition['columns'] = array_map( function( $column ){
|
|
if( \in_array( mb_strtoupper( $column['type'] ), array( 'CHAR', 'VARCHAR', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'ENUM', 'SET' ) ) )
|
|
{
|
|
$column['collation'] = \IPS\Db::i()->collation;
|
|
}
|
|
|
|
return $column;
|
|
}, $tableDefinition['columns'] );
|
|
|
|
/* And store our definition */
|
|
$compareDefinition = \IPS\Db::i()->normalizeDefinition( $tableDefinition );
|
|
$tableDefinition = \IPS\Db::i()->updateDefinitionIndexLengths( $tableDefinition );
|
|
|
|
if( isset( $compareDefinition['comment'] ) AND !$compareDefinition['comment'] )
|
|
{
|
|
unset( $compareDefinition['comment'] );
|
|
}
|
|
|
|
/* 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 )
|
|
{
|
|
$dropped = array();
|
|
|
|
/* Loop the columns */
|
|
foreach ( $tableDefinition['columns'] as $columnName => $columnData )
|
|
{
|
|
/* If it doesn't exist in the local database, create it */
|
|
if ( !isset( $localDefinition['columns'][ $columnName ] ) )
|
|
{
|
|
$tableChanges[] = "ADD COLUMN {$db->compileColumnDefinition( $columnData )}";
|
|
}
|
|
/* Or if it's wrong, change it */
|
|
elseif ( $compareDefinition['columns'][ $columnName ] != $localDefinition['columns'][ $columnName ] )
|
|
{
|
|
/* If the only difference is MEDIUMIT or INT should be BIGINT UNSIGNED - that's where we changed the member ID column. We don't need to flag it */
|
|
$differences = array();
|
|
foreach ( $columnData as $k => $v )
|
|
{
|
|
if ( isset( $localDefinition['columns'][ $columnName ][ $k ] ) AND $v != $localDefinition['columns'][ $columnName ][ $k ] )
|
|
{
|
|
$differences[ $k ] = array( 'is' => $localDefinition['columns'][ $columnName ][ $k ], 'shouldBe' => $v );
|
|
}
|
|
}
|
|
if ( isset( $differences['type'] ) and ( $differences['type']['is'] == 'MEDIUMINT' or $differences['type']['is'] == 'INT' ) and $differences['type']['shouldBe'] == 'BIGINT' )
|
|
{
|
|
unset( $differences['type'] );
|
|
if ( isset( $differences['length'] ) )
|
|
{
|
|
unset( $differences['length'] );
|
|
}
|
|
if ( isset( $differences['unsigned'] ) and !$differences['unsigned']['is'] and $differences['unsigned']['shouldBe'] )
|
|
{
|
|
unset( $differences['unsigned'] );
|
|
}
|
|
}
|
|
|
|
/* If this is a decimal column, ignore unsigned attribute */
|
|
if( $compareDefinition['columns'][ $columnName ]['type'] == 'DECIMAL' AND isset( $differences['unsigned'] ) )
|
|
{
|
|
unset( $differences['unsigned'] );
|
|
}
|
|
|
|
/* If there were other differences, carry on... */
|
|
if ( $differences )
|
|
{
|
|
/* We re-add indexes after changing columns */
|
|
$indexesToAdd = array();
|
|
|
|
/* First check indexes to see if any need to be adjusted */
|
|
foreach( $localDefinition['indexes'] as $indexName => $indexData )
|
|
{
|
|
/* We skip the primary key as it can cause errors related to auto-increment */
|
|
if( $indexName == 'PRIMARY' )
|
|
{
|
|
if ( isset( $tableDefinition['columns'][ $indexData['columns'][0] ] ) and isset( $tableDefinition['columns'][ $indexData['columns'][0] ]['auto_increment'] ) and $tableDefinition['columns'][ $indexData['columns'][0] ]['auto_increment'] === TRUE )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
foreach( $indexData['columns'] as $indexColumn )
|
|
{
|
|
/* If the column we are about to adjust is included in this index, see if it needs adjusting */
|
|
if( $indexColumn == $columnName AND !\in_array( $indexName, $dropped ) )
|
|
{
|
|
$thisIndex = $db->updateDefinitionIndexLengths( $compareDefinition );
|
|
|
|
if( !isset( $thisIndex['indexes'][ $indexName ] ) )
|
|
{
|
|
$tableChanges[] = "DROP INDEX `{$db->escape_string( $indexName )}`";
|
|
$dropped[] = $indexName;
|
|
}
|
|
elseif( $thisIndex['indexes'][ $indexName ] !== $localDefinition['indexes'][ $indexName ] )
|
|
{
|
|
$tableChanges[] = "DROP INDEX `{$db->escape_string( $indexName )}`";
|
|
$indexesToAdd[] = $db->buildIndex( $tableName, $thisIndex['indexes'][ $indexName ], $tableDefinition );
|
|
$dropped[] = $indexName;
|
|
|
|
if( $tableDefinition['indexes'][ $indexName ]['type'] == 'unique' OR $tableDefinition['indexes'][ $indexName ]['type'] == 'primary' )
|
|
{
|
|
$needIgnore = TRUE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If we are about to adjust the column to not allow NULL values then adjust those values first... */
|
|
if( isset( $columnData['allow_null'] ) and $columnData['allow_null'] === FALSE )
|
|
{
|
|
$defaultValue = "''";
|
|
|
|
/* Default value */
|
|
if( isset( $columnData['default'] ) and !\in_array( \strtoupper( $columnData['type'] ), array( 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'BLOB', 'MEDIUMBLOB', 'BIGBLOB', 'LONGBLOB' ) ) )
|
|
{
|
|
if( $columnData['type'] == 'BIT' )
|
|
{
|
|
$defaultValue = "{$columnData['default']}";
|
|
}
|
|
else
|
|
{
|
|
$defaultValue = \in_array( $columnData['type'], array( 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT', 'REAL', 'DOUBLE', 'FLOAT', 'DECIMAL', 'NUMERIC' ) ) ? \floatval( $columnData['default'] ) : ( ! \in_array( $columnData['default'], array( 'CURRENT_TIMESTAMP', 'BIT' ) ) ? '\'' . $db->escape_string( $columnData['default'] ) . '\'' : $columnData['default'] );
|
|
}
|
|
}
|
|
|
|
$changesToMake[] = array( 'table' => $tableName, 'query' => "UPDATE `{$db->prefix}{$db->escape_string( $tableName )}` SET `{$db->escape_string( $columnName )}`={$defaultValue} WHERE `{$db->escape_string( $columnName )}` IS NULL;" );
|
|
}
|
|
|
|
$tableChanges[] = "CHANGE COLUMN `{$db->escape_string( $columnName )}` {$db->compileColumnDefinition( $columnData )}";
|
|
|
|
if( \count( $indexesToAdd ) )
|
|
{
|
|
$tableChanges = array_merge( $tableChanges, $indexesToAdd );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Loop the index */
|
|
foreach ( $compareDefinition['indexes'] as $indexName => $indexData )
|
|
{
|
|
if( \in_array( $indexName, $dropped ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( !isset( $localDefinition['indexes'][ $indexName ] ) )
|
|
{
|
|
/* InnoDB FullText indexes must be added one at a time */
|
|
if( $tableDefinition['engine'] === 'InnoDB' AND $tableDefinition['indexes'][ $indexName ]['type'] === 'fulltext' )
|
|
{
|
|
$innoDbFullTextIndexes[] = $db->buildIndex( $tableName, $tableDefinition['indexes'][ $indexName ], $tableDefinition );
|
|
}
|
|
else
|
|
{
|
|
$tableChanges[] = $db->buildIndex( $tableName, $tableDefinition['indexes'][ $indexName ], $tableDefinition );
|
|
}
|
|
|
|
if( $tableDefinition['indexes'][ $indexName ]['type'] == 'unique' OR $tableDefinition['indexes'][ $indexName ]['type'] == 'primary' )
|
|
{
|
|
$needIgnore = TRUE;
|
|
}
|
|
}
|
|
elseif ( $indexData != $localDefinition['indexes'][ $indexName ] )
|
|
{
|
|
$tableChanges[] = ( ( $indexName == 'PRIMARY KEY' ) ? "DROP " . $indexName . ", " : "DROP INDEX `" . $db->escape_string( $indexName ) . "`, " ) . $db->buildIndex( $tableName, $tableDefinition['indexes'][ $indexName ], $tableDefinition );
|
|
|
|
if( $tableDefinition['indexes'][ $indexName ]['type'] == 'unique' OR $tableDefinition['indexes'][ $indexName ]['type'] == 'primary' )
|
|
{
|
|
$needIgnore = TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Remove unnecessary indexes, which can be an issue if, for example, there is a UNIQUE index that the schema doesn't think should be there */
|
|
foreach ( $localDefinition['indexes'] as $indexName => $indexData )
|
|
{
|
|
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
|
|
{
|
|
$tableChanges[] = $dropIndexQuery;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if( \count( $tableChanges ) )
|
|
{
|
|
if( $needIgnore )
|
|
{
|
|
$changesToMake[] = array( 'table' => $tableName, 'query' => "CREATE TABLE `{$db->prefix}{$db->escape_string( $tableName )}_new` LIKE `{$db->prefix}{$db->escape_string( $tableName )}`;" );
|
|
$changesToMake[] = array( 'table' => $tableName, 'query' => "ALTER TABLE `{$db->prefix}{$db->escape_string( $tableName )}_new` " . implode( ", ", $tableChanges ) . ";" );
|
|
$changesToMake[] = array( 'table' => $tableName, 'query' => "INSERT IGNORE INTO `{$db->prefix}{$db->escape_string( $tableName )}_new` SELECT * FROM `{$db->prefix}{$db->escape_string( $tableName )}`;" );
|
|
$changesToMake[] = array( 'table' => $tableName, 'query' => "DROP TABLE `{$db->prefix}{$db->escape_string( $tableName )}`;" );
|
|
$changesToMake[] = array( 'table' => $tableName, 'query' => "RENAME TABLE `{$db->prefix}{$db->escape_string( $tableName )}_new` TO `{$db->prefix}{$db->escape_string( $tableName )}`;" );
|
|
}
|
|
else
|
|
{
|
|
$changesToMake[] = array( 'table' => $tableName, 'query' => "ALTER TABLE `{$db->prefix}{$db->escape_string( $tableName )}` " . implode( ", ", $tableChanges ) . ";" );
|
|
}
|
|
}
|
|
|
|
/* InnoDB FullText indexes must be added one at a time */
|
|
if( \count( $innoDbFullTextIndexes ) )
|
|
{
|
|
foreach( $innoDbFullTextIndexes as $newIndex )
|
|
{
|
|
$changesToMake[] = array( 'table' => $tableName, 'query' => "ALTER TABLE `{$db->prefix}{$db->escape_string( $tableName )}` " . $newIndex . ";" );
|
|
}
|
|
}
|
|
}
|
|
/* If the table doesn't exist, create it */
|
|
catch ( \OutOfRangeException $e )
|
|
{
|
|
$changesToMake[] = array( 'table' => $tableName, 'query' => $db->_createTableQuery( $tableDefinition ) );
|
|
}
|
|
}
|
|
|
|
/* And loop any install routine for columns added to other tables */
|
|
if ( file_exists( $this->getApplicationPath() . "/setup/install/queries.json" ) )
|
|
{
|
|
foreach( json_decode( file_get_contents( $this->getApplicationPath() . "/setup/install/queries.json" ), TRUE ) as $query )
|
|
{
|
|
switch ( $query['method'] )
|
|
{
|
|
/* Add column */
|
|
case 'addColumn':
|
|
$localDefinition = \IPS\Db::i()->getTableDefinition( $query['params'][0] );
|
|
if ( !isset( $localDefinition['columns'][ $query['params'][1]['name'] ] ) )
|
|
{
|
|
$changesToMake[] = array( 'table' => $query['params'][0], 'query' => "ALTER TABLE `{$db->prefix}{$query['params'][0]}` ADD COLUMN {$db->compileColumnDefinition( $query['params'][1] )}" );
|
|
}
|
|
else
|
|
{
|
|
$correctDefinition = $db->compileColumnDefinition( $query['params'][1] );
|
|
if ( $correctDefinition != $db->compileColumnDefinition( $localDefinition['columns'][ $query['params'][1]['name'] ] ) )
|
|
{
|
|
$changesToMake[] = array( 'table' => $query['params'][0], 'query' => "ALTER TABLE `{$db->prefix}{$query['params'][0]}` CHANGE COLUMN `{$query['params'][1]['name']}` {$correctDefinition}" );
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Return */
|
|
return $changesToMake;
|
|
}
|
|
|
|
/**
|
|
* Create a new version number and move current working version
|
|
* code into it
|
|
*
|
|
* @param int $long The "long" version number (e.g. 100000)
|
|
* @param string $human The "human" version number (e.g. "1.0.0")
|
|
* @return void
|
|
*/
|
|
public function assignNewVersion( $long, $human )
|
|
{
|
|
/* Add to versions.json */
|
|
$json = json_decode( \file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/versions.json" ), TRUE );
|
|
$json[ $long ] = $human;
|
|
static::writeJson( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/versions.json", $json );
|
|
|
|
/* Do stuff */
|
|
$setupDir = \IPS\ROOT_PATH . "/applications/{$this->directory}/setup";
|
|
$workingDir = $setupDir . "/upg_working";
|
|
if ( file_exists( $workingDir ) )
|
|
{
|
|
/* We need to make sure the array is 1-indexed otherwise the upgrader gets confused */
|
|
$queriesJsonFile = $workingDir . "/queries.json";
|
|
if ( file_exists( $queriesJsonFile ) )
|
|
{
|
|
$write = array();
|
|
$i = 0;
|
|
foreach ( json_decode( \file_get_contents( $queriesJsonFile ), TRUE ) as $query )
|
|
{
|
|
$write[ ++$i ] = $query;
|
|
}
|
|
static::writeJson( $queriesJsonFile, $write );
|
|
}
|
|
|
|
/* Add the actual version number in upgrade.php & options.php */
|
|
$versionReplacement = function( $file ) use ( $human, $long )
|
|
{
|
|
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}" );
|
|
}
|
|
|
|
/* Update core_dev */
|
|
\IPS\Db::i()->update( 'core_dev', array(
|
|
'working_version' => $long,
|
|
), array( 'app_key=? AND working_version=?', $this->directory, 'working' ) );
|
|
}
|
|
|
|
/**
|
|
* Build application for release
|
|
*
|
|
* @return void
|
|
* @throws \RuntimeException
|
|
*/
|
|
public function build()
|
|
{
|
|
/* Write the application data to the application.json file */
|
|
$applicationData = array(
|
|
'application_title' => \IPS\Member::loggedIn()->language()->get('__app_' . $this->directory ),
|
|
'app_author' => $this->author,
|
|
'app_directory' => $this->directory,
|
|
'app_protected' => $this->protected,
|
|
'app_website' => $this->website,
|
|
'app_update_check' => $this->update_check,
|
|
);
|
|
|
|
\IPS\Application::writeJson( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/application.json', $applicationData );
|
|
|
|
/* Update app version data */
|
|
$versions = $this->getAllVersions();
|
|
$longVersions = array_keys( $versions );
|
|
$humanVersions = array_values( $versions );
|
|
if( \count($versions) )
|
|
{
|
|
$latestLVersion = array_pop( $longVersions );
|
|
$latestHVersion = array_pop( $humanVersions );
|
|
|
|
\IPS\Db::i()->update( 'core_applications', array( 'app_version' => $latestHVersion, 'app_long_version' => $latestLVersion ), array( 'app_directory=?', $this->directory ) );
|
|
|
|
$this->long_version = $latestLVersion;
|
|
$this->version = $latestHVersion;
|
|
}
|
|
$setupDir = \IPS\ROOT_PATH . '/applications/' . $this->directory . '/setup/upg_' . $this->long_version;
|
|
if ( !is_dir( $setupDir ) )
|
|
{
|
|
mkdir( $setupDir );
|
|
}
|
|
|
|
/* Take care of languages for this app */
|
|
$languageChanges = $this->buildLanguages();
|
|
$langChangesFile = $setupDir . '/lang.json';
|
|
if ( \count( array_filter( $languageChanges['normal'] ) ) or \count( array_filter( $languageChanges['js'] ) ) )
|
|
{
|
|
if ( file_exists( $langChangesFile ) )
|
|
{
|
|
$previousLangChanges = json_decode( file_get_contents( $langChangesFile ), TRUE );
|
|
$languageChanges['normal'] = $this->_combineChanges( $languageChanges['normal'], $previousLangChanges['normal'] );
|
|
$languageChanges['js'] = $this->_combineChanges( $languageChanges['js'], $previousLangChanges['js'] );
|
|
}
|
|
|
|
if ( \count( array_filter( $languageChanges['normal'] ) ) or \count( array_filter( $languageChanges['js'] ) ) )
|
|
{
|
|
\file_put_contents( $langChangesFile, json_encode( $languageChanges, JSON_PRETTY_PRINT ) );
|
|
}
|
|
elseif ( \file_exists( $langChangesFile ) )
|
|
{
|
|
\unlink( $langChangesFile );
|
|
}
|
|
}
|
|
$this->installLanguages();
|
|
|
|
/* Take care of skins for this app */
|
|
$themeChanges = $this->buildThemeTemplates();
|
|
$themeChangesFile = $setupDir . '/theme.json';
|
|
if ( \count( array_filter( $themeChanges['html'] ) ) or \count( array_filter( $themeChanges['css'] ) ) or \count( array_filter( $themeChanges['resources'] ) ) )
|
|
{
|
|
if ( file_exists( $themeChangesFile ) )
|
|
{
|
|
$previousThemeChanges = json_decode( file_get_contents( $themeChangesFile ), TRUE );
|
|
$themeChanges['html'] = $this->_combineChanges( $themeChanges['html'], $previousThemeChanges['html'] );
|
|
$themeChanges['css'] = $this->_combineChanges( $themeChanges['css'], $previousThemeChanges['css'] );
|
|
$themeChanges['resources'] = $this->_combineChanges( $themeChanges['resources'], $previousThemeChanges['resources'] );
|
|
}
|
|
|
|
if ( \count( array_filter( $themeChanges['html'] ) ) or \count( array_filter( $themeChanges['css'] ) ) or \count( array_filter( $themeChanges['resources'] ) ) )
|
|
{
|
|
\file_put_contents( $themeChangesFile, json_encode( $themeChanges, JSON_PRETTY_PRINT ) );
|
|
}
|
|
elseif ( \file_exists( $themeChangesFile ) )
|
|
{
|
|
\unlink( $themeChangesFile );
|
|
}
|
|
}
|
|
|
|
/* Take care of emails for this app */
|
|
$emailTemplateChanges = $this->buildEmailTemplates();
|
|
$emailTemplateChangesFile = $setupDir . '/emailTemplates.json';
|
|
if ( \count( array_filter( $emailTemplateChanges ) ) )
|
|
{
|
|
if ( file_exists( $emailTemplateChangesFile ) )
|
|
{
|
|
$emailTemplateChanges = $this->_combineChanges( $emailTemplateChanges, json_decode( file_get_contents( $emailTemplateChangesFile ), TRUE ) );
|
|
}
|
|
|
|
if ( \count( array_filter( $emailTemplateChanges ) ) )
|
|
{
|
|
\file_put_contents( $emailTemplateChangesFile, json_encode( $emailTemplateChanges, JSON_PRETTY_PRINT ) );
|
|
}
|
|
elseif ( \file_exists( $emailTemplateChangesFile ) )
|
|
{
|
|
\unlink( $emailTemplateChangesFile );
|
|
}
|
|
}
|
|
$this->installEmailTemplates();
|
|
|
|
/* Take care of javascript for this app */
|
|
$jsChanges = $this->buildJavascript();
|
|
$jsChangesFile = $setupDir . '/javascript.json';
|
|
if ( \count( array_filter( $jsChanges['files'] ) ) or \count( array_filter( $jsChanges['orders'] ) ) )
|
|
{
|
|
if ( file_exists( $jsChangesFile ) )
|
|
{
|
|
$previousJsChanges = json_decode( file_get_contents( $jsChangesFile ), TRUE );
|
|
$jsChanges['files'] = $this->_combineChanges( $jsChanges['files'], $previousJsChanges['files'] );
|
|
$jsChanges['orders'] = $this->_combineChanges( $jsChanges['orders'], $previousJsChanges['orders'] );
|
|
}
|
|
|
|
if ( \count( array_filter( $jsChanges['files'] ) ) or \count( array_filter( $jsChanges['orders'] ) ) )
|
|
{
|
|
\file_put_contents( $jsChangesFile, json_encode( $jsChanges, JSON_PRETTY_PRINT ) );
|
|
}
|
|
elseif ( \file_exists( $jsChangesFile ) )
|
|
{
|
|
\unlink( $jsChangesFile );
|
|
}
|
|
}
|
|
$this->installJavascript();
|
|
|
|
/* Take care of hooks for this app */
|
|
$this->buildHooks();
|
|
|
|
/* Take care of features for this app */
|
|
$this->buildNewFeatures();
|
|
|
|
/* And custom build routines */
|
|
foreach( $this->extensions( 'core', 'Build' ) as $builder )
|
|
{
|
|
$builder->build();
|
|
}
|
|
|
|
/* Write a build.xml file with the current json data so we know what has changed next time we build */
|
|
$jsonChanges = $this->buildJsonData();
|
|
foreach ( array( 'modules', 'tasks', 'settings', 'widgets', 'acpSearchKeywords', 'hooks', 'themeSettings' ) as $k )
|
|
{
|
|
if ( isset( $jsonChanges[ $k ] ) and ( $jsonChanges[ $k ]['added'] or $jsonChanges[ $k ]['edited'] or $jsonChanges[ $k ]['removed'] ) )
|
|
{
|
|
$changesFile = "{$setupDir}/{$k}.json";
|
|
|
|
if ( file_exists( $changesFile ) )
|
|
{
|
|
$jsonChanges[ $k ] = $this->_combineChanges( $jsonChanges[ $k ], json_decode( file_get_contents( $changesFile ), TRUE ) );
|
|
}
|
|
|
|
if ( $jsonChanges[ $k ]['added'] or $jsonChanges[ $k ]['edited'] or $jsonChanges[ $k ]['removed'] )
|
|
{
|
|
\file_put_contents( $changesFile, json_encode( $jsonChanges[ $k ], JSON_PRETTY_PRINT ) );
|
|
}
|
|
elseif ( \file_exists( $changesFile ) )
|
|
{
|
|
\unlink( $changesFile );
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Included CMS Templates */
|
|
if( file_exists( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/dev/cmsTemplates.json' ) )
|
|
{
|
|
$pagesTemplates = json_decode( file_get_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/dev/cmsTemplates.json' ), TRUE );
|
|
$xml = \IPS\cms\Templates::exportAsXml( $pagesTemplates );
|
|
|
|
if( $xml )
|
|
{
|
|
if ( is_writable( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data' ) )
|
|
{
|
|
\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/cmsTemplates.xml', $xml->outputMemory() );
|
|
}
|
|
else
|
|
{
|
|
throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Write the version data file */
|
|
\file_put_contents( $setupDir . '/data.json', json_encode( array(
|
|
'id' => $this->long_version,
|
|
'name' => $this->version,
|
|
'steps' => array(
|
|
'queries' => file_exists( $setupDir . "/queries.json" ),
|
|
'lang' => file_exists( $langChangesFile ),
|
|
'theme' => file_exists( $themeChangesFile ),
|
|
'themeSettings' => file_exists( $setupDir . "/themeSettings.json" ),
|
|
'javascript' => file_exists( $jsChangesFile ),
|
|
'emailTemplates' => file_exists( $emailTemplateChangesFile ),
|
|
'hooks' => file_exists( $setupDir . "/hooks.json" ),
|
|
'acpSearchKeywords' => file_exists( $setupDir . "/acpSearchKeywords.json" ),
|
|
'settings' => file_exists( $setupDir . "/settings.json" ),
|
|
'tasks' => file_exists( $setupDir . "/tasks.json" ),
|
|
'modules' => file_exists( $setupDir . "/modules.json" ),
|
|
'widgets' => file_exists( $setupDir . "/widgets.json" ),
|
|
'whatsnew' => file_exists( $setupDir . "/whatsnew.json" ),
|
|
'customOptions' => file_exists( $setupDir . "/options.php" ),
|
|
'customRoutines' => file_exists( $setupDir . "/upgrade.php" ),
|
|
),
|
|
'forceMainUpgrader' => FALSE,
|
|
'forceManualDownloadNoCiC' => FALSE,
|
|
'forceManualDownloadCiC' => FALSE,
|
|
), JSON_PRETTY_PRINT ) );
|
|
}
|
|
|
|
/**
|
|
* Combine information about changes when rebuilding a version after it was already built once before
|
|
*
|
|
* @param array $newChanges The changes detected in this build
|
|
* @param array $previousChanges The changes detected in the previous build
|
|
* @param bool $keysOnly Set to TRUE if the changes is just a list of keys, or FALSE if they're key/values
|
|
* @return array
|
|
*/
|
|
protected function _combineChanges( $newChanges, $previousChanges, $keysOnly=TRUE )
|
|
{
|
|
if ( $keysOnly )
|
|
{
|
|
foreach ( $newChanges['added'] as $v )
|
|
{
|
|
if ( \in_array( $v, $previousChanges['removed'] ) )
|
|
{
|
|
unset( $previousChanges['removed'][ array_search( $v, $previousChanges['removed'] ) ] );
|
|
}
|
|
else
|
|
{
|
|
$previousChanges['added'][] = $v;
|
|
}
|
|
}
|
|
foreach ( $newChanges['edited'] as $v )
|
|
{
|
|
if ( !\in_array( $v, $previousChanges['added'] ) )
|
|
{
|
|
$previousChanges['edited'][] = $v;
|
|
}
|
|
}
|
|
foreach ( $newChanges['removed'] as $v )
|
|
{
|
|
if ( \in_array( $v, $previousChanges['added'] ) )
|
|
{
|
|
unset( $previousChanges['added'][ array_search( $v, $previousChanges['added'] ) ] );
|
|
}
|
|
elseif ( \in_array( $v, $previousChanges['edited'] ) )
|
|
{
|
|
unset( $previousChanges['edited'][ array_search( $v, $previousChanges['edited'] ) ] );
|
|
$previousChanges['removed'][] = $v;
|
|
}
|
|
elseif ( !\in_array( $v, $previousChanges['removed'] ) )
|
|
{
|
|
$previousChanges['removed'][] = $v;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach ( $newChanges['added'] as $k => $v )
|
|
{
|
|
if ( isset( $previousChanges['removed'][ $k ] ) )
|
|
{
|
|
unset( $previousChanges['removed'][ $k ] );
|
|
}
|
|
else
|
|
{
|
|
$previousChanges['added'][ $k ] = $v;
|
|
}
|
|
}
|
|
foreach ( $newChanges['edited'] as $k => $v )
|
|
{
|
|
if ( !isset( $previousChanges['added'][ $k ] ) )
|
|
{
|
|
$previousChanges['edited'][ $k ] = $v;
|
|
}
|
|
}
|
|
foreach ( $newChanges['removed'] as $k => $v )
|
|
{
|
|
if ( isset( $previousChanges['added'][ $k ] ) )
|
|
{
|
|
unset( $previousChanges['added'][ $k ] );
|
|
}
|
|
elseif ( isset( $previousChanges['edited'][ $k ] ) )
|
|
{
|
|
unset( $previousChanges['edited'][ $k ] );
|
|
$previousChanges['removed'][ $k ] = $v;
|
|
}
|
|
elseif ( !isset( $previousChanges['removed'][ $k ] ) )
|
|
{
|
|
$previousChanges['removed'][ $k ] = $v;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $previousChanges;
|
|
}
|
|
|
|
/**
|
|
* Build New Feature Highlights
|
|
*
|
|
* @return void
|
|
*/
|
|
public function buildNewFeatures()
|
|
{
|
|
/* Assign any that are flagged is "current" to this version */
|
|
\IPS\Db::i()->update( 'core_members_features', array( 'feature_version_added' => $this->long_version ), array( "feature_version_added=? AND feature_app=?", 0, $this->directory ) );
|
|
|
|
/* Write the JSON file */
|
|
\IPS\Application\Feature::writeJsonFile( $this->directory, $this->long_version );
|
|
}
|
|
|
|
/**
|
|
* Install new feature highlights
|
|
*
|
|
* @param string $appKey The application key
|
|
* @param int $versionId The version ID
|
|
* @return void
|
|
*/
|
|
public static function installNewFeatures( string $appKey, int $versionId )
|
|
{
|
|
if ( !\in_array( $appKey, \IPS\IPS::$ipsApps ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( file_exists( \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_{$versionId}/whatsnew.json" ) )
|
|
{
|
|
if ( $data = @json_decode( \file_get_contents( $this->getApplicationPath() . "/setup/upg_{$versionId}/whatsnew.json" ), TRUE ) )
|
|
{
|
|
$insert = array();
|
|
foreach( $data AS $uid => $row )
|
|
{
|
|
$filename = explode( '/', $row['feature_image'] );
|
|
$filename = array_pop( $filename );
|
|
\IPS\File::create( 'core_Feature', $filename, base64_decode( $row['_image_data'], 'features', TRUE, NULL, FALSE ) );
|
|
unset( $row['_image_data'] );
|
|
$insert[] = $row;
|
|
}
|
|
|
|
\IPS\Db::i()->insert( 'core_members_features', $insert );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build skin templates for an app
|
|
*
|
|
* @return void
|
|
* @throws \RuntimeException
|
|
*/
|
|
public function buildThemeTemplates()
|
|
{
|
|
/* Delete compiled items */
|
|
\IPS\Theme::deleteCompiledTemplate( $this->directory );
|
|
\IPS\Theme::deleteCompiledCss( $this->directory );
|
|
\IPS\Theme::removeResources( $this->directory );
|
|
|
|
\IPS\Theme::i()->importDevHtml( $this->directory, 0 );
|
|
\IPS\Theme::i()->importDevCss( $this->directory, 0 );
|
|
|
|
/* Get current XML file for calculating differences */
|
|
$return = array( 'html' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ), 'css' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ), 'resources' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ) );
|
|
$current = array( 'html' => array(), 'css' => array(), 'resources' => array() );
|
|
$currentFile = \IPS\ROOT_PATH . "/applications/{$this->directory}/data/theme.xml";
|
|
if ( file_exists( $currentFile ) )
|
|
{
|
|
$xml = \IPS\Xml\SimpleXML::loadFile( $currentFile );
|
|
foreach ( $xml->template as $html )
|
|
{
|
|
$attributes = iterator_to_array( $html->attributes() );
|
|
|
|
$current['html'][ "{$attributes['template_location']}/{$attributes['template_group']}/{$attributes['template_name']}" ] = array(
|
|
'params' => $attributes['template_data'],
|
|
'content' => (string) $html
|
|
);
|
|
}
|
|
foreach ( $xml->css as $css )
|
|
{
|
|
$attributes = iterator_to_array( $css->attributes() );
|
|
|
|
$current['css'][ "{$attributes['css_location']}/" . ( $attributes['css_path'] == '.' ? '' : "{$attributes['css_path']}/" ) . $attributes['css_name'] ] = array(
|
|
'params' => $attributes['css_attributes'],
|
|
'content' => (string) $css
|
|
);
|
|
}
|
|
foreach ( $xml->resource as $resource )
|
|
{
|
|
$attributes = iterator_to_array( $resource->attributes() );
|
|
$current['resources'][ "{$attributes['location']}{$attributes['path']}{$attributes['name']}" ] = (string) $resource;
|
|
}
|
|
}
|
|
|
|
/* Build XML and write to app directory */
|
|
$xml = new \XMLWriter;
|
|
$xml->openMemory();
|
|
$xml->setIndent( TRUE );
|
|
$xml->startDocument( '1.0', 'UTF-8' );
|
|
|
|
/* Root tag */
|
|
$xml->startElement('theme');
|
|
$xml->startAttribute('name');
|
|
$xml->text( "Default" );
|
|
$xml->endAttribute();
|
|
$xml->startAttribute('author_name');
|
|
$xml->text( "Invision Power Services, Inc" );
|
|
$xml->endAttribute();
|
|
$xml->startAttribute('author_url');
|
|
$xml->text( "https://www.invisioncommunity.com" );
|
|
$xml->endAttribute();
|
|
|
|
/* Skin settings */
|
|
foreach (
|
|
\IPS\Db::i()->select(
|
|
'core_theme_settings_fields.*',
|
|
'core_theme_settings_fields',
|
|
array( 'sc_set_id=? AND sc_app=?', 1, $this->directory ),
|
|
'sc_key ASC'
|
|
)
|
|
as $row
|
|
)
|
|
{
|
|
/* Initiate the <fields> tag */
|
|
$xml->startElement('field');
|
|
|
|
unset( $row['sc_id'], $row['sc_set_id'] );
|
|
|
|
foreach( $row as $k => $v )
|
|
{
|
|
if ( $k != 'sc_content' )
|
|
{
|
|
$xml->startAttribute( $k );
|
|
$xml->text( $v );
|
|
$xml->endAttribute();
|
|
}
|
|
}
|
|
|
|
/* Write value */
|
|
if ( preg_match( '/<|>|&/', $row['sc_content'] ) )
|
|
{
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $row['sc_content'] ) );
|
|
}
|
|
else
|
|
{
|
|
$xml->text( $row['sc_content'] );
|
|
}
|
|
|
|
/* Close the <fields> tag */
|
|
$xml->endElement();
|
|
}
|
|
|
|
/* Templates */
|
|
foreach ( \IPS\Db::i()->select( '*', 'core_theme_templates', array( 'template_set_id=? AND template_user_added=? AND template_app=?', 0, 0 , $this->directory ), 'template_group, template_name, template_location' ) as $template )
|
|
{
|
|
/* Initiate the <template> tag */
|
|
$xml->startElement('template');
|
|
$attributes = array();
|
|
foreach( $template as $k => $v )
|
|
{
|
|
if ( \in_array( \substr( $k, 9 ), array('app', 'location', 'group', 'name', 'data' ) ) )
|
|
{
|
|
$attributes[ $k ] = $v;
|
|
$xml->startAttribute( $k );
|
|
$xml->text( $v );
|
|
$xml->endAttribute();
|
|
}
|
|
}
|
|
|
|
/* Write value */
|
|
if ( preg_match( '/<|>|&/', $template['template_content'] ) )
|
|
{
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $template['template_content'] ) );
|
|
}
|
|
else
|
|
{
|
|
$xml->text( $template['template_content'] );
|
|
}
|
|
|
|
/* Close the <template> tag */
|
|
$xml->endElement();
|
|
|
|
/* Note it */
|
|
$k = "{$attributes['template_location']}/{$attributes['template_group']}/{$attributes['template_name']}";
|
|
if ( !isset( $current['html'][ $k ] ) )
|
|
{
|
|
$return['html']['added'][] = $k;
|
|
}
|
|
elseif ( $current['html'][ $k ]['params'] != $attributes['template_data'] or $current['html'][ $k ]['content'] != $template['template_content'] )
|
|
{
|
|
$return['html']['edited'][] = $k;
|
|
}
|
|
unset( $current['html'][ $k ] );
|
|
}
|
|
|
|
/* Css */
|
|
foreach ( \IPS\Db::i()->select( '*', 'core_theme_css', array( 'css_set_id=? AND css_added_to=? AND css_app=?', 0, 0 , $this->directory ), 'css_path, css_name, css_location' ) as $css )
|
|
{
|
|
$xml->startElement('css');
|
|
$attributes = array();
|
|
foreach( $css as $k => $v )
|
|
{
|
|
if ( \in_array( \substr( $k, 4 ), array('app', 'location', 'path', 'name', 'attributes' ) ) )
|
|
{
|
|
$attributes[ $k ] = $v;
|
|
$xml->startAttribute( $k );
|
|
$xml->text( $v );
|
|
$xml->endAttribute();
|
|
}
|
|
}
|
|
|
|
/* Write value */
|
|
if ( preg_match( '/<|>|&/', $css['css_content'] ) )
|
|
{
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $css['css_content'] ) );
|
|
}
|
|
else
|
|
{
|
|
$xml->text( $css['css_content'] );
|
|
}
|
|
$xml->endElement();
|
|
|
|
/* Note it */
|
|
$k = "{$attributes['css_location']}/" . ( $attributes['css_path'] === '.' ? '' : "{$attributes['css_path']}/" ) . $attributes['css_name'];
|
|
if ( !isset( $current['css'][ $k ] ) )
|
|
{
|
|
$return['css']['added'][] = $k;
|
|
}
|
|
elseif ( $current['css'][ $k ]['params'] != $attributes['css_attributes'] or $current['css'][ $k ]['content'] != $css['css_content'] )
|
|
{
|
|
$return['css']['edited'][] = $k;
|
|
}
|
|
unset( $current['css'][ $k ] );
|
|
}
|
|
|
|
/* Resources */
|
|
$_resources = $this->_buildThemeResources();
|
|
|
|
foreach ( $_resources as $data )
|
|
{
|
|
$xml->startElement('resource');
|
|
|
|
$xml->startAttribute('name');
|
|
$xml->text( $data['resource_name'] );
|
|
$xml->endAttribute();
|
|
|
|
$xml->startAttribute('app');
|
|
$xml->text( $data['resource_app'] );
|
|
$xml->endAttribute();
|
|
|
|
$xml->startAttribute('location');
|
|
$xml->text( $data['resource_location'] );
|
|
$xml->endAttribute();
|
|
|
|
$xml->startAttribute('path');
|
|
$xml->text( $data['resource_path'] );
|
|
$xml->endAttribute();
|
|
|
|
/* Write value */
|
|
$encoded = base64_encode( $data['resource_data'] );
|
|
$xml->text( $encoded );
|
|
|
|
$xml->endElement();
|
|
|
|
/* Note it */
|
|
$k = "{$data['resource_location']}{$data['resource_path']}{$data['resource_name']}";
|
|
if ( !isset( $current['resources'][ $k ] ) )
|
|
{
|
|
$return['resources']['added'][] = $k;
|
|
}
|
|
elseif ( $current['resources'][ $k ] != $encoded )
|
|
{
|
|
$return['resources']['edited'][] = $k;
|
|
}
|
|
unset( $current['resources'][ $k ] );
|
|
}
|
|
|
|
/* Finish */
|
|
$xml->endDocument();
|
|
|
|
/* Write it */
|
|
if ( is_writable( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data' ) )
|
|
{
|
|
\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/theme.xml', $xml->outputMemory() );
|
|
}
|
|
else
|
|
{
|
|
throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
|
|
}
|
|
|
|
/* Return */
|
|
$return['html']['removed'] = array_keys( $current['html'] );
|
|
$return['css']['removed'] = array_keys( $current['css'] );
|
|
$return['resources']['removed'] = array_keys( $current['resources'] );
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Build Resources ready for non IN_DEV use
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function _buildThemeResources()
|
|
{
|
|
$resources = array();
|
|
$path = \IPS\ROOT_PATH . "/applications/" . $this->directory . "/dev/resources/";
|
|
|
|
\IPS\Theme::i()->importDevResources( $this->directory, 0 );
|
|
|
|
if ( is_dir( $path ) )
|
|
{
|
|
foreach( new \DirectoryIterator( $path ) as $location )
|
|
{
|
|
if ( $location->isDot() || \substr( $location->getFilename(), 0, 1 ) === '.' )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( $location->isDir() )
|
|
{
|
|
$resources = $this->_buildResourcesRecursive( $location->getFilename(), '/', $resources );
|
|
}
|
|
}
|
|
}
|
|
|
|
return $resources;
|
|
}
|
|
|
|
/**
|
|
* Build Resources ready for non IN_DEV use (Iterable)
|
|
* Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
|
|
*
|
|
* @param string $location Location Folder Name
|
|
* @param string $path Path
|
|
* @param array $resources Array of resources to append to
|
|
* @return array
|
|
*/
|
|
protected function _buildResourcesRecursive( $location, $path='/', $resources=array() )
|
|
{
|
|
$root = \IPS\ROOT_PATH . "/applications/{$this->directory}/dev/resources/{$location}";
|
|
|
|
foreach( new \DirectoryIterator( $root . $path ) as $file )
|
|
{
|
|
if ( $file->isDot() || \substr( $file->getFilename(), 0, 1 ) === '.' || $file == 'index.html' )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( $file->isDir() )
|
|
{
|
|
$resources = $this->_buildResourcesRecursive( $location, $path . $file->getFilename() . '/', $resources );
|
|
}
|
|
else
|
|
{
|
|
$resources[] = array(
|
|
'resource_app' => $this->directory,
|
|
'resource_location' => $location,
|
|
'resource_path' => $path,
|
|
'resource_name' => $file->getFilename(),
|
|
'resource_data' => \file_get_contents( $root . $path . $file->getFilename() ),
|
|
'resource_added' => time()
|
|
);
|
|
}
|
|
}
|
|
|
|
return $resources;
|
|
}
|
|
|
|
/**
|
|
* Build languages for an app
|
|
*
|
|
* @return array
|
|
* @throws \RuntimeException
|
|
*/
|
|
public function buildLanguages()
|
|
{
|
|
$return = array( 'normal' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ), 'js' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ) );
|
|
|
|
/* Start with current XML file */
|
|
$currentFile = \IPS\ROOT_PATH . "/applications/{$this->directory}/data/lang.xml";
|
|
|
|
$current = array( '0' => array(), '1' => array() );
|
|
|
|
if ( file_exists( $currentFile ) )
|
|
{
|
|
foreach ( \IPS\Xml\SimpleXML::loadFile( $currentFile )->app->word as $word )
|
|
{
|
|
$attributes = iterator_to_array( $word->attributes() );
|
|
$current[ (string) $attributes['js'] ][ (string) $attributes['key'] ] = (string) $word;
|
|
}
|
|
}
|
|
|
|
/* Create the lang.xml file */
|
|
$xml = new \XMLWriter;
|
|
$xml->openMemory();
|
|
$xml->setIndent( TRUE );
|
|
$xml->startDocument( '1.0', 'UTF-8' );
|
|
|
|
/* Root tag */
|
|
$xml->startElement('language');
|
|
|
|
/* Initiate the <app> tag */
|
|
$xml->startElement('app');
|
|
|
|
/* Set key */
|
|
$xml->startAttribute('key');
|
|
$xml->text( $this->directory );
|
|
$xml->endAttribute();
|
|
|
|
/* Set version */
|
|
$xml->startAttribute('version');
|
|
$xml->text( $this->long_version );
|
|
$xml->endAttribute();
|
|
|
|
/* Import the language files */
|
|
$lang = array();
|
|
|
|
require \IPS\ROOT_PATH . "/applications/{$this->directory}/dev/lang.php";
|
|
foreach ( $lang as $k => $v )
|
|
{
|
|
/* Start */
|
|
$xml->startElement( 'word' );
|
|
|
|
/* Add key */
|
|
$xml->startAttribute('key');
|
|
$xml->text( $k );
|
|
$xml->endAttribute();
|
|
|
|
/* Add javascript flag */
|
|
$xml->startAttribute('js');
|
|
$xml->text( 0 );
|
|
$xml->endAttribute();
|
|
|
|
/* Write value */
|
|
if ( preg_match( '/<|>|&/', $v ) )
|
|
{
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $v ) );
|
|
}
|
|
else
|
|
{
|
|
$xml->text( $v );
|
|
}
|
|
|
|
/* End */
|
|
$xml->endElement();
|
|
|
|
/* Note it */
|
|
if ( !isset( $current['0'][ $k ] ) )
|
|
{
|
|
$return['normal']['added'][] = $k;
|
|
}
|
|
elseif ( isset( $current['0'][ $k ] ) AND $current['0'][ $k ] != $v )
|
|
{
|
|
$return['normal']['edited'][] = $k;
|
|
}
|
|
|
|
if ( isset( $current['0'][ $k ] ) )
|
|
{
|
|
unset( $current['0'][ $k ] );
|
|
}
|
|
}
|
|
|
|
$lang = array();
|
|
|
|
require \IPS\ROOT_PATH . "/applications/{$this->directory}/dev/jslang.php";
|
|
foreach ( $lang as $k => $v )
|
|
{
|
|
/* Start */
|
|
$xml->startElement( 'word' );
|
|
|
|
/* Add key */
|
|
$xml->startAttribute('key');
|
|
$xml->text( $k );
|
|
$xml->endAttribute();
|
|
|
|
/* Add javascript flag */
|
|
$xml->startAttribute('js');
|
|
$xml->text( 1 );
|
|
$xml->endAttribute();
|
|
|
|
/* Write value */
|
|
if ( preg_match( '/<|>|&/', $v ) )
|
|
{
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $v ) );
|
|
}
|
|
else
|
|
{
|
|
$xml->text( $v );
|
|
}
|
|
|
|
/* End */
|
|
$xml->endElement();
|
|
|
|
/* Note it */
|
|
if ( !isset( $current['1'][ $k ] ) )
|
|
{
|
|
$return['js']['added'][] = $k;
|
|
}
|
|
elseif ( isset( $current['1'][ $k ] ) AND $current['1'][ $k ] != $v )
|
|
{
|
|
$return['js']['edited'][] = $k;
|
|
}
|
|
if ( isset( $current['1'][ $k ] ) )
|
|
{
|
|
unset( $current['1'][ $k ] );
|
|
}
|
|
}
|
|
|
|
/* Finish */
|
|
$xml->endDocument();
|
|
|
|
/* Write it */
|
|
if ( is_writable( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data' ) )
|
|
{
|
|
\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/lang.xml', $xml->outputMemory() );
|
|
}
|
|
else
|
|
{
|
|
throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
|
|
}
|
|
|
|
/* Return */
|
|
$return['normal']['removed'] = array_keys( $current['0'] );
|
|
$return['js']['removed'] = array_keys( $current['1'] );
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Build email templates for an app
|
|
*
|
|
* @return void
|
|
* @throws \RuntimeException
|
|
*/
|
|
public function buildEmailTemplates()
|
|
{
|
|
/* Get current XML file for calculating differences */
|
|
$return = array( 'added' => array(), 'edited' => array(), 'removed' => array() );
|
|
$current = array();
|
|
$currentFile = \IPS\ROOT_PATH . "/applications/{$this->directory}/data/emails.xml";
|
|
if ( file_exists( $currentFile ) )
|
|
{
|
|
$xml = \IPS\Xml\SimpleXML::loadFile( $currentFile );
|
|
foreach ( $xml->template as $template )
|
|
{
|
|
$attributes = iterator_to_array( $template->attributes() );
|
|
|
|
$current[ (string) (string) $template->template_name ] = array(
|
|
'params' => (string) $template->template_data,
|
|
'html' => (string) $template->template_content_html,
|
|
'plaintext' => (string) $template->template_content_plaintext,
|
|
'pinned' => (string) $template->template_pinned,
|
|
);
|
|
}
|
|
}
|
|
|
|
/* Where are we looking? */
|
|
$path = \IPS\ROOT_PATH . "/applications/{$this->directory}/dev/email";
|
|
|
|
/* We create an array and store the templates temporarily so we can merge plaintext and HTML together */
|
|
$templates = array();
|
|
$templateKeys = array();
|
|
|
|
/* Loop over files in the directory */
|
|
if ( is_dir( $path ) )
|
|
{
|
|
foreach( new \DirectoryIterator( $path ) as $location )
|
|
{
|
|
if ( $location->isDir() and mb_substr( $location, 0, 1 ) !== '.' and ( $location->getFilename() === 'plain' or $location->getFilename() === 'html' ) )
|
|
{
|
|
foreach( new \DirectoryIterator( $path . '/' . $location->getFilename() ) as $sublocation )
|
|
{
|
|
if ( $sublocation->isDir() and mb_substr( $sublocation, 0, 1 ) !== '.' )
|
|
{
|
|
foreach( new \DirectoryIterator( $path . '/' . $location->getFilename() . '/' . $sublocation->getFilename() ) as $file )
|
|
{
|
|
if ( $file->isDot() or !$file->isFile() or mb_substr( $file, 0, 1 ) === '.' or $file->getFilename() === 'index.html' )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$data = $this->_buildEmailTemplateFromInDev( $path . '/' . $location->getFilename() . '/' . $sublocation->getFilename(), $file, $sublocation->getFilename() . '__' );
|
|
$extension = mb_substr( $file->getFilename(), mb_strrpos( $file->getFilename(), '.' ) + 1 );
|
|
$type = ( $extension === 'txt' ) ? "plaintext" : "html";
|
|
|
|
if ( ! isset( $templates[ $data['template_name'] ] ) )
|
|
{
|
|
$templates[ $data['template_name'] ] = array();
|
|
}
|
|
|
|
$templates[ $data['template_name'] ] = array_merge( $templates[ $data['template_name'] ], $data );
|
|
|
|
/* Delete the template in the store */
|
|
$key = $templates[ $data['template_name'] ]['template_key'] . '_email_' . $type;
|
|
unset( \IPS\Data\Store::i()->$key );
|
|
|
|
/* Remember our templates */
|
|
$templateKeys[] = $data['template_key'];
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
if ( $location->isDot() or !$location->isFile() or mb_substr( $location, 0, 1 ) === '.' or $location->getFilename() === 'index.html' )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$data = $this->_buildEmailTemplateFromInDev( $path, $location );
|
|
$extension = mb_substr( $location->getFilename(), mb_strrpos( $location->getFilename(), '.' ) + 1 );
|
|
$type = ( $extension === 'txt' ) ? "plaintext" : "html";
|
|
|
|
if ( ! isset( $templates[ $data['template_name'] ] ) )
|
|
{
|
|
$templates[ $data['template_name'] ] = array();
|
|
}
|
|
|
|
$templates[ $data['template_name'] ] = array_merge( $templates[ $data['template_name'] ], $data );
|
|
|
|
/* Delete the template in the store */
|
|
$key = $templates[ $data['template_name'] ]['template_key'] . '_email_' . $type;
|
|
unset( \IPS\Data\Store::i()->$key );
|
|
|
|
/* Remember our templates */
|
|
$templateKeys[] = $data['template_key'];
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Clear out invalid templates */
|
|
\IPS\Db::i()->delete( 'core_email_templates', array( "template_app=? AND template_key NOT IN('" . implode( "','", $templateKeys ) . "')", $this->directory ) );
|
|
|
|
/* If we have any templates, put them in the database */
|
|
if( \count($templates) )
|
|
{
|
|
foreach( $templates as $template )
|
|
{
|
|
\IPS\Db::i()->insert( 'core_email_templates', $template, TRUE );
|
|
}
|
|
|
|
/* Build the executable copies */
|
|
$this->parseEmailTemplates();
|
|
}
|
|
|
|
$xml = \IPS\Xml\SimpleXML::create('emails');
|
|
|
|
/* Templates */
|
|
foreach ( \IPS\Db::i()->select( '*', 'core_email_templates', array( 'template_parent=? AND template_app=?', 0, $this->directory ), 'template_key ASC' ) as $template )
|
|
{
|
|
$forXml = array();
|
|
foreach( $template as $k => $v )
|
|
{
|
|
if ( \in_array( \substr( $k, 9 ), array('app', 'name', 'content_html', 'data', 'content_plaintext', 'pinned' ) ) )
|
|
{
|
|
$forXml[ $k ] = $v;
|
|
}
|
|
}
|
|
|
|
$xml->addChild( 'template', $forXml );
|
|
|
|
/* Note it */
|
|
$compare = array(
|
|
'params' => $template['template_data'],
|
|
'html' => $template['template_content_html'],
|
|
'plaintext' => $template['template_content_plaintext'],
|
|
'pinned' => $template['template_pinned']
|
|
);
|
|
if ( !isset( $current[ $template['template_name'] ] ) )
|
|
{
|
|
$return['added'][] = $template['template_name'];
|
|
}
|
|
elseif ( $current[ $template['template_name'] ] != $compare )
|
|
{
|
|
$return['edited'][] = $template['template_name'];
|
|
}
|
|
unset( $current[ $template['template_name'] ] );
|
|
}
|
|
|
|
/* Write it */
|
|
if ( is_writable( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data' ) )
|
|
{
|
|
\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/emails.xml', $xml->asXML() );
|
|
}
|
|
else
|
|
{
|
|
throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
|
|
}
|
|
|
|
/* Return */
|
|
$return['removed'] = array_keys( $current );
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Imports an IN_DEV email template into the database
|
|
*
|
|
* @param string $path Path to file
|
|
* @param object $file DirectoryIterator File Object
|
|
* @param string|null $namePrefix Name prefix
|
|
* @return array
|
|
*/
|
|
protected function _buildEmailTemplateFromInDev( $path, $file, $namePrefix='' )
|
|
{
|
|
/* Get the content */
|
|
$html = file_get_contents( $path . '/' . $file->getFilename() );
|
|
$params = array();
|
|
|
|
/* Parse the header tag */
|
|
preg_match( '/^<ips:template parameters="(.+?)?" \/>(\r\n?|\n)/', $html, $params );
|
|
|
|
/* Strip the params tag */
|
|
$html = str_replace( $params[0], '', $html );
|
|
|
|
/* Figure out some details */
|
|
$extension = mb_substr( $file->getFilename(), mb_strrpos( $file->getFilename(), '.' ) + 1 );
|
|
$name = $namePrefix . str_replace( '.' . $extension, '', $file->getFilename() );
|
|
$type = ( $extension === 'txt' ) ? "plaintext" : "html";
|
|
|
|
$return = array(
|
|
'template_app' => $this->directory,
|
|
'template_name' => $name,
|
|
'template_data' => ( isset( $params[1] ) ) ? $params[1] : '',
|
|
'template_content_' . $type => $html,
|
|
'template_key' => md5( $this->directory . ';' . $name ),
|
|
);
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Build javascript for this app
|
|
*
|
|
* @return array
|
|
* @throws \RuntimeException
|
|
*/
|
|
public function buildJavascript()
|
|
{
|
|
/* Get current XML file for calculating differences */
|
|
$return = array( 'files' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ), 'orders' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ) );
|
|
$currentFile = \IPS\ROOT_PATH . "/applications/{$this->directory}/data/javascript.xml";
|
|
$current = array( 'files' => array(), 'orders' => array() );
|
|
if ( file_exists( $currentFile ) )
|
|
{
|
|
$xml = \IPS\Xml\SimpleXML::loadFile( $currentFile );
|
|
foreach ( $xml->file as $javascript )
|
|
{
|
|
$attributes = iterator_to_array( $javascript->attributes() );
|
|
|
|
$current['files'][ "{$attributes['javascript_app']}/{$attributes['javascript_location']}/" . ( trim( $attributes['javascript_path'] ) ? "{$attributes['javascript_path']}/" : '' ) . "{$attributes['javascript_name']}" ] = (string) $javascript;
|
|
}
|
|
foreach ( $xml->order as $order )
|
|
{
|
|
$attributes = iterator_to_array( $order->attributes() );
|
|
|
|
$current['orders'][ "{$attributes['app']}/{$attributes['path']}" ] = (string) $order;
|
|
}
|
|
}
|
|
|
|
/* Remove existing file object maps */
|
|
$map = isset( \IPS\Data\Store::i()->javascript_map ) ? \IPS\Data\Store::i()->javascript_map : array();
|
|
$map[ $this->directory ] = array();
|
|
|
|
\IPS\Data\Store::i()->javascript_map = $map;
|
|
|
|
$xml = \IPS\Output\Javascript::createXml( $this->directory, $current, $return );
|
|
|
|
/* Write it */
|
|
if ( is_writable( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data' ) )
|
|
{
|
|
\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/javascript.xml', $xml->outputMemory() );
|
|
}
|
|
else
|
|
{
|
|
throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
|
|
}
|
|
|
|
/* Return */
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Build hooks for an app
|
|
*
|
|
* @return void
|
|
* @throws \RuntimeException
|
|
*/
|
|
public function buildHooks()
|
|
{
|
|
/* Build data */
|
|
$data = array();
|
|
foreach ( \IPS\Db::i()->select( '*', 'core_hooks', array( 'app=?', $this->directory ) ) as $hook )
|
|
{
|
|
$data[ $hook['filename'] ] = array(
|
|
'type' => $hook['type'],
|
|
'class' => $hook['class'],
|
|
);
|
|
}
|
|
|
|
/* Write it */
|
|
try
|
|
{
|
|
\IPS\Application::writeJson( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/hooks.json', $data );
|
|
}
|
|
catch ( \RuntimeException $e )
|
|
{
|
|
throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build extensions.json file for an app
|
|
*
|
|
* @return array
|
|
* @throws \DomainException
|
|
*/
|
|
public function buildExtensionsJson()
|
|
{
|
|
$json = array();
|
|
$appsMainExtensionDir = new \DirectoryIterator( \IPS\ROOT_PATH . "/applications/{$this->directory}/extensions/" );
|
|
foreach ( $appsMainExtensionDir as $appDir )
|
|
{
|
|
if ( $appDir->isDir() and !$appDir->isDot() )
|
|
{
|
|
foreach ( new \DirectoryIterator( $appDir->getPathname() ) as $extensionDir )
|
|
{
|
|
if ( $extensionDir->isDir() and !$extensionDir->isDot() )
|
|
{
|
|
foreach ( new \DirectoryIterator( $extensionDir->getPathname() ) as $extensionFile )
|
|
{
|
|
if ( !$extensionFile->isDir() and !$extensionFile->isDot() and mb_substr( $extensionFile, -4 ) === '.php' AND mb_substr( $extensionFile, 0, 2 ) != '._' )
|
|
{
|
|
$classname = 'IPS\\' . $this->directory . '\extensions\\' . $appDir . '\\' . $extensionDir . '\\' . mb_substr( $extensionFile, 0, -4 );
|
|
|
|
/* Check if class exists - sometimes we have to use blank files to wipe out old extensions */
|
|
try
|
|
{
|
|
if( !class_exists( $classname ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( method_exists( $classname, 'deprecated' ) )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
catch( \ErrorException $e )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$json[ (string) $appDir ][ (string) $extensionDir ][ mb_substr( $extensionFile, 0, -4 ) ] = $classname;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $json;
|
|
}
|
|
|
|
/**
|
|
* Write a build.xml file with the current json data so we know what has changed between builds
|
|
*
|
|
* @return void
|
|
*/
|
|
public function buildJsonData()
|
|
{
|
|
$file = \IPS\ROOT_PATH . "/applications/{$this->directory}/data/build.xml";
|
|
|
|
/* Get current XML file for calculating differences */
|
|
$return = array(
|
|
'modules' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ),
|
|
'tasks' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ),
|
|
'settings' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ),
|
|
'widgets' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ),
|
|
'acpSearchKeywords' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ),
|
|
'hooks' => array( 'added' => array(), 'edited' => array(), 'removed' => array() ),
|
|
'themeSettings' => array( 'added' => array(), 'edited' => array(), 'removed' => array() )
|
|
);
|
|
$current = array(
|
|
'modules' => array(),
|
|
'tasks' => array(),
|
|
'settings' => array(),
|
|
'widgets' => array(),
|
|
'acpSearchKeywords' => array(),
|
|
'hooks' => array(),
|
|
'themeSettings' => array()
|
|
);
|
|
if ( file_exists( $file ) )
|
|
{
|
|
$xml = \IPS\Xml\SimpleXML::loadFile( $file );
|
|
foreach ( $xml->module as $module )
|
|
{
|
|
$attributes = iterator_to_array( $module->attributes() );
|
|
$current['modules'][ (string) $module['key'] ] = (string) $module;
|
|
}
|
|
foreach ( $xml->task as $task )
|
|
{
|
|
$attributes = iterator_to_array( $task->attributes() );
|
|
$current['tasks'][ (string) $task ] = (string) $task['frequency'];
|
|
}
|
|
foreach ( $xml->setting as $setting )
|
|
{
|
|
$attributes = iterator_to_array( $setting->attributes() );
|
|
$current['settings'][ (string) $setting['key'] ] = (string) $setting;
|
|
}
|
|
foreach ( $xml->widget as $widget )
|
|
{
|
|
$attributes = iterator_to_array( $widget->attributes() );
|
|
$current['widgets'][ (string) $attributes['key'] ] = (string) $widget;
|
|
}
|
|
foreach ( $xml->acpsearch as $searchKeyword )
|
|
{
|
|
$attributes = iterator_to_array( $searchKeyword->attributes() );
|
|
$current['acpSearchKeywords'][ (string) $attributes['key'] ] = (string) $searchKeyword;
|
|
}
|
|
foreach ( $xml->hook as $hook )
|
|
{
|
|
$attributes = iterator_to_array( $hook->attributes() );
|
|
$current['hooks'][ (string) $attributes['key'] ] = (string) $hook;
|
|
}
|
|
foreach ( $xml->themesetting as $themeSetting )
|
|
{
|
|
$attributes = iterator_to_array( $themeSetting->attributes() );
|
|
$current['themeSettings'][ (string) $attributes['key'] ] = (string) $themeSetting;
|
|
}
|
|
}
|
|
|
|
/* Build XML and write to app directory */
|
|
$xml = new \XMLWriter;
|
|
$xml->openMemory();
|
|
$xml->setIndent( TRUE );
|
|
$xml->startDocument( '1.0', 'UTF-8' );
|
|
|
|
/* Root tag */
|
|
$xml->startElement('build');
|
|
|
|
/* Modules */
|
|
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/modules.json" ) )
|
|
{
|
|
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/modules.json" ), TRUE ) as $area => $modules )
|
|
{
|
|
foreach ( $modules as $moduleKey => $moduleData )
|
|
{
|
|
$val = json_encode( $moduleData );
|
|
|
|
$xml->startElement('module');
|
|
$xml->startAttribute('key');
|
|
$xml->text( "{$area}/{$moduleKey}" );
|
|
$xml->endAttribute();
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $val ) );
|
|
$xml->endElement();
|
|
|
|
if ( !isset( $current['modules'][ "{$area}/{$moduleKey}" ] ) )
|
|
{
|
|
$return['modules']['added'][] = "{$area}/{$moduleKey}";
|
|
}
|
|
elseif ( $current['modules'][ "{$area}/{$moduleKey}" ] != $val )
|
|
{
|
|
$return['modules']['edited'][] = "{$area}/{$moduleKey}";
|
|
}
|
|
unset( $current['modules'][ "{$area}/{$moduleKey}" ] );
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Tasks */
|
|
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/tasks.json" ) )
|
|
{
|
|
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/tasks.json" ), TRUE ) as $taskKey => $taskFrequency )
|
|
{
|
|
$xml->startElement('task');
|
|
$xml->startAttribute('frequency');
|
|
$xml->text( $taskFrequency );
|
|
$xml->endAttribute();
|
|
$xml->text( $taskKey );
|
|
$xml->endElement();
|
|
|
|
if ( !isset( $current['tasks'][ $taskKey ] ) )
|
|
{
|
|
$return['tasks']['added'][] = $taskKey;
|
|
}
|
|
elseif ( $current['tasks'][ $taskKey ] != $taskFrequency )
|
|
{
|
|
$return['tasks']['edited'][] = $taskKey;
|
|
}
|
|
unset( $current['tasks'][ $taskKey ] );
|
|
}
|
|
}
|
|
|
|
/* Settings */
|
|
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/settings.json" ) )
|
|
{
|
|
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/settings.json" ), TRUE ) as $setting )
|
|
{
|
|
$val = json_encode( $setting );
|
|
|
|
$xml->startElement('setting');
|
|
$xml->startAttribute('key');
|
|
$xml->text( $setting['key'] );
|
|
$xml->endAttribute();
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $val ) );
|
|
$xml->endElement();
|
|
|
|
if ( !isset( $current['settings'][ $setting['key'] ] ) )
|
|
{
|
|
$return['settings']['added'][] = $setting['key'];
|
|
}
|
|
elseif ( $current['settings'][ $setting['key'] ] != $val )
|
|
{
|
|
$return['settings']['edited'][] = $setting['key'];
|
|
}
|
|
unset( $current['settings'][ $setting['key'] ] );
|
|
}
|
|
}
|
|
|
|
/* Widgets */
|
|
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/widgets.json" ) )
|
|
{
|
|
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/widgets.json" ), TRUE ) as $widgetKey => $widgetData )
|
|
{
|
|
$val = json_encode( $widgetData );
|
|
|
|
$xml->startElement('widget');
|
|
$xml->startAttribute('key');
|
|
$xml->text( $widgetKey );
|
|
$xml->endAttribute();
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $val ) );
|
|
$xml->endElement();
|
|
|
|
if ( !isset( $current['widgets'][ $widgetKey ] ) )
|
|
{
|
|
$return['widgets']['added'][] = $widgetKey;
|
|
}
|
|
elseif ( $current['widgets'][ $widgetKey ] != $val )
|
|
{
|
|
$return['widgets']['edited'][] = $widgetKey;
|
|
}
|
|
unset( $current['widgets'][ $widgetKey ] );
|
|
}
|
|
}
|
|
|
|
/* ACP Search Keywords */
|
|
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/acpsearch.json" ) )
|
|
{
|
|
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/acpsearch.json" ), TRUE ) as $searchUrl => $searchData )
|
|
{
|
|
$val = json_encode( $searchData );
|
|
|
|
$xml->startElement('acpsearch');
|
|
$xml->startAttribute('key');
|
|
$xml->text( $searchUrl );
|
|
$xml->endAttribute();
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $val ) );
|
|
$xml->endElement();
|
|
|
|
if ( !isset( $current['acpSearchKeywords'][ $searchUrl ] ) )
|
|
{
|
|
$return['acpSearchKeywords']['added'][] = $searchUrl;
|
|
}
|
|
elseif ( $current['acpSearchKeywords'][ $searchUrl ] != $val )
|
|
{
|
|
$return['acpSearchKeywords']['edited'][] = $searchUrl;
|
|
}
|
|
unset( $current['acpSearchKeywords'][ $searchUrl ] );
|
|
}
|
|
}
|
|
|
|
/* Hooks */
|
|
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/hooks.json" ) )
|
|
{
|
|
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/hooks.json" ), TRUE ) as $hookKey => $hookData )
|
|
{
|
|
$val = json_encode( $hookData );
|
|
|
|
$xml->startElement('hook');
|
|
$xml->startAttribute('key');
|
|
$xml->text( $hookKey );
|
|
$xml->endAttribute();
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $val ) );
|
|
$xml->endElement();
|
|
|
|
if ( !isset( $current['hooks'][ $hookKey ] ) )
|
|
{
|
|
$return['hooks']['added'][] = $hookKey;
|
|
}
|
|
elseif ( $current['hooks'][ $hookKey ] != $val )
|
|
{
|
|
$return['hooks']['edited'][] = $hookKey;
|
|
}
|
|
unset( $current['hooks'][ $hookKey ] );
|
|
}
|
|
}
|
|
|
|
/* Master Theme Settings */
|
|
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/themesettings.json" ) )
|
|
{
|
|
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/themesettings.json" ), TRUE ) as $themeSetting )
|
|
{
|
|
$val = json_encode( $themeSetting );
|
|
|
|
$xml->startElement('themesetting');
|
|
$xml->startAttribute('key');
|
|
$xml->text( $themeSetting['sc_key'] );
|
|
$xml->endAttribute();
|
|
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $val ) );
|
|
$xml->endElement();
|
|
|
|
if ( !isset( $current['themeSettings'][ $themeSetting['sc_key'] ] ) )
|
|
{
|
|
$return['themeSettings']['added'][] = $themeSetting['sc_key'];
|
|
}
|
|
elseif ( $current['themeSettings'][ $themeSetting['sc_key'] ] != $val )
|
|
{
|
|
$return['themeSettings']['edited'][] = $themeSetting['sc_key'];
|
|
}
|
|
unset( $current['themeSettings'][ $themeSetting['sc_key'] ] );
|
|
}
|
|
}
|
|
|
|
/* Finish */
|
|
$xml->endDocument();
|
|
|
|
/* Write it */
|
|
\file_put_contents( $file, $xml->outputMemory() );
|
|
|
|
/* Return */
|
|
$return['modules']['removed'] = array_keys( $current['modules'] );
|
|
$return['tasks']['removed'] = array_keys( $current['tasks'] );
|
|
$return['settings']['removed'] = array_keys( $current['settings'] );
|
|
$return['widgets']['removed'] = array_keys( $current['widgets'] );
|
|
$return['acpSearchKeywords']['removed'] = array_keys( $current['acpSearchKeywords'] );
|
|
$return['hooks']['removed'] = array_keys( $current['hooks'] );
|
|
$return['themeSettings']['removed'] = array_keys( $current['themeSettings'] );
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Compile email template into executable template
|
|
*
|
|
* @return void
|
|
*/
|
|
public function parseEmailTemplates()
|
|
{
|
|
foreach( \IPS\Db::i()->select( '*','core_email_templates', NULL, 'template_parent DESC' ) as $template )
|
|
{
|
|
/* Rebuild built copies */
|
|
$htmlFunction = 'namespace IPS\Theme;' . "\n" . \IPS\Theme::compileTemplate( $template['template_content_html'], "email_html_{$template['template_app']}_{$template['template_name']}", $template['template_data'] );
|
|
$ptFunction = 'namespace IPS\Theme;' . "\n" . \IPS\Theme::compileTemplate( $template['template_content_plaintext'], "email_plaintext_{$template['template_app']}_{$template['template_name']}", $template['template_data'] );
|
|
|
|
$key = $template['template_key'] . '_email_html';
|
|
\IPS\Data\Store::i()->$key = $htmlFunction;
|
|
|
|
$key = $template['template_key'] . '_email_plaintext';
|
|
\IPS\Data\Store::i()->$key = $ptFunction;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write JSON file
|
|
*
|
|
* @param string $file Filepath
|
|
* @param array $data Data to write
|
|
* @return void
|
|
* @throws \RuntimeException Could not write
|
|
*/
|
|
public static function writeJson( $file, $data )
|
|
{
|
|
$json = json_encode( $data, JSON_PRETTY_PRINT );
|
|
|
|
/* No idea why, but for some people blank structures have line breaks in them and for some people they don't
|
|
which unecessarily makes version control think things have changed - so let's make it the same for everyone */
|
|
$json = preg_replace( '/\[\s*\]/', '[]', $json );
|
|
$json = preg_replace( '/\{\s*\}/', '{}', $json );
|
|
|
|
/* Write it */
|
|
if( \file_put_contents( $file, $json ) === FALSE )
|
|
{
|
|
throw new \RuntimeException;
|
|
}
|
|
@chmod( $file, 0777 );
|
|
}
|
|
|
|
/**
|
|
* Can the user access this application?
|
|
*
|
|
* @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( $memberOrGroup=NULL )
|
|
{
|
|
/* If it's not enabled, we can't */
|
|
if( !$this->enabled )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
/* If we are in the AdminCP, and we have permission to manage applications, then we have access */
|
|
if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'admin' AND \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'app_manage' ) )
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
/* If all groups have access, we can */
|
|
if( $this->disabled_groups === NULL )
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
/* If all groups do not have access, we cannot */
|
|
if( $this->disabled_groups == '*' )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
/* Check member */
|
|
if ( $memberOrGroup instanceof \IPS\Member\Group )
|
|
{
|
|
$memberGroups = array( $memberOrGroup->g_id );
|
|
}
|
|
else
|
|
{
|
|
$member = ( $memberOrGroup === NULL ) ? \IPS\Member::loggedIn() : $memberOrGroup;
|
|
$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? */
|
|
if( \count( array_intersect( $accessGroups, $memberGroups ) ) )
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Can manage the widgets
|
|
*
|
|
* @param \IPS\Member|NULL $member Member we are checking against or NULL for currently logged on user
|
|
* @return boolean
|
|
*/
|
|
public function canManageWidgets( $member=NULL )
|
|
{
|
|
/* Check member */
|
|
$member = ( $member === NULL ) ? \IPS\Member::loggedIn() : $member;
|
|
|
|
return $member->modPermission('can_manage_sidebar');
|
|
}
|
|
|
|
/**
|
|
* Save Changes
|
|
*
|
|
* @param bool $skipMember Skip clearing member cache clearing
|
|
* @return void
|
|
*/
|
|
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 );
|
|
unset( \IPS\Data\Store::i()->acpNotifications );
|
|
unset( \IPS\Data\Store::i()->acpNotificationIds );
|
|
unset( \IPS\Data\Store::i()->furl_configuration );
|
|
|
|
/* Clear out member's cached "Create Menu" contents */
|
|
if( !$skipMember )
|
|
{
|
|
\IPS\Member::clearCreateMenu();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete Record
|
|
*
|
|
* @return void
|
|
*/
|
|
public function delete()
|
|
{
|
|
/* Get our uninstall callback script(s) if present. They are stored in an array so that we only create one object per extension, instead of one each time we loop. */
|
|
$uninstallExtensions = array();
|
|
foreach( $this->extensions( 'core', 'Uninstall', TRUE ) as $extension )
|
|
{
|
|
$uninstallExtensions[] = $extension;
|
|
}
|
|
|
|
/* Call preUninstall() so that application may perform any necessary cleanup before other data is removed (i.e. database tables) */
|
|
foreach( $uninstallExtensions as $extension )
|
|
{
|
|
if( method_exists( $extension, 'preUninstall' ) )
|
|
{
|
|
$extension->preUninstall( $this->directory );
|
|
}
|
|
}
|
|
|
|
/* Call onOtherAppUninstall so that other applications may perform any necessary cleanup */
|
|
/* @todo This is deprecated - we need to migrate to onOtherUninstall() but I'm leaving this call as-is to prevent breaking
|
|
backwards-compatibility until a major release */
|
|
foreach( static::allExtensions( 'core', 'Uninstall', FALSE ) as $extension )
|
|
{
|
|
if( method_exists( $extension, 'onOtherAppUninstall' ) )
|
|
{
|
|
$extension->onOtherAppUninstall( $this->directory );
|
|
}
|
|
}
|
|
|
|
$templatesToRecompile = array();
|
|
|
|
/* Note any templates that will need recompiling */
|
|
foreach ( \IPS\Db::i()->select( 'class', 'core_hooks', array( 'app=? AND type=?', $this->directory, 'S' ) ) as $class )
|
|
{
|
|
$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 ) ) ) );
|
|
\IPS\Db::i()->delete( 'core_modules', array( 'sys_module_application=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_dev', array( 'app_key=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_hooks', array( 'app=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_item_markers', array( 'item_app=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_reputation_index', array( 'app=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_permission_index', array( 'app=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_upgrade_history', array( 'upgrade_app=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_admin_logs', array( 'appcomponent=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_sys_conf_settings', array( 'conf_app=?', $this->directory ) );
|
|
\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 ) );
|
|
\IPS\Db::i()->delete( 'core_member_history', array( 'log_app=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_acp_notifications', array( 'app=?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_solved_index', array( 'app=?', $this->directory ) );
|
|
|
|
foreach( $this->extensions( 'core', 'AdminNotifications', FALSE ) AS $adminNotificationExtension )
|
|
{
|
|
$exploded = explode( '\\', $adminNotificationExtension );
|
|
\IPS\Db::i()->delete( 'core_acp_notifications_preferences', array( 'type=?', "{$this->directory}_{$exploded[5]}" ) );
|
|
}
|
|
|
|
$classes = array();
|
|
foreach( $this->extensions( 'core', 'ContentRouter' ) AS $contentRouter )
|
|
{
|
|
foreach ( $contentRouter->classes as $class )
|
|
{
|
|
$classes[] = $class;
|
|
|
|
if ( isset( $class::$commentClass ) )
|
|
{
|
|
$classes[] = $class::$commentClass;
|
|
}
|
|
|
|
if ( isset( $class::$reviewClass ) )
|
|
{
|
|
$classes[] = $class::$reviewClass;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( \count( $classes ) )
|
|
{
|
|
$queueWhere = array();
|
|
$queueWhere[] = array( 'app=?', 'core' );
|
|
$queueWhere[] = array( \IPS\Db::i()->in( '`key`', array( 'rebuildPosts', 'RebuildReputationIndex' ) ) );
|
|
|
|
foreach ( \IPS\Db::i()->select( '*', 'core_queue', $queueWhere ) as $queue )
|
|
{
|
|
$queue['data'] = json_decode( $queue['data'], TRUE );
|
|
if( \in_array( $queue['data']['class'], $classes ) )
|
|
{
|
|
\IPS\Db::i()->delete( 'core_queue', array( 'id=?', $queue['id'] ) );
|
|
}
|
|
}
|
|
|
|
\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 ) );
|
|
|
|
/* Delete Promoted Content from this app */
|
|
\IPS\Db::i()->delete( 'core_social_promote', \IPS\Db::i()->in( 'promote_class', $classes ) );
|
|
|
|
/* Delete ratings from this app */
|
|
\IPS\Db::i()->delete( 'core_ratings', \IPS\Db::i()->in( 'class', $classes ) );
|
|
|
|
/* Delete Soft Deletion Log data */
|
|
$softDeleteKeys = array();
|
|
foreach ( $classes as $class )
|
|
{
|
|
if ( isset( $class::$hideLogKey ) AND $class::$hideLogKey )
|
|
{
|
|
$softDeleteKeys[] = $class::$hideLogKey;
|
|
}
|
|
}
|
|
|
|
if ( \count( $softDeleteKeys ) )
|
|
{
|
|
\IPS\Db::i()->delete( 'core_soft_delete_log', \IPS\Db::i()->in( 'sdl_obj_key', $softDeleteKeys ) );
|
|
}
|
|
|
|
/* Delete PBR Data */
|
|
\IPS\Db::i()->delete( 'core_post_before_registering', \IPS\Db::i()->in( 'class', $classes ) );
|
|
}
|
|
|
|
/* Delete attachment maps - if the attachment is unused, the regular cleanup task will remove the file later */
|
|
$extensions = array();
|
|
|
|
foreach( $this->extensions( 'core', 'EditorLocations', FALSE ) AS $key => $extension )
|
|
{
|
|
$extensions[] = $this->directory . '_' . $key;
|
|
}
|
|
|
|
\IPS\Db::i()->delete( 'core_attachments_map', array( \IPS\Db::i()->in( 'location_key', $extensions ) ) );
|
|
|
|
/* Cleanup some caches */
|
|
\IPS\Settings::i()->clearCache();
|
|
unset( \IPS\Data\Store::i()->acpNotifications );
|
|
unset( \IPS\Data\Store::i()->acpNotificationIds );
|
|
|
|
/* Delete tasks and task logs */
|
|
\IPS\Db::i()->delete( 'core_tasks_log', array( 'task IN(?)', \IPS\Db::i()->select( 'id', 'core_tasks', array( 'app=?', $this->directory ) ) ) );
|
|
\IPS\Db::i()->delete( 'core_tasks', array( 'app=?', $this->directory ) );
|
|
|
|
/* Delete reports */
|
|
\IPS\Db::i()->delete( 'core_rc_reports', array( 'rid IN(?)', \IPS\Db::i()->select('id', 'core_rc_index', \IPS\Db::i()->in( 'class', $classes ) ) ) );
|
|
\IPS\Db::i()->delete( 'core_rc_comments', array( 'rid IN(?)', \IPS\Db::i()->select('id', 'core_rc_index', \IPS\Db::i()->in( 'class', $classes ) ) ) );
|
|
\IPS\Db::i()->delete( 'core_rc_index', \IPS\Db::i()->in('class', $classes) );
|
|
|
|
/* Delete language strings */
|
|
\IPS\Db::i()->delete( 'core_sys_lang_words', array( 'word_app=?', $this->directory ) );
|
|
|
|
/* Delete email templates */
|
|
$emailTemplates = \IPS\Db::i()->select( '*', 'core_email_templates', array( 'template_app=?', $this->directory ) );
|
|
|
|
if( $emailTemplates->count() )
|
|
{
|
|
foreach( $emailTemplates as $template )
|
|
{
|
|
if( $template['template_content_html'] )
|
|
{
|
|
$k = $template['template_key'] . '_email_html';
|
|
unset( \IPS\Data\Store::i()->$k );
|
|
}
|
|
|
|
if( $template['template_content_plaintext'] )
|
|
{
|
|
$k = $template['template_key'] . '_email_plaintext';
|
|
unset( \IPS\Data\Store::i()->$k );
|
|
}
|
|
}
|
|
|
|
\IPS\Db::i()->delete( 'core_email_templates', array( 'template_app=?', $this->directory ) );
|
|
}
|
|
|
|
/* Delete skin template/CSS/etc. */
|
|
\IPS\Theme::removeTemplates( $this->directory, NULL, NULL, NULL, TRUE );
|
|
\IPS\Theme::removeCss( $this->directory, NULL, NULL, NULL, TRUE );
|
|
\IPS\Theme::removeResources( $this->directory, NULL, NULL, NULL, TRUE );
|
|
|
|
/* Invalidate disk templates */
|
|
\IPS\Theme::resetAllCacheKeys();
|
|
|
|
/* Delete theme settings */
|
|
$valueIds = iterator_to_array( \IPS\Db::i()->select( 'sc_id', 'core_theme_settings_fields', array( array( 'sc_app=?', $this->directory ) ) ) );
|
|
|
|
\IPS\Db::i()->delete( 'core_theme_settings_fields', array( 'sc_app=?', $this->directory ) );
|
|
|
|
if ( \count( $valueIds ) )
|
|
{
|
|
\IPS\Db::i()->delete( 'core_theme_settings_values', \IPS\Db::i()->in('sv_id', $valueIds ) );
|
|
}
|
|
|
|
unset( \IPS\Data\Store::i()->themes );
|
|
|
|
/* Delete any stored files */
|
|
foreach( $this->extensions( 'core', 'FileStorage', TRUE ) as $extension )
|
|
{
|
|
try
|
|
{
|
|
$extension->delete();
|
|
}
|
|
catch( \Exception $e ){}
|
|
}
|
|
|
|
$notificationTypes = array();
|
|
foreach( $this->extensions( 'core', 'Notifications' ) as $key => $class )
|
|
{
|
|
if ( method_exists( $class, 'getConfiguration' ) )
|
|
{
|
|
$defaults = $class->getConfiguration( NULL );
|
|
|
|
foreach( $defaults AS $k => $config )
|
|
{
|
|
$notificationTypes[] = $k;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( \count( $notificationTypes ) )
|
|
{
|
|
\IPS\Db::i()->delete( 'core_notification_defaults', "notification_key IN('" . implode( "','", $notificationTypes ) . "')");
|
|
\IPS\Db::i()->delete( 'core_notification_preferences', "notification_key IN('" . implode( "','", $notificationTypes ) . "')");
|
|
}
|
|
|
|
/* Delete database tables */
|
|
if( file_exists( $this->getApplicationPath() . "/data/schema.json" ) )
|
|
{
|
|
$schema = @json_decode( file_get_contents( $this->getApplicationPath() . "/data/schema.json" ), TRUE );
|
|
|
|
if( \is_array( $schema ) AND \count( $schema ) )
|
|
{
|
|
foreach( $schema as $tableName => $definition )
|
|
{
|
|
try
|
|
{
|
|
\IPS\Db::i()->dropTable( $tableName, TRUE );
|
|
}
|
|
catch( \IPS\Db\Exception $e )
|
|
{
|
|
/* Ignore "Cannot drop table because it does not exist" */
|
|
if( $e->getCode() <> 1051 )
|
|
{
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Revert other database changes performed by installation */
|
|
if( file_exists( $this->getApplicationPath() . "/setup/install/queries.json" ) )
|
|
{
|
|
$schema = json_decode( file_get_contents( $this->getApplicationPath() . "/setup/install/queries.json" ), TRUE );
|
|
|
|
ksort($schema);
|
|
|
|
foreach( $schema as $instruction )
|
|
{
|
|
switch ( $instruction['method'] )
|
|
{
|
|
case 'addColumn':
|
|
try
|
|
{
|
|
\IPS\Db::i()->dropColumn( $instruction['params'][0], $instruction['params'][1]['name'] );
|
|
}
|
|
catch( \IPS\Db\Exception $e )
|
|
{
|
|
/* Ignore "Cannot drop key because it does not exist" */
|
|
if( $e->getCode() <> 1091 )
|
|
{
|
|
throw $e;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'addIndex':
|
|
try
|
|
{
|
|
\IPS\Db::i()->dropIndex( $instruction['params'][0], $instruction['params'][1]['name'] );
|
|
}
|
|
catch( \IPS\Db\Exception $e )
|
|
{
|
|
/* Ignore "Cannot drop key because it does not exist" */
|
|
if( $e->getCode() <> 1091 )
|
|
{
|
|
throw $e;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* delete widgets */
|
|
\IPS\Db::i()->delete( 'core_widgets', array( 'app = ?', $this->directory ) );
|
|
\IPS\Db::i()->delete( 'core_widget_areas', array( 'app = ?', $this->directory ) );
|
|
|
|
/* clean up widget areas table */
|
|
foreach ( \IPS\Db::i()->select( '*', 'core_widget_areas' ) as $row )
|
|
{
|
|
$data = json_decode( $row['widgets'], true );
|
|
|
|
foreach ( $data as $key => $widget)
|
|
{
|
|
if ( isset( $widget['app'] ) and $widget['app'] == $this->directory )
|
|
{
|
|
unset( $data[$key]) ;
|
|
}
|
|
}
|
|
|
|
\IPS\Db::i()->update( 'core_widget_areas', array( 'widgets' => json_encode( $data ) ), array( 'id=?', $row['id'] ) );
|
|
}
|
|
|
|
/* Clean up widget trash table */
|
|
$trash = array();
|
|
foreach( \IPS\Db::i()->select( '*', 'core_widget_trash' ) AS $garbage )
|
|
{
|
|
$data = json_decode( $garbage['data'], TRUE );
|
|
|
|
if ( isset( $data['app'] ) AND $data['app'] == $this->directory )
|
|
{
|
|
$trash[] = $garbage['id'];
|
|
}
|
|
}
|
|
|
|
\IPS\Db::i()->delete( 'core_widget_trash', \IPS\Db::i()->in( 'id', $trash ) );
|
|
|
|
/* Call postUninstall() so that application may perform any necessary cleanup after other data is removed */
|
|
foreach( $uninstallExtensions as $extension )
|
|
{
|
|
if( method_exists( $extension, 'postUninstall' ) )
|
|
{
|
|
$extension->postUninstall( $this->directory );
|
|
}
|
|
}
|
|
|
|
/* Clean up FURL Definitions */
|
|
if ( file_exists( $this->getApplicationPath() . "/data/furl.json" ) )
|
|
{
|
|
$current = json_decode( \IPS\Db::i()->select( 'conf_value', 'core_sys_conf_settings', array( "conf_key=?", 'furl_configuration' ) )->first(), true );
|
|
$default = json_decode( preg_replace( '/\/\*.+?\*\//s', '', @file_get_contents( $this->getApplicationPath() . "/data/furl.json" ) ), true );
|
|
|
|
if ( isset( $default['pages'] ) and $current !== NULL )
|
|
{
|
|
foreach( $default['pages'] AS $key => $def )
|
|
{
|
|
if ( isset( $current[$key] ) )
|
|
{
|
|
unset( $current[$key] );
|
|
}
|
|
}
|
|
|
|
\IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => json_encode( $current ) ), array( "conf_key=?", 'furl_configuration' ) );
|
|
}
|
|
}
|
|
|
|
/* Delete from DB */
|
|
\IPS\File::unclaimAttachments( 'core_Admin', $this->id, NULL, 'appdisabled' );
|
|
parent::delete();
|
|
|
|
/* Rebuild hooks file */
|
|
\IPS\Plugin\Hook::writeDataFile();
|
|
foreach ( $templatesToRecompile as $k )
|
|
{
|
|
$exploded = explode( '_', $k );
|
|
\IPS\Theme::deleteCompiledTemplate( $exploded[1], $exploded[2], $exploded[3] );
|
|
}
|
|
|
|
/* Clear out member's cached "Create Menu" contents */
|
|
\IPS\Member::clearCreateMenu();
|
|
|
|
/* Clear out data store for updated values */
|
|
unset( \IPS\Data\Store::i()->modules );
|
|
unset( \IPS\Data\Store::i()->applications );
|
|
unset( \IPS\Data\Store::i()->widgets );
|
|
unset( \IPS\Data\Store::i()->furl_configuration );
|
|
|
|
\IPS\Settings::i()->clearCache();
|
|
|
|
/* Remove the files and folders, if possible (if not IN_DEV and not in DEMO_MODE) */
|
|
if ( \IPS\CIC2 )
|
|
{
|
|
if ( !\in_array( $this->directory, \IPS\IPS::$ipsApps ) )
|
|
{
|
|
\IPS\Cicloud\folderDelete( "applications/{$this->directory}" );
|
|
}
|
|
}
|
|
elseif ( !\IPS\IN_DEV AND !\IPS\DEMO_MODE AND file_exists( \IPS\ROOT_PATH . '/applications/' . $this->directory ) )
|
|
{
|
|
try
|
|
{
|
|
$iterator = new \RecursiveDirectoryIterator( \IPS\ROOT_PATH . '/applications/' . $this->directory, \FilesystemIterator::SKIP_DOTS );
|
|
foreach ( new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::CHILD_FIRST ) as $file )
|
|
{
|
|
if ( $file->isDir() )
|
|
{
|
|
@rmdir( $file->getPathname() );
|
|
}
|
|
else
|
|
{
|
|
@unlink( $file->getPathname() );
|
|
}
|
|
}
|
|
$dir = \IPS\ROOT_PATH . '/applications/' . $this->directory;
|
|
$handle = opendir( $dir );
|
|
closedir ( $handle );
|
|
@rmdir( $dir );
|
|
}
|
|
catch( \UnexpectedValueException $e ){}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return an array of version upgrade folders this application contains
|
|
*
|
|
* @param int $start If provided, only upgrade steps above this version will be returned
|
|
* @return array
|
|
*/
|
|
public function getUpgradeSteps( $start=0 )
|
|
{
|
|
$path = $this->getApplicationPath() . "/setup";
|
|
|
|
if( !is_dir( $path ) )
|
|
{
|
|
return array();
|
|
}
|
|
|
|
$versions = array();
|
|
|
|
foreach( new \DirectoryIterator( $path ) as $file )
|
|
{
|
|
if( $file->isDir() AND !$file->isDot() )
|
|
{
|
|
if( mb_substr( $file->getFilename(), 0, 4 ) == 'upg_' )
|
|
{
|
|
$_version = \intval( mb_substr( $file->getFilename(), 4 ) );
|
|
|
|
if( $_version > $start )
|
|
{
|
|
$versions[] = $_version;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Sort the versions lowest to highest */
|
|
sort( $versions, SORT_NUMERIC );
|
|
|
|
return $versions;
|
|
}
|
|
|
|
/**
|
|
* Can view page even when user is a guest when guests cannot access the site
|
|
*
|
|
* @param \IPS\Application\Module $module The module
|
|
* @param string $controller The controller
|
|
* @param string|NULL $do To "do" parameter
|
|
* @return bool
|
|
*/
|
|
public function allowGuestAccess( \IPS\Application\Module $module, $controller, $do )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Can view page even when site is offline
|
|
*
|
|
* @param \IPS\Application\Module $module The module
|
|
* @param string $controller The controller
|
|
* @param string|NULL $do To "do" parameter
|
|
* @return bool
|
|
*/
|
|
public function allowOfflineAccess( \IPS\Application\Module $module, $controller, $do )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* [Node] Does the currently logged in user have permission to edit this node?
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function canEdit()
|
|
{
|
|
return ( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'app_manage' ) );
|
|
}
|
|
|
|
/**
|
|
* Get any third parties this app uses for the privacy policy
|
|
*
|
|
* @return array( title => language bit, description => language bit, privacyUrl => privacy policy URL )
|
|
*/
|
|
public function privacyPolicyThirdParties()
|
|
{
|
|
/* Apps can overload this */
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Get any settings that are uploads
|
|
*
|
|
* @return array
|
|
*/
|
|
public function uploadSettings()
|
|
{
|
|
/* Apps can overload this */
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Search
|
|
*
|
|
* @param string $column Column to search
|
|
* @param string $query Search query
|
|
* @param string|null $order Column to order by
|
|
* @param mixed $where Where clause
|
|
* @return array
|
|
*/
|
|
public static function search( $column, $query, $order=NULL, $where=array() )
|
|
{
|
|
if ( $column === '_title' )
|
|
{
|
|
$return = array();
|
|
foreach( \IPS\Member::loggedIn()->language()->words as $k => $v )
|
|
{
|
|
if ( preg_match( '/^__app_([a-z]*)$/', $k, $matches ) and mb_strpos( mb_strtolower( $v ), mb_strtolower( $query ) ) !== FALSE )
|
|
{
|
|
try
|
|
{
|
|
$application = static::load( $matches[1] );
|
|
$return[ $application->_id ] = $application;
|
|
}
|
|
catch ( \OutOfRangeException $e ) { }
|
|
}
|
|
}
|
|
return $return;
|
|
}
|
|
return parent::search( $column, $query, $order, $where );
|
|
}
|
|
|
|
/**
|
|
* remove the furl prefix from all metadata rows
|
|
*
|
|
* @param Application $application
|
|
*/
|
|
public static function removeMetaPrefix( \IPS\Application $application )
|
|
{
|
|
$metaWhere = array();
|
|
$prefix = '';
|
|
$oldDefaultAppDefinition = ( file_exists( static::getRootPath( $application->directory ) . "/applications/{$application->directory}/data/furl.json" ) ) ? json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( static::getRootPath( $application->directory ) . "/applications/{$application->directory}/data/furl.json" ) ), TRUE ) : array();
|
|
if ( isset( $oldDefaultAppDefinition['topLevel'] ) and $oldDefaultAppDefinition['topLevel'] )
|
|
{
|
|
$prefix = $oldDefaultAppDefinition['topLevel'] .'/';
|
|
$metaWhere[] = \IPS\Db::i()->like( 'meta_url', $oldDefaultAppDefinition['topLevel'] . '/' );
|
|
|
|
/* Replace the root */
|
|
\IPS\Db::i()->update( 'core_seo_meta', array( 'meta_url' => '' ), array( 'meta_url=?', $oldDefaultAppDefinition['topLevel'] ) );
|
|
}
|
|
|
|
$rows = iterator_to_array( \IPS\Db::i()->select( '*', 'core_seo_meta', $metaWhere )->setKeyField('meta_id') );
|
|
|
|
foreach( $rows as $id => $row )
|
|
{
|
|
/* The old urls need now the new prefix */
|
|
$newUrl = str_replace( $prefix, '', $row['meta_url'] );
|
|
\IPS\Db::i()->update( 'core_seo_meta', array( 'meta_url' => $newUrl ), array( 'meta_id=?', $id ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the new prefix to the metadata rows
|
|
*
|
|
* @param Application $application
|
|
*/
|
|
public static function addMetaPrefix( \IPS\Application $application )
|
|
{
|
|
$metaWhere = array();
|
|
$oldDefaultAppDefinition = ( file_exists( static::getRootPath( $application->directory ) . "/applications/{$application->directory}/data/furl.json" ) ) ? json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( static::getRootPath( $application->directory ) . "/applications/{$application->directory}/data/furl.json" ) ), TRUE ) : array();
|
|
if ( isset( $oldDefaultAppDefinition['topLevel'] ) and $oldDefaultAppDefinition['topLevel'] )
|
|
{
|
|
$prefix = $oldDefaultAppDefinition['topLevel'] .'/';
|
|
}
|
|
else
|
|
{
|
|
$prefix = "";
|
|
}
|
|
|
|
$existingTopLevels = [];
|
|
foreach ( \IPS\Application::applications() as $app )
|
|
{
|
|
/* If it has a furl.json file... */
|
|
if ( $application->directory != $app->directory AND file_exists( static::getRootPath( $application->directory ) . "/applications/{$application->directory}/data/furl.json" ) )
|
|
{
|
|
/* Open it up */
|
|
$data = json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( static::getRootPath( $application->directory ) . "/applications/{$application->directory}/data/furl.json" ) ), TRUE );
|
|
$existingTopLevels[] = $data['topLevel'];
|
|
}
|
|
}
|
|
|
|
$rows = iterator_to_array( \IPS\Db::i()->select( '*', 'core_seo_meta', $metaWhere )->setKeyField('meta_id') );
|
|
|
|
foreach( $rows as $id => $row )
|
|
{
|
|
/* The old urls need now the new prefix */
|
|
$newUrl = $prefix . $row['meta_url'];
|
|
\IPS\Db::i()->update( 'core_seo_meta', array( 'meta_url' => $newUrl ), array( 'meta_id=?', $id ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Application Path
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getApplicationPath(): string
|
|
{
|
|
return static::getRootPath( $this->directory ) . '/applications/' . $this->directory;
|
|
}
|
|
|
|
/**
|
|
* Get Root Path
|
|
*
|
|
* @param string $appKey Application to check if it's an IPS app or third party, or NULL to not check.
|
|
* @return string
|
|
*/
|
|
public static function getRootPath( ?string $appKey = NULL ): string
|
|
{
|
|
if ( $appKey AND \in_array( $appKey, \IPS\IPS::$ipsApps ) )
|
|
{
|
|
return \IPS\ROOT_PATH;
|
|
}
|
|
else
|
|
{
|
|
return \IPS\SITE_FILES_PATH;
|
|
}
|
|
}
|
|
} |