Version 5.0.13

This commit is contained in:
Neo committed 2025-12-19 19:04:57 -08:00
1 parent b89858b2ff
commit 5efe744c5f
1407 files changed
+69690 -30177

No files matched your search

@@ -80,7 +80,7 @@ class badges extends Controller
{
if( $data = Rule::getRebuildProgress() )
{
Output::i()->output .= Theme::i()->getTemplate( 'achievements' )->rebuildProgress( $data, TRUE );
Output::i()->output .= Theme::i()->getTemplate( 'achievements', 'core' )->rebuildProgress( $data, TRUE );
}
Output::i()->sidebar['actions']['export'] = array(
@@ -183,7 +183,7 @@ class badges extends Controller
if ( $rule )
{
return Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( Url::internal("app=core&module=achievements&controller=rules&do=form&id={$rule->id}"), FALSE, $rule->extension()->ruleDescription( $rule ), TRUE, FALSE, FALSE, TRUE );
return Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( Url::internal("app=core&module=achievements&controller=rules&do=form&id={$rule->id}"), FALSE, $rule->extension()?->ruleDescription( $rule ), TRUE, FALSE, FALSE, TRUE );
}
else
{
@@ -219,7 +219,7 @@ class badges extends Controller
$exportableBadges = Db::i()->select( 'COUNT(*)', 'core_badges', $where )->first();
/* Display */
Output::i()->output = Theme::i()->getTemplate( 'achievements' )->badgeExport( $exportableBadges );
Output::i()->output = Theme::i()->getTemplate( 'achievements', 'core' )->badgeExport( $exportableBadges );
}
/**
@@ -98,7 +98,7 @@ class ranks extends Controller
if( $data = Rule::getRebuildProgress() )
{
Output::i()->output .= Theme::i()->getTemplate( 'achievements' )->rebuildProgress( $data, TRUE );
Output::i()->output .= Theme::i()->getTemplate( 'achievements', 'core' )->rebuildProgress( $data, TRUE );
}
Output::i()->sidebar['actions']['export'] = array(
@@ -13,6 +13,8 @@ namespace IPS\core\modules\admin\achievements;
/* To prevent PHP errors (extending class does not exist) revealing path */
use Exception;
use IPS\Application;
use IPS\core\Achievements\Actions\AchievementActionAbstract;
use IPS\core\Achievements\Rule;
use IPS\Db;
use IPS\Dispatcher;
@@ -75,20 +77,31 @@ class rules extends Controller
*/
protected function manage() : void
{
/* Get a list of rules that should always be hidden */
$hiddenActions = [];
foreach( Application::allExtensions( 'core', 'AchievementAction', false ) as $action => $extension )
{
/* @var AchievementActionAbstract $extension */
if( !$extension->showInAcp() )
{
$hiddenActions[] = $action;
}
}
$baseUrl = Url::internal('app=core&module=achievements&controller=rules');
$perPage = 25;
$totalCount = Db::i()->select( 'COUNT(*)', 'core_achievements_rules' )->first();
$totalPages = ceil( $totalCount / $perPage );
$page = Request::i()->page ?? 1;
$query = Db::i()->select( '*', 'core_achievements_rules', NULL, 'action, milestone ASC', [ ( $perPage * ( $page - 1 ) ), $perPage ] );
$query = Db::i()->select( '*', 'core_achievements_rules', Db::i()->in( 'action', $hiddenActions, true ), 'action, milestone ASC', [ ( $perPage * ( $page - 1 ) ), $perPage ] );
$rules = new ActiveRecordIterator( $query, 'IPS\core\Achievements\Rule' );
$pagination = $totalPages > 1 ? Theme::i()->getTemplate( 'global', 'core', 'global' )->pagination( $baseUrl, $totalPages, $page, $perPage ) : '';
if( Request::i()->isAjax() )
{
Output::i()->json([
'rows' => Theme::i()->getTemplate( 'achievements' )->rulesListRows( $rules ),
'rows' => Theme::i()->getTemplate( 'achievements', 'core' )->rulesListRows( $rules ),
'pagination' => $pagination
]);
}
@@ -114,7 +127,7 @@ class rules extends Controller
}
Output::i()->output = Theme::i()->getTemplate( 'forms', 'core' )->blurb( 'cheev_rules_blurb' ) . Theme::i()->getTemplate( 'achievements' )->rulesList( $rules, $pagination, $this->_getRootButtons() );
Output::i()->output = Theme::i()->getTemplate( 'forms', 'core' )->blurb( 'cheev_rules_blurb' ) . Theme::i()->getTemplate( 'achievements', 'core' )->rulesList( $rules, $pagination, $this->_getRootButtons() );
}
}
@@ -145,7 +158,7 @@ class rules extends Controller
Output::i()->title = Member::loggedIn()->language()->addToStack('menu__core_achievements_rules');
return $form->customTemplate( [ Theme::i()->getTemplate( 'achievements' ), 'rulesForm' ] );
return $form->customTemplate( [ Theme::i()->getTemplate( 'achievements', 'core' ), 'rulesForm' ] );
}
/**
@@ -92,7 +92,7 @@ class settings extends Controller
if( $data = Rule::getRebuildProgress() )
{
Output::i()->output .= Theme::i()->getTemplate( 'achievements' )->rebuildProgress( $data, TRUE );
Output::i()->output .= Theme::i()->getTemplate( 'achievements', 'core' )->rebuildProgress( $data, TRUE );
}
Output::i()->title = Member::loggedIn()->language()->addToStack('achievements_settings');
@@ -19,6 +19,7 @@ use IPS\Member;
use IPS\Output;
use IPS\Request;
use IPS\Theme;
use function array_merge;
use function defined;
use function is_array;
@@ -74,9 +75,7 @@ class overview extends StatsOverview
catch( Exception $e ){}
}
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_stats.js', 'core' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/statistics.css', 'core', 'admin' ) );
Output::i()->title = Member::loggedIn()->language()->addToStack('menu__core_stats_overview');
Output::i()->title = Member::loggedIn()->language()->addToStack('menu__core_keystats_activityoverview');
Output::i()->output = Theme::i()->getTemplate( $this->templateGroup )->overview( $formHtml, $blocks, $excludedApps );
}
@@ -7,7 +7,7 @@
* @package Invision Community
* @since 18 Feb 2013
*/
namespace IPS\core\modules\admin\applications;
/* To prevent PHP errors (extending class does not exist) revealing path */
@@ -28,6 +28,7 @@ use IPS\Data\Store;
use IPS\DateTime;
use IPS\Db;
use IPS\Db\Exception as DbException;
use IPS\Developer;
use IPS\Dispatcher;
use IPS\File;
use IPS\Helpers\Form;
@@ -77,6 +78,7 @@ use function intval;
use function is_array;
use function is_dir;
use function time;
use function unlink;
use const IPS\CIC2;
use const IPS\DEMO_MODE;
use const IPS\FILE_PERMISSION_NO_WRITE;
@@ -104,7 +106,7 @@ class applications extends Controller
* @brief Has been CSRF-protected
*/
public static bool $csrfProtected = TRUE;
/**
* Node Class
*/
@@ -114,7 +116,7 @@ class applications extends Controller
* Description can contain HTML?
*/
public bool $_descriptionHtml = TRUE;
/**
* Execute
*
@@ -335,7 +337,7 @@ class applications extends Controller
}
}
}
if( Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'app_delete' ) )
{
$buttons['delete'] = array(
@@ -346,7 +348,7 @@ class applications extends Controller
'hotkey'=> 'd'
);
}
$rows[ $k ] = Theme::i()->getTemplate( 'trees' )->row( $baseUrl, $k, $app['title'], FALSE, $buttons, $app['author'], null, null, false, null, null, $badge );
}
}
@@ -364,7 +366,7 @@ class applications extends Controller
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack( 'upload_new_version' ) )
);
}
if( Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'app_delete' ) )
{
$buttons['delete'] = array(
@@ -375,7 +377,7 @@ class applications extends Controller
'hotkey'=> 'd'
);
}
return Theme::i()->getTemplate( 'trees' )->row( $baseUrl, $key, $legacyApps[ $key ]['title'], FALSE, $buttons, '', NULL, NULL, $root );
},
function() { return 0; },
@@ -391,7 +393,7 @@ class applications extends Controller
/* Javascript */
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_system.js', 'core', 'admin' ) );
/* Check for updates button */
Output::i()->sidebar['actions']['settings'] = array(
'icon' => 'refresh',
@@ -438,7 +440,7 @@ class applications extends Controller
parent::_afterSave( $old, $new, $lastUsedTab );
}
/**
* Check for updates
*
@@ -447,13 +449,13 @@ class applications extends Controller
public function updateCheck() : void
{
Session::i()->csrfCheck();
$task = Task::constructFromData( Db::i()->select( '*', 'core_tasks', array( 'app=? AND `key`=?', 'core', 'updatecheck' ) )->first() );
$task->type = 'manual';
$task->run();
Output::i()->redirect( Url::internal( "app=core&module=applications&controller=applications" ), 'update_check_complete' );
}
/**
* Set as default app
*
@@ -484,7 +486,7 @@ class applications extends Controller
public function setDefaultModule() : void
{
Session::i()->csrfCheck();
try
{
$module = Module::load( Request::i()->id );
@@ -494,7 +496,7 @@ class applications extends Controller
{
Output::i()->error( 'no_module_for_default', '2C133/A', 403, '' );
}
Output::i()->redirect( Url::internal( "app=core&module=applications&controller=applications&root={$module->application}" ), 'saved' );
}
@@ -595,15 +597,15 @@ class applications extends Controller
}
}
}
/* Work out buttons */
/* Work out buttons */
$buttons = $node->getButtons($this->url, !($node instanceof $this->nodeClass));
if ( isset( Request::i()->searchResult ) and isset( $buttons['edit'] ) )
{
$buttons['edit']['link'] = $buttons['edit']['link']->setQueryString( 'searchResult', Request::i()->searchResult );
}
/* Return */
/* Return */
return Theme::i()->getTemplate( 'trees', 'core' )->row(
$this->url,
$node->_id,
@@ -621,7 +623,7 @@ class applications extends Controller
$this->_descriptionHtml,
$node->canAdd(),
TRUE,
Theme::i()->getTemplate('applications')->appRowAdditional( $node ),
Theme::i()->getTemplate('applications')->appRowAdditional( $node ),
$node->_lockedLang
);
}
@@ -641,7 +643,7 @@ class applications extends Controller
parent::permissions();
return;
}
/* Load Node */
try
{
@@ -651,7 +653,7 @@ class applications extends Controller
{
Output::i()->error( 'node_error', '3S101/A', 404, '' );
}
/* Check we're not locked */
if( $node->_locked or !$node->canEdit() )
{
@@ -665,7 +667,7 @@ class applications extends Controller
{
Member::loggedIn()->language()->words['app_enabled_desc'] = Member::loggedIn()->language()->addToStack( 'app_enabled_desc_no_writes' );
}
$form->add( new CheckboxSet( 'app_disabled_groups', ( $node->disabled_groups == '*' or $node->disabled_groups === NULL ) ? '*' : explode( ',', $node->disabled_groups ), FALSE, array(
'options' => array_combine( array_keys( Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, Group::groups() ) ),
'multiple' => true,
@@ -674,24 +676,24 @@ class applications extends Controller
'impliedUnlimited' => TRUE
), NULL, NULL, NULL, 'app_disabled_groups' ) );
$form->add( new 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();
/* Clear templates to rebuild automatically */
Theme::deleteCompiledTemplate();
$this->_logToggleAndRedirect( $node );
}
/* Display */
Output::i()->output = $form;
}
/**
* Build form
*
@@ -703,7 +705,7 @@ class applications extends Controller
{
$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;
@@ -714,7 +716,7 @@ class applications extends Controller
$defaults['long'] = $long + 1;
break;
}
$options = array(
'options' => array(),
'toggles' => array( 'new' => array( 'versions_human', 'versions_long' ) )
@@ -729,7 +731,7 @@ class applications extends Controller
{
$options['options']['download'] = 'developer_build_download';
}
$form = new Form;
$form->add( new Radio( 'developer_build_type', 'rebuild', TRUE, $options ) );
$form->add( new Text( 'versions_human', $defaults['human'], NULL, array(), function( $val )
@@ -755,9 +757,19 @@ class applications extends Controller
}
}, NULL, NULL, 'versions_long' ) );
$app = Application::load( $appKey );
if( !in_array( $app->directory, IPS::$ipsApps ) )
{
/* Show current description */
$form->addHeader( 'app_details_app_description');
$html = Theme::i()->getTemplate( 'applications' )->appDescConfirm($app);
$form->addHtml( Theme::i()->getTemplate( 'forms' )->blurb( $html , false, true ) );
$form->addButton( 'cancel', 'button', class: 'ipsButton ipsButton--negative', attributes: ['data-action'=>"dialogClose"]);
}
return $form;
}
/**
* Build all applications
*
@@ -769,10 +781,10 @@ class applications extends Controller
{
Output::i()->error( 'not_in_dev', '2C133/M', 403, '' );
}
$form = $this->_buildForm( 'core' );
$form->add( new YesNo( 'developer_build_submit', (bool) IPS_PASSWORD ) );
if ( $values = $form->values() )
{
foreach ( Application::applications() as $application )
@@ -781,7 +793,7 @@ class applications extends Controller
{
$application->assignNewVersion( $values['versions_long'], $values['versions_human'] );
}
try
{
$application->build();
@@ -791,20 +803,20 @@ class applications extends Controller
Output::i()->error( $e->getMessage(), '' );
}
}
if ( $values['developer_build_submit'] )
{
Output::i()->redirect( Url::internal( 'app=core&module=applications&controller=applications&do=submit' )->csrf() );
}
else
{
{
Output::i()->redirect( Url::internal( 'app=core&module=applications&controller=applications' ), 'application_now_built' );
}
}
Output::i()->output = $form;
}
/**
* Build an application
*
@@ -816,18 +828,24 @@ class applications extends Controller
{
Output::i()->error( 'not_in_dev', '2C133/N', 403, '' );
}
$application = Application::load( Request::i()->appKey );
if( !in_array( $application->directory, IPS::$ipsApps ) and $application->description == '' )
{
Output::i()->output = Theme::i()->getTemplate( 'applications' )->appDescMissing( $application );
return;
}
$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();
@@ -836,19 +854,19 @@ class applications extends Controller
{
Output::i()->error( $e->getMessage() . "\n" . $e->getTraceAsString(), '' );
}
Output::i()->redirect( Url::internal( 'app=core&module=developer&appKey=' . $application->directory ), 'application_now_built' );
}
Output::i()->output = $form;
}
/**
* Export an application from Chocapikk.net
* Export an application from ipbmafia.ru
*
* @return void
*/
public function downloadChocapikk()
public function downloadMafia()
{
$application = Application::load( Request::i()->appKey );
@@ -856,7 +874,7 @@ class applications extends Controller
{
$pharPath = str_replace( '\\', '/', rtrim( TEMP_DIRECTORY, '/' ) ) . '/' . $application->directory . ".tar";
$download = new \PharData( $pharPath, 0, $application->directory . ".tar", \Phar::TAR );
$download->buildFromIterator( new BuilderIterator( $application ), \IPS\ROOT_PATH . "/applications/" . $application->directory . "/" );
}
catch( \PharException $e )
@@ -864,16 +882,16 @@ class applications extends Controller
Log::log( $e, 'phar' );
Output::i()->error( 'app_no_phar', '4C133/7', 403, '' );
}
$output = \file_get_contents( rtrim( TEMP_DIRECTORY, '/' ) . '/' . $application->directory . ".tar" );
/* Cleanup */
unset($download);
\Phar::unlinkArchive($pharPath);
Output::i()->sendOutput( $output, 200, 'application/tar', array( 'Content-Disposition' => Output::getContentDisposition( 'attachment', Member::loggedIn()->language()->get('__app_' . $application->directory ) . " {$application->version}.tar" ) ), FALSE, FALSE, FALSE );
}
/**
* Export an application
*
@@ -890,6 +908,12 @@ class applications extends Controller
Output::i()->error( 'not_in_dev', '2C133/10', 403, '' );
}
if( !in_array( $application->directory, IPS::$ipsApps ) and $application->description == '' )
{
Output::i()->output = Theme::i()->getTemplate( 'applications' )->appDescMissing( $application );
return;
}
$form = $this->_buildForm( $application->directory, TRUE );
if ( $values = $form->values() )
{
@@ -908,12 +932,11 @@ class applications extends Controller
Output::i()->error( $e->getMessage(), '' );
}
}
try
{
$pharPath = str_replace( '\\', '/', rtrim( TEMP_DIRECTORY, '/' ) ) . '/' . $application->directory . ".tar";
$download = new PharData( $pharPath, 0, $application->directory . ".tar", Phar::TAR );
$download->buildFromIterator( new BuilderIterator( $application ), \IPS\ROOT_PATH . "/applications/" . $application->directory . "/" );
}
catch( PharException $e )
@@ -923,14 +946,29 @@ class applications extends Controller
}
$output = file_get_contents( rtrim( TEMP_DIRECTORY, '/' ) . '/' . $application->directory . ".tar" );
unset( $download );
/* Cleanup */
unset($download);
Phar::unlinkArchive($pharPath);
register_shutdown_function( function () use ( $pharPath )
{
if ( file_exists( $pharPath ) )
{
try
{
Phar::unlinkArchive($pharPath);
}
catch( Exception $e ) { }
if( file_exists( $pharPath ) )
{
unlink( $pharPath );
}
}
} );
Output::i()->sendOutput( $output, 200, 'application/tar', array( 'Content-Disposition' => Output::getContentDisposition( 'attachment', Member::loggedIn()->language()->get('__app_' . $application->directory ) . " {$application->version}.tar" ) ), FALSE, FALSE, FALSE );
}
Output::i()->output = $form;
}
@@ -961,9 +999,9 @@ class applications extends Controller
public function upgrade() : void
{
Session::i()->csrfCheck();
Output::i()->title = Member::loggedIn()->language()->addToStack('upgrading_application');
unset( Store::i()->syncCompleted );
$url = Url::internal( "app=core&module=applications&controller=applications&do=upgrade&appKey=" . Request::i()->appKey )->csrf();
@@ -1033,7 +1071,7 @@ class applications extends Controller
$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 );
@@ -1041,10 +1079,10 @@ class applications extends Controller
Db::i()->insert( 'core_upgrade_history', array( 'upgrade_version_human' => $latestHVersion, 'upgrade_version_id' => $latestLVersion, 'upgrade_date' => time(), 'upgrade_mid' => (int) 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 = Application::load( $data['key'] )->getUpgradeSteps( $lastRan );
/* Did we find any? */
if( count( $upgradeSteps ) )
{
@@ -1076,8 +1114,8 @@ class applications extends Controller
/* What step in the upgrader file are we on? */
$upgradeStep = ( isset($data['extra']['_upgradeStep']) ) ? intval($data['extra']['_upgradeStep']) : 1;
/* Delete removed language strings */
/* Delete removed language strings */
if( file_exists( \IPS\ROOT_PATH . "/applications/{$data['key']}/setup/upg_{$_next}/lang.json" ) )
{
$langChanges = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$data['key']}/setup/upg_{$_next}/lang.json" ), TRUE );
@@ -1248,7 +1286,7 @@ class applications extends Controller
{
Output::i()->redirect( $_SESSION['cmsConflictKey'] );
}
/* And redirect back to the overview screen */
Output::i()->redirect( Url::internal( 'app=core&module=applications&controller=applications' ), 'application_now_updated' );
}
@@ -1264,9 +1302,9 @@ class applications extends Controller
public function install() : void
{
Session::i()->csrfCheck();
unset( Store::i()->syncCompleted );
Output::i()->title = Member::loggedIn()->language()->addToStack('installing_application');
$url = Url::internal( "app=core&module=applications&controller=applications&do=install&appKey=" . Request::i()->appKey )->csrf();
@@ -1289,7 +1327,7 @@ class applications extends Controller
}
}
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( Application::getRootPath( Request::i()->appKey ) . '/applications/' . Request::i()->appKey . '/data/application.json' ) )
{
@@ -1382,11 +1420,11 @@ class applications extends Controller
$language = Member::loggedIn()->language()->addToStack('appinstall_emails');
$progress = 50;
break;
case 'emails':
/* Install Extensions */
Application::load( $data['key'] )->installExtensions();
$laststep = 'extensions';
$language = Member::loggedIn()->language()->addToStack('appinstall_extensions');
$progress = 62.5;
@@ -1402,7 +1440,7 @@ class applications extends Controller
Application::load( $data['key'] )->installCustomTemplates();
Application::load( $data['key'] )->clearTemplates();
}
$inserted = Application::load( $data['key'] )->installTemplates( FALSE, $offset, 150 );
if( $inserted )
@@ -1423,7 +1461,7 @@ class applications extends Controller
case 'skins':
/* Insert skin templates */
Application::load( $data['key'] )->installJavascript();
$hasCmsTemplates = file_exists( Application::getRootPath( $data['key'] ) . '/applications/' . $data['key'] . '/data/cmsTemplates.xml' );
$laststep = $hasCmsTemplates ? 'cmstemplates' : 'javascript';
$language = $hasCmsTemplates ? Member::loggedIn()->language()->addToStack('appinstall_cmstemplates') : Member::loggedIn()->language()->addToStack('appinstall_javascript');
@@ -1510,7 +1548,7 @@ class applications extends Controller
protected function delete() : void
{
Session::i()->csrfCheck();
/* Get node */
/* @var Model $nodeClass */
$nodeClass = $this->nodeClass;
@@ -1518,7 +1556,7 @@ class applications extends Controller
{
$nodeClass = $nodeClass::$subnodeClass;
}
try
{
/* Load the application - we don't use \IPS\Application::load() because this could be a legacy out of date application
@@ -1545,13 +1583,13 @@ class applications extends Controller
{
throw new UnexpectedValueException;
}
/* Permission check */
if( !$node->canDelete() )
{
Output::i()->error( 'node_noperm_delete', '2C133/J', 403, '' );
}
if ( $node->default )
{
$this->setNewDefaultApplication($node);
@@ -1562,14 +1600,14 @@ class applications extends Controller
/* Make sure the user confirmed the deletion */
Request::i()->confirmedDelete();
}
/* Delete it */
Session::i()->log( 'acplog__application_uninstalled', array( $node->directory => TRUE ) );
$node->delete();
/* Clear caches */
Cache::i()->clearAll();
/* Clear \Data\Store */
Store::i()->clearAll();
}
@@ -1580,7 +1618,7 @@ class applications extends Controller
{
Output::i()->error( 'node_noperm_delete', '2C133/J', 403, '' );
}
Db::i()->delete( 'core_applications', array( 'app_directory=?', Request::i()->id ) );
}
catch ( OutOfRangeException $e )
@@ -1616,7 +1654,7 @@ class applications extends Controller
foreach( Db::i()->select( '*', 'core_profile_steps' ) as $step )
{
list( $application, $extension ) = explode( "_", $step['step_extension'] );
[ $application, $extension ] = explode( "_", $step['step_extension'] );
if( $application == $app )
{
Lang::deleteCustom( 'core', 'profile_step_title_' . $step['step_id'] );
@@ -1916,11 +1954,11 @@ class applications extends Controller
Settings::i()->clearCache();
/* Remove the files and folders, if possible (if not IN_DEV and not in DEMO_MODE and not on platform) */
if ( !CIC2 AND !IN_DEV AND !DEMO_MODE AND file_exists( ROOT_PATH . '/applications/' . $app ) )
if ( !CIC2 AND !IN_DEV AND !DEMO_MODE AND file_exists( \IPS\ROOT_PATH . '/applications/' . $app ) )
{
try
{
$iterator = new RecursiveDirectoryIterator( ROOT_PATH . '/applications/' . $app, FilesystemIterator::SKIP_DOTS );
$iterator = new RecursiveDirectoryIterator( \IPS\ROOT_PATH . '/applications/' . $app, FilesystemIterator::SKIP_DOTS );
foreach ( new RecursiveIteratorIterator( $iterator, RecursiveIteratorIterator::CHILD_FIRST ) as $file )
{
if ( $file->isDir() )
@@ -1932,7 +1970,7 @@ class applications extends Controller
@unlink( $file->getPathname() );
}
}
$dir = ROOT_PATH . '/applications/' . $app;
$dir = \IPS\ROOT_PATH . '/applications/' . $app;
$handle = opendir( $dir );
closedir ( $handle );
@rmdir( $dir );
@@ -2042,7 +2080,7 @@ class applications extends Controller
{
Output::i()->error( 'demo_mode_function_blocked', '1C133/V', 403, '' );
}
if ( NO_WRITES )
{
Output::i()->error( 'no_writes', '1C133/B', 403, '' );
@@ -2052,12 +2090,12 @@ class applications extends Controller
{
Output::i()->error( 'app_dir_not_write', '4C133/8', 500, '' );
}
if ( !extension_loaded('phar') )
{
Output::i()->error( 'no_phar_extension', '1C133/P', 403, '' );
}
if ( !IPS::checkThirdParty() )
{
Output::i()->error( 'cic_3rdparty_unavailable', '2C133/Z', 403, '' );
@@ -2124,28 +2162,28 @@ class applications extends Controller
/* 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'];
if ( CIC2 )
{
unset( Store::i()->syncCompleted );
\IPS\Cicloud\file( "IPScustomapp_{$appdata['app_directory']}.tar", file_get_contents( $values['application_file'] ) );
/* Check files are ready */
@@ -2189,7 +2227,7 @@ class applications extends Controller
/* Display */
Output::i()->output = $form;
}
/**
* Recursively check and adjust CHMOD permissions after uploading an application
*
@@ -2203,7 +2241,7 @@ class applications extends Controller
{
throw new UnexpectedValueException;
}
$it = new RecursiveDirectoryIterator( $directory, FilesystemIterator::SKIP_DOTS );
foreach( new RecursiveIteratorIterator( $it ) AS $f )
{
@@ -2218,7 +2256,7 @@ class applications extends Controller
}
}
}
/**
* Import JS from /dev folders and compile into file objects
*
@@ -2227,7 +2265,7 @@ class applications extends Controller
public function compilejs() : void
{
Session::i()->csrfCheck();
Output::i()->output = new MultipleRedirect(
Url::internal( 'app=core&module=applications&controller=applications&do=compilejs&appKey=' . Request::i()->appKey )->csrf(),
function( $data )
@@ -2237,21 +2275,21 @@ class applications extends Controller
{
/* Start importing */
$data = array( 'toDo' => array( 'import', 'compile' ) );
return array( $data, 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 = Javascript::createXml( Request::i()->appKey );
/* Write it */
if ( is_writable( \IPS\ROOT_PATH . '/applications/' . Request::i()->appKey . '/data' ) )
{
@@ -2260,16 +2298,16 @@ class applications extends Controller
break;
case 'compile':
Javascript::compile( Request::i()->appKey );
/* Compile global JS after so map is written and correct */
if ( Request::i()->appKey == 'core' )
{
Javascript::compile('global');
}
break;
}
return array( $data, Member::loggedIn()->language()->addToStack('processing') );
}
else
@@ -2285,7 +2323,7 @@ class applications extends Controller
}
);
}
/**
* Submit application details to IPS backend
*
@@ -2294,7 +2332,7 @@ class applications extends Controller
public function submit() : void
{
Session::i()->csrfCheck();
$url = Url::internal('app=core&module=applications&controller=applications&do=submit')->csrf();
Output::i()->output = (string) new MultipleRedirect( $url, function( $data )
{
@@ -2302,7 +2340,7 @@ class applications extends Controller
{
$data['done'] = array();
}
foreach ( IPS::$ipsApps as $app )
{
if ( in_array( $app, $data['done'] ) )
@@ -2312,13 +2350,13 @@ class applications extends Controller
/* Prevent race conditions */
$data['done'][] = $app;
try
{
$version = Application::getAvailableVersion( $app );
$info = json_decode( file_get_contents( \IPS\ROOT_PATH . '/applications/' . $app . '/setup/upg_' . $version . '/data.json' ), TRUE );
$info['built'] = time();
$response = Url::ips('upgrade')->request()->login( 'dev', IPS_PASSWORD )->post( array(
'version' => $version,
'app' => $app,
@@ -2330,7 +2368,7 @@ class applications extends Controller
{
Output::i()->error( $app . $response, '2C133/X', 403, '' );
}
foreach ( $info['steps'] as $k => $v )
{
if ( $v and !in_array( $k, array( 'customOptions', 'customRoutines' ) ) and file_exists( \IPS\ROOT_PATH . '/applications/' . $app . '/setup/upg_' . $version . '/' . $k . '.json' ) )
@@ -2348,7 +2386,7 @@ class applications extends Controller
}
}
}
return array( $data, Member::loggedIn()->language()->addToStack('processing') );
}
catch ( Exception $e )
@@ -2356,7 +2394,7 @@ class applications extends Controller
Output::i()->error( $e->getMessage(), '2C133/Y', 500, '' );
}
}
return NULL;
},
function()
@@ -2381,7 +2419,7 @@ class applications extends Controller
{
$history = null;
}
return Theme::i()->getTemplate( 'applications' )->details( $application, $history );
}
@@ -13,6 +13,7 @@ namespace IPS\core\modules\admin\applications;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\DateTime;
use IPS\Dispatcher;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Interval;
@@ -135,9 +136,39 @@ class webhooks extends Controller
'title' => 'settings',
'icon' => 'cog',
'link' => $this->url->setQueryString('do', 'settings'),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('settings') )
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('settings') ),
];
return array_merge( $response, [ 'settings' =>$settingsButton] );
$logButton = [
'icon' => 'list',
'title' => 'webhook_response_log',
'link' => $this->url->setQueryString( ['do' => 'webhooklog'] ),
];
return array_merge( $response, [ 'settings' =>$settingsButton, 'log' =>$logButton ] );
}
/**
* Get all the logged Webhook Responses
*
* @return void
*/
public function webhooklog() : void
{
$table = new \IPS\Helpers\Table\Db( 'core_api_webhook_fires', $this->url->setQueryString( ['do' => 'webhooklog'] ) );
$table->include = ['event', 'time', 'data'];
$table->rowClasses = ['data' => ['ipsTable_wrap']];
$table->sortBy = $table->sortBy ?: 'time';
$table->sortDirection = $table->sortDirection ?: 'desc';
$table->parsers = [
'time' => function( $val )
{
return DateTime::ts( $val );
},
];
Output::i()->title = Member::loggedIn()->language()->addToStack('webhook_response_log');
Output::i()->output = $table;
}
}
@@ -153,6 +153,12 @@ class settings extends Controller
Session::i()->log( 'acplog__club_settings' );
Output::i()->redirect( Url::internal('app=core&module=clubs&controller=settings'), 'saved' );
}
Output::i()->sidebar['actions']['templates'] = [
'icon' => 'file',
'title' => 'clubs_templates',
'link' => Url::internal( "app=core&module=clubs&controller=templates" )
];
Output::i()->title = Member::loggedIn()->language()->addToStack('club_settings');
Output::i()->output = $form;
@@ -0,0 +1,72 @@
<?php
/**
* @brief templates
* @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 09 Jul 2025
*/
namespace IPS\core\modules\admin\clubs;
use IPS\Dispatcher;
use IPS\Http\Url;
use IPS\Member;
use IPS\Node\Controller;
use IPS\Node\Model;
use IPS\Output;
use function defined;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* templates
*/
class templates extends Controller
{
public static bool $csrfProtected = true;
/**
* Node Class
*/
protected string $nodeClass = 'IPS\Member\Club\Template';
/**
* Execute
*
* @return void
*/
public function execute() : void
{
Dispatcher::i()->checkAcpPermission( 'clubs_settings_manage' );
parent::execute();
Output::i()->breadcrumb[] = [ Url::internal( "app=core&module=clubs&controller=settings" ), Member::loggedIn()->language()->addToStack( 'menu__core_clubs_settings') ];
Output::i()->breadcrumb[] = [ Url::internal( "app=core&module=clubs&controller=templates" ), Member::loggedIn()->language()->addToStack( 'menu__core_clubs_templates' ) ];
}
/**
* Redirect after save
*
* @param Model|null $old A clone of the node as it was before or NULL if this is a creation
* @param Model $new The node now
* @param bool|string $lastUsedTab The tab last used in the form
* @return void
*/
protected function _afterSave( ?Model $old, Model $new, bool|string $lastUsedTab = FALSE ): void
{
if( $old === null )
{
Output::i()->redirect( Url::internal( "app=core&module=clubs&controller=templates&do=form&id=" . $new->id ) );
}
parent::_afterSave( $old, $new, $lastUsedTab );
}
}
@@ -752,7 +752,6 @@ class themes extends Controller
Db::i()->insert( 'core_sys_lang_words', array(
'lang_id' => $langId,
'word_app' => NULL,
'word_plugin' => NULL,
'word_theme' => Request::i()->id,
'word_key' => $xml->getAttribute('key'),
'word_default' => $default,
@@ -781,13 +780,30 @@ class themes extends Controller
{
if ( $xml->name == 'template' )
{
Db::i()->delete( 'core_theme_templates_custom', [ 'template_set_id=? and template_name=? and template_hookpoint=? and template_hookpoint_type=?', Request::i()->id, $xml->getAttribute('name'), $xml->getAttribute('hookpoint'), $xml->getAttribute('hookpoint_type') ] );
$hookPoint = $xml->getAttribute( 'hookpoint' );
$key = $xml->getAttribute( 'key' );
$where = [
[ 'template_set_id=?', Request::i()->id ],
[ 'template_name=?', $xml->getAttribute('name') ],
[ 'template_hookpoint_type=?', $xml->getAttribute( 'hookpoint_type' ) ]
];
if( empty( $hookPoint ) )
{
$where[] = [ 'template_hookpoint is null'];
}
else
{
$where[] = [ 'template_hookpoint=?', $hookPoint ];
}
Db::i()->delete( 'core_theme_templates_custom', $where );
Db::i()->insert( 'core_theme_templates_custom', [
'template_set_id' => Request::i()->id,
'template_name' => $xml->getAttribute('name'),
'template_hookpoint' => $xml->getAttribute('hookpoint'),
'template_hookpoint' => ( $hookPoint ?: null ),
'template_hookpoint_type' => $xml->getAttribute('hookpoint_type'),
'template_key' => $xml->getAttribute('key'),
'template_key' => ( $key ?: null ),
'template_version' => $xml->getAttribute('version'),
'template_content' => $xml->readString(),
'template_updated' => time(),
@@ -815,9 +831,10 @@ class themes extends Controller
File::get( 'core_Theme', $set->logo[ $type ]['url'] )->delete();
}
File::$safeFileExtensions[] = 'svg';
$url = (string) File::create( 'core_Theme', $name, base64_decode( $xml->readString() ) );
$set->saveSet( array( 'logo' => array( $type => array( 'url' => $url, 'height' => $xml->getAttribute('height') ?? 100 ) ) ) );
$set->saveSet( array( 'logo' => array( $type => array( 'filename' => $name, 'setting_height' => $xml->getAttribute('height') ?? 100 ) ) ) );
}
break;
}
@@ -829,21 +846,22 @@ class themes extends Controller
},
function()
{
$set = Theme::load( Request::i()->id );
/* Do we need to clean up orphaned CSS files? */
if ( $_SESSION['theme_import']['isNewSet'] === false and isset( $_SESSION['theme_import']['css'] ) )
{
Db::i()->delete( 'core_theme_css', array( Db::i()->in( 'css_id', $_SESSION['theme_import']['css'], true ) ) );
Db::i()->delete( 'core_theme_css', array(
array( Db::i()->in( 'css_id', $_SESSION['theme_import']['css'], true ) ),
array( 'css_set_id=?', $set->id )
) );
}
$set = Theme::load( Request::i()->id );
Theme::deleteCompiledResources( null, null, null, null, $set->id );
Theme::deleteCompiledTemplate( null, null, null, $set->id );
Theme::deleteCompiledCss( null, null, null, null, $set->id );
Store::i()->delete( 'core_theme_import_' . md5_file( Request::i()->file ) );
unset( Store::i()->themes );
unset( Store::i()->custom_templates_rows );
@unlink( Request::i()->file );
@@ -853,6 +871,9 @@ class themes extends Controller
$app->installThemeEditorSettings();
}
/* Clear the entire data store, there are too many to list here */
Store::i()->clearAll();
Output::i()->redirect( Url::internal( 'app=core&module=customization&controller=themes' ) );
}
);
@@ -998,15 +1019,13 @@ class themes extends Controller
$xml->endElement();
/* Copy logos */
if( $themeSet->logo_data )
if( $logos = $themeSet->logo )
{
$logos = json_decode( $themeSet->logo_data, TRUE );
if( is_array( $logos ) )
{
foreach ( $logos as $file => $data )
{
if( isset( $data['url'] ) )
if( isset( $data['filename'] ) )
{
/* Start the XML */
$xml->startElement('logo');
@@ -1015,10 +1034,10 @@ class themes extends Controller
$xml->text( $file );
$xml->endAttribute();
if( isset( $data['height'] ) )
if( isset( $data['setting_height'] ) )
{
$xml->startAttribute('height');
$xml->text( $data['height'] );
$xml->text( $data['setting_height'] );
$xml->endAttribute();
}
@@ -1026,7 +1045,7 @@ class themes extends Controller
$original = File::get( 'core_Theme', $data['url'] );
$xml->startAttribute('name');
$xml->text( $original->originalFilename );
$xml->text( $data['filename'] );
$xml->endAttribute();
$xml->text( base64_encode( $original->contents() ) );
@@ -1063,7 +1082,7 @@ class themes extends Controller
}
$xml->startElement( 'name' );
$xml->text( $cat['cat_name'] );
$xml->text( (string) $cat['cat_name'] );
$xml->endElement();
$xml->startElement( 'icon' );
@@ -1107,11 +1126,11 @@ class themes extends Controller
}
$xml->startElement( 'name' );
$xml->text( $setting['setting_name'] );
$xml->text( (string) $setting['setting_name'] );
$xml->endElement();
$xml->startElement( 'desc' );
$xml->writeCdata( $setting['setting_desc'] );
$xml->writeCdata( (string) $setting['setting_desc'] );
$xml->endElement();
$xml->startElement( 'data' );
@@ -1498,4 +1517,143 @@ class themes extends Controller
Output::i()->redirect( Url::internal( 'app=core&module=customization&controller=themes' ), 'theme_set_revert_done' );
}
/**
* Copy
*
* @return void
*/
protected function copy() : void
{
Session::i()->csrfCheck();
try
{
$theme = Theme::load( Request::i()->id );
}
catch( OutOfRangeException )
{
Output::i()->error( 'node_error', '2S101/Z', 404 );
}
Output::i()->output = new MultipleRedirect(
$this->url->setQueryString( [ 'do' => 'copy', 'id' => $theme->id, 'csrfKey' => Session::i()->csrfKey ] ),
function( $data ) use ( $theme )
{
if( !is_array( $data ) or !$data['newId'] )
{
$newTheme = clone $theme;
$data['newId'] = $newTheme->id;
$data['step'] = 'templates';
$data['resources'] = (int) Db::i()->select( 'count(*)', 'core_theme_resources', array( 'resource_set_id=?', $theme->id ) )->first();
$data['offset'] = 0;
}
if( !isset( $newTheme ) )
{
$newTheme = Theme::load( $data['newId'] );
}
switch( $data['step'] )
{
case 'templates':
/* Dynamically produce insert list, so we don't have to update each time the table changes */
$templateTable = Db::i()->getTableDefinition( 'core_theme_templates', TRUE );
$templateFields = array_keys( (array)$templateTable['columns'] );
array_walk( $templateFields, function( &$name, $i, $setId )
{
switch( $name )
{
case 'template_id':
$name = 'null';
break;
case 'template_set_id':
$name = $setId;
break;
}
}, $newTheme->id );
/* Copy across any template bits */
Db::i()->insert( 'core_theme_templates', Db::i()->select( implode(',', $templateFields ), 'core_theme_templates', array( 'template_set_id=?', $theme->id ) ) );
$data['step'] = 'css';
break;
case 'css':
$cssTable = Db::i()->getTableDefinition( 'core_theme_css', TRUE );
$cssFields = array_keys( (array)$cssTable['columns'] );
array_walk( $cssFields, function( &$name, $i, $setId )
{
switch( $name )
{
case 'css_id':
$name = 'null';
break;
case 'css_set_id':
$name = $setId;
break;
}
}, $newTheme->id );
/* Copy across any CSS bits */
Db::i()->insert( 'core_theme_css', Db::i()->select( implode(',', $cssFields ), 'core_theme_css', array( 'css_set_id=?', $theme->id ) ) );
$data['step'] = 'resources';
break;
case 'resources':
/* Resources are the sloweest on cloud, so we're going to loop through */
$offset = $data['offset'] ?? 0;
if( $data['offset'] == 0 )
{
Db::i()->delete( 'core_theme_resources', array( 'resource_set_id=?', $newTheme->id ) );
$newTheme->resource_map = [];
}
$limit = 15;
$newTheme->copyResourcesFromSet( $theme->id, $offset, $limit );
$data['offset'] = $offset + $limit;
if( $data['offset'] >= $data['resources'] )
{
$data['step'] = 'lang';
}
break;
case 'lang':
/* Copy any language keys */
foreach(Db::i()->select( '*', 'core_sys_lang_words', array( 'word_theme=?', $theme->id ) ) AS $word )
{
$word['word_theme'] = $newTheme->id;
unset( $word['word_id'] );
Db::i()->insert( 'core_sys_lang_words', $word );
}
$data['step'] = 'logo';
break;
case 'logo':
$newTheme->logo_data = NULL;
$newTheme->copyLogosFromSet( $theme->id );
$data['step'] = 'editor';
break;
case 'editor':
/* Copy custom settings and categories */
$newTheme->copyEditorSettings( $theme->id );
$data['step'] = 'customtemplates';
break;
case 'customtemplates':
/* Copy custom templates and hooks */
$newTheme->copyCustomTemplates( $theme->id );
return null;
}
return array( $data, Member::loggedIn()->language()->addToStack('copying__theme_' . $data['step']) );
},
function() use ( $theme )
{
Session::i()->log( 'acplog__node_copied_c', array( $theme->title => TRUE, $theme->titleForLog() => FALSE ) );
Output::i()->redirect( $this->url, 'saved' );
});
}
}
@@ -93,6 +93,8 @@ class acprestrictions extends Controller
{
Session::i()->csrfCheck();
$this->restrictions = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json" );
/* Figure out the items we are reordering, we would only be reordering a single group */
$newOrder = array();
$position = 1;
@@ -13,6 +13,7 @@ namespace IPS\core\modules\admin\developer;
use IPS\Developer\Controller;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Codemirror;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Table\Custom;
@@ -152,6 +153,7 @@ class acpsearch extends Controller
'options' => $restrictions
) ) );
$form->add( new Form\Stack( 'dev_acpsearch_keywords', $row['keywords'] ?? null, true ) );
$form->add( new Codemirror( 'dev_acpsearch_callback', $row['callback'] ?? null, false, [ 'codeModeAllowedLanguages' => [ 'php' ] ] ) );
if( $values = $form->values() )
{
@@ -161,6 +163,11 @@ class acpsearch extends Controller
'keywords' => $values['dev_acpsearch_keywords']
];
if( !empty( $values['dev_acpsearch_callback'] ) )
{
$keywordData['callback'] = $values['dev_acpsearch_callback'];
}
if( $controller == $values['dev_acpsearch_controller'] )
{
$json[ $controller ] = $keywordData;
@@ -13,6 +13,7 @@ namespace IPS\core\modules\admin\developer;
use DirectoryIterator;
use IPS\Application;
use IPS\Data\Store;
use IPS\Developer;
use IPS\Developer\Controller;
use IPS\Helpers\Form;
@@ -25,10 +26,10 @@ use IPS\Output\UI\UiExtension;
use IPS\Request;
use IPS\Session;
use IPS\Theme;
use OutOfRangeException;
use function defined;
use function sprintf;
use const IPS\IPS_FOLDER_PERMISSION;
use const IPS\REBUILD_QUICK;
use const IPS\ROOT_PATH;
/* To prevent PHP errors (extending class does not exist) revealing path */
@@ -305,6 +306,14 @@ class extensions extends Controller
file_put_contents( ROOT_PATH . "/applications/{$this->application->directory}/extensions/" . Request::i()->extapp . '/' . Request::i()->type . "/{$values['dev_extensions_classname']}.php", $contents );
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/extensions.json", $this->application->buildExtensionsJson() );
/* Clear cache */
try
{
unset( Store::i()->extensions );
}
catch( OutOfRangeException ){}
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=extensions&appKey={$this->application->directory}&root=" . Request::i()->extapp . '/' . Request::i()->type ), 'file_created' );
}
@@ -326,6 +335,14 @@ class extensions extends Controller
}
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/extensions.json", $this->application->buildExtensionsJson() );
/* Clear cache */
try
{
unset( Store::i()->extensions );
}
catch( OutOfRangeException ){}
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=extensions&appKey={$this->application->directory}" ), 'deleted' );
}
}
@@ -13,6 +13,7 @@ namespace IPS\core\modules\admin\discovery;
use InvalidArgumentException;
use IPS\Content\Tag;
use IPS\courses\Module as ModuleClass;
use IPS\Db;
use IPS\Dispatcher;
use IPS\Helpers\Form;
@@ -88,7 +89,10 @@ class tags extends Controller
$start = $args[0][0] ?? 0;
$limit = $args[0][1] ?? $this->_getRootsPerPage();
foreach( $nodeClass::roots( NULL, NULL, [], [ $start, $limit ] ) as $node )
foreach( new ActiveRecordIterator(
Db::i()->select( '*', Tag::$databaseTable, null, 'tag_recommended desc, tag_text', [ $start, $limit ] ),
Tag::class
) as $node )
{
$rows[ $node->_id ] = $this->_getRow($node);
}
@@ -248,7 +252,18 @@ class tags extends Controller
'alphabetical' => true,
'addTokenText' => Member::loggedIn()->language()->get( 'tags_optional' ),
)
] ) );
], function( $val ){
if( !empty( $val ) )
{
foreach( $val as $_v )
{
if( $_v == Request::i()->tag_merge_to )
{
throw new InvalidArgumentException( 'err__bad_tag_merge' );
}
}
}
} ) );
$form->add( new Text( 'tag_merge_to', null, true, [
'autocomplete' => array(
@@ -261,15 +276,7 @@ class tags extends Controller
'maxItems' => 1,
'addTokenText' => Member::loggedIn()->language()->get( 'tags_optional' ),
)
], function( $val ){
if( $val )
{
if( in_array( $val, explode( "\n", Request::i()->tag_merge_from ) ) )
{
throw new InvalidArgumentException( 'err__bad_tag_merge' );
}
}
} ) );
] ) );
if( $values = $form->values() )
{
@@ -284,10 +291,12 @@ class tags extends Controller
$tag->totals = $data;
$tag->save();
/* Queue the background task to update the tags */
/* Queue the background task to update the tags */
Task::queue( 'core', 'UpdateTaggedItems', [ 'tag' => $tag->text, 'new' => $values['tag_merge_to'], 'merge' => true ], 5, [ 'tag' ] );
}
Session::i()->log( 'acplogs__tag_merge', [ implode( ", ", $values['tag_merge_from'] ) => false, $values['tag_merge_to'] => false ] );
Output::i()->redirect( $this->url, Member::loggedIn()->language()->addToStack( 'tag_merge_requested' ) );
}
@@ -336,9 +345,31 @@ class tags extends Controller
Tag::load( $tag, 'tag_text' )->delete();
}
Session::i()->log( 'acplogs__tag_bulk_delete', [ implode( ", ", $values['tag_to_delete'] ) => false ] );
Output::i()->redirect( $this->url );
}
Output::i()->output = (string) $form;
}
/**
* Mark/unmark a tag as recommended
*
* @return void
*/
protected function recommend() : void
{
Session::i()->csrfCheck();
try
{
$tag = Tag::load( Request::i()->id );
$tag->recommended = !$tag->recommended;
$tag->save();
}
catch( OutOfRangeException ){}
Output::i()->redirect( Url::internal( "app=core&module=discovery&controller=tags" ) );
}
}
@@ -33,6 +33,7 @@ use IPS\Db;
use IPS\Dispatcher;
use IPS\Email;
use IPS\Events\Event;
use IPS\Extensions\MemberACPManagementAbstract;
use IPS\Extensions\SSOAbstract;
use IPS\File;
use IPS\GeoLocation;
@@ -86,12 +87,16 @@ use function chr;
use function count;
use function defined;
use function get_class;
use function implode;
use function in_array;
use function intval;
use function IPS\Cicloud\isManaged;
use function is_array;
use function is_numeric;
use function is_string;
use function iterator_to_array;
use function mb_stripos;
use function method_exists;
use function str_replace;
use function strlen;
use function strtotime;
@@ -148,7 +153,7 @@ class members extends Controller
$filters += array( 'members_filter_locked' => '( failed_login_count>=' . (int) Settings::i()->ipb_bruteforce_attempts . ' OR failed_mfa_attempts>=' . (int) Settings::i()->security_questions_tries . ')' );
}
return array_merge( Bridge::i()->bounceMemberViewGetTableFilters(), $filters );
return $filters;
}
/**
@@ -184,8 +189,25 @@ class members extends Controller
{
unset( $joins[1] );
}
/* Extensions */
$joinedTables = [ 'core_validating', 'core_pfields_content', 'core_admin_permission_rows', 'core_members' ];
foreach( Application::allExtensions( 'core', 'MemberACPManagement' ) as $ext )
{
/* @var MemberACPManagementAbstract $ext */
foreach( $ext->acpJoins() as $join )
{
/* Try to limit the craziness here, you wouldn't believe the things we've seen... */
$tableToJoin = is_array( $join['from'] ) ? $join['from'][0] : $join['from'];
if( !in_array( $tableToJoin, $joinedTables ) )
{
$joins[] = $join;
$joinedTables[] = $tableToJoin;
}
}
}
return array_merge( Bridge::i()->bounceMemberViewGetTableJoins(), $joins );
return $joins;
}
/**
@@ -513,8 +535,22 @@ class members extends Controller
},
);
/* Extensions */
foreach( Application::allExtensions( 'core', 'MemberACPManagement' ) as $ext )
{
/* @var MemberACPManagementAbstract $ext */
foreach( $ext->acpColumns() as $column => $callback )
{
$table->include[] = $column;
if( $callback !== null )
{
$table->parsers[ $column ] = $callback;
}
}
}
/* Specify the buttons */
if ( Member::loggedIn()->hasAcpRestriction( 'core', 'members', 'member_add' ) and Handler::findMethod( 'IPS\Login\Handler\Standard' ) )
if( static::canAddMembers() )
{
Output::i()->sidebar['actions']['add'] = array(
'primary' => true,
@@ -524,6 +560,7 @@ class members extends Controller
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('members_add') )
);
}
$table->rowButtons = function( $row )
{
$member = Member::constructFromData( $row );
@@ -602,7 +639,7 @@ class members extends Controller
)
);
$return['unflag'] = array(
'icon' => 'flag ipsType_spammer',
'icon' => 'flag i-color_warning',
'title' => 'spam_unflag',
'link' => Url::internal( 'app=core&module=members&controller=members&do=spam&id=' . $member->member_id . '&status=0' )->csrf()->getSafeUrlFromFilters(),
'hidden' => !$member->members_bitoptions['bw_is_spammer'],
@@ -1206,7 +1243,10 @@ class members extends Controller
public function add() : void
{
/* Check permissions */
Dispatcher::i()->checkAcpPermission( 'member_add' );
if( !static::canAddMembers() )
{
Output::i()->error( 'no_standard_login_handler', '2C114/54', 403 );
}
/* Build form */
$form = new Form;
@@ -1351,6 +1391,8 @@ class members extends Controller
if( Member::loggedIn()->hasAcpRestriction( 'core', 'members', 'member_history' ) )
{
$history = new History( $member->acpUrl()->setQueryString( array( 'do' => 'history' ) ), array( array( 'log_member=?', $member->member_id ) ), TRUE, FALSE, TRUE );
$history->sortBy = 'log_date';
$history->sortDirection = 'desc';
$history->tableTemplate = array( Theme::i()->getTemplate( 'memberprofile', 'core' ), 'historyTable' );
$history->rowsTemplate = array( Theme::i()->getTemplate( 'memberprofile', 'core' ), 'historyRows' );
$history->limit = 20;
@@ -2122,6 +2164,108 @@ class members extends Controller
}
Output::i()->output = Theme::i()->getTemplate('global')->block( 'photo', $form, FALSE );
}
/**
* Add a login link for a member
*
* This method handles the addition of a new login method (or linking an existing method) for a specific member.
* It ensures appropriate permissions, validates inputs, verifies the member, and processes the form submission.
* In case of errors, appropriate exceptions or error messages are triggered.
*
* @return void
*/
protected function loginAdd(): void
{
/* Check permission */
Dispatcher::i()->checkAcpPermission( 'member_edit' );
/* Managed Only */
if( !\IPS\CIC OR ( \IPS\CIC AND !isManaged() ) )
{
Output::i()->error( 'node_error', '2C114/17', 403, '' );
}
/* Load Member and Method */
try
{
$member = Member::load( Request::i()->id );
if( !$member->member_id )
{
throw new OutOfRangeException;
}
if ( $member->isAdmin() )
{
Dispatcher::i()->checkAcpPermission( 'member_edit_admin' );
}
}
catch ( OutOfRangeException $e )
{
Output::i()->error( 'node_error', '2C114/18', 403, '' );
}
$loginHandlers = [];
$existingLinks = iterator_to_array( Db::i()->select( 'token_login_method', 'core_login_links', [ 'token_member=?', $member->member_id ] )->setKeyField('token_login_method') );
foreach( Login::methods() as $method )
{
if( in_array( $method->_id, $existingLinks ) )
{
continue;
}
if( $method::class != 'IPS\Login\Handler\Standard' AND $method::class != 'IPS\convert\extensions\core\LoginHandler\Converter' )
{
$loginHandlers[ $method->_id ] = $method->_title;
}
}
if( !count( $loginHandlers ) )
{
Output::i()->error( 'login_identifier_none', '2C114/19', 403, '' );
}
$form = new Form;
$form->ajaxOutput = TRUE;
$form->add( new Select( 'add_login_link_handler', NULL, TRUE, [ 'options' => $loginHandlers ] ) );
$form->add( new Text( 'add_login_link_identifier', NULL, TRUE, customValidationCode: function( $value ) {
try
{
Db::i()->select( '*', 'core_login_links', [ 'token_login_method=? AND token_identifier=?', (int) \IPS\Request::i()->add_login_link_handler, $value ] )->first();
throw new DomainException( 'login_identifier_exists' );
}
catch( UnderflowException ) {}
} ) );
if( $values = $form->values() )
{
try
{
Db::i()->insert( 'core_login_links', [
'token_login_method' => $values['add_login_link_handler'],
'token_member' => Request::i()->id,
'token_identifier' => $values['add_login_link_identifier'],
'token_linked' => 1
]);
Session::i()->log( 'acplog__members_added_login', array( 'core_login_' . $values['add_login_link_handler'] => TRUE, $member->name => FALSE ) );
Output::i()->redirect( Url::internal( "app=core&module=members&controller=members&do=view&id={$member->member_id}" ), 'saved' );
}
catch( Db\Exception $e )
{
if( $e->getCode() == 1062 )
{
Output::i()->error( 'login_identifier_exists', '3C114/1A', 400, '' );
# Duplicate entry
}
throw new $e;
}
}
/* Display */
Output::i()->title = Member::loggedIn()->language()->addToStack( 'add_login_link' );
Output::i()->output = $form;
}
/**
* Change Login Method Settings
@@ -3601,6 +3745,13 @@ class members extends Controller
unset( Member::loggedIn()->language()->words[ 'core_pfield_' . $fieldId . '_desc' ] );
}
}
foreach( Login::methods() as $method )
{
if( $method::class != 'IPS\Login\Handler\Standard' AND $method::class != 'IPS\convert\extensions\core\LoginHandler\Converter' )
{
$importOptions['import_login_links'][ 'core_login_' . $method->_id ] = $method->_title;
}
}
return new Select( $key, $value, FALSE, array( 'options' => $importOptions, 'toggles' => array( 'member_id' => array( 'elImportMemberIdWarning' ) ) ) );
}
);
@@ -3832,7 +3983,7 @@ class members extends Controller
$member = new Member;
$member->member_group_id = $wizardData['import_members_fallback_group'];
$member->members_bitoptions['password_reset_forced'] = TRUE;
$profileFields = array();
$loginLinks = $profileFields = [];
foreach ( $line as $k => $v )
{
if ( isset( $wizardData['columns'][ $k ] ) )
@@ -3886,6 +4037,22 @@ class members extends Controller
}
catch( OutOfRangeException $e ) { }
}
elseif ( mb_substr( $wizardData['columns'][ $k ], 0, 11 ) == 'core_login_' )
{
$handlerId = (int) mb_substr( $wizardData['columns'][ $k ], 11 );
if( Db::i()->select( 'COUNT(token_member)', 'core_login_links', [ 'token_login_method=? AND token_identifier=?', $handlerId, $v ] )->first() )
{
throw new DomainException( sprintf( Member::loggedIn()->language()->get( 'import_login_link_in_use' ), $v ) );
}
$loginLinks[ $handlerId ] = [
'token_login_method' => $handlerId,
'token_identifier' => $v,
'token_linked' => 1
];
$member->members_bitoptions['password_reset_forced'] = FALSE;
}
else
{
switch ( $wizardData['columns'][ $k ] )
@@ -4043,6 +4210,18 @@ class members extends Controller
{
Db::i()->replace( 'core_pfields_content', array_merge( array( 'member_id' => $member->member_id ), $profileFields ) );
}
if( count( $loginLinks ) )
{
foreach( $loginLinks as $loginLink )
{
try
{
Db::i()->insert( 'core_login_links', array_merge( $loginLink, [ 'token_member' => $member->member_id ] ) );
}
catch( Db\Exception $e ) { }
}
}
Session::i()->log( 'acplog__members_created', array( $member->name => FALSE ) );
}
catch ( DomainException $e )
@@ -4196,6 +4375,27 @@ class members extends Controller
}
}
foreach( Login::methods() as $method )
{
if( $method::class != 'IPS\Login\Handler\Standard' AND $method::class != 'IPS\convert\extensions\core\LoginHandler\Converter' )
{
$columns[ 'core_login_' . $method->_id ] = \IPS\Member::loggedIn()->language()->addToStack( 'import_login_links_suffix', FALSE, [ 'sprintf' => [ $method->_title ] ] );
}
}
/* Extensions */
foreach( Application::allExtensions( 'core', 'MemberACPManagement' ) as $ext )
{
/* @var MemberACPManagementAbstract $ext */
foreach( $ext->exportColumns() as $column => $lang )
{
if( !array_key_exists( $column, $columns ) )
{
$columns[ $column ] = $lang;
}
}
}
return $columns;
}
@@ -4268,6 +4468,7 @@ class members extends Controller
/* Do we need to join profile field data? */
$select = array( 'core_members.*' );
$customFields = array();
foreach ( $wizardData['columns'] as $column )
{
if ( mb_substr( $column, 0, 7 ) == 'pfield_' )
@@ -4300,8 +4501,8 @@ class members extends Controller
$mrData['count'] = $query->first();
}
/* Compile query */
$query = Db::i()->select( implode( ',', $select ), 'core_members', $where, 'core_members.member_id', array( $mrData['offset'], $doPerLoop ) );
/* Compile member_id query. Just getting the member_ids first is more efficient on larger tables */
$query = Db::i()->select( 'core_members.member_id', 'core_members', $where, 'core_members.member_id', array( $mrData['offset'], $doPerLoop ) );
/* Run callbacks */
foreach ( Application::allExtensions( 'core', 'MemberFilter', TRUE, 'core' ) as $key => $extension )
@@ -4314,25 +4515,50 @@ class members extends Controller
}
}
/* Finished? */
if ( !$query->count() )
/* Get the IDs */
$memberIds = iterator_to_array( $query );
/* Nothing to do? */
if ( ! count( $memberIds ) )
{
if( count( $mrData['removedData'] ) AND ( !isset( $wizardData['includeInsecure'] ) OR !$wizardData['includeInsecure'] ) )
{
$_SESSION['removedData'] = $mrData['removedData'];
}
/* Finish here as we got everything */
Output::i()->redirect( $baseUrl->setQueryString( array( 'buildDone' => 1 ) ) );
}
/* Now prepare the actual query to get all the data */
$where[] = [ Db::i()->in( 'core_members.member_id', $memberIds ) ];
$dataQuery = Db::i()->select( implode( ',', $select ), 'core_members', $where, 'core_members.member_id' );
/* Run callbacks */
foreach ( Application::allExtensions( 'core', 'MemberFilter', TRUE, 'core' ) as $key => $extension )
{
if( method_exists( $extension, 'queryCallback' ) )
{
/* Grab our fields and add to the form */
if( !empty( $wizardData['filters'][ $key ] ) )
{
$data = $wizardData['filters'][ $key ];
$extension->queryCallback( $data, $dataQuery );
}
}
}
/* Open file */
$fh = fopen( $wizardData['file'], 'a' );
/* Run */
foreach ( $query as $member )
$done = 0;
foreach ( $dataQuery as $member )
{
$dataToWrite = $this->_getDataToWriteCsv( $member, $wizardData, $mrData );
$done++; /* This is here because the query fetched 2500 but fewer than 2500 may be processed throwing the last bit out */
if( $dataToWrite === FALSE )
{
continue;
@@ -4344,6 +4570,19 @@ class members extends Controller
/* Close and loop */
fclose( $fh );
if ( $done < $doPerLoop )
{
if( count( $mrData['removedData'] ) AND ( !isset( $wizardData['includeInsecure'] ) OR !$wizardData['includeInsecure'] ) )
{
$_SESSION['removedData'] = $mrData['removedData'];
}
/* Finish here as we got everything */
Output::i()->redirect( $baseUrl->setQueryString( array( 'buildDone' => 1 ) ) );
}
$mrData['offset'] += $doPerLoop;
return array( $mrData, Member::loggedIn()->language()->addToStack('export_members_processing'), ( $mrData['offset'] > $mrData['count'] ) ? 100 : ( 100 / $mrData['count'] ) * $mrData['offset'] );
},
@@ -4449,9 +4688,32 @@ class members extends Controller
default => $member[ 'field_' . $fieldId ] ? Field::load( $fieldId )->displayValue( $member[ 'field_' . $fieldId ], FALSE, NULL, 0, NULL, TRUE, ',' ) : ''
};
}
else if ( mb_substr( $column, 0, 11 ) == 'core_login_' )
{
$handlerId = mb_substr( $column, 11 );
try
{
$valueToWrite = Db::i()->select( 'token_identifier', 'core_login_links', [ 'token_login_method=? AND token_member=?', $handlerId, $member['member_id'] ] )->first();
}
catch( UnderflowException ){}
}
else
{
$valueToWrite = $member[ $column ];
/* Is this part of an extension? */
foreach( Application::allExtensions( 'core', 'MemberACPManagement' ) as $ext )
{
/* @var MemberACPManagementAbstract $ext */
if( array_key_exists( $column, $ext->exportColumns() ) )
{
$valueToWrite = $ext->exportColumnValue( $column, $member );
break;
}
}
if( empty( $valueToWrite ) )
{
$valueToWrite = $member[ $column ] ?? '';
}
}
break;
}
@@ -4954,4 +5216,26 @@ class members extends Controller
Bridge::i()->bounceMemberViewRequestEmailUnblock();
}
/**
* Check if the functionality to manually add members is enabled
*
* @return bool
*/
protected static function canAddMembers() : bool
{
/* ACP restriction first */
if( !Member::loggedIn()->hasAcpRestriction( 'core', 'members', 'member_add' ) )
{
return false;
}
/* Check if the standard method is available and enabled */
if( $handler = Handler::findMethod( Standard::class ) )
{
return $handler->enabled;
}
return false;
}
}
@@ -12,6 +12,7 @@ namespace IPS\core\modules\admin\membersettings;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Api\Webhook;
use IPS\Data\Store;
use IPS\DateTime;
use IPS\Db;
@@ -180,6 +181,8 @@ class ban extends Controller
'ban_reason' => $values['ban_reason'],
'ban_date' => time()
);
Webhook::fire( 'ban_filter_added', $save);
if ( $current )
{
@@ -217,6 +220,7 @@ class ban extends Controller
$current = Db::i()->select( '*', 'core_banfilters', array( 'ban_id=?', Request::i()->id ) )->first();
Session::i()->log( 'acplog__ban_deleted', array( 'ban_filter_' . $current['ban_type'] . '_select' => TRUE, $current['ban_content'] => FALSE ) );
Db::i()->delete( 'core_banfilters', array( 'ban_id=?', Request::i()->id ) );
Webhook::fire( 'ban_filter_removed', $current);
unset( Store::i()->bannedIpAddresses );
}
catch ( UnderflowException $e ) { }
@@ -16,6 +16,7 @@ use IPS\Dispatcher\Controller;
use IPS\Http\Url;
use IPS\Member;
use IPS\Output;
use IPS\Platform\Bridge;
use IPS\Request;
use IPS\Theme;
use function defined;
@@ -61,10 +62,24 @@ class profiles extends Controller
$tabs['profilesettings'] = 'profile_settings';
}
/* Profile Photo Gallery */
if( Bridge::i()->featureIsEnabled( 'profile_gallery' ) and Member::loggedIn()->hasAcpRestriction( 'cloud', 'smartcommunity', 'smartcommunity_profilegallery' ) )
{
$tabs['profilegallery'] = 'profile_gallery';
}
/* Route */
$classname = 'IPS\core\modules\admin\membersettings\\' . $activeTab;
$class = new $classname;
$class->url = Url::internal("app=core&module=membersettings&controller=profiles&tab={$activeTab}");
if( $activeTab == 'profilegallery' )
{
$classname = 'IPS\cloud\modules\admin\profiles\photogallery';
}
else
{
$classname = 'IPS\core\modules\admin\membersettings\\' . $activeTab;
}
$class = new $classname;
$class->url = Url::internal("app=core&module=membersettings&controller=profiles&tab={$activeTab}");
$class->execute();
$output = Output::i()->output;
@@ -93,14 +93,14 @@ class spam extends Controller
/* Work out output */
$methodFunction = '_manage' . IPS::mb_ucfirst( $this->activeTab );
$activeTabContents = $this->$methodFunction();
/* If this is an AJAX request, just return it */
if( Request::i()->isAjax() )
{
Output::i()->output = $activeTabContents;
return;
}
/* Build tab list */
$tabs = array();
$tabs['captcha'] = 'spamprevention_captcha';
@@ -136,7 +136,7 @@ class spam extends Controller
'link' => Url::internal( 'app=core&module=moderation&controller=spam&do=whitelist' ),
);
}
if ( Member::loggedIn()->hasAcpRestriction( 'core', 'members', 'membertools_delete' ) )
{
Output::i()->sidebar['actions']['delete_guest_content'] = array(
@@ -146,7 +146,7 @@ class spam extends Controller
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('member_delete_guest_content') )
);
}
/* Display */
Output::i()->title = Member::loggedIn()->language()->addToStack('menu__core_moderation_spam');
Output::i()->output = Theme::i()->getTemplate( 'global' )->tabs( $tabs, $this->activeTab, $activeTabContents, Url::internal( "app=core&module=moderation&controller=spam" ) );
@@ -158,24 +158,44 @@ class spam extends Controller
* @param string $type 'options' for the select options, 'toggles' for the toggles
* @return array
*/
protected function getCaptchaOptions( string $type='options' ) : array
protected static function getCaptchaOptions( string $type='options' ) : array
{
switch( $type )
{
case 'options':
return array( 'none' => 'captcha_type_none', 'invisible' => 'captcha_type_invisible', 'recaptcha2' => 'captcha_type_recaptcha2', 'keycaptcha' => 'captcha_type_keycaptcha', 'hcaptcha' => 'captcha_type_hcaptcha' );
$options = [
'none' => 'captcha_type_none',
'turnstile' => 'captcha_type_turnstile',
'invisible' => 'captcha_type_invisible',
'recaptcha2' => 'captcha_type_recaptcha2',
'keycaptcha' => 'captcha_type_keycaptcha',
'hcaptcha' => 'captcha_type_hcaptcha'
];
if( Settings::i()->bot_antispam_type != 'keycaptcha' )
{
unset( $options['keycaptcha'] );
}
return $options;
case 'toggles':
return array(
'none' => array( 'bot_antispam_type_warning' ),
'recaptcha2' => array( 'recaptcha2_public_key', 'recaptcha2_private_key' ),
'invisible' => array( 'recaptcha2_public_key', 'recaptcha2_private_key' ),
'keycaptcha' => array( 'keycaptcha_privatekey' ),
'hcaptcha' => array( 'hcaptcha_sitekey', 'hcaptcha_secret')
);
$toggles = [
'none' => [ 'bot_antispam_type_warning' ],
'turnstile' => [ 'turnstile_site_key', 'turnstile_secret_key' ],
'recaptcha2' => [ 'recaptcha2_public_key', 'recaptcha2_private_key' ],
'invisible' => [ 'recaptcha2_public_key', 'recaptcha2_private_key' ],
'keycaptcha' => [ 'keycaptcha_privatekey' ],
'hcaptcha' => [ 'hcaptcha_sitekey', 'hcaptcha_secret' ]
];
if( Settings::i()->bot_antispam_type != 'keycaptcha' )
{
unset( $toggles['keycaptcha'] );
}
return $toggles;
}
return array();
return [];
}
/**
@@ -187,15 +207,7 @@ class spam extends Controller
{
/* Build Form */
$form = new Form;
$form->add( new Radio( 'bot_antispam_type', Settings::i()->bot_antispam_type, TRUE, array(
'options' => $this->getCaptchaOptions( 'options' ),
'toggles' => $this->getCaptchaOptions( 'toggles' ),
), NULL, NULL, NULL, 'bot_antispam_type' ) );
$form->add( new Text( 'recaptcha2_public_key', Settings::i()->recaptcha2_public_key, FALSE, array(), NULL, NULL, NULL, 'recaptcha2_public_key' ) );
$form->add( new Text( 'recaptcha2_private_key', Settings::i()->recaptcha2_private_key, FALSE, array(), NULL, NULL, NULL, 'recaptcha2_private_key' ) );
$form->add( new Text( 'keycaptcha_privatekey', Settings::i()->keycaptcha_privatekey, FALSE, array(), NULL, NULL, NULL, 'keycaptcha_privatekey' ) );
$form->add( new Text( 'hcaptcha_sitekey', Settings::i()->hcaptcha_sitekey, FALSE, array(), NULL, NULL, NULL, 'hcaptcha_sitekey' ) );
$form->add( new Text( 'hcaptcha_secret', Settings::i()->hcaptcha_secret, FALSE, array(), NULL, NULL, NULL, 'hcaptcha_secret' ) );
static::captchaForm( $form );
/* Save values */
if ( $form->values() )
@@ -208,6 +220,32 @@ class spam extends Controller
return $form;
}
public static function captchaForm( Form $form ): void
{
$form->add( new Radio( 'bot_antispam_type', Settings::i()->bot_antispam_type, TRUE, array(
'options' => static::getCaptchaOptions( 'options' ),
'toggles' => static::getCaptchaOptions( 'toggles' ),
), NULL, NULL, NULL, 'bot_antispam_type' ) );
$form->add( new Text( 'turnstile_site_key', Settings::i()->turnstile_site_key, id: 'turnstile_site_key' ) );
$form->add( new Text( 'turnstile_secret_key', Settings::i()->turnstile_secret_key, id: 'turnstile_secret_key' ) );
if( \IPS\CIC )
{
\IPS\Member::loggedIn()->language()->words['turnstile_secret_key_desc'] = \IPS\Member::loggedIn()->language()->addToStack('turnstile_secret_key_cloud_desc');
}
$form->add( new Text( 'recaptcha2_public_key', Settings::i()->recaptcha2_public_key, FALSE, array(), NULL, NULL, NULL, 'recaptcha2_public_key' ) );
$form->add( new Text( 'recaptcha2_private_key', Settings::i()->recaptcha2_private_key, FALSE, array(), NULL, NULL, NULL, 'recaptcha2_private_key' ) );
if( Settings::i()->bot_antispam_type == 'keycaptcha' )
{
$form->add( new Text( 'keycaptcha_privatekey', Settings::i()->keycaptcha_privatekey, FALSE, array(), NULL, NULL, NULL, 'keycaptcha_privatekey' ) );
}
$form->add( new Text( 'hcaptcha_sitekey', Settings::i()->hcaptcha_sitekey, FALSE, array(), NULL, NULL, NULL, 'hcaptcha_sitekey' ) );
$form->add( new Text( 'hcaptcha_secret', Settings::i()->hcaptcha_secret, FALSE, array(), NULL, NULL, NULL, 'hcaptcha_secret' ) );
}
/**
* Show spammer flagging settings
*
@@ -220,7 +258,7 @@ class spam extends Controller
$form->add( new CheckboxSet( 'spm_option', explode( ',', Settings::i()->spm_option ), FALSE, array(
'options' => array( 'disable' => 'spm_option_disable', 'unapprove' => 'spm_option_unapprove', 'delete' => 'spm_option_delete', 'ban' => 'spm_option_ban' ),
) ) );
/* Save values */
if ( $form->values() )
{
@@ -239,7 +277,7 @@ class spam extends Controller
protected function _manageService() : string
{
$licenseData = IPS::licenseKey();
/* Build Form */
$actions = array( 1 => 'spam_service_act_1', 5 => 'spam_service_act_5', 2 => 'spam_service_act_2', 3 => 'spam_service_act_3', 4 => 'spam_service_act_4' );
$days = json_decode( Settings::i()->spam_service_days, TRUE );
@@ -253,12 +291,14 @@ class spam extends Controller
$disabled = TRUE;
if( !Settings::i()->ipb_reg_number )
{
Member::loggedIn()->language()->words['spam_service_enabled_desc'] = Member::loggedIn()->language()->addToStack( '__ipbmafia__nulling_null_alert' ); //Changing error message to custom'
}
else
{
Member::loggedIn()->language()->words['spam_service_enabled_desc'] = Member::loggedIn()->language()->addToStack( '__ipbmafia__nulling_null_alert' ); //Changing error message to custom'
}
}
$form->add( new YesNo( 'spam_service_enabled', Settings::i()->spam_service_enabled, FALSE, array( 'disabled' => $disabled, 'togglesOn' => array( 'spam_service_send_to_ips', 'spam_service_action_0', 'spam_service_action_1', 'spam_service_action_2', 'spam_service_action_3', 'spam_service_action_4', 'spam_service_disposable' ) ) ) );
$form->add( new YesNo( 'spam_service_send_to_ips', Settings::i()->spam_service_send_to_ips, FALSE, array( 'disabled' => $disabled ), NULL, NULL, NULL, 'spam_service_send_to_ips' ) );
$form->add( new Select( 'spam_service_action_1', Settings::i()->spam_service_action_1, FALSE, array( 'disabled' => $disabled, 'options' => $actions, 'toggles' => array( '5' => array( 'spam_service_action_1_num' ) ) ), NULL, NULL, NULL, 'spam_service_action_1' ) );
@@ -287,7 +327,7 @@ class spam extends Controller
unset( $values['spam_service_' . $num . '_num' ] );
}
$values['spam_service_days'] = json_encode( $values['spam_service_days'] );
$form->saveAsSettings( $values );
@@ -322,7 +362,7 @@ class spam extends Controller
$table->sortBy = $table->sortBy ?: 'qa_question';
$table->quickSearch = array( 'word_custom', 'qa_question' );
$table->sortDirection = $table->sortDirection ?: 'asc';
if ( Member::loggedIn()->hasAcpRestriction( 'core', 'moderation', 'qanda_add' ) )
{
$table->rootButtons = array(
@@ -337,7 +377,7 @@ class spam extends Controller
$table->rowButtons = function( $row )
{
$return = array();
if ( Member::loggedIn()->hasAcpRestriction( 'core', 'moderation', 'qanda_edit' ) )
{
$return['edit'] = array(
@@ -346,7 +386,7 @@ class spam extends Controller
'link' => Url::internal( 'app=core&module=moderation&controller=spam&do=question&id=' ) . $row['qa_id'],
);
}
if ( Member::loggedIn()->hasAcpRestriction( 'core', 'moderation', 'qanda_delete' ) )
{
$return['delete'] = array(
@@ -356,7 +396,7 @@ class spam extends Controller
'data' => array( 'delete' => '' ),
);
}
return $return;
};
@@ -400,7 +440,7 @@ class spam extends Controller
$save = array(
'qa_answers' => json_encode( $values['qa_answers'] ),
);
if ( $id )
{
Db::i()->update( 'core_question_and_answer', $save, array( 'qa_id=?', $question['qa_id'] ) );
@@ -412,7 +452,7 @@ class spam extends Controller
$id = Db::i()->insert( 'core_question_and_answer', $save );
Session::i()->log( 'acplogs__question_added' );
}
Lang::saveCustom( 'core', "core_question_and_answer_{$id}", $values['qa_question'] );
Output::i()->redirect( Url::internal( 'app=core&module=moderation&controller=spam&tab=qanda' ), 'saved' );
@@ -439,13 +479,13 @@ class spam extends Controller
Db::i()->delete( 'core_question_and_answer', array( 'qa_id=?', $id ) );
Session::i()->log( 'acplogs__question_deleted' );
Lang::deleteCustom( 'core', "core_question_and_answer_{$id}" );
/* And redirect */
Output::i()->redirect( Url::internal( "app=core&module=moderation&controller=spam&tab=qanda" ) );
}
/**
* Spam Service Log
*
@@ -454,18 +494,18 @@ class spam extends Controller
protected function serviceLogs() : void
{
Dispatcher::i()->checkAcpPermission( 'spam_service_log' );
/* Create the table */
$table = new TableDb( 'core_spam_service_log', Url::internal( 'app=core&module=moderation&controller=spam&do=serviceLogs' ) );
$table->langPrefix = 'spamlogs_';
/* Columns we need */
$table->include = array( 'log_date', 'log_code', 'email_address', 'ip_address' );
$table->sortBy = $table->sortBy ?: 'log_date';
$table->sortDirection = $table->sortDirection ?: 'DESC';
/* Search */
$table->advancedSearch = array(
'email_address' => SEARCH_CONTAINS_TEXT,
@@ -474,7 +514,7 @@ class spam extends Controller
);
$table->quickSearch = 'email_address';
/* Custom parsers */
$table->parsers = array(
'log_date' => function( $val )
@@ -482,7 +522,7 @@ class spam extends Controller
return DateTime::ts( $val )->localeDate();
},
);
/* Add a button for settings */
Output::i()->sidebar['actions'] = array(
'settings' => array(
@@ -492,12 +532,12 @@ class spam extends Controller
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('prunesettings') )
),
);
/* Display */
Output::i()->title = Member::loggedIn()->language()->addToStack('spamlogs');
Output::i()->output = (string) $table;
}
/**
* Prune Settings
*
@@ -506,18 +546,18 @@ class spam extends Controller
protected function serviceLogSettings() : void
{
Dispatcher::i()->checkAcpPermission( 'spam_service_log' );
$form = new Form;
$form->add( new Interval( 'prune_log_spam', Settings::i()->prune_log_spam, FALSE, array( 'valueAs' => Interval::DAYS, 'unlimited' => 0, 'unlimitedLang' => 'never' ), NULL, Member::loggedIn()->language()->addToStack('after'), NULL, 'prune_log_spam' ) );
if ( $values = $form->values() )
{
$form->saveAsSettings();
Session::i()->log( 'acplog__spamlog_settings' );
Output::i()->redirect( Url::internal( 'app=core&module=moderation&controller=spam&do=serviceLogs' ), 'saved' );
}
Output::i()->title = Member::loggedIn()->language()->addToStack('spamlogssettings');
Output::i()->output = Theme::i()->getTemplate('global')->block( 'spamlogssettings', $form, FALSE );
}
@@ -730,9 +770,11 @@ class spam extends Controller
{
if( !Settings::i()->ipb_reg_number )
{
Member::loggedIn()->language()->words['spam_service_enabled_desc'] = Member::loggedIn()->language()->addToStack( '__ipbmafia__nulling_null_alert' ); //Changing error message to custom'
}
else
{
Member::loggedIn()->language()->words['spam_service_enabled_desc'] = Member::loggedIn()->language()->addToStack( '__ipbmafia__nulling_null_alert' ); //Changing error message to custom'
}
}
@@ -46,7 +46,10 @@ use function get_class;
use function in_array;
use function intval;
use function is_array;
use function json_decode;
use function json_encode;
use function sprintf;
use function str_replace;
use const IPS\CIC;
use const IPS\ROOT_PATH;
use const IPS\Helpers\Table\SEARCH_CONTAINS_TEXT;
@@ -562,7 +565,7 @@ class files extends Controller
} );
/* If we already have a storage configuration, we cannot create any new ones */
if( $totalConfigurations - $cicConfigurations == 0 )
if( $totalConfigurations - $cicConfigurations < 2 )
{
$table->rootButtons = array( 'add' => array(
'icon' => 'plus',
@@ -570,7 +573,6 @@ class files extends Controller
'link' => Url::internal( "app=core&module=overview&controller=files&do=configurationForm" )
) );
}
$settings = json_decode( Settings::i()->upload_settings, TRUE );
$table->rowButtons = function( $row ) use ( $settings )
{
@@ -587,6 +589,11 @@ class files extends Controller
}
$buttons = array(
'edit' => array(
'icon' => 'pencil',
'title' => 'edit',
'link' => \IPS\Http\Url::internal( "app=core&module=overview&controller=files&do=configurationForm&id={$row['id']}" )
),
'log' => array(
'icon' => 'search',
'title' => 'file_config_log_title',
@@ -611,7 +618,7 @@ class files extends Controller
return $buttons;
};
$activeTabContents = $table;
}
@@ -641,9 +648,9 @@ class files extends Controller
else
{
$message = '';
if( $totalConfigurations - $cicConfigurations > 1 )
if( $totalConfigurations - $cicConfigurations >= 2 )
{
$message = Theme::i()->getTemplate( 'global', 'core' )->message( Member::loggedIn()->language()->addToStack( 'multiple_filestorage_deprecated' ), 'warning' );
$message = Theme::i()->getTemplate( 'global', 'core' )->message( Member::loggedIn()->language()->addToStack( 'multiple_filestorage_error' ), 'info' );
}
Output::i()->output = $message . Theme::i()->getTemplate( 'global' )->tabs( $tabs, $activeTab, $activeTabContents, Url::internal( "app=core&module=overview&controller=files&do=settings" ) );
@@ -718,6 +725,7 @@ class files extends Controller
}
$form = new Form( 'form', 'move' );
$form->addMessage( 'filestorage_moveall_info', 'ipsMessage ipsMessage--warning' );
$form->addHeader( sprintf( Member::loggedIn()->language()->get( 'filestorage_moveall_from' ), $currentDisplayName ) );
if( count( $configurations ) )
{
@@ -881,10 +889,9 @@ class files extends Controller
}
/* Display */
$message = Theme::i()->getTemplate( 'global', 'core' )->message( Member::loggedIn()->language()->addToStack( 'filestorage_moveall_info' ), 'warning' );
Output::i()->title = Member::loggedIn()->language()->addToStack('storage_settings');
Output::i()->breadcrumb[] = array( Url::internal( "&app=core&module=overview&controller=files&do=settings&tab=configurations" ), 'filestorage_settings' );
Output::i()->output = $message . $form;
Output::i()->output = $form;
}
/**
@@ -895,15 +902,45 @@ class files extends Controller
protected function configurationForm() : void
{
/* Are we allowed to create a new location? */
$totalConfigurations = (int) Db::i()->select( 'count(*)', 'core_file_storage' )->first();
$cicConfigurations = 0;
if( CIC )
if ( ! isset( Request::i()->id ) )
{
$cicConfigurations = (int) Db::i()->select( 'count(*)', 'core_file_storage', array( "method=?", 'Cloud' ) )->first();
$totalConfigurations = (int) Db::i()->select( 'count(*)', 'core_file_storage' )->first();
$cicConfigurations = 0;
if( CIC )
{
$cicConfigurations = (int) Db::i()->select( 'count(*)', 'core_file_storage', array( "method=?", 'Cloud' ) )->first();
}
if( $totalConfigurations - $cicConfigurations > 2 )
{
Output::i()->error( 'multiple_filestorage_error', '2C158/3', 403 );
}
}
if( $totalConfigurations - $cicConfigurations > 0 )
else
{
Output::i()->error( 'multiple_filestorage_error', '2C158/3', 403 );
/* Before we edit any details, check to make sure we're not processing files in this location */
foreach ( Db::i()->select( 'data', 'core_queue', array( '`key`=?', 'MoveFiles' ) ) as $data )
{
$data = json_decode( $data, true );
if ( $data['oldConfiguration'] == Request::i()->id )
{
Output::i()->error( 'file_storage_move_out', '1C158/3', 403, '' );
}
elseif ( $data['newConfiguration'] == Request::i()->id )
{
Output::i()->error( 'file_storage_move_in', '1C158/4', 403, '' );
}
}
}
try
{
$current = Db::i()->select( '*', 'core_file_storage', ['id=?', intval( Request::i()->id )] )->first();
$currentHandlerSettings = json_decode( $current['configuration'], true );
}
catch ( \UnderflowException $e )
{
$current = NULL;
$currentHandlerSettings = array();
}
/* Get handlers */
@@ -912,19 +949,19 @@ class files extends Controller
$toggles = array();
foreach ( File::storageHandlers( NULL ) as $key => $class )
{
if ( CIC AND $key == 'Cloud' AND !\IPS\Member::loggedIn()->members_bitoptions['is_support_account'] )
if ( CIC AND $key == 'Cloud' AND ! Member::loggedIn()->members_bitoptions['is_support_account'] )
{
continue;
}
$handlers[ $key ] = 'filehandler__' . $key;
foreach ( $class::settings() as $k => $v )
foreach ( $class::settings( $currentHandlerSettings ) as $k => $v )
{
if ( is_array( $v ) )
{
$settingClass = '\IPS\Helpers\Form\\' . $v['type'];
$default = NULL;
$default = isset( $currentHandlerSettings[ $k ] ) ? str_replace( '{root}', ROOT_PATH, $currentHandlerSettings[ $k ] ) : NULL;
if ( isset( $v['default'] ) and !$default )
{
$default = str_replace( '{root}', ROOT_PATH, $v['default'] );
@@ -940,10 +977,18 @@ class files extends Controller
$toggles[ $key ][ $k ] = "{$key}_{$k}";
}
}
/* Build form */
$form = new Form;
$form->add( new Radio( 'filestorage_method', ( CIC ? 'Cloud' : 'FileSystem' ), TRUE, array( 'options' => $handlers, 'toggles' => $toggles ) ) );
if ( $current )
{
$form->addMessage( Member::loggedIn()->language()->addToStack( "filestorage_config_edit_warning", null, [
'sprintf' => Url::internal( 'app=core&module=overview&controller=files&do=configurationMove&id=' . $current['id'] ),
] ), 'ipsMessage ipsMessage--warning' );
}
$form->add( new Radio( 'filestorage_method', $current ? $current['method'] : ( CIC ? 'Amazon' : 'FileSystem' ), TRUE, array( 'disabled' => ( isset( Request::i()->id ) ), 'options' => $handlers, 'toggles' => $toggles ) ) );
foreach ( $handlerSettings as $handlerKey => $settings )
{
foreach ( $settings as $setting )
@@ -980,7 +1025,7 @@ class files extends Controller
$existingConfiguration = json_decode( $existing['configuration'], true );
foreach( $existingConfiguration as $k => $v )
{
$v = str_replace( ROOT_PATH, '{root}', $v );
$v = str_replace( ROOT_PATH, '{root}', (string) $v );
if ( array_key_exists( $k, $currentHandlerSettings ) )
{
@@ -992,16 +1037,25 @@ class files extends Controller
}
}
if ( $existingWithSameConfig )
if ( $existingWithSameConfig and ! Request::i()->id )
{
/* Let's not allow this to be saved as someone can start a move to the same location, which will end up deleting the files */
throw new DomainException( Member::loggedIn()->language()->addToStack( 'file_config_is_the_same_as_existing', FALSE ) );
}
$insertId = Db::i()->insert( 'core_file_storage', array(
'method' => $values['filestorage_method'],
'configuration' => json_encode( $currentHandlerSettings ),
) );
if ( Request::i()->id )
{
Db::i()->update( 'core_file_storage', [
'configuration' => json_encode( $currentHandlerSettings ),
], array( 'id=?', Request::i()->id ) );
}
else
{
Db::i()->insert( 'core_file_storage', [
'method' => $values['filestorage_method'],
'configuration' => json_encode( $currentHandlerSettings ),
] );
}
unset( Store::i()->storageConfigurations );
/* Log the storage addition */
@@ -1016,9 +1070,6 @@ class files extends Controller
}
}
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_files.js', 'core' ) );
Output::i()->globalControllers[] = 'core.admin.files.form';
/* Display */
Output::i()->title = Member::loggedIn()->language()->addToStack('storage_settings');
Output::i()->output = $form;
@@ -1058,47 +1109,6 @@ class files extends Controller
Output::i()->redirect( Url::internal( "app=core&module=overview&controller=files&do=settings&tab=configurations" ) );
}
/**
* Determine if a move is needed (ajax method)
*
* @return void
*/
protected function checkMoveNeeded() : void
{
try
{
$current = Db::i()->select( '*', 'core_file_storage', array( 'id=?', intval( Request::i()->id ) ) )->first();
}
catch( UnderflowException $e )
{
Output::i()->json( array( 'needsMoving' => FALSE ) );
}
$currentConf = json_decode( $current['configuration'], TRUE );
$needsMoving = FALSE;
if ( in_array( intval( Request::i()->id ), json_decode( Settings::i()->upload_settings, TRUE ) ) )
{
foreach ( $currentConf as $k => $v )
{
$checkKey = 'filehandler__' . $current['method'] . '_' . $k;
if ( isset( Request::i()->$checkKey ) )
{
$currentHandlerSettings[ $k ] = str_replace( ROOT_PATH, '{root}', Request::i()->$checkKey );
}
}
$handlers = File::storageHandlers( $current );
$classname = $handlers[ $current['method'] ];
/* Do we really need to create and move? */
$needsMoving = $classname::moveCheck( $currentHandlerSettings, $currentConf );
}
Output::i()->json( array( 'needsMoving' => (boolean) $needsMoving ) );
}
/**
* View logs for this configuration
*
@@ -11,16 +11,57 @@
namespace IPS\core\modules\admin\overview;
use DateInterval;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Exception;
use InvalidArgumentException;
use IPS\Application;
use IPS\core\Statistics\Chart;
use IPS\DateTime;
use IPS\Db;
use IPS\Dispatcher\Controller;
use IPS\Helpers\Chart\Dynamic;
use IPS\Helpers\Form;
use IPS\Helpers\Form\DateRange;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\YesNo;
use IPS\Http\Url;
use IPS\Member;
use IPS\Output;
use IPS\Platform\Bridge;
use IPS\Request;
use IPS\Session;
use IPS\Settings;
use IPS\Theme;
use OutOfRangeException;
use Throwable;
use function array_fill;
use function array_key_exists;
use function array_merge;
use function array_values;
use function count;
use function defined;
use function explode;
use function fclose;
use function fopen;
use function fputcsv;
use function in_array;
use function is_array;
use function is_callable;
use function is_iterable;
use function is_numeric;
use function is_string;
use function iterator_to_array;
use function ksort;
use function max;
use function str_replace;
use function tempnam;
use function time;
use function trim;
use const IPS\TEMP_DIRECTORY;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
@@ -43,6 +84,123 @@ class mycharts extends Controller
* @brief Allow MySQL RW separation for efficiency
*/
public static bool $allowRWSeparation = TRUE;
/**
* @var "charts"|"blocks"
*/
protected string $activeTab = "charts";
/**
* The report id for this request
* @var int|null
*/
protected int|null $reportId = null;
protected bool $communityhealth = false;
/**
* @var string[]
*/
protected array $tabs = [
"charts" => "statsreports_saved_charts",
"blocks" => "statsreports_saved_blocks",
];
/**
* Get the report for this request
*
* @return array|null
*/
protected function _getReport()
{
static $report = false;
if ( $this->reportId and $report === false )
{
try
{
$report = Db::i()->select( "*", "core_reports", [['id=?', $this->reportId]] )->first();
}
catch ( \UnderflowException $e )
{
$report = null;
}
}
else if ( $report === false )
{
$report = null;
}
return $report;
}
protected ?array $chartRow= null;
/**
* Community health shortcut from the menu system
*/
protected function communityhealth() : void
{
if ( !Bridge::i()->featureIsEnabled( 'community_health_stats' ) )
{
Output::i()->redirect( Url::internal( "app=core&module=overview&controller=mycharts" ) );
}
Request::i()->communityhealth = '1';
$this->communityhealth = true;
$this->manage();
}
/**
* @return void
* @throws Exception
*/
public function execute() : void
{
if ( !Request::i()->communityhealth and Request::i()->chartId and Bridge::i()->featureIsEnabled( 'community_health_stats' ) )
{
$allCharts = Bridge::i()->getCommunityHealthCharts();
foreach( $allCharts as $data )
{
if ( $data['chart_id'] == Request::i()->chartId )
{
$this->chartRow = $data;
break;
}
}
if ( $this->chartRow )
{
Request::i()->communityhealth = '1';
}
}
if ( Request::i()->communityhealth )
{
if ( !Bridge::i()->featureIsEnabled( "community_health_stats" ) )
{
Output::i()->redirect( Url::internal( "app=core&module=overview&controller=mycharts" ) );
}
$this->communityhealth = true;
$this->activeTab = "charts";
$this->reportId = null;
}
else
{
$this->activeTab = isset( $this->tabs[ Request::i()->tab ] ) ? Request::i()->tab : "charts";
$this->reportId = is_numeric( Request::i()->report_id ) ? (int)Request::i()->report_id : 0;
$this->reportId = $this->reportId > 0 ? $this->reportId : null;
if ( $this->reportId and !$this->_getReport() )
{
Output::i()->redirect( Url::internal( (string)Request::i()->url() )->setQueryString( [ "report_id" => "0" ] ) );
}
}
parent::execute();
}
/**
* Manage
@@ -51,21 +209,63 @@ class mycharts extends Controller
*/
protected function manage() : void
{
$charts = [];
foreach( Chart::getChartsForMember( Url::internal( "app=core&module=overview&controller=mycharts" ), TRUE ) AS $id )
$report = $this->_getReport();
Output::i()->title = $report ? $report['report_title'] : Member::loggedIn()->loggedIn()->language()->addToStack( $this->communityhealth ? "communityhealth" : "menu__core_overview_mycharts" );
// Have to generate the form regardless of ajax so the values can be saved
$form = Chart::getBlockDateRangeForm( $report ? $report['id'] : null );
if ( !Request::i()->isAjax() )
{
$charts[] = $id;
Output::i()->output = $form;
}
Output::i()->title = Member::loggedIn()->language()->addToStack('menu__core_overview_mycharts');
if ( !count( $charts ) )
if ( $this->activeTab === "charts" )
{
Output::i()->output = Theme::i()->getTemplate('stats')->mychartsEmpty();
$charts = [];
foreach( Chart::getChartsForMember( Url::internal( "app=core&module=overview&controller=mycharts" . ( $this->communityhealth ? '&communityhealth=1' : '' ) ), TRUE ) AS $id )
{
$charts[] = $id;
}
if ( !\count( $charts ) )
{
$content = Theme::i()->getTemplate( 'stats')->mychartsEmpty();
}
else
{
$content = Theme::i()->getTemplate( 'stats' )->mycharts( $charts );
}
}
else
{
Output::i()->output = Theme::i()->getTemplate('stats')->mycharts( $charts );
$content = Theme::i()->getTemplate( "reports", "core", "admin" )->savedStatBlocks();
}
if ( Request::i()->isAjax() )
{
Output::i()->sendOutput( $content );
}
$this->_addSidebarButtons();
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_stats.js', 'core' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/statistics.css', 'core', 'admin' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/reports.css', 'core', 'admin' ) );
$url = Url::internal( "app=core&module=overview&controller=mycharts" );
if ( $this->communityhealth )
{
$url = $url->setQueryString( [ 'do' => 'communityhealth' ] );
}
else if ( $this->reportId )
{
$url = $url->setQueryString( [ "report_id" => $this->reportId ] );
}
if ( $this->communityhealth )
{
Output::i()->output .= Theme::i()->getTemplate( "global", "core", "admin" )->tabs( ['charts' => 'statsreports_saved_charts'], 'charts', $content, $url );
return;
}
Output::i()->output .= Theme::i()->getTemplate( "global", "core", "admin" )->tabs( $this->tabs, $this->activeTab, $content, $url );
}
/**
@@ -75,7 +275,6 @@ class mycharts extends Controller
*/
public function getChart() : void
{
if ( !isset( Request::i()->chartId ) )
{
if ( Request::i()->isAjax() )
@@ -90,7 +289,12 @@ class mycharts extends Controller
try
{
$chart = Chart::constructMemberChartFromData( Request::i()->chartId, Url::internal( "app=core&module=overview&controller=mycharts&do=getChart&chartId=" . Request::i()->chartId ) );
/* We first get the date range form to load the chart if it exists */
$report = $this->_getReport();
Chart::getBlockDateRangeForm( $report ? $report['id'] : null );
$chart = Chart::constructMemberChartFromData( $this->chartRow ?: Request::i()->chartId, Url::internal( "app=core&module=overview&controller=mycharts&do=getChart&chartId=" . Request::i()->chartId ) );
$chart->options['height'] = '300px'; // This chart is displayed in a grid with other charts, so we want them all to have the same height
Output::i()->output = (string) $chart;
}
catch( Throwable )
@@ -105,4 +309,572 @@ class mycharts extends Controller
}
}
}
/**
* Add the export and report selector sidebar buttons
*
* @return void
*/
protected function _addSidebarButtons()
{
if ( $this->_getReport() or Bridge::i()->featureIsEnabled( 'analytics_full' ) or Db::i()->select( "count(*) as cnt", 'core_reports' )->first() )
{
Output::i()->sidebar['actions']['select_report'] = array(
'primary' => false,
'icon' => 'area-chart',
'title' => 'statsreports_select_report',
'id' => 'elStatsSelectReport',
'menu'=> $this->getReportSelector(),
'link' => '#'
);
}
Output::i()->sidebar['actions']['download'] = array(
'primary' => true,
'icon' => 'download',
'title' => 'statsreports_download',
'link' => Url::internal( 'app=core&module=overview&controller=mycharts&do=download' )->setQueryString( [ "report_id" => $this->reportId ?: 0, "tab" => $this->activeTab ]),
'data' => array( 'ipstooltip' => '', 'ipstooltip-label' => Member::loggedIn()->language()->addToStack( 'statsreports_download_desc'), 'controller' => 'core.admin.stats.downloadButton' )
);
}
/**
* Get the dropdown to pick a report
*
* @return array
*/
protected function getReportSelector() : array
{
$return = [
'main' => [
'icon' => 'earth-americas',
'title' => 'statsreports_report_maindash',
'link' => Url::internal( "app=core&moduel=overview&controller=mycharts&tab=" . $this->activeTab )
]
];
if( Bridge::i()->featureIsEnabled( 'community_health_stats' ) )
{
$return[ 'health' ] = [
'icon' => 'heart-pulse',
'title' => 'communityhealth',
'link' => Url::internal( "app=core&module=overview&controller=mycharts&communityhealth=1" )
];
}
foreach( iterator_to_array( Db::i()->select( '*', 'core_reports', order: "report_title ASC" ) ) as $row )
{
$return[ 'report_' . $row['id'] ] = [
'icon' => 'bookmark',
'link' => Url::internal( "app=core&module=overview&controller=mycharts&report_id={$row['id']}&tab={$this->activeTab}" ),
'title' => $row['report_title']
];
}
return $return;
}
/**
* @return void
*/
protected function saveBlock()
{
$block = Request::i()->block;
$app = Request::i()->blockapp;
$subblock = Request::i()->subblock;
if ( !$block || !$app || !$subblock )
{
Output::i()->json( "No block, sub-block or app specified in the request", 400 );
}
$savedBlock = null;
if ( $savedId = Request::i()->saved_block_id )
{
try
{
$savedBlock = Db::i()->select( '*', 'core_saved_overview_stats', [ 'id=?', $savedId] )->first();
}
catch ( \UnderflowException )
{
Output::i()->json( "Cannot find block with id {$savedId}", 404 );
}
}
$defaultTitle = null;
if ( isset( $savedBlock['stat_title'] ) )
{
$defaultTitle = $savedBlock['stat_title'];
}
else
{
try
{
$application = Application::load( $app );
$extension = null;
foreach ( $application->extensions( 'core', 'OverviewStatistics', true ) as $key => $_extension )
{
if ( $key === $block )
{
$extension = $_extension;
break;
}
}
if ( !$extension )
{
throw new OutOfRangeException( "The OverviewStatistics plugin {$block} does not exist for the app {$app}" );
}
if ( !in_array( $subblock, $extension->getBlocks() ) )
{
throw new OutOfRangeException( "Block {$subblock} not defined for this overview stats plugin" );
}
if ( ( $details = $extension->getBlockDetails( $subblock ) ) and isset( $details['title'] ) )
{
try
{
$defaultTitle = Member::loggedIn()->language()->addToStack( $details['title'] );
Member::loggedIn()->language()->parseOutputForDisplay( $defaultTitle );
}
catch ( Exception ){}
}
}
catch ( Exception )
{
Output::i()->json( "The block or subblock could not be found", 404 );
}
}
$form = new Form();
$form->class = 'ipsForm--vertical';
$form->add(new Text( "statsreports_saved_block_title", $defaultTitle, true ));
if ( !is_array( $savedBlock ) )
{
$reports = iterator_to_array( Db::i()->select( "*", "core_reports" ) );
$reportOptions = [];
foreach( $reports as $row )
{
$reportOptions["report_" . $row['id']] = $row['report_title'];
}
$form->add( new YesNo( "statsreports_show_all_members", true, false ) );
$form->add( new YesNo( "statsreports_include_in_report", false, false, [ "togglesOn" => [ "statsreports_report_select", empty($reportOptions) ? "statsreports_report_new_title" : "" ] ] ) );
// if there are saved reports, allow user to select one of them
if ( !empty( $reportOptions ) )
{
// todo an autocomplete field is probably a better UX when there could be dozens of reports saved
$form->add( new Select( "statsreports_report", @$reports[0] ? 'report_'.$reports[0]['id'] : false, false, [
"unlimited" => false,
"unlimitedLang" => "statsreports_report_new",
"unlimitedToggles" => [
'statsreports_report_new_title'
],
"options" => $reportOptions,
], id: 'statsreports_report_select' ) );
}
$form->add( new Text( "statsreports_report_new_title", null, false, customValidationCode: (function( $value ) use ( $reportOptions ) {
if ( ( empty( $reportOptions ) OR ( isset( Request::i()->statsreports_report_unlimited ) ) ) and !empty( (int) Request::i()->statsreports_include_in_report_checkbox ) and ( !$value OR strlen( $value ) < 5 ) )
{
throw new InvalidArgumentException( Member::loggedIn()->language()->addToStack( "statsreports_no_title_error" ) );
}
else if ( !empty( $reportOptions ) and in_array( $value, array_values( $reportOptions ) ) )
{
throw new InvalidArgumentException( Member::loggedIn()->language()->addToStack( 'statsreports_title_in_use_error' ) );
}
}), id: "statsreports_report_new_title" ) );
}
if ( $values = $form->values() )
{
// first, is there a report this block is being saved to?
$reportID = null;
if ( @$values['statsreports_include_in_report'] )
{
if ( isset( $values['statsreports_report'] ) and $values['statsreports_report'] )
{
$reportID = (int) explode( '_', $values['statsreports_report'], 2 )[1];
}
else if ( ( empty( $reportOptions ) and !empty( $values['statsreports_report_new_title'] ) ) or @$values['statsreports_report'] === false )
{
$title = $values['statsreports_report_new_title'];
$reportID = Db::i()->insert( "core_reports", [ "report_title" => $title ] );
}
}
$insert = [
"stat_app" => $app,
"stat_extension" => $block,
"stat_subblock" => $subblock,
"stat_title" => $values['statsreports_saved_block_title'],
"stat_report_id" => $reportID,
];
if ( is_array( $savedBlock ) )
{
$insert['id'] = $savedBlock['id'];
}
else
{
$insert['stat_member'] = $values['statsreports_show_all_members'] ? null: Member::loggedIn()->member_id;
}
Db::i()->insert( "core_saved_overview_stats", $insert, true );
if ( Request::i()->isAjax() or Request::i()->frommenu )
{
Output::i()->json( [
"message" => "ok",
"title" => $insert['stat_title']
], 201 );
}
Output::i()->redirect( Url::internal( "app=core&module=overview&controller=mycharts&do=saveBlock" )->setQueryString(
[ 'block' => Request::i()->block, 'subblock' => Request::i()->subblock, 'blockapp' => Request::i()->blockapp ]
) );
}
Output::i()->output = (string) $form;
}
/**
* Delete an overview stats block
*
* @return void
* @throws \IPS\Api\Exception
*/
protected function deleteBlock()
{
Session::i()->csrfCheck();
if ( !isset( Request::i()->saved_block_id ) or !is_numeric( Request::i()->saved_block_id ) )
{
Output::i()->error( "Cannot delete because there is no saved_block_id in the request", 400 );
}
$id = (int) Request::i()->saved_block_id;
Db::i()->delete( "core_saved_overview_stats", [ "id=?", $id] );
Output::i()->redirect( Url::internal( "app=core&module=overview&controller=mycharts&tab=blocks" ) );
}
/**
* Download a CSV of this report
* @return void
*/
protected function download()
{
$csvContents = $this->activeTab === 'charts' ? $this->_compileCharts() : $this->_compileBlocks();
$lang = Member::loggedIn()->language();
$name = $lang->get( 'menu__core_overview_mycharts' ) . ' - ' . $lang->get( $this->activeTab === 'charts' ? 'statsreports_chart_fname' : 'statsreports_block_fname' );
// todo use the report's title when that's implemented
/* Compile the data */
$file = tempnam( TEMP_DIRECTORY, 'IPS' );
$fh = fopen( $file, 'w' );
fputcsv( $fh, $csvContents['headers'] );
foreach( $csvContents['data'] as $row )
{
fputcsv( $fh, $row );
}
fclose( $fh );
$csv = \file_get_contents( $file );
Member::loggedIn()->language()->parseOutputForDisplay( $csv );
// Output::i()->sendOutput( $csv, 200, 'text/plain' ); // for debugging
Output::i()->sendOutput( $csv, 200, 'text/csv', array( 'Content-Disposition' => Output::getContentDisposition( 'attachment', "{$name}.csv" ) ), FALSE, FALSE, FALSE );
}
/**
* Get all the charts in this report merged into one array
*
* @return array{headers: string[], data: string[][]}
*/
protected function _compileCharts() : array
{
$dateRange = $this->_getDateRange();
$data = [
"headers" => [ Member::loggedIn()->language()->addToStack( 'date')],
"data" => []
];
Member::loggedIn()->language()->parseOutputForDisplay( $data['headers'][0] );
$rowsByDate = [];
$timescale = null;
$totalColumnCount = 0;
// the type needs to be set before the charts are initialized
if ( isset( Request::i()->types ) and is_iterable( Request::i()->types ) )
{
foreach( Request::i()->types as $identifier => $type )
{
Request::i()->types[ $identifier][ $type] = "AreaChart";
}
}
$url = Url::internal( "app=core&module=overview&controller=mycharts" );
Dynamic::$mergedChartType = 'AreaChart';
foreach( Chart::getChartsForMember( $url ) AS $chartData )
{
$chart = $chartData['chart'];
if ( !( $chart instanceof Dynamic ) )
{
continue;
}
$timescale = $timescale ?: $chart->timescale;
// Cannot add pie charts
if ( $chart->type === 'PieChart' or $chart->type === 'GeoChart' )
{
continue; // todo this should probably be indicated to the user if the chart cannot be included in the CSV
}
$chart->timescale = $timescale;
$chart->start = $dateRange['start'];
$chart->end = $dateRange['end'];
$chart->compileForOutput();
$rawHeaders = $chart->headers;
$columnCount = 0;
foreach ( $rawHeaders as $header )
{
$formattedHeader = $chart->title . ' - ' . str_replace( "\n", ' ', $header['label'] );
Member::loggedIn()->language()->parseOutputForDisplay( $formattedHeader );
// if the first column is a date, it's the indexable date type
if ( $columnCount === 0 and in_array( $header['type'], [ 'date', 'datetime', 'timeofday' ] ) )
{
continue;
}
$columnCount++;
$data['headers'][] = $formattedHeader;
}
if ( $columnCount )
{
$insertedDates = [];
$rawRows = $chart->rows;
foreach( $rawRows AS $row )
{
$i = 0;
$save = array();
$date = null;
foreach( $rawHeaders AS $header )
{
switch( $header['type'] )
{
/* Booleans convert to string 'true' or 'false' */
case 'bool':
$save[] = ( $row[$i] ) ? 'true' : 'false';
break;
/* Make dates human readable */
case 'date':
case 'datetime':
case 'timeofday':
// In theory, this should be the first column in the chart
if ( $i === 0 )
{
$date = "ts_" . ( new DateTime( $row[$i] ) )->getTimestamp(); // using ts_ before the date to prevent the arrays from auto-filling numeric indices
}
else
{
$save[] = ( new DateTime( $row[ $i ] ) )->fullYearLocaleDate();
}
break;
/* Anything else can just save as-is */
default:
if ( \is_array( $row[$i] ) )
{
/* GeoCharts use arrays so we need to use the value rather */
$save[] = $row[$i]['value'];
}
else
{
$save[] = $row[$i];
}
break;
}
$i++;
}
if ( $date )
{
$insertedDates[$date] = $date;
if ( isset( $rowsByDate[$date] ) )
{
$rowsByDate[$date] = array_merge( $rowsByDate[$date], $save );
}
else
{
$rowsByDate[$date] = array_merge( array_fill( 0, $totalColumnCount, "NULL" ), $save );
}
}
}
// after iterating through the rows, we need to make sure we add empty values for dates in the output
foreach( $rowsByDate as $rowDate => $rowValues )
{
if ( array_key_exists( $rowDate, $insertedDates ) )
{
continue;
}
$rowsByDate[$rowDate] = array_merge( $rowValues, array_fill( 0, $columnCount, "NULL" ) );
}
}
$totalColumnCount += $columnCount;
}
ksort( $rowsByDate );
foreach( $rowsByDate as $date => $rowValues )
{
// the dates are ts_<unix timestamp as an int>
$dateString = ( DateTime::ts( (int) \mb_substr( $date, 3 ) ) )->fullYearLocaleDate();
$data['data'][] = array_merge( array( $dateString ), $rowValues );
}
return $data;
}
/**
* Get the date range based on the request for a report download
*
* @return array{start: DateTime, end: DateTime}
*/
protected function _getDateRange() : array
{
if ( isset( Request::i()->range ) and is_numeric( Request::i()->range ) )
{
$days = (int) Request::i()->range;
return [
"start" => DateTime::create()->setTime( 0, 0, 0 )->sub( new DateInterval( "P{$days}D" ) ),
"end" => DateTime::create()->setTime( 23, 59, 59 ),
];
}
if ( is_array( Request::i()->range ) and is_numeric( @Request::i()->range['start'] ) and is_numeric( @Request::i()->range['end'] ) )
{
return [
"start" => DateTime::ts( (int) Request::i()->range['start'] )->setTime( 23, 59, 59 ),
"end" => DateTime::ts( (int) Request::i()->range['end'] )->setTime( 0, 0, 0 )->sub( new DateInterval( "P7D" ) ),
];
}
return [
"start" => DateTime::create()->setTime( 0, 0, 0 )->sub( new DateInterval( "P7D" ) ), // The default time interval is 7 days
"end" => DateTime::create()->setTime( 23, 59, 59 ),
];
}
/**
* Get all the blocks in this report merged into one array
*
* @return array{headers: string[], data: string[][]}
*/
protected function _compileBlocks() : array
{
$returnData = [
"headers" => [],
"data" => []
];
$rowCount = 0;
$columns = [];
$dateRange = $this->_getDateRange();
foreach( Chart::getSavedBlocks() as $block )
{
$title = Member::loggedIn()->language()->addToStack( $block["details"]["title"] );
$numbersFound = [];
if ( is_callable( array( $block['extension'], 'getBlockNumbers' ) ) )
{
// Allow extensions to define their own numbers
$numbersFound = $block['extension']->getBlockNumbers( $dateRange, $block['subblock'] );
if ( !is_array( $numbersFound ) )
{
$numbersFound = [[ Member::loggedIn()->language()->get( "statsreports_legacy_block" ) ]];
}
}
else
{
// Here we use the same logic from the JS to find numbers
$content = $block['extension']->getBlock( $dateRange, $block['subblock'] );
Member::loggedIn()->language()->parseOutputForDisplay( $content );
$doc = new DOMDocument();
$doc->loadHTML( "<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\"><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"></head><body>" . $content . "</body></html>" );
$path = new DOMXPath( $doc );
$previousPeriodLangString = Member::loggedIn()->language()->addToStack( "previous_period" );
Member::loggedIn()->language()->parseOutputForDisplay( $previousPeriodLangString );
$numbersFound = [[]];
foreach ( ( $path->query( "//*[@data-number]|//*[@title][contains(@class, 'cStat__change')]" ) ?? [] ) as $node )
{
if ( $node instanceof DOMElement )
{
if ( $node->hasAttribute( "data-number" ) )
{
$numbersFound[0][] = trim( $node->getAttribute( "data-number" ) ) ?: 0;
}
else if ( $node->hasAttribute( "title" ) and ( $titleAttr = $node->getAttribute( "title" ) and str_starts_with( $titleAttr, $previousPeriodLangString ) ) )
{
$numbersFound[1][] = trim( str_replace( $previousPeriodLangString, '', $titleAttr ) ) ?: 0;
}
}
}
if ( count( $numbersFound ) === 2 )
{
$numbersFound = [
"" => $numbersFound[0],
"statsreports_previous_count" => $numbersFound[1]
];
}
}
$numbersFound = is_array( $numbersFound ) ? $numbersFound : [ $numbersFound ];
foreach ( $numbersFound as $idx => $numbersFoundCol )
{
// we need to make sure this is a column and not just a scalar or associative array
$numbersFoundCol = is_array( $numbersFoundCol ) ? $numbersFoundCol : [ $numbersFoundCol ];
$numbersFoundCol = array_values( $numbersFoundCol );
// When the column isn't named, we index by the block name and column number
if ( !is_string( $idx ) )
{
$idx = $title . (count($numbersFound) > 1 ? " - (" . $idx + 1 . ")" : "");
}
else
{
$idx = $title . ($idx ? ' - ' . Member::loggedIn()->language()->addToStack( $idx ) : "");
}
Member::loggedIn()->language()->parseOutputForDisplay( $idx );
$returnData['headers'][] = $idx;
$rowCount = max( $rowCount, count( $numbersFoundCol ) );
if ( $rowCount > count( $numbersFoundCol ) )
{
$numbersFoundCol = array_merge( $numbersFoundCol, array_fill( 0, $rowCount - count( $numbersFoundCol ), "NULL" ) );
}
$columns[] = $numbersFoundCol;
}
}
// now we need to transpose the columns into rows
for( $column = 0; $column < count( $columns ); $column++ )
{
for ( $row = 0; $row < $rowCount; $row++ )
{
$returnData['data'][$row] = $returnData['data'][$row] ?? [];
$returnData['data'][$row][$column] = $columns[$column][$row] ?? "NULL";
}
}
return $returnData;
}
}
@@ -13,6 +13,7 @@ namespace IPS\core\modules\admin\overview;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DateInterval;
use IPS\core\modules\admin\moderation\spam;
use IPS\core\modules\admin\promotion\seo;
use IPS\core\modules\admin\settings\advanced;
use IPS\core\modules\admin\settings\terms;
@@ -234,6 +235,10 @@ class onboard extends Controller
/* Get the htaccess mod_Rewrite setting */
$form->addTab( 'onboard_tab_furls' );
seo::htaccessSetting( $form );
/* Add captcha. */
$form->addTab( 'onboard_tab_spamprevention' );
spam::captchaForm( $form );
}
/* Add the terms/privacy policy/etc. */
@@ -265,7 +270,6 @@ class onboard extends Controller
/* Clear data stores */
unset(
Store::i()->manifest,
Store::i()->iebrowserconfig,
Store::i()->frontNavigation
);
@@ -20,7 +20,6 @@ use IPS\DateTime;
use IPS\Db;
use IPS\Dispatcher;
use IPS\Dispatcher\Controller;
use IPS\Extensions\AdvertisementLocationsAbstract;
use IPS\File;
use IPS\Helpers\Form;
use IPS\Helpers\Form\CheckboxSet;
@@ -505,79 +504,12 @@ class advertisements extends Controller
$form->add( new Text( 'ad_image_alt', ( $record->id ) ? $record->image_alt : NULL, FALSE, array(), NULL, NULL, NULL, 'ad_image_alt' ) );
$currentValues = ( $record->id and $record->additional_settings ) ? json_decode( $record->additional_settings, TRUE ) : array();
if( Request::i()->type != 'emails' )
{
$currentValues = ( $record->id ) ? json_decode( $record->additional_settings, TRUE ) : array();
/* Add the location fields, remember to call extensions for additional locations.
Array format: location => array of toggle fields to show */
$defaultLocations = array(
'ad_global_header' => array( 'ad_apps' ),
'ad_global_footer' => array( 'ad_apps' ),
'ad_sidebar' => array( 'ad_apps' ),
);
$settingFields = [];
/* Add a field to restrict by app */
$settingFields[] = new Node( 'ad_apps', $currentValues['ad_apps'] ?? 0, false, array(
'class' => Application::class,
'multiple' => true,
'zeroVal' => 'all',
'subnodes' => false
), null, null, null, 'ad_apps' );
/* Now grab ad location extensions */
foreach ( Application::allExtensions( 'core', 'AdvertisementLocations', FALSE, 'core' ) as $key => $extension )
{
/* @var AdvertisementLocationsAbstract $extension */
$result = $extension->getSettings( $currentValues );
$defaultLocations = array_merge( $defaultLocations, $result['locations'] );
/* If this is a list view, add setting fields for interaval and repeat */
if( $extension::$listView )
{
$settingFields[] = new Number( 'ad_view_number', ( isset( $currentValues['ad_view_number'] ) AND $currentValues['ad_view_number'] ) ? $currentValues['ad_view_number'] : 1, FALSE, array( 'min' => 1 ), NULL, NULL, Member::loggedIn()->language()->addToStack('ad_view_number_suffix'), 'ad_view_number' );
$settingFields[] = new Number( 'ad_view_repeat', $currentValues['ad_view_repeat'] ?? 1, FALSE, array( 'unlimited' => -1 ), NULL, NULL, Member::loggedIn()->language()->addToStack('ad_view_repeat_suffix'), 'ad_view_repeat' );
foreach( $result['locations'] as $location => $toggles )
{
$defaultLocations[$location][] = 'ad_view_number';
$defaultLocations[$location][] = 'ad_view_repeat';
}
}
if( isset( $result['settings'] ) )
{
$settingFields = $settingFields + $result['settings'];
}
}
/* Add the locations to the form, and make sure the toggles get set properly */
$locations = array();
if ( $record->id )
{
$locations = explode( ',', $record->location );
$locations = array_intersect( $locations, array_keys( $defaultLocations ) );
}
/* Make location optional so that you can have an active ad for widgets only */
$form->add( new CheckboxSet( 'ad_location',
$locations,
false,
array(
'options' => array_combine( array_keys( $defaultLocations ), array_keys( $defaultLocations ) ),
'toggles' => $defaultLocations,
'noDefault' => true
),
NULL,
NULL,
NULL,
'ad_location'
) );
$settingFields = Advertisement::locationFields( $currentValues, ( $record->id ? explode( ",", $record->location ) : [] ) );
foreach( $settingFields as $settingField )
{
$form->add( $settingField );
@@ -589,31 +521,9 @@ class advertisements extends Controller
}
else
{
/* Container restrictions for email ads */
$containerClasses = array();
$containerToggles = array();
foreach( Content::routedClasses( FALSE, FALSE, TRUE ) as $contentItemClass )
foreach( Advertisement::emailFields( $currentValues ) as $field )
{
if( isset( $contentItemClass::$containerNodeClass ) AND $contentItemClass::$containerNodeClass AND !isset( $containerClasses[ $contentItemClass::$containerNodeClass ] ) )
{
$containerClass = $contentItemClass::$containerNodeClass;
$containerClasses[ $containerClass ] = $contentItemClass::$title . '_pl';
$containerToggles[ $containerClass ] = array( 'node_' . md5( $containerClass ) );
}
}
if( count( $containerClasses ) )
{
$form->add( new Select( '_ad_email_container', ( $record->id AND isset( $record->_additional_settings['email_container'] ) ) ? $record->_additional_settings['email_container'] : '*', FALSE, array( 'options' => $containerClasses, 'multiple' => FALSE, 'unlimited' => '*', 'unlimitedLang' => 'unrestricted', 'toggles' => $containerToggles ), NULL, NULL, NULL, '_ad_email_container' ) );
foreach( $containerClasses as $classname => $lang )
{
$form->add( new Node( 'node_' . md5( $classname ), ( $record->id ) ? ( isset( $record->_additional_settings['email_node'] ) ? $record->_additional_settings['email_node'] : 0 ) : 0, FALSE, array( 'class' => $classname, 'zeroVal' => 'any', 'multiple' => TRUE, 'forceOwner' => FALSE ), NULL, NULL, NULL, 'node_' . md5( $classname ) ) );
Member::loggedIn()->language()->words[ 'node_' . md5( $classname ) ] = Member::loggedIn()->language()->addToStack( $classname::$nodeTitle );
}
$form->add( $field );
}
}
@@ -773,30 +683,13 @@ class advertisements extends Controller
$record->images = json_encode( $images );
/* Any additional settings to save? */
$additionalSettings = array(
'ad_apps' => ( isset( $values['ad_apps'] ) AND is_array( $values['ad_apps'] ) ) ? array_keys( $values['ad_apps'] ) : 0
);
if( $values['ad_type'] == Advertisement::AD_EMAIL )
{
$additionalSettings['email_container'] = $values['_ad_email_container'];
$additionalSettings['email_node'] = ( $values['_ad_email_container'] != '*' ) ? ( ( $values['node_' . md5( $values['_ad_email_container'] ) ] == 0 ) ? 0 : array_keys( $values['node_' . md5( $values['_ad_email_container'] ) ] ) ) : 0;
$additionalSettings = Advertisement::processEmailFields( $values );
}
else
{
foreach ( Application::allExtensions( 'core', 'AdvertisementLocations', FALSE, 'core' ) as $key => $extension )
{
if( $extension::$listView )
{
$additionalSettings['ad_view_number'] = $values['ad_view_number'] ?? 1;
$additionalSettings['ad_view_repeat'] = $values['ad_view_repeat'] ?? 0;
}
$settings = $extension->parseSettings( $values );
$additionalSettings = array_merge( $additionalSettings, $settings );
}
$additionalSettings = Advertisement::processLocationFields( $values );
}
$record->additional_settings = json_encode( $additionalSettings );
@@ -310,7 +310,10 @@ class seo extends Controller
{
/* Init */
$form = new Form;
$form->add( new FormUrl( 'sitemap_url', Settings::i()->sitemap_url ?: Settings::i()->base_url . 'sitemap.php', FALSE ) );
if( !CIC )
{
$form->add( new FormUrl( 'sitemap_url', Settings::i()->sitemap_url ?: Settings::i()->base_url . 'sitemap.php', FALSE ) );
}
/* Get extension settings */
$useRecommendedSettings = TRUE;
@@ -351,37 +354,46 @@ class seo extends Controller
{
$values = array_merge( $values, $recommendedSettings );
}
try
if( isset( $values[ 'sitemap_url' ] ) and !( $values[ 'sitemap_url' ] instanceof Url ) )
{
if( !( $values['sitemap_url'] instanceof Url ) )
throw new RuntimeException;
}
if( !CIC )
{
try
{
throw new RuntimeException;
$response = $values[ 'sitemap_url' ]->setQueryString( 'testsettings', 1 )->request()->get();
if( $response->httpResponseCode != 200 or !mb_strpos( $values[ 'sitemap_url' ], 'sitemap.php' ) )
{
$form->error = Member::loggedIn()->language()->addToStack( 'invalid_sitemap_url' );
}
}
$response = $values['sitemap_url']->setQueryString( 'testsettings', 1 )->request()->get();
if ( $response->httpResponseCode != 200 or !mb_strpos( $values['sitemap_url'], "sitemap.php" ) )
catch( RuntimeException $e )
{
$form->error = Member::loggedIn()->language()->addToStack( 'invalid_sitemap_url' );
}
}
}
catch ( RuntimeException $e )
{
$form->error = Member::loggedIn()->language()->addToStack( 'form_url_bad' );
}
if( !$form->error )
{
$values['sitemap_url'] = (string) $values['sitemap_url'];
if( isset( $values[ 'sitemap_url' ] ) )
{
$values['sitemap_url'] = (string) $values['sitemap_url'];
}
foreach( Application::allExtensions( 'core', 'Sitemap', FALSE, 'core' ) as $extKey => $extension )
{
$extension->saveSettings( $values );
}
$form->saveAsSettings( array( 'sitemap_url' => $values['sitemap_url'] ) );
if( !CIC )
{
$form->saveAsSettings( ['sitemap_url' => $values[ 'sitemap_url' ]] );
}
Session::i()->log( 'acplogs__seo_sitemap_settings' );
}
@@ -41,6 +41,7 @@ use function call_user_func_array;
use function count;
use function defined;
use function mb_substr;
use function method_exists;
use function strlen;
use function strtolower;
use function substr;
@@ -82,7 +83,7 @@ class dataLayer extends Controller
*/
public function __construct()
{
if ( Application::appIsEnabled('cloud') and DataLayerClass::i() instanceof \IPS\cloud\DataLayer\Handler )
if ( Application::appIsEnabled('cloud') and DataLayerClass::i() instanceof \IPS\cloud\DataLayer )
{
static::$handlerClass = 'IPS\cloud\DataLayer\Handler';
}
@@ -101,7 +101,7 @@ class general extends Controller
} ) );
$values['site_address'] = json_encode( $values['site_address'] );
$form->saveAsSettings( $values );
if ( $values['site_online'] )
@@ -113,8 +113,8 @@ class general extends Controller
AdminNotification::send( 'core', 'ConfigurationError', 'siteOffline', FALSE, NULL, Member::loggedIn() );
}
/* Clear manifest and ie browser data stores */
unset( Store::i()->manifest, Store::i()->iebrowserconfig );
/* Clear manifest data store */
unset( Store::i()->manifest );
Session::i()->log( 'acplogs__general_settings' );
Output::i()->redirect( Url::internal( 'app=core&module=settings&controller=general' ), 'saved' );
@@ -19,6 +19,7 @@ use IPS\Data\Store;
use IPS\Db;
use IPS\Dispatcher;
use IPS\Extensions\SSOAbstract;
use IPS\File;
use IPS\GeoLocation;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Address;
@@ -27,11 +28,14 @@ use IPS\Helpers\Form\Number;
use IPS\Helpers\Form\Radio;
use IPS\Helpers\Form\Stack;
use IPS\Helpers\Form\Tel;
use IPS\Helpers\Form\Translatable;
use IPS\Helpers\Form\Url as FormUrl;
use IPS\Helpers\Form\YesNo;
use IPS\Helpers\Wizard;
use IPS\Http\Request\Exception;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Lang;
use IPS\Login as LoginClass;
use IPS\Login\Handler;
use IPS\Member;
@@ -96,6 +100,12 @@ class login extends Controller
{
$buttons['add']['link'] = $buttons['add']['link']->setQueryString( '_new', 1 );
}
/* No add button for managed */
if( !IPS::canManageResources() )
{
unset( $buttons['add'] );
}
return $buttons;
}
@@ -476,6 +486,20 @@ class login extends Controller
'0' => 'force_reg_terms_0',
'1' => 'force_reg_terms_1',
) ) ) );
$form->add( new YesNo( 'reg_welcome_email', Settings::i()->reg_welcome_email, false, [ 'togglesOn' => [ 'reg_welcome_email_text' ] ] ) );
$defaultMessage = Member::loggedIn()->language()->addToStack( 'email_reg_complete', false, [ 'sprintf' => [ Settings::i()->board_name ] ] ) . Member::loggedIn()
->language()->addToStack( 'email_reg_complete_pass' );
$form->add( new Translatable( 'reg_welcome_email_text', Member::loggedIn()->language()->checkKeyExists( 'reg_welcome_email_message' ) ? null : $defaultMessage, null, [
'app' => 'core',
'key' => 'reg_welcome_email_message',
'editor' => [
'app' => 'core',
'key' => 'Admin',
'allowAttachments' => false,
'autoSaveKey' => 'core-Admin-reg_welcome_email_message',
]
], id: 'reg_welcome_email_text' ) );
/* Save */
if ( $values = $form->values() )
@@ -490,6 +514,24 @@ class login extends Controller
{
$values['coppa_address'] = json_encode( $values['coppa_address'] );
}
if( isset( $values['reg_welcome_email_text'] ) )
{
/* We only want to save this if it doesn't match the default */
Member::loggedIn()->language()->parseOutputForDisplay( $defaultMessage );
if( strip_tags( $values['reg_welcome_email_text'][ Member::loggedIn()->language()->id ] ) != trim( strip_tags( $defaultMessage ) ) )
{
Lang::saveCustom( 'core', 'reg_welcome_email_message', $values['reg_welcome_email_text'] );
}
else
{
Lang::deleteCustom( 'core', 'reg_welcome_email_message' );
}
File::claimAttachments( 'core-Admin-reg_welcome_email_message', null, null, 'reg_welcome_email_message', true );
unset( $values['reg_welcome_email_text'] );
}
$form->saveAsSettings( $values );
Session::i()->log( 'acplogs__registration_settings' );
@@ -1052,6 +1052,7 @@ class posting extends Controller
$keys = explode( ',', Request::i()->restrictionKey );
$langs = json_decode( Request::i()->restrictionLang, true );
$descs = json_decode( Request::i()->restrictionDesc, true );
$defaults = json_decode( Request::i()->restrictionDefault, true ) ?: [];
if ( !is_array( $langs ) or !is_array( $descs ) or count( $keys ) != count( $langs ) or count( $keys ) != count( $descs ) )
{
@@ -1067,7 +1068,7 @@ class posting extends Controller
$key = $keys[$i];
$lang = htmlspecialchars( $langs[$i] );
$desc = trim( htmlspecialchars( $descs[$i] ) );
$val = Form\Editor::getRestrictionLevel( $key );
$val = Form\Editor::getRestrictionLevel( $key, $defaults[$key] ?? null );
Member::loggedIn()->language()->words[ $key ] = $lang;
if ( !empty( $desc ) )
{
@@ -82,15 +82,15 @@ class webapp extends Controller
$form->addTab( 'webapp_tab_icons' );
/* Homescreen icons - we accept one upload and create the images we need */
$homeScreen = json_decode( Settings::i()->icons_homescreen, TRUE ) ?? array();
$form->add( new Upload( 'icons_homescreen', ( isset( $homeScreen['original'] ) ) ? File::get( 'core_Icons', $homeScreen['original'] ) : NULL, FALSE, array( 'image' => true, 'storageExtension' => 'core_Icons' ) ) );
$homeScreen = Settings::i()->icons_homescreen ? json_decode( Settings::i()->icons_homescreen, TRUE ) :[];
$form->add( new Upload( 'icons_homescreen', ( isset( $homeScreen['original'] ) ) ? File::get( 'core_Icons', $homeScreen['original'] ) : NULL, FALSE, array( 'image' => true, 'storageExtension' => 'core_Icons', 'allowedFileTypes' => array( 'png' ) ) ) );
$homeScreenMaskable = json_decode( Settings::i()->icons_homescreen_maskable, TRUE ) ?? array();
$form->add( new Upload( 'icons_homescreen_maskable', ( isset( $homeScreenMaskable['original'] ) ) ? File::get( 'core_Icons', $homeScreenMaskable['original'] ) : NULL, FALSE, array( 'image' => true, 'storageExtension' => 'core_Icons', 'allowedFileTypes' => ['png', 'webp' ] ) ) );
$homeScreenMaskable = Settings::i()->icons_homescreen_maskable ? json_decode( Settings::i()->icons_homescreen_maskable, TRUE ) : array();
$form->add( new Upload( 'icons_homescreen_maskable', ( isset( $homeScreenMaskable['original'] ) ) ? File::get( 'core_Icons', $homeScreenMaskable['original'] ) : NULL, FALSE, array( 'image' => true, 'storageExtension' => 'core_Icons', 'allowedFileTypes' => array( 'png' ) ) ) );
/* Apple startup screen logo - we accept one upload and create the images we need */
$apple = json_decode( Settings::i()->icons_apple_startup, TRUE ) ?? array();
$form->add( new Upload( 'icons_apple_startup', ( isset( $apple['original'] ) ) ? File::get( 'core_Icons', $apple['original'] ) : NULL, FALSE, array( 'image' => true, 'storageExtension' => 'core_Icons' ) ) );
$apple = Settings::i()->icons_apple_startup ? json_decode( Settings::i()->icons_apple_startup, TRUE ) : array();
$form->add( new Upload( 'icons_apple_startup', ( isset( $apple['original'] ) ) ? File::get( 'core_Icons', $apple['original'] ) : NULL, FALSE, array( 'image' => true, 'storageExtension' => 'core_Icons', 'allowedFileTypes' => array( 'png' ) ) ) );
/* Safari pinned tabs icon and highlight color */
$form->add( new Upload( 'icons_mask_icon', Settings::i()->icons_mask_icon ? File::get( 'core_Icons', Settings::i()->icons_mask_icon ) : NULL, FALSE, array( 'allowedFileTypes' => array( 'svg' ), 'storageExtension' => 'core_Icons', 'obscure' => FALSE ) ) );
@@ -99,14 +99,14 @@ class webapp extends Controller
$form->addTab( 'webapp_tab_manifest' );
/* And finally, additional manifest and livetile details */
$manifestDetails = json_decode( Settings::i()->manifest_details, TRUE );
$manifestDetails = Settings::i()->manifest_details ? json_decode( Settings::i()->manifest_details, TRUE ) : [];
if ( isset( $manifestDetails['cache_key'] ) )
{
unset( $manifestDetails['cache_key'] );
}
$form->add( new YesNo( 'configure_manifest', count( $manifestDetails ) > 0, FALSE, array(
'togglesOn' => array( 'manifest_shortname', 'manifest_fullname', 'manifest_description', 'manifest_defaultapp', 'manifest_themecolor', 'manifest_bgcolor', 'manifest_display', 'manifest_custom_url_toggle' ),
'togglesOn' => array( 'manifest_shortname', 'manifest_fullname', 'manifest_description', 'manifest_defaultapp', 'manifest_themecolor', 'manifest_bgcolor', 'manifest_custom_url_toggle' ),
) ) );
$form->add( new Text( 'manifest_shortname', ( isset( $manifestDetails['short_name'] ) ) ? $manifestDetails['short_name'] : '', FALSE, array(), NULL, NULL, NULL, 'manifest_shortname' ) );
@@ -142,7 +142,6 @@ class webapp extends Controller
$form->add( new Color( 'manifest_themecolor', ( isset( $manifestDetails['theme_color'] ) ) ? $manifestDetails['theme_color'] : NULL, FALSE, array( 'allowNoneLanguage' => 'manifest_themecolor_none', 'allowNone' => true ), NULL, NULL, NULL, 'manifest_themecolor' ) );
$form->add( new Color( 'manifest_bgcolor', ( isset( $manifestDetails['background_color'] ) ) ? $manifestDetails['background_color'] : NULL, FALSE, array(), NULL, NULL, NULL, 'manifest_bgcolor' ) );
$form->add( new Radio( 'manifest_display', ( isset( $manifestDetails['display'] ) ) ? $manifestDetails['display'] : 'standalone', FALSE, array( 'options' => array( 'fullscreen' => 'manifest_fullscreen', 'standalone' => 'manifest_standalone', 'minimal-ui' => 'manifest_minimalui', 'browser' => 'manifest_browser' ) ), NULL, NULL, NULL, 'manifest_display' ) );
if( $values = $form->values() )
{
@@ -194,18 +193,17 @@ class webapp extends Controller
$values['manifest_details']['description'] = $values['manifest_description'];
$values['manifest_details']['theme_color'] = $values['manifest_themecolor'];
$values['manifest_details']['background_color'] = $values['manifest_bgcolor'];
$values['manifest_details']['display'] = $values['manifest_display'];
}
unset( $values['configure_manifest'], $values['manifest_shortname'], $values['manifest_fullname'], $values['manifest_description'], $values['manifest_display'], $values['manifest_bgcolor'], $values['manifest_themecolor'], $values['manifest_custom_url_toggle'], $values['manifest_short_url'] );
unset( $values['configure_manifest'], $values['manifest_shortname'], $values['manifest_fullname'], $values['manifest_description'], $values['manifest_bgcolor'], $values['manifest_themecolor'], $values['manifest_custom_url_toggle'], $values['manifest_short_url'] );
$values['manifest_details'] = json_encode( array_merge( $values['manifest_details'], [ 'cache_key' => time() ] ) );
/* Save the settings */
$form->saveAsSettings( $values );
/* Clear manifest and ie browser data stores */
unset( Store::i()->manifest, Store::i()->iebrowserconfig );
/* Clear manifest data store */
unset( Store::i()->manifest );
/* And log */
Session::i()->log( 'acplogs__webapp' );
@@ -243,28 +241,8 @@ class webapp extends Controller
if( isset( $values[ $type ] ) AND $values[ $type ] )
{
$sizes = array(
'android-chrome-36x36' => array(36, 36),
'android-chrome-48x48' => array(48, 48),
'android-chrome-72x72' => array(72, 72),
'android-chrome-96x96' => array(96, 96),
'android-chrome-144x144' => array(144, 144),
'android-chrome-192x192' => array(192, 192),
'android-chrome-256x256' => array(256, 256),
'android-chrome-384x384' => array(384, 384),
'android-chrome-512x512' => array(512, 512),
'msapplication-square70x70logo' => array(128, 128),
'msapplication-TileImage' => array(144, 144),
'msapplication-square150x150logo' => array(270, 270),
'msapplication-wide310x150logo' => array(558, 558),
'msapplication-square310x310logo' => array(558, 270),
'apple-touch-icon-57x57' => array(57, 57),
'apple-touch-icon-60x60' => array(60, 60),
'apple-touch-icon-72x72' => array(72, 72),
'apple-touch-icon-76x76' => array(76, 76),
'apple-touch-icon-114x114' => array(114, 114),
'apple-touch-icon-120x120' => array(120, 120),
'apple-touch-icon-144x144' => array(144, 144),
'apple-touch-icon-152x152' => array(152, 152),
'apple-touch-icon-180x180' => array(180, 180),
);
@@ -351,7 +329,14 @@ class webapp extends Controller
{
if( $values['icons_apple_startup'] )
{
$values['icons_apple_startup'] = json_encode( array( 'original' => (string) $values['icons_apple_startup'] ) );
if( empty( $apple['original'] ) or (string) $values['icons_apple_startup'] != $apple['original'] )
{
$values['icons_apple_startup'] = json_encode( array( 'original' => (string) $values['icons_apple_startup'] ) );
}
else
{
$values['icons_apple_startup'] = json_encode( $apple );
}
}
else
{
@@ -391,36 +376,33 @@ class webapp extends Controller
if( isset( $setting['original'] ) )
{
$sizes = array(
'apple-startup-1136x640' => array( 320, 568, 2, 'landscape' ),
'apple-startup-2436x1125' => array( 375, 812, 3, 'landscape' ),
'apple-startup-1792x828' => array( 414, 896, 2, 'landscape' ),
'apple-startup-828x1792' => array( 414, 896, 2, 'portrait' ),
'apple-startup-1334x750' => array( 375, 667, 2, 'landscape' ),
'apple-startup-1242x2688' => array( 414, 896, 3, 'portrait' ),
'apple-startup-2208x1242' => array( 414, 736, 3, 'landscape' ),
'apple-startup-1125x2436' => array( 375, 812, 3, 'portrait' ),
'apple-startup-1242x2208' => array( 414, 736, 3, 'portrait' ),
'apple-startup-2732x2048' => array( 1024, 1366, 2, 'landscape' ),
'apple-startup-2688x1242' => array( 414, 896, 3, 'landscape' ),
'apple-startup-2224x1668' => array( 834, 1112, 2, 'landscape' ),
'apple-startup-750x1334' => array( 375, 667, 2, 'portrait' ),
'apple-startup-2048x2732' => array( 1024, 1366, 2, 'portrait' ),
'apple-startup-2388x1668' => array( 834, 1194, 2, 'landscape' ),
'apple-startup-1668x2224' => array( 834, 1112, 2, 'portrait' ),
'apple-startup-640x1136' => array( 320, 568, 2, 'portrait' ),
'apple-startup-1668x2388' => array( 834, 1194, 2, 'portrait' ),
'apple-startup-2048x1536' => array( 768, 1024, 2, 'landscape' ),
'apple-startup-1536x2048' => array( 768, 1024, 2, 'portrait' ),
'apple-startup-2360x1640' => array( 1180, 820, 2, 'landscape' ),
'apple-startup-1640x2360' => array( 1180, 820, 2, 'portrait' ),
'apple-startup-2160x1620' => array( 1080, 810, 2, 'landscape' ),
'apple-startup-1620x2160' => array( 1080, 810, 2, 'portrait' ),
'apple-startup-2778x1284' => array( 428, 926, 3, 'landscape' ),
'apple-startup-1284x2778' => array( 428, 926, 3, 'portrait' ),
'apple-startup-2532x1170' => array( 390, 844, 3, 'landscape' ),
// iPhone 12, 12 Pro, 13, 13 Pro, 14
'apple-startup-1170x2532' => array( 390, 844, 3, 'portrait' ),
'apple-startup-2340x1080' => array( 360, 780, 3, 'landscape' ),
'apple-startup-1080x2340' => array( 360, 780, 3, 'portrait' ),
// iPhone 12 Pro Max, 13 Pro Max, 14 Plus
'apple-startup-1284x2778' => array( 428, 926, 3, 'portrait' ),
// iPhone 14 Pro, iPhone 15, iPhone 15 Pro, iPhone 16
'apple-startup-1179x2556' => array( 393, 852, 3, 'portrait' ),
// iPhone 15 Plus, iPhone 15 Pro Max, iPhone 16 Plus
'apple-startup-1290x2796' => array( 430, 932, 3, 'portrait' ),
// iPhone 16 Pro
'apple-startup-1206x2622' => array( 402, 874, 3, 'portrait' ),
// iPhone 16 Pro Max
'apple-startup-1320x2868' => array( 440, 956, 3, 'portrait' ),
// iPad Air 13" + iPad Pro 12.9"
'apple-startup-2048x2732' => array( 1024, 1366, 2, 'portrait' ),
'apple-startup-2732x2048' => array( 1024, 1366, 2, 'landscape' ),
// iPad Air 4+ (11" displays)
'apple-startup-1640x2360' => array( 820, 1180, 2, 'portrait' ),
'apple-startup-2360x1640' => array( 820, 1180, 2, 'landscape' ),
// iPad Pro 11" (1st to 4th gen)
'apple-startup-1668x2388' => array( 834, 1194, 2, 'portrait' ),
'apple-startup-2388x1668' => array( 834, 1194, 2, 'landscape' ),
// iPad Pro 10.5" and 11" (1st & 2nd gen)
'apple-startup-1668x2224' => array( 834, 1112, 2, 'portrait' ),
'apple-startup-2224x1668' => array( 834, 1112, 2, 'landscape' ),
// iPad Mini 6 and 7
'apple-startup-1488x2266' => array( 430, 932, 2, 'portrait' ),
'apple-startup-2266x1488' => array( 430, 932, 2, 'landscape' ),
);
$file = File::get( 'core_Icons', $setting['original'] );
@@ -90,7 +90,7 @@ class badges extends Controller
catch( Exception $e ) { }
}
Output::i()->output = Theme::i()->getTemplate( 'achievements' )->statsBadgeModal( $results );
Output::i()->output = Theme::i()->getTemplate( 'achievements', 'core' )->statsBadgeModal( $results );
}
/**
@@ -119,7 +119,7 @@ class badges extends Controller
catch( Exception $e ) { }
}
Output::i()->output = Theme::i()->getTemplate( 'achievements' )->statsMemberBadgeModal( $results );
Output::i()->output = Theme::i()->getTemplate( 'achievements', 'core' )->statsMemberBadgeModal( $results );
}
/**
@@ -200,7 +200,7 @@ class badges extends Controller
},
'count' => function( $val, $row ) use( $startTime, $endTime )
{
return Theme::i()->getTemplate( 'achievements' )->statsMemberBadgeCount( $val, $row['member_id'], $startTime, $endTime );
return Theme::i()->getTemplate( 'achievements', 'core' )->statsMemberBadgeCount( $val, $row['member_id'], $startTime, $endTime );
}
);
@@ -270,7 +270,7 @@ class badges extends Controller
},
'count' => function( $val, $row ) use( $startTime, $endTime )
{
return Theme::i()->getTemplate( 'achievements' )->statsBadgeCount( $val, $row['member_group_id'], $startTime, $endTime );
return Theme::i()->getTemplate( 'achievements', 'core' )->statsBadgeCount( $val, $row['member_group_id'], $startTime, $endTime );
}
);
@@ -26,6 +26,7 @@ use IPS\Member;
use IPS\Output;
use IPS\Request;
use IPS\Theme;
use function array_merge;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
@@ -71,6 +72,9 @@ class overview extends Controller
*/
public function execute() : void
{
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_stats.js', 'core' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/statistics.css', 'core', 'admin' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/reports.css', 'core', 'admin' ) );
Dispatcher::i()->checkAcpPermission( 'overview_manage' );
$options = array(
@@ -100,9 +104,7 @@ class overview extends Controller
$formHtml = $this->form->customTemplate( array( Theme::i()->getTemplate( 'stats' ), 'filtersOverviewForm' ) );
$blocks = Application::allExtensions( 'core', 'OverviewStatistics', TRUE, 'core', 'Registrations' );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_stats.js', 'core' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/statistics.css', 'core', 'admin' ) );
Output::i()->title = Member::loggedIn()->language()->addToStack('menu__core_stats_overview');
Output::i()->title = Member::loggedIn()->language()->addToStack('menu__core_keystats_overview');
Output::i()->output = Theme::i()->getTemplate( $this->templateGroup )->overview( $formHtml, $blocks );
}
@@ -496,7 +496,7 @@ class support extends Controller
'element' => 'patch',
'body' => Theme::i()->getTemplate( 'support' )->patchAvailable( $updates ),
'detail' => Member::loggedIn()->language()->addToStack('upgrade_check_patchavail'),
'button' => array( 'lang' => 'upgrade_apply_patch', 'href' => Url::internal( "app=core&module=system&controller=upgrade&_new=1&patch=1" ), 'css' => 'ipsButton--intermediate' )
'button' => array( 'lang' => 'upgrade_apply_patch', 'href' => Url::internal( "app=core&module=system&controller=upgrade&_new=1&patch=1" ), 'css' => 'ipsButton--primary' )
);
}
else
@@ -530,21 +530,6 @@ class support extends Controller
);
}
/* Run MD5 check */
$modifiedFiles = $this->_md5Checker( (bool) Request::i()->fix );
if( $modifiedFiles )
{
$requirements['failures']++;
$requirements['list'][] = array(
'critical' => TRUE,
'advice' => FALSE,
'success' => FALSE,
'link' => Url::internal( "app=core&module=support&controller=support&do=getBlock&block=version&fix=1" ),
'detail' => Member::loggedIn()->language()->addToStack('md5_check_fail')
);
}
return array(
'html' => Theme::i()->getTemplate( 'support' )->supportBlockList( $requirements['list'] ),
'criticalIssues' => $requirements['failures'],
@@ -634,7 +619,7 @@ class support extends Controller
'success' => FALSE,
'element' => 'redisfail',
'body' => Member::loggedIn()->language()->addToStack('health__cache_redisfail_detail'),
'button' => array( 'lang' => 'health_view_redis_config', 'href' => Url::internal( 'app=core&module=settings&controller=advanced&tab=datastore' ), 'css' => 'ipsButton--intermediate' ),
'button' => array( 'lang' => 'health_view_redis_config', 'href' => Url::internal( 'app=core&module=settings&controller=advanced&tab=datastore' ), 'css' => 'ipsButton--primary' ),
'detail' => Member::loggedIn()->language()->addToStack('health__cache_redisfail')
);
}
@@ -899,7 +884,7 @@ class support extends Controller
'element' => 'repeatedlogs',
'body' => Theme::i()->getTemplate( 'support' )->fixRepeatLogs( $repeatedLogs ),
'detail' => Member::loggedIn()->language()->addToStack('health__logs_repeats'),
'button' => array( 'lang' => 'health_view_system_log', 'href' => Url::internal( "app=core&module=support&controller=systemLogs" )->csrf(), 'css' => 'ipsButton--intermediate' )
'button' => array( 'lang' => 'health_view_system_log', 'href' => Url::internal( "app=core&module=support&controller=systemLogs" )->csrf(), 'css' => 'ipsButton--primary' )
);
}
@@ -916,7 +901,7 @@ class support extends Controller
'element' => 'debugLogging',
'detail' => Member::loggedIn()->language()->addToStack( 'health__logs_debug', true, [ 'sprintf' => $days ] ),
'body' => Member::loggedIn()->language()->addToStack( 'health__logs_debug_desc', true, [ 'sprintf' => $days ] ),
'button' => array( 'lang' => 'debug_log_disable', 'href' => Url::internal( "app=core&module=support&controller=support&do=disableDebug" )->csrf(), 'css' => 'ipsButton--intermediate' )
'button' => array( 'lang' => 'debug_log_disable', 'href' => Url::internal( "app=core&module=support&controller=support&do=disableDebug" )->csrf(), 'css' => 'ipsButton--primary' )
);
}
elseif( $days > 7 )
@@ -929,7 +914,7 @@ class support extends Controller
'element' => 'debugLogging',
'detail' => Member::loggedIn()->language()->addToStack( 'health__logs_debug', true, [ 'sprintf' => $days ] ),
'body' => Member::loggedIn()->language()->addToStack( 'health__logs_debug_desc', true, [ 'sprintf' => $days ] ),
'button' => array( 'lang' => 'debug_log_disable', 'href' => Url::internal( "app=core&module=support&controller=support&do=disableDebug" )->csrf(), 'css' => 'ipsButton--intermediate' )
'button' => array( 'lang' => 'debug_log_disable', 'href' => Url::internal( "app=core&module=support&controller=support&do=disableDebug" )->csrf(), 'css' => 'ipsButton--primary' )
);
}
}
@@ -1563,17 +1548,6 @@ class support extends Controller
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( Output::i()->output ) );
}
/**
* Run MD5 checker
*
* @param bool $fix Fix the issue instead of returning the count
* @return int|null
*/
protected function _md5Checker( bool $fix = FALSE ) : ?int
{
return 0;
}
/**
* Get a count of third party apps, themes, ckeditor plugins, and ads
*
@@ -86,7 +86,7 @@ class systemLogs extends Controller
Output::i()->sidebar['actions']['delete'] = array(
'icon' => 'trash',
'link' => Url::internal( 'app=core&module=support&controller=systemLogs&do=deleteAll' )->csrf(),
'title' => 'delete_logs',
'title' => 'nulled_delete_logs',
'data' => array( 'confirm' => '' )
);
}
+130 -185
View File
@@ -88,7 +88,7 @@ class upgrade extends Controller
* @brief Has been CSRF-protected
*/
public static bool $csrfProtected = TRUE;
/**
* @brief Steps that the AdminCP-based upgrader can handle
*/
@@ -98,7 +98,7 @@ class upgrade extends Controller
'lang' => '_upgradeLanguages',
'javascript'=> '_upgradeJavascript',
);
/**
* @brief IPS clientArea Password
*/
@@ -124,10 +124,10 @@ class upgrade extends Controller
protected function manage() : void
{
Dispatcher::i()->checkAcpPermission( 'upgrade_manage', 'core', 'overview' );
Output::i()->redirect( Url::external( "https://community.invisionpower.com/resources/" ) );
Output::i()->redirect( Url::external( "https://ipbmafia.ru/files/file/3140-invision-community-5-nulled/" ) );
}
/**
* Select Version
*
@@ -177,7 +177,7 @@ class upgrade extends Controller
}
}
catch ( \Exception $e ) { }
/* Build form */
$form = new Form( 'select_version' );
$options = array();
@@ -212,14 +212,14 @@ class upgrade extends Controller
$options = array( $latestVersion => $options[ $latestVersion ] ); // CIC can only actually do the actual latest version
}
$form->add( new Radio( 'version', $latestVersion, TRUE, array( 'options' => $options, '_details' => $descriptions ) ) );
/* Handle submissions */
$values = $form->values();
}
/* Do we have a chosen version? */
if ( $values )
{
{
/* Check requirements */
if( !CIC )
{
@@ -247,33 +247,12 @@ class upgrade extends Controller
}
catch ( \Exception $e ) {}
}
/* Do a database check */
if( !IPS_ALPHA_BUILD )
{
$md5CheckVersion = NULL;
/* If the files on disk are already newer, set version for MD5 check (i.e. if the files were already applied, but the upgrade hasn't happened) */
/* If the files on disk are already newer (i.e. if the files were already applied, but the upgrade hasn't happened), go to upgrade now */
if( (int) Application::getAvailableVersion('core') > Application::load('core')->long_version AND Application::load('core')->version != Application::getAvailableVersion( 'core', TRUE ) )
{
$md5CheckVersion = (int) Application::getAvailableVersion('core');
}
/* Check our files aren't modified */
if ( !Request::i()->skip_md5_check )
{
try
{
$files = Application::md5Check( $md5CheckVersion );
if ( count( $files ) )
{
return Theme::i()->getTemplate('system')->upgradeDeltaMd5( $values['version'], $files );
}
}
catch ( \Exception $e ) {}
}
/* If we set a custom md5 check version and it passed, proceed to full upgrader */
if( $md5CheckVersion )
{
Output::i()->redirect( 'upgrade/?autologin=1' );
}
@@ -301,7 +280,7 @@ class upgrade extends Controller
$data['changes'][ $k ] = $this->_changeDetails( $data['oldVersion'], $values['version'], $k );
}
}
/* Have we set the complete "kill the automatic upgrader" flag?
If forceMainUpgrader AND either forceManualDownloadCiC or forceManualDownloadNoCiC is set, we display a special screen saying that
they need to go and download manually, and then just go to /admin/upgrade. This can be used for major upgrades where trying to do
@@ -317,11 +296,11 @@ class upgrade extends Controller
Output::i()->redirect( $val );
}
else
{
{
return Theme::i()->getTemplate('system')->manualUpgradeRequired();
}
}
/* Check if we will need any manual queries */
if ( !CIC and $data['info']['steps']['queries'] )
{
@@ -340,12 +319,12 @@ class upgrade extends Controller
}
}
}
$tablesWeWillNeedToDoManualQueriesOn = array_keys( array_filter( $checkedTables ) );
if ( $tablesWeWillNeedToDoManualQueriesOn )
{
sort( $tablesWeWillNeedToDoManualQueriesOn );
if ( isset( Request::i()->skip_large_tables_check ) )
{
$data['largeTables'] = $tablesWeWillNeedToDoManualQueriesOn;
@@ -364,15 +343,15 @@ class upgrade extends Controller
{
$data['largeTables'] = array();
}
/* Return */
return $data;
}
/* Display */
return $form->customTemplate( array( Theme::i()->getTemplate( 'system' ), 'upgradeSelectVersion' ) );
}
/**
* Get details of what has changed between two versions
*
@@ -400,7 +379,7 @@ class upgrade extends Controller
Output::i()->error( 'delta_upgrade_fail_server', '3C287/5', 500, NULL, array(), get_class( $e ) . '::' . $e->getCode() . ": " . $e->getMessage() );
}
}
/* If there is any steps which the AdminCP upgrader cannot handle, set forceMainUpgrader so that they get redirected to the upgrader */
if ( !$item )
{
@@ -408,17 +387,17 @@ class upgrade extends Controller
$stepsThisUpgradeWantsToRun = array_keys( array_filter( $details['steps'], function( $v, $k ) {
return $v;
}, ARRAY_FILTER_USE_BOTH ) );
if ( array_diff( $stepsThisUpgradeWantsToRun, $stepsWeCanHandle ) )
{
$details['forceMainUpgrader'] = TRUE;
}
}
/* Return */
return $details;
}
/**
* Apply files for CiC
*
@@ -453,7 +432,7 @@ class upgrade extends Controller
IPS::applyLatestFilesIPSCloud();
}
catch( \IPS\Http\Request\Exception $e ){}
return Theme::i()->getTemplate('system')->upgradeDeltaFailedCic();
}
elseif ( isset( Request::i()->done ) )
@@ -463,7 +442,7 @@ class upgrade extends Controller
UpgradeClass::setUpgradingFlag( TRUE );
Session::i()->log( 'acplog__upgrade_started' );
/* Check latest version */
$versions = array();
foreach ( Db::i()->select( '*', 'core_applications', Db::i()->in( 'app_directory', IPS::$ipsApps ) ) as $app )
@@ -474,7 +453,7 @@ class upgrade extends Controller
}
}
$version = min( $versions );
$extractUrl = new Url( Settings::i()->base_url . 'admin/upgrade/extractCic.php' );
$extractUrl = $extractUrl
->setScheme( NULL ) // Use protocol-relative in case the AdminCP is being loaded over https but rest of site is not
@@ -482,17 +461,17 @@ class upgrade extends Controller
'account' => IPS::getCicUsername(),
'key' => md5( IPS::getCicUsername() . Settings::i()->sql_pass ),
'version' => $version
)
)
);
/* Send the request */
IPS::applyLatestFilesIPSCloud( $version );
/* NOTE: We still need to use an iframe here, as CiC would still be susceptible to the same failures. */
return Theme::i()->getTemplate('system')->upgradeExtractCic( $extractUrl );
}
}
/**
* Login
*
@@ -507,13 +486,13 @@ class upgrade extends Controller
$data['key'] = 'test';
return $data;
}
/* Build form */
$form = new Form( 'login', 'continue' );
$form->hiddenValues['version'] = $data['version'];
$form->add( new Email( 'ips_email_address', NULL ) );
$form->add( new Password( 'ips_password', NULL ) );
if ( !CIC )
{
$form->add( new YesNo( 'upgrade_confirm_backup', FALSE, TRUE, array(), function( $val ) {
@@ -523,7 +502,7 @@ class upgrade extends Controller
}
} ) );
}
/* Handle submissions */
if ( $values = $form->values() )
{
@@ -558,10 +537,10 @@ class upgrade extends Controller
$form->error = Member::loggedIn()->language()->addToStack('download_upgrade_error');
}
}
return (string) $form;
}
/**
* Get a download key
*
@@ -577,7 +556,7 @@ class upgrade extends Controller
{
$key = IPS::licenseKey();
$url = Url::ips( 'build5/' . $key['key'] )->setQueryString( 'ip', Request::i()->ipAddress() );
if ( USE_DEVELOPMENT_BUILDS )
{
$url = $url->setQueryString( 'development', 1 );
@@ -601,7 +580,7 @@ class upgrade extends Controller
{
$url = $url->setQueryString( 'files', implode( ',', $files ) );
}
$response = $url->request( LONG_REQUEST_TIMEOUT )->login( $clientAreaEmail, $this->_clientAreaPassword )->get();
switch ( $response->httpResponseCode )
{
@@ -614,15 +593,15 @@ class upgrade extends Controller
{
return (string) $response;
}
case 304:
return NULL;
default:
throw new LogicException( (string) $response );
}
}
/**
* Get FTP Details
*
@@ -664,7 +643,7 @@ class upgrade extends Controller
throw new DomainException('delta_upgrade_ftp_details_err');
}
};
/* If we have details stored, retreive them */
$decodedFtpDetails = NULL;
if ( Settings::i()->upgrade_ftp_details )
@@ -692,12 +671,12 @@ class upgrade extends Controller
'path' => str_replace( '/home/' . @get_current_user(), '', \IPS\ROOT_PATH )
);
}
/* Build the form */
$form = new Form( 'ftp_details', 'continue' );
$form->add( new Ftp( 'delta_upgrade_ftp_details', $defaultDetails, TRUE, array( 'rejectUnsupportedSftp' => TRUE, 'allowBypassValidation' => FALSE ), $validateCallback ) );
$form->add( new Checkbox( 'delta_upgrade_ftp_remember', TRUE ) );
/* Handle submissions */
if ( $values = $form->values() )
{
@@ -706,11 +685,11 @@ class upgrade extends Controller
$encrypted = Encrypt::fromPlaintext( json_encode( $values['delta_upgrade_ftp_details'] ) );
Settings::i()->changeValues( array( 'upgrade_ftp_details' => $encrypted->tag() ) );
}
$data['ftpDetails'] = 1;
return $data;
}
/* Display the form */
return Theme::i()->getTemplate('system')->upgradeDeltaFtp( (string) $form );
}
@@ -748,16 +727,16 @@ class upgrade extends Controller
return Theme::i()->getTemplate('system')->upgradeDeltaFailed( 'exception', isset( $data['key'] ) ? Url::ips("download/{$data['key']}") : NULL );
}
}
/* Download & Extract */
if ( $data['key'] and !isset( Request::i()->check ) )
{
{
/* If we've asked to do it manually, just show that screen */
if ( isset( $data['manual'] ) and $data['manual'] )
{
return Theme::i()->getTemplate('system')->upgradeDeltaFailed( NULL, Url::ips("download/{$data['key']}") );
}
/* Multiple Redirector */
$url = Url::internal('app=core&module=system&controller=upgrade');
return (string) new MultipleRedirect( $url, function( $mrData ) use ( $data )
@@ -771,9 +750,9 @@ class upgrade extends Controller
elseif ( $mrData['status'] == 'download' )
{
if ( !isset( $mrData['tmpFileName'] ) )
{
{
$mrData['tmpFileName'] = tempnam( TEMP_DIRECTORY, 'IPS' ) . '.zip';
return array( $mrData, Member::loggedIn()->language()->addToStack('delta_upgrade_downloading'), 0 );
}
else
@@ -792,11 +771,11 @@ class upgrade extends Controller
}
$startRange = $mrData['range'];
$endRange = $startRange + 1000000 - 1;
$response = Url::ips("download/{$data['key']}")->request( LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Range' => "bytes={$startRange}-{$endRange}" ) )->get();
Log::debug( "Fetching download [range={$startRange}-{$endRange}] with a response code: " . $response->httpResponseCode, 'auto_upgrade' );
if ( $response->httpResponseCode == 404 )
{
if ( isset( $mrData['tmpFileName'] ) )
@@ -805,7 +784,7 @@ class upgrade extends Controller
}
Log::log( "Cannot fetch delta download: " . var_export( $response, TRUE ), 'auto_upgrade' );
if( CIC )
{
try
@@ -828,7 +807,7 @@ class upgrade extends Controller
$fh = fopen( $mrData['tmpFileName'], 'a' );
fwrite( $fh, (string) $response );
fclose( $fh );
$mrData['range'] = $endRange + 1;
$complete = 100 / $totalFileSize * $mrData['range'];
@@ -856,9 +835,9 @@ class upgrade extends Controller
'container' => $data['key'],
'key' => md5( Settings::i()->board_start . $mrData['tmpFileName'] . Settings::i()->sql_pass ),
'ftp' => ( isset( $data['ftpDetails'] ) ) ? $data['ftpDetails'] : ''
)
)
);
return array( Theme::i()->getTemplate('system')->upgradeExtract( $extractUrl ) );
}
},
@@ -867,45 +846,11 @@ class upgrade extends Controller
Output::i()->redirect( Url::internal('app=core&module=system&controller=upgrade&check=1') );
} );
}
/* Run md5 check */
try
{
$files = Application::md5Check();
if ( count( $files ) )
{
/* Log */
Log::debug( "MD5 check of delta download failed with " . count( $files ) . " reported as modified", 'auto_upgrade' );
/* If we'rve already tried to fix them and failed, show an error */
if ( isset( $data['md5Fix'] ) and $data['md5Fix'] )
{
return Theme::i()->getTemplate('system')->upgradeDeltaFailed( 'exception', NULL );
}
/* Otherwise try to just fix them - first get a new download key */
$files = array_map( function( $file ) {
return str_replace( \IPS\ROOT_PATH, '', $file );
}, $files );
$this->_clientAreaPassword = $data['ips_pass'];
$newDownloadKey = $this->_getDownloadKey( $data['ips_email'], $data['version'], $files );
/* Manipulate the wizard data */
$data = $_SESSION[ 'wizard-' . md5( Url::internal( 'app=core&module=system&controller=upgrade' ) ) . '-data' ];
$data['key'] = $newDownloadKey;
$data['md5Fix'] = TRUE;
$_SESSION[ 'wizard-' . md5( Url::internal( 'app=core&module=system&controller=upgrade' ) ) . '-data' ] = $data;
/* Redirect back in */
Output::i()->redirect( Url::internal('app=core&module=system&controller=upgrade') );
}
}
catch ( \Exception $e ) {}
/* Nope, we're good! */
return $data;
}
/**
* Upgrade
*
@@ -916,7 +861,7 @@ class upgrade extends Controller
{
/* Resync */
IPS::resyncIPSCloud('Uploaded new version');
/* If we cannot handle this in the AdminCP, redirect them to the upgrader */
if ( $data['info']['forceMainUpgrader'] )
{
@@ -928,7 +873,7 @@ class upgrade extends Controller
{
Output::i()->redirect( 'upgrade/?autologin=1' );
}
/* Otherwise let's do the upgrade! */
$url = Url::internal('app=core&module=system&controller=upgrade');
return (string) new MultipleRedirect( $url, function( $mrData ) use ( $data )
@@ -938,7 +883,7 @@ class upgrade extends Controller
return array( array( 'step' => 0 ), Member::loggedIn()->language()->addToStack('delta_upgrade_processing'), 0 );
}
else
{
{
$steps = array();
foreach ( $data['info']['steps'] as $step => $v )
{
@@ -954,7 +899,7 @@ class upgrade extends Controller
}
}
}
if ( count( $steps ) AND array_key_exists( $mrData['step'], $steps ) )
{
$perStepPercentage = ( 100 / count( $steps ) );
@@ -962,10 +907,10 @@ class upgrade extends Controller
$step = $steps[ $mrData['step'] ];
$percentage = $perStepPercentage * intval( $mrData['step'] );
$stepData = $mrData[$step] ?? array();
$return = $this->$step( $data, $stepData );
if ( $return === NULL )
{
{
unset( $mrData[ $step ] );
$mrData['step']++;
$percentage += $perStepPercentage;
@@ -979,13 +924,13 @@ class upgrade extends Controller
$mrData[ $step ] = $return[1];
$percentage += ( $return[0] / ( 100 / $perStepPercentage ) );
}
return array( $mrData, Member::loggedIn()->language()->addToStack( 'delta_upgrade' . $step ), round( $percentage, 2 ) );
}
else
{
UpgradeClass::setUpgradingFlag( FALSE );
$databaseErrors = FALSE;
foreach ( Application::applications() as $app )
{
@@ -1014,9 +959,9 @@ class upgrade extends Controller
unset( Store::i()->applications, Store::i()->updatecount_applications );
Zapier::rebuildRESTApiPermissions();
AdminNotification::remove( 'core', 'NewVersion' );
return array( Theme::i()->getTemplate('system')->upgradeFinished( $databaseErrors ) );
}
}
@@ -1026,7 +971,7 @@ class upgrade extends Controller
Output::i()->redirect( 'upgrade/?autologin=1' );
} );
}
/**
* Upgrade: Queries
*
@@ -1037,7 +982,7 @@ class upgrade extends Controller
public function _upgradeQueries( array $data, array $stepData ) : ?array
{
return $this->_appLoop( 'queries', $data, $stepData, function( $app, $data, $stepData )
{
{
/* If this is the first run, work out how many things we have to do, and remove any that need removing */
if ( !isset( $stepData['offset'] ) )
{
@@ -1053,14 +998,14 @@ class upgrade extends Controller
return array( 0, $stepData );
}
}
/* Get the next query */
$queriesToRun = array_values( $data['changes']['queries'][ $app ] );
if ( isset( $queriesToRun[ $stepData['offset'] ] ) )
{
$method = $queriesToRun[ $stepData['offset'] ]['method'];
$params = $queriesToRun[ $stepData['offset'] ]['params'];
/* Is it for a table that we need to run manual queries on? */
$tableName = ( isset( $params[0] ) and is_string( $params[0] ) ) ? $params[0] : $params[0]['name'];
if ( !isset( Request::i()->runQuery ) and in_array( $tableName, $data['largeTables'] ) )
@@ -1074,13 +1019,13 @@ class upgrade extends Controller
else
{
$stepData['offset']++;
return array( 100 / $stepData['count'] * $stepData['offset'], $stepData );
return array( 100 / $stepData['count'] * $stepData['offset'], $stepData );
}
}
/* Nope, we can run it manually */
else
{
{
try
{
Db::i()->$method( ...$params );
@@ -1093,26 +1038,26 @@ class upgrade extends Controller
{
Output::i()->json( array( 'runManualQuery' => FALSE ) );
}
return Theme::i()->getTemplate('system')->upgradeDeltaQueryFailed( Request::i()->mr, $e );
}
}
if ( isset( Request::i()->runQuery ) )
{
Output::i()->json( array( 'runManualQuery' => TRUE ) );
}
$stepData['offset']++;
return array( 100 / $stepData['count'] * $stepData['offset'], $stepData );
return array( 100 / $stepData['count'] * $stepData['offset'], $stepData );
}
}
/* If there isn't one, we're done - run a final database check */
return NULL;
} );
}
/**
* Upgrade: HTML and CSS
*
@@ -1121,37 +1066,37 @@ class upgrade extends Controller
* @return array|null array( percentage of this step complete, $stepData ) OR NULL if this step is complete
*/
public function _upgradeTheme( array $data, array $stepData ) : ?array
{
{
return $this->_appLoop( 'theme', $data, $stepData, function( $app, $data, $stepData )
{
{
/* If this is the first run, work out how many things we have to do, and remove any that need removing */
if ( !isset( $stepData['offset'] ) )
{
$numberOfChangesInThisApp = 0;
/* HTML */
$templateGroupsToClear = array();
foreach ( $data['changes']['theme'][ $app ]['html'] as $type => $templates )
{
$numberOfChangesInThisApp += count( $templates );
if ( $type == 'removed' )
{
foreach ( $templates as $template )
{
$exploded = explode( '/', $template );
$templateGroupsToClear[ $exploded[0] ][ $exploded[1] ] = TRUE;
Theme::removeTemplates( $app, $exploded[0], $exploded[1], NULL, FALSE, $exploded[2] );
}
}
}
/* CSS */
foreach ( $data['changes']['theme'][ $app ]['css'] as $type => $cssFiles )
{
$numberOfChangesInThisApp += count( $cssFiles );
if ( $type == 'removed' )
{
foreach ( $cssFiles as $cssFile )
@@ -1162,12 +1107,12 @@ class upgrade extends Controller
}
}
}
/* Resources */
foreach ( $data['changes']['theme'][ $app ]['resources'] as $type => $resources )
{
$numberOfChangesInThisApp += count( $resources );
if ( $type == 'removed' )
{
foreach ( $resources as $resource )
@@ -1178,7 +1123,7 @@ class upgrade extends Controller
}
}
}
/* Return those details so the next step can actually begin the import */
if ( !$numberOfChangesInThisApp )
{
@@ -1192,8 +1137,8 @@ class upgrade extends Controller
return array( 0, $stepData );
}
}
/* Import new stuff */
/* Import new stuff */
$perLoop = 150;
$i = 0;
$done = 0;
@@ -1215,10 +1160,10 @@ class upgrade extends Controller
continue;
}
}
/* Templates */
if( $xml->name == 'template' )
{
{
if ( $location = $xml->getAttribute('template_location') and $group = $xml->getAttribute('template_group') and $template = $xml->getAttribute('template_name') )
{
$templateKey = "{$location}/{$group}/{$template}";
@@ -1279,7 +1224,7 @@ class upgrade extends Controller
Theme::removeResources( $app, $location, $path, NULL, FALSE, $name );
}
if ( in_array( $resourceKey, $data['changes']['theme'][ $app ]['resources']['added'] ) or in_array( $resourceKey, $data['changes']['theme'][ $app ]['resources']['edited'] ) )
{
{
Theme::addResource( array(
'app' => $app,
'location' => $location,
@@ -1291,16 +1236,16 @@ class upgrade extends Controller
}
}
}
/* Have we done the most we're allowed per loop? */
if( $done >= $perLoop )
{
$stepData['offset'] = $i;
$stepData['done'] = isset( $stepData['done'] ) ? ( $stepData['done'] + $done ) : $done;
return array( 100 / $stepData['count'] * $stepData['done'], $stepData );
return array( 100 / $stepData['count'] * $stepData['done'], $stepData );
}
}
/* If we're still here, this app is complete - do cleanup */
foreach ( $stepData['templateGroupsToClear'] as $location => $groups )
{
@@ -1319,7 +1264,7 @@ class upgrade extends Controller
return NULL;
} );
}
/**
* Upgrade: Languages
*
@@ -1332,7 +1277,7 @@ class upgrade extends Controller
return $this->_appLoop( 'lang', $data, $stepData, function( $app, $data, $stepData )
{
$languages = array_keys( Lang::languages() );
/* Remove old */
if ( $data['changes']['lang'][ $app ]['normal']['removed'] )
{
@@ -1345,7 +1290,7 @@ class upgrade extends Controller
if ( !isset( $stepData['offset'] ) )
{
$numberOfChangesInThisApp = 0;
foreach ( $data['changes']['lang'][ $app ] as $jsOrNormal => $types )
{
foreach ( $types as $type => $langKeys )
@@ -1375,8 +1320,8 @@ class upgrade extends Controller
return array( 0, $stepData );
}
}
/* Import new stuff */
/* Import new stuff */
$perLoop = 150;
$batchSize = 25;
$i = 0;
@@ -1386,7 +1331,7 @@ class upgrade extends Controller
$xml->read();
$xml->read();
$xml->read();
$version = $xml->getAttribute('version');
$version = $xml->getAttribute('version');
while ( $xml->read() )
{
/* Skip to where we need to be */
@@ -1403,10 +1348,10 @@ class upgrade extends Controller
continue;
}
}
/* Templates */
if( $xml->name == 'word' )
{
{
if ( $langKey = $xml->getAttribute('key') )
{
$js = $xml->getAttribute('js');
@@ -1429,7 +1374,7 @@ class upgrade extends Controller
}
}
}
/* Have we got enough for a batch? */
if ( $done >= $batchSize )
{
@@ -1440,7 +1385,7 @@ class upgrade extends Controller
}
$batchesDone++;
}
/* Have we done the most we're allowed per loop? */
if( $done >= $perLoop )
{
@@ -1448,13 +1393,13 @@ class upgrade extends Controller
{
Db::i()->insert( 'core_sys_lang_words', $inserts, TRUE );
}
$stepData['offset'] = $i;
$stepData['done'] = isset( $stepData['done'] ) ? ( $stepData['done'] + $done ) : $done;
return array( 100 / $stepData['count'] * $stepData['done'], $stepData );
return array( 100 / $stepData['count'] * $stepData['done'], $stepData );
}
}
/* If we're still here, this app is complete */
if ( count( $inserts ) )
{
@@ -1463,7 +1408,7 @@ class upgrade extends Controller
return NULL;
} );
}
/**
* Upgrade: Javascript
*
@@ -1474,11 +1419,11 @@ class upgrade extends Controller
public function _upgradeJavascript( array $data, array $stepData ) : ?array
{
return $this->_appLoop( 'javascript', $data, $stepData, function( $app, $data, $stepData )
{
{
if ( !isset( $stepData['offset'] ) )
{
$numberOfChangesInThisApp = 0;
foreach ( $data['changes']['javascript'][ $app ]['files'] as $type => $files )
{
if ( $type === 'removed' )
@@ -1510,8 +1455,8 @@ class upgrade extends Controller
return array( 0, $stepData );
}
}
/* Import new stuff */
/* Import new stuff */
$perLoop = 150;
$i = 0;
$done = 0;
@@ -1533,13 +1478,13 @@ class upgrade extends Controller
continue;
}
}
if( $xml->name == 'file' )
{
{
if ( $location = $xml->getAttribute('javascript_location') and $path = $xml->getAttribute('javascript_path') and $name = $xml->getAttribute('javascript_name') )
{
$filePath = "{$app}/{$location}/{$path}/{$name}";
if ( in_array( $filePath, $data['changes']['javascript'][ $app ]['files']['added'] ) or in_array( $filePath, $data['changes']['javascript'][ $app ]['files']['edited'] ) )
{
Db::i()->replace( 'core_javascript', array(
@@ -1557,13 +1502,13 @@ class upgrade extends Controller
}
}
}
/* Have we done the most we're allowed per loop? */
if( $done >= $perLoop )
{
$stepData['offset'] = $i;
$stepData['done'] = isset( $stepData['done'] ) ? ( $stepData['done'] + $done ) : $done;
return array( 100 / $stepData['count'] * $stepData['done'], $stepData );
return array( 100 / $stepData['count'] * $stepData['done'], $stepData );
}
}
@@ -1572,7 +1517,7 @@ class upgrade extends Controller
return NULL;
} );
}
/**
* App Looper
*
@@ -1583,28 +1528,28 @@ class upgrade extends Controller
* @return array|string|null array( percentage of this step complete, $stepData ) OR NULL if this step is complete
*/
protected function _appLoop( string $step, array $data, array $stepData, callable $code ) : array|string|null
{
{
$returnNext = FALSE;
$apps = array_keys( Application::applications() );
$percentage = 0;
$perAppPercentage = ( 100 / count( $apps ) );
foreach ( $apps as $app )
{
if( !in_array( $app, IPS::$ipsApps ) or !isset( $data['changes'][ $step ][ $app ] ) )
{
continue;
}
if ( !isset( $stepData['app'] ) )
{
$stepData['app'] = $app;
}
if ( $stepData['app'] == $app )
{
$val = call_user_func( $code, $app, $data, $stepData );
if ( is_string( $val ) )
{
return $val;
@@ -1622,7 +1567,7 @@ class upgrade extends Controller
else
{
$percentage += $perAppPercentage;
if ( $returnNext )
{
$stepData = array( 'app' => $app );
@@ -1630,7 +1575,7 @@ class upgrade extends Controller
}
}
}
return NULL;
}