<?php
/**
* @brief Application & Module Management Controller
* @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\core\modules\admin\applications;
/* 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;
}
/**
* Application & Module Management Controller
*/
class _applications extends \IPS\Node\Controller
{
/**
* Node Class
*/
protected $nodeClass = 'IPS\Application';
/**
* Execute
*
* @return void
*/
public function execute()
{
\IPS\Dispatcher::i()->checkAcpPermission( 'app_manage' );
parent::execute();
}
/**
* Manage
*
* @return void
*/
protected function manage()
{
/* Create the basic tree */
parent::manage();
/* Find uninstalled applications */
$uninstalled = array();
$installed = array_keys( \IPS\Application::applications() );
foreach ( new \DirectoryIterator( \IPS\ROOT_PATH . "/applications/" ) as $file )
{
if ( $file->isDir() AND !in_array( $file->getFilename(), $installed ) AND !$file->isDot() )
{
if( file_exists( $file->getPathname() . '/data/application.json' ) )
{
$application = json_decode( file_get_contents( $file->getPathname() . '/data/application.json' ), TRUE );
$uninstalled[ $file->getFilename() ] = array(
'title' => $application['application_title'],
'author' => $application['app_author'],
'website' => $application['app_website'],
);
}
}
}
if( count( $uninstalled ) AND empty( \IPS\Request::i()->root ) )
{
$baseUrl = $this->url;
$tree = new \IPS\Helpers\Tree\Tree(
$this->url,
\IPS\Member::loggedIn()->language()->addToStack('uninstalled_applications'),
function() use ( $uninstalled, $baseUrl )
{
$rows = array();
if( !empty($uninstalled) AND is_array($uninstalled) )
{
foreach ( $uninstalled as $k => $app )
{
$rows[ $k ] = \IPS\Theme::i()->getTemplate( 'trees' )->row( $baseUrl, $k, $app['title'], FALSE, array(
'add' => array(
'icon' => 'plus-circle',
'title' => 'install',
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$k}&do=install" ),
)
) );
}
}
return $rows;
},
function( $key, $root=FALSE ) use ( $uninstalled, $baseUrl )
{
return \IPS\Theme::i()->getTemplate( 'trees' )->row( $baseUrl, $key, $uninstalled[ $key ]['title'], FALSE, array(
'add' => array(
'icon' => 'plus-circle',
'title' => 'install',
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$key}&do=install" ),
)
), '', NULL, NULL, $root );
},
function() { return 0; },
function() { return array(); },
function() { return array(); },
FALSE,
TRUE,
TRUE
);
\IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'applications' )->applicationWrapper( $tree, 'uninstalled_applications' );
}
/* And 3.x applications not yet upgraded */
$legacyApps = array();
foreach ( \IPS\Db::i()->select( '*', 'core_applications', NULL, 'app_position' ) as $application )
{
try
{
\IPS\Application::constructFromData( $application );
}
catch( \UnexpectedValueException $e )
{
if ( mb_stristr( $e->getMessage(), 'Missing:' ) )
{
$legacyApps[ $application['app_directory'] ] = array(
'title' => isset( $application['app_title'] ) ? $application['app_title'] : $application['app_directory'],
'author' => $application['app_author'],
'website' => $application['app_website'],
);
}
}
}
if( count( $legacyApps ) AND empty( \IPS\Request::i()->root ) )
{
$baseUrl = $this->url;
$legacyTree = new \IPS\Helpers\Tree\Tree(
$this->url,
\IPS\Member::loggedIn()->language()->addToStack('legacy_applications'),
function() use ( $legacyApps, $baseUrl )
{
$rows = array();
if( !empty( $legacyApps ) AND is_array( $legacyApps ) )
{
foreach ( $legacyApps as $k => $app )
{
$buttons = array(
'upgrade' => array(
'icon' => 'upload',
'title' => 'upload_new_version',
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$k}&do=upload" ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('upload_new_version') )
)
);
if( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'app_delete' ) )
{
$buttons['delete'] = array(
'icon' => 'times-circle',
'title' => 'uninstall',
'link' => $baseUrl->setQueryString( array( 'do' => 'delete', 'id' => $k ) ),
'data' => array( 'delete' => '' ),
'hotkey'=> 'd'
);
}
$rows[ $k ] = \IPS\Theme::i()->getTemplate( 'trees' )->row( $baseUrl, $k, $app['title'], FALSE, $buttons );
}
}
return $rows;
},
function( $key, $root=FALSE ) use ( $legacyApps, $baseUrl )
{
$buttons = array(
'upgrade' => array(
'icon' => 'upload',
'title' => 'upload_new_version',
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$key}&do=upload" ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('upload_new_version') )
)
);
if( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'app_delete' ) )
{
$buttons['delete'] = array(
'icon' => 'times-circle',
'title' => 'uninstall',
'link' => $baseUrl->setQueryString( array( 'do' => 'delete', 'id' => $key ) ),
'data' => array( 'delete' => '' ),
'hotkey'=> 'd'
);
}
return \IPS\Theme::i()->getTemplate( 'trees' )->row( $baseUrl, $key, $legacyApps[ $key ]['title'], FALSE, $buttons, '', NULL, NULL, $root );
},
function() { return 0; },
function() { return array(); },
function() { return array(); },
FALSE,
TRUE,
TRUE
);
\IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'applications' )->applicationWrapper( $legacyTree, 'legacy_applications' );
}
/* Javascript */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'admin_system.js', 'core', 'admin' ) );
/* Check for updates button */
\IPS\Output::i()->sidebar['actions']['settings'] = array(
'icon' => 'refresh',
'link' => \IPS\Http\Url::internal( 'app=core&module=applications&controller=applications&do=updateCheck' ),
'title' => 'check_for_updates',
);
if ( \IPS\IN_DEV )
{
\IPS\Output::i()->sidebar['actions']['build_all'] = array(
'icon' => 'cogs',
'link' => \IPS\Http\Url::internal( 'app=core&module=applications&controller=applications&do=buildAll' ),
'title' => 'build_all_apps',
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('build_all_apps') )
);
}
}
/**
* Check for updates
*
* @return void
*/
public function updateCheck()
{
$task = \IPS\Task::constructFromData( \IPS\Db::i()->select( '*', 'core_tasks', array( 'app=? AND `key`=?', 'core', 'updatecheck' ) )->first() );
$task->type = 'manual';
$task->run();
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=core&module=applications&controller=applications" ), 'update_check_complete' );
}
/**
* Set as default app
*
* @return void
*/
public function setAsDefault()
{
$application = \IPS\Application::load( \IPS\Request::i()->appKey );
$application->setAsDefault();
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=core&module=applications&controller=applications" ), 'saved' );
}
/**
* Specify the default module
*
* @return void
*/
public function setDefaultModule()
{
try
{
$module = \IPS\Application\Module::load( \IPS\Request::i()->id );
$module->setAsDefault();
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'no_module_for_default', '2C133/A', 403, '' );
}
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&root={$module->application}" ), 'saved' );
}
/**
* Get Root Buttons
*
* @return array
*/
public function _getRootButtons()
{
$buttons = parent::_getRootButtons();
\IPS\Output::i()->sidebar['actions']['install'] = array(
'primary' => true,
'icon' => 'upload',
'title' => 'install',
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&do=upload" ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('install') )
);
return $buttons;
}
/**
* Get Child Rows
*
* @param int|string $id Row ID
* @return array
*/
public function _getChildren( $id )
{
$rows = array();
$nodeClass = $this->nodeClass;
try
{
$node = $nodeClass::load( $id );
}
catch( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2S101/R', 404, '' );
}
foreach ( $node->children( NULL ) as $child )
{
if( $child->area == 'admin' )
{
continue;
}
$id = ( $child instanceof $this->nodeClass ? '' : 's.' ) . $child->_id;
$rows[ $id ] = $this->_getRow( $child );
}
return $rows;
}
/**
* Get Single Row
*
* @param mixed $id May be ID number (or key) or an \IPS\Node\Model object
* @param bool $root Format this as the root node?
* @param bool $noSort If TRUE, sort options will be disabled (used for search results)
* @return string
* @note Overridden so we can set the status toggle information to provide the offline message/permissions functionality
*/
public function _getRow( $id, $root=FALSE, $noSort=FALSE )
{
/* Load the node first */
if ( $id instanceof \IPS\Node\Model )
{
$node = $id;
}
else
{
try
{
$nodeClass = $this->nodeClass;
$node = $nodeClass::load( $id );
}
catch( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2S101/P', 404, '' );
}
}
/* Don't do this for modules, just applications */
if( $node instanceof \IPS\Application\Module )
{
return parent::_getRow( $node, $root, $noSort );
}
/* Work out buttons */
$buttons = $node->getButtons( $this->url, !( $node instanceof $this->nodeClass ) );
if ( isset( \IPS\Request::i()->searchResult ) and isset( $buttons['edit'] ) )
{
$buttons['edit']['link'] = $buttons['edit']['link']->setQueryString( 'searchResult', \IPS\Request::i()->searchResult );
}
/* Return */
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->row(
$this->url,
$node->_id,
\IPS\Theme::i()->getTemplate('applications')->appRowTitle( $node ),
$node->childrenCount( NULL ),
$buttons,
$node->_description,
$node->_icon ? $node->_icon : NULL,
( $node->canEdit() ) ? $node->_position : NULL,
$root,
$node->_enabled,
( $node->_locked or !$node->canEdit() or \IPS\NO_WRITES ),
( ( $node instanceof \IPS\Node\Model ) ? $node->_badge : $this->_getRowBadge( $node ) ),
TRUE,
$this->_descriptionHtml,
$node->canAdd()
);
}
/**
* Permissions Form
*
* @return void
*/
protected function permissions()
{
/* Work out which class we're using */
$nodeClass = $this->nodeClass;
if ( \IPS\Request::i()->subnode )
{
return parent::permissions();
}
/* Load Node */
try
{
$node = $nodeClass::load( \IPS\Request::i()->id );
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '3S101/A', 404, '' );
}
/* Check we're not locked */
if( $node->_locked or !$node->canEdit() )
{
\IPS\Output::i()->error( 'node_noperm_enable', '2S101/3', 403, '' );
}
/* Create the form */
$form = new \IPS\Helpers\Form;
$form->add( new \IPS\Helpers\Form\YesNo( 'app_enabled', $node->disabled_groups === NULL, TRUE, array( 'togglesOff' => array( 'app_disabled_groups', 'app_disabled_message_editor' ), 'disabled' => \IPS\NO_WRITES ) ) );
if ( \IPS\NO_WRITES )
{
\IPS\Member::loggedIn()->language()->words['app_enabled_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'app_enabled_desc_no_writes' );
}
$form->add( new \IPS\Helpers\Form\Select( 'app_disabled_groups', ( $node->disabled_groups == '*' or $node->disabled_groups === NULL ) ? '*' : explode( ',', $node->disabled_groups ), FALSE, array(
'options' => array_combine( array_keys( \IPS\Member\Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, \IPS\Member\Group::groups() ) ),
'multiple' => true,
'unlimited' => '*',
'unlimitedLang' => 'all'
), NULL, NULL, NULL, 'app_disabled_groups' ) );
$form->add( new \IPS\Helpers\Form\Editor( 'app_disabled_message', $node->disabled_message, FALSE, array( 'app' => 'core', 'key' => 'Admin', 'autoSaveKey' => $node->_key . 'app_disabled_message', 'attachIds' => array( $node->id, NULL, 'appdisabled' ) ), NULL, NULL, NULL, 'app_disabled_message_editor' ) );
/* And then save the values, if appropriate */
if ( $values = $form->values() )
{
$node->disabled_message = $values['app_disabled_message'];
$node->disabled_groups = $values['app_enabled'] ? NULL : ( $values['app_disabled_groups'] == '*' ? '*' : implode( ',', $values['app_disabled_groups'] ) );
$node->save();
if ( !\IPS\NO_WRITES )
{
\IPS\Plugin\Hook::writeDataFile();
}
/* Clear templates to rebuild automatically */
\IPS\Theme::deleteCompiledTemplate();
/* Clear guest page caches */
\IPS\Data\Cache::i()->clearAll();
$this->logToggleAndRedirect( $node );
}
/* Display */
\IPS\Output::i()->output = $form;
}
/**
* Build form
*
* @param string $appKey The application key
* @param bool $includeDownloadOption If a "just download" option should be included
* @return \IPS\Helpers\Form
*/
protected function _buildForm( $appKey, $includeDownloadOption=FALSE )
{
$json = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$appKey}/data/versions.json" ), TRUE );
ksort( $json );
$defaults = array( 'human' => '1.0.0', 'long' => '10000' );
$long = NULL;
$human = NULL;
foreach ( array_reverse( $json, TRUE ) as $long => $human )
{
$exploded = explode( '.', $human );
$defaults['human'] = "{$exploded[0]}.{$exploded[1]}." . ( intval( $exploded[2] ) + 1 );
$defaults['long'] = $long + 1;
break;
}
$options = array(
'options' => array(),
'toggles' => array( 'new' => array( 'versions_human', 'versions_long' ) )
);
if ( $human !== NULL )
{
$options['options']['rebuild'] = 'developer_build_type_rebuild';
\IPS\Member::loggedIn()->language()->words['developer_build_type_rebuild'] = sprintf( \IPS\Member::loggedIn()->language()->get('developer_build_type_rebuild'), $human );
}
$options['options']['new'] = 'developer_build_new';
if ( $includeDownloadOption )
{
$options['options']['download'] = 'developer_build_download';
}
$form = new \IPS\Helpers\Form;
$form->add( new \IPS\Helpers\Form\Radio( 'developer_build_type', 'rebuild', TRUE, $options ) );
$form->add( new \IPS\Helpers\Form\Text( 'versions_human', $defaults['human'], NULL, array(), NULL, NULL, NULL, 'versions_human' ) );
$form->add( new \IPS\Helpers\Form\Text( 'versions_long', $defaults['long'], NULL, array(), function( $val ) use ( $json )
{
if ( !preg_match( '/^\d*$/', $val ) )
{
throw new \DomainException( 'form_number_bad' );
}
if( isset( $json[ $val ] ) )
{
throw new \DomainException( 'versions_long_exists' );
}
}, NULL, NULL, 'versions_long' ) );
return $form;
}
/**
* Build all applications
*
* @return void
*/
public function buildAll()
{
if ( !\IPS\IN_DEV )
{
\IPS\Output::i()->error( 'not_in_dev', '2C133/M', 403, '' );
}
$form = $this->_buildForm( 'core' );
if ( $values = $form->values() )
{
foreach ( \IPS\Application::applications() as $application )
{
if ( $values['developer_build_type'] === 'new' )
{
$application->assignNewVersion( $values['versions_long'], $values['versions_human'] );
}
try
{
$application->build();
}
catch ( \Exception $e )
{
\IPS\Output::i()->error( $e->getMessage(), '' );
}
}
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=applications&controller=applications' ), 'application_now_built' );
}
\IPS\Output::i()->output = $form;
}
/**
* Build an application
*
* @return void
*/
public function build()
{
if ( !\IPS\IN_DEV )
{
\IPS\Output::i()->error( 'not_in_dev', '2C133/N', 403, '' );
}
$application = \IPS\Application::load( \IPS\Request::i()->appKey );
$form = $this->_buildForm( $application->directory );
if ( $values = $form->values() )
{
if ( $values['developer_build_type'] === 'new' )
{
$application->assignNewVersion( $values['versions_long'], $values['versions_human'] );
}
try
{
$application->build();
}
catch ( \Exception $e )
{
\IPS\Output::i()->error( $e->getMessage(), '' );
}
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=applications&controller=applications' ), 'application_now_built' );
}
\IPS\Output::i()->output = $form;
}
/**
* Export an application
*
* @return void
* @note We have to use a custom RecursiveDirectoryIterator in order to skip the /dev folder
*/
public function download()
{
$application = \IPS\Application::load( \IPS\Request::i()->appKey );
$form = $this->_buildForm( $application->directory, TRUE );
if ( $values = $form->values() )
{
if ( $values['developer_build_type'] !== 'download' )
{
if ( $values['developer_build_type'] === 'new' )
{
$application->assignNewVersion( $values['versions_long'], $values['versions_human'] );
}
try
{
$application->build();
}
catch ( \Exception $e )
{
\IPS\Output::i()->error( $e->getMessage(), '' );
}
}
try
{
$pharPath = str_replace( '\\', '/', rtrim( \IPS\TEMP_DIRECTORY, '/' ) ) . '/' . $application->directory . ".tar";
$download = new \PharData( $pharPath, 0, $application->directory . ".tar", \Phar::TAR );
$download->buildFromIterator( new \IPS\Application\BuilderIterator( $application ) );
}
catch( \PharException $e )
{
\IPS\Log::log( $e, 'phar' );
\IPS\Output::i()->error( 'app_no_phar', '4C133/7', 403, '' );
}
$output = \file_get_contents( rtrim( \IPS\TEMP_DIRECTORY, '/' ) . '/' . $application->directory . ".tar" );
/* Cleanup */
unset($download);
\Phar::unlinkArchive($pharPath);
\IPS\Output::i()->sendOutput( $output, 200, 'application/tar', array( 'Content-Disposition' => \IPS\Output::getContentDisposition( 'attachment', $application->directory . '.tar' ) ), FALSE, FALSE, FALSE );
}
\IPS\Output::i()->output = $form;
}
/**
* Upgrade an application that is currently installed. After importing a PHAR the user is redirected to this method.
*
* @see \IPS\core\modules\admin\applications\applications::import()
* @return void
*/
public function upgrade()
{
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('upgrading_application');
\IPS\Output::i()->output = new \IPS\Helpers\MultipleRedirect(
\IPS\Http\Url::internal( "app=core&module=applications&controller=applications&do=upgrade&appKey=" . \IPS\Request::i()->appKey ),
function( $data )
{
/* On first cycle return data */
if ( !is_array( $data ) )
{
/* Does this application exist in the database? */
try
{
$app = \IPS\Application::load( \IPS\Request::i()->appKey );
}
catch( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'no_app_to_update', '3C133/G', 403, '' );
}
/* Get the application data to update the application record */
if( file_exists( \IPS\ROOT_PATH . '/applications/' . \IPS\Request::i()->appKey . '/data/application.json' ) )
{
$application = json_decode( file_get_contents( \IPS\ROOT_PATH . '/applications/' . $app->directory . '/data/application.json' ), TRUE );
//\IPS\Lang::saveCustom( $app->directory, "__app_{$app->directory}", $application['application_title'] );
unset( $application['app_directory'], $application['app_protected'], $application['application_title'] );
foreach( $application as $column => $value )
{
$column = preg_replace( "/^app_/", "", $column );
$app->$column = $value;
}
$app->save();
}
else
{
\IPS\Output::i()->error( 'app_invalid_data', '3C133/H', 403, '' );
}
return array(
array( 'laststep' => 'start', 'key' => \IPS\Request::i()->appKey ),
\IPS\Member::loggedIn()->language()->addToStack('installing_application'),
1
);
}
/* Install the application in stages */
$laststep = NULL;
$language = NULL;
$progress = 1;
$extra = NULL;
switch( $data['laststep'] )
{
case 'start':
/* Determine our current version and the last version we ran */
$currentVersion = \IPS\Application::load( $data['key'] )->long_version;
$allVersions = \IPS\Application::load( $data['key'] )->getAllVersions();
$longVersions = array_keys( $allVersions );
$humanVersions = array_values( $allVersions );
$lastRan = ( isset( $data['extra']['_last'] ) ) ? intval( $data['extra']['_last'] ) : $currentVersion;
if( count($allVersions) )
{
$latestLVersion = array_pop( $longVersions );
$latestHVersion = array_pop( $humanVersions );
\IPS\Db::i()->insert( 'core_upgrade_history', array( 'upgrade_version_human' => $latestHVersion, 'upgrade_version_id' => $latestLVersion, 'upgrade_date' => time(), 'upgrade_mid' => (int) \IPS\Member::loggedIn()->member_id, 'upgrade_app' => $data['key'] ) );
}
/* Now find any upgrade paths since the last one we ran that need to be executed */
$upgradeSteps = \IPS\Application::load( $data['key'] )->getUpgradeSteps( $lastRan );
/* Did we find any? */
if( count( $upgradeSteps ) )
{
/* Re-initialize $extra variable */
$extra = array();
/* Store a count of all the upgrade steps for later use */
if( !$lastRan )
{
$extra['_totalSteps'] = count($upgradeSteps);
$data['extra']['_totalSteps'] = $extra['_totalSteps'];
}
else
{
$extra['_totalSteps'] = $data['extra']['_totalSteps'];
}
/* We need to populate \IPS\Request with the extra data returned from the last upgrader step call */
if( isset( $data['extra']['_upgradeData'] ) )
{
\IPS\Request::i()->extra = $data['extra']['_upgradeData'];
}
/* Grab next upgrade step to run */
$_next = array_shift( $upgradeSteps );
/* Set this now - we can reset later if we need to re-run this step */
$extra['_last'] = $_next;
/* What step in the upgrader file are we on? */
$upgradeStep = ( isset($data['extra']['_upgradeStep']) ) ? intval($data['extra']['_upgradeStep']) : 1;
/* If we haven't run the raw queries yet, do so */
if( $upgradeStep == 1 AND !isset( $data['extra']['_upgradeData'] ) )
{
\IPS\Application::load( $data['key'] )->installDatabaseUpdates( $_next );
}
/* Get the object */
$_className = "\\IPS\\{$data['key']}\\setup\\upg_{$_next}\\Upgrade";
$_methodName = "step{$upgradeStep}";
if( class_exists( $_className ) )
{
$upgrader = new $_className;
/* If the next step exists, run it */
if( method_exists( $upgrader, $_methodName ) )
{
/* Get custom title first as the step may unset session variables that are being referenced */
$customTitleMethod = 'step' . $upgradeStep . 'CustomTitle';
if ( method_exists( $upgrader, $customTitleMethod ) )
{
$language = $upgrader->$customTitleMethod();
}
$result = $upgrader->$_methodName();
/* If the result is 'true' we move on to the next step, otherwise we need to run the same step again and store the data returned */
if( $result === TRUE )
{
$_nextMethodStep = "step" . ( $upgradeStep + 1 );
if( method_exists( $upgrader, $_nextMethodStep ) )
{
/* We have another step to run - set the data and move along */
$extra['_last'] = $lastRan;
$extra['_upgradeStep'] = $upgradeStep + 1;
}
}
else
{
/* Store the data returned, set the step to the same/current one, and re-run */
$extra['_upgradeData'] = $result;
$extra['_upgradeStep'] = $upgradeStep;
$extra['_last'] = $lastRan;
}
}
}
$laststep = 'start';
$language = $language ?: \IPS\Member::loggedIn()->language()->addToStack('appupdate_databasechanges', FALSE, array( 'sprintf' => $allVersions[ $_next ] ) );
$progress = round( ( 30 * ( $data['extra']['_totalSteps'] - count($upgradeSteps) ) ) / ( $data['extra']['_totalSteps'] ?: 1 ) );
}
else
{
$laststep = 'db';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_databasechanges');
$progress = 30;
}
break;
case 'db':
/* Rebuild data */
\IPS\Application::load( $data['key'] )->installJsonData();
$laststep = 'basics';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_basics');
$progress = 40;
break;
case 'basics':
/* Insert lang data */
\IPS\Application::load( $data['key'] )->installLanguages();
$laststep = 'lang';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_languages');
$progress = 60;
break;
case 'lang':
/* Insert email templates */
\IPS\Application::load( $data['key'] )->installEmailTemplates();
$laststep = 'emails';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_emails');
$progress = 75;
break;
case 'emails':
/* Insert skin templates */
\IPS\Application::load( $data['key'] )->installSkins( TRUE );
\IPS\Application::load( $data['key'] )->installJavascript();
$laststep = 'skins';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_skins');
$progress = 100;
break;
}
/* Return null to indicate we are done */
if( $laststep === NULL )
{
\IPS\Session::i()->log( 'acplog__application_updated', array( \IPS\Application::load( $data['key'] )->titleForLog() => FALSE, \IPS\Application::load( $data['key'] )->version => TRUE ) );
return NULL;
}
else
{
return array( array( 'laststep' => $laststep, 'key' => $data['key'], 'extra' => $extra ), $language, $progress );
}
},
function()
{
/* IPS Cloud Sync */
\IPS\IPS::resyncIPSCloud('Upgraded application in ACP');
/* Invalidate disk templates */
\IPS\Theme::resetAllCacheKeys();
/* And redirect back to the overview screen */
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=applications&controller=applications' ), 'application_now_updated' );
}
);
}
/**
* Install an application that is currently stored on disk. After importing a PHAR the user is redirected to this method.
*
* @see \IPS\core\modules\admin\applications\applications::import()
* @return void
*/
public function install()
{
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('installing_application');
\IPS\Output::i()->output = new \IPS\Helpers\MultipleRedirect(
\IPS\Http\Url::internal( "app=core&module=applications&controller=applications&do=install&appKey=" . \IPS\Request::i()->appKey ),
function( $data )
{
/* On first cycle return data */
if ( !is_array( $data ) )
{
/* Does this application exist in the database? */
try
{
$application = \IPS\Application::load( \IPS\Request::i()->appKey );
if( $application->id )
{
\IPS\Output::i()->error( 'app_already_installed', '2C133/4', 403, '' );
}
}
catch( \OutOfRangeException $e ){} // We don't need to do anything if it hasn't loaded - that's good
/* Get the application data to insert the application record */
if( file_exists( \IPS\ROOT_PATH . '/applications/' . \IPS\Request::i()->appKey . '/data/application.json' ) )
{
$application = json_decode( file_get_contents( \IPS\ROOT_PATH . '/applications/' . \IPS\Request::i()->appKey . '/data/application.json' ), TRUE );
if( !$application['app_directory'] )
{
\IPS\Output::i()->error( 'app_invalid_data', '4C133/5', 403, '' );
}
$application['app_position'] = \IPS\Db::i()->select( 'MAX(app_position)', 'core_applications' )->first() + 1;
$application['app_added'] = time();
$application['app_protected'] = 0;
$application['app_enabled'] = 0; /* We will reset this post-installation */
//\IPS\Lang::saveCustom( $application['app_directory'], "__app_{$application['app_directory']}", $application['application_title'] );
unset($application['application_title']);
\IPS\Db::i()->insert( 'core_applications', $application );
}
else
{
\IPS\Output::i()->error( 'app_invalid_data', '4C133/6', 403, '' );
}
return array(
array( 'laststep' => 'start', 'key' => \IPS\Request::i()->appKey ),
\IPS\Member::loggedIn()->language()->addToStack('installing_application'),
1
);
}
/* Install the application in stages */
$laststep = NULL;
$language = NULL;
$progress = 1;
switch( $data['laststep'] )
{
case 'start':
/* Perform database changes */
\IPS\Application::load( $data['key'] )->installDatabaseSchema();
$laststep = 'db';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_databasechanges');
$progress = 12.5;
break;
case 'db':
/* Rebuild data */
\IPS\Application::load( $data['key'] )->installJsonData();
$laststep = 'basics';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_basics');
$progress = 25;
break;
case 'basics':
/* Insert lang data */
$offset = ( isset( $data['offset'] ) ) ? intval( $data['offset'] ) : 0;
$inserted = \IPS\Application::load( $data['key'] )->installLanguages( $offset, 250 );
if( $inserted )
{
$laststep = 'basics';
$data['offset'] = $offset + $inserted;
}
else
{
$laststep = 'lang';
unset( $data['offset'] );
}
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_languages');
$progress = 37.5;
break;
case 'lang':
/* Insert email templates */
\IPS\Application::load( $data['key'] )->installEmailTemplates();
$laststep = 'emails';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_emails');
$progress = 50;
break;
case 'emails':
/* Install Extensions */
\IPS\Application::load( $data['key'] )->installExtensions();
$laststep = 'extensions';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_extensions');
$progress = 62.5;
break;
case 'extensions':
/* Insert skin templates */
$offset = ( isset( $data['offset'] ) ) ? intval( $data['offset'] ) : 0;
if( !$offset )
{
\IPS\Application::load( $data['key'] )->installThemeSettings();
\IPS\Application::load( $data['key'] )->clearTemplates();
}
$inserted = \IPS\Application::load( $data['key'] )->installTemplates( FALSE, $offset, 150 );
if( $inserted )
{
$laststep = 'extensions';
$data['offset'] = $offset + $inserted;
}
else
{
$laststep = 'skins';
unset( $data['offset'] );
}
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_skins');
$progress = 75;
break;
case 'skins':
/* Insert skin templates */
\IPS\Application::load( $data['key'] )->installJavascript();
$laststep = 'javascript';
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_javascript');
$progress = 87.5;
break;
case 'javascript':
/* Insert other data */
\IPS\Application::load( $data['key'] )->installOther();
$laststep = NULL;
$language = \IPS\Member::loggedIn()->language()->addToStack('appinstall_finish');
$progress = 100;
break;
}
/* Return null to indicate we are done */
if( $laststep === NULL )
{
\IPS\Session::i()->log( 'acplog__application_installed', array( "__app_" . $data['key'] => TRUE ) );
return NULL;
}
else
{
$data['laststep'] = $laststep;
return array( $data, $language, $progress );
}
},
function()
{
/* Enable the application now */
$application = \IPS\Application::load( \IPS\Request::i()->appKey );
$application->enabled = 1;
$application->save();
/* Install hooks - do this after enabling the application */
$application->installHooks();
/* Clear caches so templates can rebuild and so on */
\IPS\Data\Store::i()->clearAll();
\IPS\Data\Cache::i()->clearAll();
\IPS\IPS::resyncIPSCloud('Installed application in ACP');
/* Invalidate disk templates */
\IPS\Theme::resetAllCacheKeys();
/* And redirect back to the overview screen */
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=applications&controller=applications' ), 'application_now_installed' );
}
);
}
/**
* Delete
*
* @return void
* @note For application uninstall we don't need the whole move children thing
*/
protected function delete()
{
/* Get node */
$nodeClass = $this->nodeClass;
if ( \IPS\Request::i()->subnode )
{
$nodeClass = $nodeClass::$subnodeClass;
}
try
{
/* Load the application - we don't use \IPS\Application::load() because this could be a legacy out of date application
and if no Application.php an exception will be thrown. We don't need to stop just based on that, we want to proceed with delete. */
$node = NULL;
foreach( \IPS\Data\Store::i()->applications as $application )
{
if( $application['app_directory'] == \IPS\Request::i()->id )
{
$node = \IPS\Application::constructFromData( $application );
break;
}
}
if( $node === NULL )
{
throw new \UnexpectedValueException;
}
/* Permission check */
if( !$node->canDelete() )
{
\IPS\Output::i()->error( 'node_noperm_delete', '2C133/J', 403, '' );
}
if ( $node->default )
{
return $this->setNewDefaultApplication($node);
}
else
{
/* Make sure the user confirmed the deletion */
\IPS\Request::i()->confirmedDelete();
}
/* Delete it */
\IPS\Session::i()->log( 'acplog__application_uninstalled', array( $node->directory => TRUE ) );
$node->delete();
/* Clear guest page caches */
\IPS\Data\Cache::i()->clearAll();
/* Clear \Data\Store */
\IPS\Data\Store::i()->clearAll();
}
/* Legacy */
catch ( \UnexpectedValueException $e )
{
if( !\IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'app_delete' ) )
{
\IPS\Output::i()->error( 'node_noperm_delete', '2C133/J', 403, '' );
}
\IPS\Db::i()->delete( 'core_applications', array( 'app_directory=?', \IPS\Request::i()->id ) );
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2C133/I', 404, '' );
}
/* Boink */
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=applications&controller=applications' ), 'deleted' );
}
/**
* Set a new default application if the current default app is being uninstalled
*
* @return void
*/
/**
* Set a new default application if the current default app is being uninstalled
*
* @param $node \IPS\Application Application to delete
* @return void
*/
protected function setNewDefaultApplication($node)
{
$form = new \IPS\Helpers\Form();
$form->hiddenValues['wasConfirmed'] = 1;
$form->add( new \IPS\Helpers\Form\Node( 'new_default_app', NULL, TRUE, array(
'class' => 'IPS\Application',
'subnodes' => false,
'permissionCheck' => function( $app ) use ( $node )
{
if ( $app->directory == 'core')
{
return false;
}
else
{
return !($node->directory == $app->directory);
}
}
) ) );
if ( $values = $form->values() )
{
$values['new_default_app']->setAsDefault();
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&do=delete&id={$node->_id}&wasConfirmed=1" ) );
}
else
{
\IPS\Output::i()->output = (string) $form;
}
}
/**
* View application details
*
* @return void
*/
public function details()
{
/* Get node */
$nodeClass = $this->nodeClass;
if ( \IPS\Request::i()->subnode )
{
$nodeClass = $nodeClass::$subnodeClass;
}
/* Get the application */
try
{
$application = call_user_func( "{$nodeClass}::load", \IPS\Request::i()->id );
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'error_no_app', '2C133/1', 404, '' );
}
/* Work out tab */
$tab = \IPS\Request::i()->tab ?: 'details';
$activeTabContents = call_user_func( array( $this, '_show' . mb_ucfirst( $tab ) ), $application );
/* If this is an AJAX request, just return tab contents */
if( \IPS\Request::i()->isAjax() && \IPS\Request::i()->tab and !isset( \IPS\Request::i()->ajaxValidate ) )
{
\IPS\Output::i()->output = $activeTabContents;
return;
}
/* Build tab list */
$tabs = array();
$tabs['details'] = 'app_details_details';
$tabs['upgrades'] = 'app_details_upgrades';
$tabs['hooks'] = 'app_details_hooks';
/* Output */
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( $tabs[ $tab ] );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global' )->tabs( $tabs, $tab, $activeTabContents, \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&do=details&id={$application->directory}" ) );
}
/**
* Upload a new application for installation
*
* @return void
*/
public function upload()
{
if ( \IPS\NO_WRITES )
{
\IPS\Output::i()->error( 'no_writes', '1C133/B', 403, '' );
}
if( !is_writable( \IPS\ROOT_PATH . "/applications/" ) )
{
\IPS\Output::i()->error( 'app_dir_not_write', '4C133/8', 500, '' );
}
if( !is_writable( \IPS\ROOT_PATH . "/plugins/" ) ) // necessary as we write the hooks.php file here
{
\IPS\Output::i()->error( 'plugin_dir_not_write', '4C133/L', 403, '' );
}
if ( !extension_loaded('phar') )
{
\IPS\Output::i()->error( 'no_phar_extension', '1C133/P', 403, '' );
}
$_type = 'install';
/* Are we upgrading an application? */
if( \IPS\Request::i()->appKey )
{
try
{
$app = NULL;
foreach( \IPS\Data\Store::i()->applications as $application )
{
if( $application['app_directory'] == \IPS\Request::i()->appKey )
{
$app = \IPS\Application::constructFromData( $application );
break;
}
}
if( $app === NULL )
{
throw new \OutOfRangeException;
}
}
catch ( \UnexpectedValueException $e )
{
// Legacy 3.x app
if ( !is_dir( \IPS\ROOT_PATH . "/applications/" . \IPS\Request::i()->appKey ) )
{
mkdir( \IPS\ROOT_PATH . "/applications/" . \IPS\Request::i()->appKey );
chmod( \IPS\ROOT_PATH . "/applications/" . \IPS\Request::i()->appKey, \IPS\IPS_FOLDER_PERMISSION );
}
}
catch( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'no_app_to_update', '2C133/C', 403, '' );
}
if( !is_writable( \IPS\ROOT_PATH . "/applications/" . $app->directory ) )
{
\IPS\Output::i()->error( \IPS\Member::loggedIn()->language()->addToStack( "app_specific_dir_nowrite", FALSE, array( 'sprintf' => $app->directory ) ), '4C133/D', 500, '' );
}
if ( \IPS\Theme::designersModeEnabled() )
{
\IPS\Output::i()->error( \IPS\Member::loggedIn()->language()->addToStack( "app_upload_designersmode", FALSE, array( 'sprintf' => \IPS\Http\Url::internal( 'app=core&module=customization&controller=themes&do=designersmode' ) ) ), '2C133/O', 403, '' );
}
$_type = 'upgrade';
}
$form = new \IPS\Helpers\Form( 'form', 'install' );
$form->add( new \IPS\Helpers\Form\Upload( 'application_file', NULL, TRUE, array( 'allowedFileTypes' => array( 'tar' ), 'temporary' => TRUE ) ) );
if ( $values = $form->values() )
{
try
{
if ( mb_substr( $values['application_file'], -4 ) !== '.tar' )
{
/* If rename fails on a significant number of customer's servers, we might have to consider using
move_uploaded_file into uploads and rename in there */
rename( $values['application_file'], $values['application_file'] . ".tar" );
$values['application_file'] .= ".tar";
}
/* Test the phar */
$application = new \PharData( $values['application_file'], 0, NULL, \Phar::TAR );
/* Get app directory */
$appdata = json_decode( file_get_contents( "phar://" . $values['application_file'] . '/data/application.json' ), TRUE );
/* Make sure that the app data is valid */
if( !isset( $appdata['app_directory'] ) )
{
throw new \UnexpectedValueException;
}
$appDirectory = $appdata['app_directory'];
/* Extract */
$application->extractTo( \IPS\ROOT_PATH . "/applications/" . $appDirectory, NULL, TRUE );
$this->_checkChmod( \IPS\ROOT_PATH . '/applications/' . $appDirectory );
\IPS\IPS::resyncIPSCloud('Uploaded new application in ACP');
}
catch( \PharException $e )
{
\IPS\Log::log( $e, 'phar' );
\IPS\Output::i()->error( 'application_notvalid', '1C133/9', 403, '' );
}
catch( \UnexpectedValueException $e )
{
\IPS\Output::i()->error( 'application_notvalid', '1C133/K', 403, '' );
}
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&do={$_type}&appKey={$appDirectory}" ), \IPS\Member::loggedIn()->language()->addToStack('installing_application') );
}
/* Display */
\IPS\Output::i()->output = $form;
}
/**
* Recursively check and adjust CHMOD permissions after uploading an application
*
* @param string $directory Directory to check
* @return void
* @see Ticket 956849
*/
protected function _checkChmod( $directory )
{
if ( !is_dir( $directory ) )
{
throw new \UnexpectedValueException;
}
$it = new \RecursiveDirectoryIterator( $directory, \FilesystemIterator::SKIP_DOTS );
foreach( new \RecursiveIteratorIterator( $it ) AS $f )
{
if ( $f->isDir() )
{
@chmod( $f->getPathname(), \IPS\IPS_FOLDER_PERMISSION );
}
else
{
/* If this is a .php file in the /interface/ folder it will be called via web directly. We cannot set permissions too high though or it won't execute in many environments */
@chmod( $f->getPathname(), ( mb_strpos( $f->getPathname(), '/interface/' ) !== FALSE AND mb_strtolower( $f->getExtension() ) == 'php' ) ? \IPS\FILE_PERMISSION_NO_WRITE : \IPS\IPS_FILE_PERMISSION );
}
}
}
/**
* Import JS from /dev folders and compile into file objects
*
* @return void
*/
public function compilejs()
{
\IPS\Output::i()->output = new \IPS\Helpers\MultipleRedirect(
\IPS\Http\Url::internal( 'app=core&module=applications&controller=applications&do=compilejs&appKey=' . \IPS\Request::i()->appKey ),
function( $data )
{
/* Is this the first cycle? */
if ( !is_array( $data ) )
{
/* Start importing */
$data = array( 'toDo' => array( 'import', 'compile' ) );
return array( $data, \IPS\Member::loggedIn()->language()->addToStack('processing') );
}
/* Grab something to build */
if ( count( $data['toDo'] ) )
{
reset( $data['toDo'] );
$command = array_shift( $data['toDo'] );
switch( $command )
{
case 'import':
$xml = \IPS\Output\Javascript::createXml( \IPS\Request::i()->appKey );
/* Write it */
if ( is_writable( \IPS\ROOT_PATH . '/applications/' . \IPS\Request::i()->appKey . '/data' ) )
{
\file_put_contents( \IPS\ROOT_PATH . '/applications/' . \IPS\Request::i()->appKey . '/data/javascript.xml', $xml->outputMemory() );
}
break;
case 'compile':
\IPS\Output\Javascript::compile( \IPS\Request::i()->appKey );
/* Compile global JS after so map is written and correct */
if ( \IPS\Request::i()->appKey == 'core' )
{
\IPS\Output\Javascript::compile('global');
}
break;
}
return array( $data, \IPS\Member::loggedIn()->language()->addToStack('processing') );
}
else
{
/* All Done */
return null;
}
},
function()
{
/* Finished */
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=applications&controller=applications' ), 'completed' );
}
);
}
/**
* Get application details
*
* @param \IPS\Node\Model $application Application node
* @return string
*/
protected function _showDetails( $application )
{
try
{
$history = \IPS\Db::i()->select( 'upgrade_date', 'core_upgrade_history', array( 'upgrade_app=?', $application->directory ), 'upgrade_version_id DESC', array( 0, 1 ) )->first();
}
catch( \UnderflowException $ex )
{
$history = null;
}
return \IPS\Theme::i()->getTemplate( 'applications' )->details( $application, $history );
}
/**
* Show the application upgrade history
*
* @param \IPS\Node\Model $application Application node
* @return string
*/
protected function _showUpgrades( $application )
{
$list = array();
$upgrades = \IPS\Db::i()->select( '*', 'core_upgrade_history', array( 'upgrade_app=?', $application->directory ), 'upgrade_version_id DESC' );
foreach( $upgrades as $version )
{
$list[ (string) \IPS\DateTime::ts( $version['upgrade_date'] ) ] = \IPS\Member::loggedIn()->language()->addToStack('app_version_string', FALSE, array( 'sprintf' => array( $version['upgrade_version_human'], $version['upgrade_version_id'] ) ) );
}
return ( count( $upgrades ) ) ? \IPS\Theme::i()->getTemplate( 'global' )->definitionTable( $list ) : \IPS\Theme::i()->getTemplate( 'global' )->paddedBlock( \IPS\Member::loggedIn()->language()->addToStack('app_no_upgrade_history') );
}
/**
* Show the application hooks
*
* @param \IPS\Application $application Application
* @return string
*/
protected function _showHooks( $application )
{
$table = new \IPS\Helpers\Table\Db( 'core_hooks', \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&do=details&id={$application->directory}&tab=hooks" ), array( 'app=?', $application->directory ) );
$table->include = array( 'filename', 'class' );
$table->langPrefix = 'plugin_hook_';
if ( !$table->sortBy )
{
$table->sortBy = 'class';
$table->sortDirection = 'asc';
}
$table->parsers = array(
'filename' => function( $val, $row )
{
return $val . '.php';
}
);
return $table;
}
}