<?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
*/
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');
/**
* 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 = \IPS\Db::i()->select( 'app_directory', 'core_applications', 'app_default=1' )->first();
if( file_exists( \IPS\ROOT_PATH . "/applications/{$previousDefaultApp}/data/furl.json" ) )
{
$oldDefaultAppDefinition = json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( \IPS\ROOT_PATH . "/applications/{$previousDefaultApp}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/furl.json" ) )
{
$newDefaultAppDefinition = json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/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 ) ) );
}
/* 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 )
{
if ( isset( \IPS\Data\Store::i()->applications ) )
{
$rows = \IPS\Data\Store::i()->applications;
}
else
{
$rows = iterator_to_array( \IPS\Db::i()->select( '*', 'core_applications', NULL, 'app_position' ) );
\IPS\Data\Store::i()->applications = $rows;
}
static::$multitons = array();
foreach ( $rows 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 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
* @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() )
{
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 = \IPS\ROOT_PATH . "/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 = \IPS\ROOT_PATH . "/applications/{$this->directory}/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 intval($data['released']) == $data['released'] AND \strlen($data['released']) == 10 )
{
$data['released'] = (string) \IPS\DateTime::ts( $data['released'] )->localeDate();
}
$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], static::$ipsApps ) AND !static::appIsEnabled( $matches[1] ) )
{
continue;
}
}
/* Ignore init.php (which always changes on build) and conf_global.dist.php (which doesn't exist after install) */
if ( in_array( $file, array( '/init.php', '/conf_global.dist.php' ) ) )
{
continue;
}
/* If 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' );
/**
* @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( \IPS\ROOT_PATH . '/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 \IPS\ROOT_PATH . '/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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();
$directory = \IPS\ROOT_PATH . "/applications/{$this->directory}/extensions/{$app}/{$extension}";
if ( 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' => '' );
$uninstall = $buttons['delete'];
unset( $buttons['delete'] );
}
/* Default */
if( $this->enabled )
{
$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" ),
);
}
/* 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 )
{
$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;
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" )
);
$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, static::$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, static::$ipsApps ) and $this->version != \IPS\Application::load('core')->version )
{
return TRUE;
}
return FALSE;
}
/**
* [Node] Get Node Description
*
* @return string|null
*/
protected function get__description()
{
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 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'], $availableUpgrade['updateurl'], $availableUpgrade['released'] )
);
}
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]+$/' ) ) );
$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( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/schema.json" ) )
{
$schema = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/install/queries.json" ) )
{
$schema = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/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 );
}
}
}
call_user_func_array( array( \IPS\Db::i(), $instruction['method'] ), $instruction['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();
$tableCounts = 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( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/upg_{$version}/queries.json" ) )
{
$schema = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/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++;
$_table = $instruction['params']['params'][0];
if ( ! is_string( $_table ) )
{
$_table = $instruction['params']['params'][0]['name'];
}
if ( ! isset( $tableCounts[ $_table ] ) and \IPS\Db::i()->checkForTable( $_table ) )
{
$tableCounts[ $_table ] = \IPS\Db::i()->select( 'count(*)', $_table )->first();
}
/* If we are deleting stuff, then make sure the counts are recounted after */
if ( $instruction['params']['method'] == 'delete' and isset( $tableCounts[ $_table ] ) )
{
unset( $tableCounts[ $_table ] );
}
/* Check table size first and store query if requested */
if( $return === TRUE )
{
if(
/* Only run manually if we have a table row count */
isset( $tableCounts[ $_table ] ) AND
/* And only if the row count is greater than the manual threshold */
$tableCounts[ $_table ] > \IPS\UPGRADE_MANUAL_THRESHOLD 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;
$query = call_user_func_array( array( \IPS\Db::i(), $instruction['params']['method'] ), $instruction['params']['params'] );
if( $query )
{
$toReturn[] = $query;
if ( $instruction['params']['method'] == 'renameTable' )
{
$tableCounts[ $instruction['params']['params'][1] ] = $tableCounts[ $_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
{
call_user_func_array( array( \IPS\Db::i(), $instruction['params']['method'] ), $instruction['params']['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
*
* @return void
*/
public function installModules()
{
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/modules.json" ) )
{
$currentModules = array();
$moduleStore = array();
foreach ( \IPS\Db::i()->select( '*', 'core_modules', array( 'sys_module_application=?', $this->directory ) ) as $row )
{
$currentModules[ $row['sys_module_area'] ][ $row['sys_module_key'] ] = array(
'default_controller' => $row['sys_module_default_controller'],
'protected' => $row['sys_module_protected']
);
$moduleStore[ $row['sys_module_area'] ][ $row['sys_module_key'] ] = $row;
}
$insert = array();
$update = array();
$position = 0;
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/modules.json" ), TRUE ) as $area => $modules )
{
foreach ( $modules as $key => $data )
{
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'];
$module->default = ( isset( $data['default'] ) and $data['default'] );
$module->save( TRUE );
}
}
}
}
/**
* Install the application's tasks
*
* @return void
*/
public function installTasks()
{
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 $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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/installLang.json" ) )
{
$inserts = array();
foreach ( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/lang.xml" ) )
{
/* Open XML file */
$xml = new \IPS\Xml\XMLReader;
$xml->open( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/javascript.xml" ) )
{
return \IPS\Output\Javascript::importXml( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/javascript.xml", $offset, $limit );
}
}
/**
* Install the application's ACP search keywords
*
* @return void
*/
public function installSearchKeywords()
{
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/hooks.json" ) )
{
\IPS\Db::i()->delete( 'core_hooks', array( 'app=?', $this->directory ) );
$inserts = array();
$templatesToRecompile = array();
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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 );
/* 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 );
/* 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 )
{
/* Normalise it a little to prevent unnecessary conflicts */
foreach ( $tableDefinition['columns'] as $k => $c )
{
foreach ( array( 'length', 'decimals' ) as $i )
{
if ( isset( $c[ $i ] ) )
{
$tableDefinition['columns'][ $k ][ $i ] = intval( $c[ $i ] );
}
else
{
$tableDefinition['columns'][ $k ][ $i ] = NULL;
}
}
if ( !isset( $c['values'] ) )
{
$tableDefinition['columns'][ $k ]['values'] = array();
}
if ( $c['type'] === 'BIT' )
{
$tableDefinition['columns'][ $k ]['default'] = ( is_null($c['default']) ) ? NULL : "b'{$c['default']}'";
}
ksort( $tableDefinition['columns'][ $k ] );
}
$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 ( $columnData != $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 ( $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 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( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/install/queries.json" ) )
{
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/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;
}
/* Take care of languages for this app */
$this->buildLanguages();
$this->installLanguages();
/* Take care of skins for this app */
$this->buildThemeTemplates();
$this->installSkins( TRUE );
/* Take care of emails for this app */
$this->buildEmailTemplates();
$this->installEmailTemplates();
/* Take care of javascript for this app */
$this->buildJavascript();
$this->installJavascript();
/* Take care of hooks for this app */
$this->buildHooks();
foreach( $this->extensions( 'core', 'Build' ) as $builder )
{
$builder->build();
}
}
/**
* 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::deleteCompiledResources( $this->directory );
\IPS\Theme::i()->importDevHtml( $this->directory, 0 );
\IPS\Theme::i()->importDevCss( $this->directory, 0 );
/* 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');
foreach( $template as $k => $v )
{
if ( in_array( \substr( $k, 9 ), array('app', 'location', 'group', 'name', 'data' ) ) )
{
$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();
}
/* 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');
foreach( $css as $k => $v )
{
if ( in_array( \substr( $k, 4 ), array('app', 'location', 'path', 'name', 'attributes' ) ) )
{
$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();
}
/* 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 */
$xml->text( base64_encode( $data['resource_data'] ) );
$xml->endElement();
}
/* 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') );
}
}
/**
* 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 void
* @throws \RuntimeException
*/
public function buildLanguages()
{
/* 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();
}
$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();
}
/* 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') );
}
}
/**
* Build email templates for an app
*
* @return void
* @throws \RuntimeException
*/
public function buildEmailTemplates()
{
/* 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 );
}
/* 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') );
}
}
/**
* 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 void
* @throws \RuntimeException
*/
public function buildJavascript()
{
/* 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 );
/* 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') );
}
}
/**
* 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') );
}
}
/**
* 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 )
{
/* Format the JSON if we can (JSON_PRETTY_PRINT) is only available on PHP 5.4+ */
if ( version_compare( PHP_VERSION, '5.4.0' ) >= 0 )
{
$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 );
}
else
{
$json = json_encode( $data );
}
/* 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 all groups have access, we can */
if( $this->disabled_groups === NULL )
{
return TRUE;
}
/* If all groups have access, we can */
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 );
/* 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 ) );
$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 Soft Deletion Log data */
$softDeleteKeys = array();
foreach ( $classes as $class )
{
if ( $class::$hideLogKey )
{
$softDeleteKeys[] = $class::$hideLogKey;
}
}
if ( count( $softDeleteKeys ) )
{
\IPS\Db::i()->delete( 'core_soft_delete_log', \IPS\Db::i()->in( 'sdl_obj_key', $softDeleteKeys ) );
}
}
\IPS\Settings::i()->clearCache();
/* 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', array( 'sv_id IN(?)', implode( ',', $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( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/schema.json" ) )
{
$schema = @json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/install/queries.json" ) )
{
$schema = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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( \IPS\ROOT_PATH . "/applications/{$this->directory}/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();
}
/**
* 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 = \IPS\ROOT_PATH . "/applications/{$this->directory}/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();
}
/**
* 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 );
}
}