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;
}
@@ -13,6 +13,7 @@ namespace IPS\core\modules\front\clubs;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DateInterval;
use Exception;
use IPS\Application;
use IPS\Content\Search\Query;
use IPS\core\Stream;
@@ -21,6 +22,7 @@ use IPS\Db;
use IPS\Dispatcher\Controller;
use IPS\GeoLocation;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Address;
use IPS\Helpers\Form\CheckboxSet;
use IPS\Helpers\Form\Radio;
use IPS\Helpers\Form\Text;
@@ -118,14 +120,19 @@ class directory extends Controller
/* All Clubs: Sort */
$sortOption = Settings::i()->clubs_default_sort;
if ( isset( Request::i()->sort ) and in_array( Request::i()->sort, array( 'last_activity', 'members', 'content', 'created', 'name' ) ) )
$allSortOptions = array( 'last_activity', 'members', 'content', 'created', 'name' );
if( Settings::i()->clubs_locations and GeoLocation::enabled() )
{
$allSortOptions[] = 'distance';
}
if ( isset( Request::i()->sort ) and in_array( Request::i()->sort, $allSortOptions ) )
{
$sortOption = Request::i()->sort;
$baseUrl = $baseUrl->setQueryString( 'sort', $sortOption );
}
/* All Clubs: Filters */
/* All Clubs: Filters */
$filters = array();
$mineOnly = FALSE;
$extraWhere = [];
@@ -139,6 +146,36 @@ class directory extends Controller
$mineOnly = TRUE;
$baseUrl = $baseUrl->setQueryString( 'filter', 'mine' );
}
/* Are we sorting by location? */
$location = null;
if( ( $sortOption == 'distance' or ( isset( Request::i()->location ) and Request::i()->location ) ) and Settings::i()->clubs_locations )
{
if( isset( Request::i()->location ) )
{
$baseUrl = $baseUrl->setQueryString( 'location', Request::i()->location );
$geo = explode( ",", Request::i()->location );
$location = array( 'lat' => $geo[0], 'lon' => $geo[1] );
/* If we are filtering by location, we can leave out clubs with no location */
$extraWhere[] = [ 'location_lat is not null AND location_long is not null' ];
}
elseif( GeoLocation::enabled() )
{
/* Do an IP lookup */
try
{
$geo = GeoLocation::getRequesterLocation();
$location = array( 'lat' => $geo->lat, 'lon' => $geo->long );
}
catch ( Exception $e )
{
$location = array( 'lat' => Settings::i()->map_center_lat, 'lon' => Settings::i()->map_center_lon );
}
}
}
if ( isset( Request::i()->type ) and in_array( Request::i()->type, array( 'free', 'paid' ) ) )
{
$baseUrl = $baseUrl->setQueryString( 'type', Request::i()->type );
@@ -207,8 +244,8 @@ class directory extends Controller
$page = 1;
}
$clubsCount = Club::clubs( Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), $sortOption, $mineOnly, $filters, $extraWhere, TRUE );
$allClubs = Club::clubs( Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), $sortOption, $mineOnly, $filters, $extraWhere );
$clubsCount = Club::clubs( Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), $sortOption, $mineOnly, $filters, $extraWhere, TRUE, $location );
$allClubs = iterator_to_array( Club::clubs( Member::loggedIn(), array( ( $page - 1 ) * $perPage, $perPage ), $sortOption, $mineOnly, $filters, $extraWhere, false, $location ) );
$pagination = Theme::i()->getTemplate( 'global', 'core', 'global' )->pagination( $baseUrl, ceil( $clubsCount / $perPage ), $page, $perPage );
/* Get my clubs and invites */
@@ -262,8 +299,6 @@ class directory extends Controller
Output::i()->sidebar['contextual'] = Theme::i()->getTemplate('clubs')->myClubsSidebar( Club::clubs( Member::loggedIn(), 10, 'last_activity', TRUE ), $myClubsActivity, $myClubsInvites );
/* Prime the cache */
$allClubs = iterator_to_array( $allClubs );
if( count( $allClubs ) )
{
foreach( $allClubs as $clubId => $clubData )
@@ -282,7 +317,7 @@ class directory extends Controller
}
Output::i()->title = Member::loggedIn()->language()->addToStack('module__core_clubs');
Output::i()->output = Theme::i()->getTemplate('clubs')->directory( $featuredClubs, $allClubs, $pagination, $baseUrl, $sortOption, $myClubsActivity, $mapMarkers, $view, $mineOnly );
Output::i()->output = Theme::i()->getTemplate('clubs')->directory( $featuredClubs, $allClubs, $pagination, $baseUrl, $sortOption, $myClubsActivity, $mapMarkers, $view, $mineOnly, $allSortOptions );
}
/**
@@ -292,7 +327,7 @@ class directory extends Controller
*/
protected function filters() : void
{
$fields = CustomField::roots();
$fields = CustomField::fields();
$form = new Form('filter', 'filter');
@@ -305,6 +340,14 @@ class directory extends Controller
'mine' => 'my_clubs'
) ) ) );
}
if( Settings::i()->clubs_locations )
{
$form->add( new Address( 'club_filter_location', null, false, [
'requireFullAddress' => false,
'minimize' => true
] ) );
}
if ( Application::appIsEnabled( 'nexus' ) and Settings::i()->clubs_paid_on )
{
@@ -353,6 +396,12 @@ class directory extends Controller
$url = $url->setQueryString( 'name', $values['club_filter_name'] );
}
if( !empty( $values['club_filter_location'] ) and $values['club_filter_location'] instanceof GeoLocation)
{
$values['club_filter_location']->getLatLong();
$url = $url->setQueryString( 'location', $values['club_filter_location']->lat . ',' . $values['club_filter_location']->long );
}
if ( Member::loggedIn()->member_id and $values['club_filter_type'] === 'mine' )
{
$url = $url->setQueryString( 'filter', 'mine' );
@@ -15,6 +15,7 @@ namespace IPS\core\modules\front\contact;
use IPS\Application;
use IPS\Db;
use IPS\Dispatcher\Controller;
use IPS\File;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Captcha;
use IPS\Helpers\Form\Editor;
@@ -103,6 +104,9 @@ class contact extends Controller
if ( $values = $form->values() )
{
/* Clear the autosave by claiming attachments */
File::claimAttachments( 'contact-' . Member::loggedIn()->member_id );
if ( ! Member::loggedIn()->member_id AND Settings::i()->contact_email_verify )
{
$key = Login::generateRandomString( 32 );
@@ -158,11 +162,14 @@ class contact extends Controller
/* Show interstitial page to prevent email clients from auto-verifying. */
Output::i()->sidebar['enabled'] = FALSE;
Output::i()->bodyClasses[] = 'ipsLayout_minimal';
$form = new Form( 'form', NULL );
/* Strip out the key and email address values from the URL which will be urlcoded (%40) and will be encoded again when used in the form action (%2540) */
$form = new Form( 'form', NULL, Request::i()->url()->stripQueryString( [ 'key', 'email' ] ) );
$form->class = 'ipsForm--vertical ipsForm--contact-confirm';
$form->actionButtons[] = Theme::i()->getTemplate( 'forms', 'core', 'global' )->button( 'contact_click_to_verify', 'submit', null, 'ipsButton ipsButton_primary ipsButton_fullWidth', array( 'tabindex' => '2', 'accesskey' => 's' ) );
$form->hiddenValues['email'] = Request::i()->email;
$form->actionButtons[] = Theme::i()->getTemplate( 'forms', 'core', 'global' )->button( 'contact_click_to_verify', 'submit', null, 'ipsButton ipsButton--primary ipsButton--wide', array( 'tabindex' => '2', 'accesskey' => 's' ) );
/* The email address will still be encoded (%40) */
$form->hiddenValues['email'] = urldecode( Request::i()->email );
$form->hiddenValues['key'] = Request::i()->key;
if ( $values = $form->values() )
@@ -96,7 +96,7 @@ class popular extends Controller
$content = $this->$activeTab();
/* Data Layer Context Property */
if ( Settings::i()->core_datalayer_enabled AND ! Request::i()->isAjax() )
if ( DataLayer::enabled() AND ! Request::i()->isAjax() )
{
DataLayer::i()->addContextProperty( 'community_area', 'leaderboard' );
}
@@ -402,15 +402,17 @@ class streams extends Controller
unset( $tmp );
}
if ( isset( Request::i()->latest ) )
if ( isset( Request::i()->latest ) and is_numeric( Request::i()->latest ) )
{
$latest = (int) Request::i()->latest;
if ( $stream->id and !$stream->include_comments )
{
$query->filterByLastUpdatedDate( DateTime::ts( Request::i()->latest ) );
$query->filterByLastUpdatedDate( DateTime::ts( $latest ) );
}
else
{
$query->filterByCreateDate( DateTime::ts( Request::i()->latest ) );
$query->filterByCreateDate( DateTime::ts( $latest) );
}
$query->setLimit(350);
@@ -687,7 +689,14 @@ class streams extends Controller
{
$url = $stream ? $stream->baseUrl->setQueryString( 'className', $class ) : Request::i()->url()->setQueryString( 'className', $class );
$containerClass = $class::$containerNodeClass;
$field = new Node( 'stream_containers_' . $class::$title, $currentContainers[$class] ?? array(), NULL, array( 'url' => $url, 'class' => $class::$containerNodeClass, 'multiple' => TRUE, 'permissionCheck' => $containerClass::searchableNodesPermission(), 'clubs' => ( Settings::i()->clubs AND IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) ) ), NULL, NULL, NULL, 'stream_containers_' . $class::$title );
$field = new Node( 'stream_containers_' . $class::$title, $currentContainers[$class] ?? array(), NULL, array(
'url' => $url,
'class' => $class::$containerNodeClass,
'multiple' => TRUE,
'permissionCheck' => $containerClass::searchableNodesPermission(),
'clubs' => ( Settings::i()->clubs AND IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) ),
'nodeGroups' => TRUE,
), NULL, NULL, NULL, 'stream_containers_' . $class::$title );
$field->label = Member::loggedIn()->language()->addToStack( 'stream_narrow_by_container_label', FALSE, array( 'sprintf' => array( Member::loggedIn()->language()->addToStack( $containerClass::$nodeTitle ) ) ) );
return Output::i()->json( array( 'node' => Theme::i()->getTemplate('streams')->filterFormContentTypeContent( $field, $class, Request::i()->key ) ) );
@@ -33,7 +33,6 @@ use IPS\Theme;
use OutOfRangeException;
use function count;
use function defined;
use function sprintf;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
@@ -71,6 +70,9 @@ class tag extends Controller
}
parent::execute();
/* Breadcrumb */
Output::i()->breadcrumb = array( array( $this->tag->url(), $this->tag->text ) );
}
/**
@@ -119,7 +121,7 @@ class tag extends Controller
if( $image = $this->tag->coverPhotoFile() )
{
Output::i()->metaTags['og:image'] = (string) $image;
Output::i()->metaTags['og:image'] = (string) $image->url;
}
/* Do not set description tags for page 2+ as you end up with duplicate tags */
@@ -41,9 +41,11 @@ use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\Upload;
use IPS\Helpers\Form\YesNo;
use IPS\Helpers\Table\Content;
use IPS\Helpers\Table\Db as TableDb;
use IPS\Http\Url;
use IPS\Image;
use IPS\IPS;
use IPS\Lang;
use IPS\Log;
use IPS\Member;
use IPS\Member\Club;
@@ -51,6 +53,7 @@ use IPS\Member\Group;
use IPS\Member\ProfileStep;
use IPS\Output;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Platform\Bridge;
use IPS\Request;
use IPS\Session;
use IPS\Settings;
@@ -64,6 +67,9 @@ use function defined;
use function in_array;
use function intval;
use function is_array;
use function is_numeric;
use function is_object;
use function is_string;
use const IPS\PHOTO_THUMBNAIL_SIZE;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
@@ -362,7 +368,7 @@ class profile extends Controller
);
/* Data Layer Stuff */
if ( Settings::i()->core_datalayer_enabled )
if ( DataLayer::enabled() )
{
try
{
@@ -620,7 +626,7 @@ class profile extends Controller
$baseUrl = $baseUrl->setQueryString( 'type', Request::i()->type );
}
if ( isset( Request::i()->page ) and Request::i()->page > 1 )
if ( isset( Request::i()->page ) and is_numeric( Request::i()->page ) and Request::i()->page > 1 )
{
$baseUrl = $baseUrl->setPage( 'page', Request::i()->page );
}
@@ -909,10 +915,11 @@ class profile extends Controller
/* Handle the submission */
if ( $values = $form->values() )
{
$this->_saveMember( $form, $values );
$dataLayerUpdates = [];
$this->_saveMember( $form, $values, $dataLayerUpdates );
/* Data Layer Stuff */
if ( Settings::i()->core_datalayer_enabled )
if ( DataLayer::enabled() )
{
try
{
@@ -927,6 +934,10 @@ class profile extends Controller
'profile_group_id' => $this->member->member_group_id ?: null,
'profile_id' => intval( $this->member->member_id ) ?: null,
'profile_name' => $this->member->name ?: null,
'updated_custom_fields' => $dataLayerUpdates['profile_fields'] ?? false,
'updated_profile_name' => false,
'updated_profile_photo' => false,
'updated_profile_coverphoto' => false,
);
DataLayer::i()->addEvent( 'social_update', $properties );
}
@@ -1067,9 +1078,10 @@ class profile extends Controller
*
* @param Form $form
* @param array $values
* @param array &$dataLayerUpdates=[] Optional: a reference to an array which will be populated with datalayer updates
* @return void
*/
protected function _saveMember( Form $form, array $values ) : void
protected function _saveMember( Form $form, array $values, array &$dataLayerUpdates=[] ) : void
{
if( isset( $values['bday'] ) )
{
@@ -1116,6 +1128,7 @@ class profile extends Controller
{
foreach ( $fields as $id => $field )
{
$originalValue = $profileFields[ 'field_' . $id ] ?? null;
if ( $field instanceof Upload )
{
$profileFields[ "field_{$id}" ] = (string) $values[ $field->name ];
@@ -1129,6 +1142,21 @@ class profile extends Controller
{
Field::load( $id )->claimAttachments( $this->member->member_id );
}
if ( $originalValue !== $profileFields[ "field_{$id}" ] )
{
$dataLayerUpdates['profile_fields'] = $dataLayerUpdates['profile_fields'] ?? [];
/* Note the char length on old and new is limited to prevent the datalayer JS object from getting too large. That should only really feasibly happen for editor fields anyway. */
$update = [
'id' => $id,
'name' => Lang::load( Lang::defaultLanguage() )->addToStack( Field::$titleLangPrefix . $id ),
'old' => DataLayer::i()->includeSSOForMember( $this->member ) ? ( is_string( $originalValue ) ? mb_substr( $originalValue, 0, 2048 ) : $originalValue ) : null,
'new' => DataLayer::i()->includeSSOForMember( $this->member ) ? ( is_string( $profileFields[ "field_{$id}" ] ) ? mb_substr( $profileFields[ "field_{$id}" ], 0, 2048 ) : $profileFields[ "field_{$id}" ] ) : null,
];
Lang::load( Lang::defaultLanguage() )->parseOutputForDisplay( $update['name'] );
$dataLayerUpdates['profile_fields'][] = $update;
}
}
$this->member->changedCustomFields = $profileFields;
@@ -1193,6 +1221,17 @@ class profile extends Controller
{
$options['custom'] = 'member_photo_upload';
}
/* Do we have a custom gallery? */
if( Bridge::i()->featureIsEnabled( 'profile_gallery' ) and Settings::i()->cloud_profile_gallery )
{
$options['profile_gallery'] = 'member_profile_gallery';
$toggles['profile_gallery'] = array( 'profile_gallery_id' );
if( $this->member->pp_photo_type == 'cloud_ProfilePhotos' )
{
$defaultType = 'profile_gallery';
}
}
/* Can we use gallery images? */
if ( Application::appIsEnabled('gallery') AND $this->member->pp_photo_type == 'gallery_Images' )
@@ -1262,9 +1301,29 @@ class profile extends Controller
}, NULL, NULL, 'member_photo_upload' ) );
}
/* And generate the profile gallery if we have it */
if( Bridge::i()->featureIsEnabled( 'profile_gallery' ) and Settings::i()->cloud_profile_gallery )
{
$form->add( new Custom( 'profile_gallery_id', null, null, [
'getHtml' => function( $element )
{
return \IPS\cloud\Profiles\Gallery::gallerySelectHtml( $this->member );
},
'validate' => function( $element )
{
if( Request::i()->pp_photo_type == 'profile_gallery' and empty( $element->value ))
{
throw new DomainException( 'form_required' );
}
}
], id: 'profile_gallery_id' ) );
}
/* Handle submissions */
if ( $values = $form->values() )
{
$oldPhotoURL = $this->member->get_photo( false );
/* Disable syncing */
$profileSync = $this->member->profilesync;
if ( isset( $profileSync['photo'] ) )
@@ -1292,6 +1351,10 @@ class profile extends Controller
}
break;
case 'profile_gallery':
\IPS\cloud\Profiles\Gallery::setGalleryPhoto( $this->member, Request::i()->profile_gallery_id );
break;
case 'none':
$this->member->pp_photo_type = NULL;
$this->member->pp_main_photo = NULL;
@@ -1303,6 +1366,30 @@ class profile extends Controller
$this->member->members_bitoptions['profile_completed'] = FALSE;
$this->member->save();
/* Data Layer Stuff */
if ( DataLayer::enabled() and ( $newPhotoURL = $this->member->get_photo( false ) ) and $newPhotoURL !== $oldPhotoURL )
{
try
{
$groupName = Group::load( $this->member->member_group_id )->formattedName;
}
catch ( UnderflowException $e )
{
$groupName = null;
}
$properties = array(
'profile_group' => $groupName,
'profile_group_id' => $this->member->member_group_id ?: null,
'profile_id' => intval( $this->member->member_id ) ?: null,
'profile_name' => $this->member->name ?: null,
'updated_custom_fields' => false,
'updated_profile_name' => false,
'updated_profile_photo' => DataLayer::i()->includeSSOForMember( $this->member ) ? [ 'old' => $oldPhotoURL, 'new' => $newPhotoURL ] : true,
'updated_profile_coverphoto' => false,
);
DataLayer::i()->addEvent( 'social_update', $properties );
}
if ( $this->member->pp_photo_type )
{
@@ -1462,7 +1549,16 @@ class profile extends Controller
}
$limit = array( ( $page - 1 ) * 50, 50 );
$followers = iterator_to_array( $this->member->followers( ( Member::loggedIn()->isAdmin() OR Member::loggedIn()->member_id === $this->member->member_id ) ? \IPS\Content::FOLLOW_PUBLIC + \IPS\Content::FOLLOW_ANONYMOUS : \IPS\Content::FOLLOW_PUBLIC, array( 'immediate', 'daily', 'weekly', 'none' ), NULL, $limit, 'name' ) );
$followerIterator = $this->member->followers( ( Member::loggedIn()->isAdmin() or Member::loggedIn()->member_id === $this->member->member_id ) ? \IPS\Content::FOLLOW_PUBLIC + \IPS\Content::FOLLOW_ANONYMOUS : \IPS\Content::FOLLOW_PUBLIC, ['immediate', 'daily', 'weekly', 'none'], NULL, $limit, 'name' );
if( is_iterable( $followerIterator ) === FALSE )
{
$followers = [];
}
else
{
$followers = iterator_to_array( $followerIterator );
}
$followersCount = $this->member->followersCount( ( Member::loggedIn()->isAdmin() OR Member::loggedIn()->member_id === $this->member->member_id ) ? \IPS\Content::FOLLOW_PUBLIC + \IPS\Content::FOLLOW_ANONYMOUS : \IPS\Content::FOLLOW_PUBLIC, array( 'immediate', 'daily', 'weekly', 'none' ) );
if ( Request::i()->isAjax() )
@@ -1494,6 +1590,9 @@ class profile extends Controller
*/
protected function _coverPhotoSet( CoverPhoto $photo, ?string $type=NULL ) : void
{
$originalPhotoUrl = $this->member->coverPhoto()?->file?->url ?: null;
$originalPhotoUrl = ( is_object( $originalPhotoUrl ) and $originalPhotoUrl instanceof Url ) ? (string) $originalPhotoUrl : $originalPhotoUrl;
/* Disable syncing */
$profileSync = $this->member->profilesync;
if ( isset( $profileSync['cover'] ) )
@@ -1513,6 +1612,32 @@ class profile extends Controller
{
$this->member->logHistory( 'core', 'coverphoto', array( 'action' => $type ) );
}
/* Data Layer Stuff */
$newPhotoUrl = $photo->file?->url ?: null;
$newPhotoUrl = ( is_object( $newPhotoUrl ) and $newPhotoUrl instanceof Url ) ? (string) $newPhotoUrl : $newPhotoUrl;
if ( DataLayer::enabled() and ( $type === 'reposition' or $newPhotoUrl !== $originalPhotoUrl ) )
{
try
{
$groupName = Group::load( $this->member->member_group_id )->formattedName;
}
catch ( UnderflowException $e )
{
$groupName = null;
}
$properties = array(
'profile_group' => $groupName,
'profile_group_id' => $this->member->member_group_id ?: null,
'profile_id' => intval( $this->member->member_id ) ?: null,
'profile_name' => $this->member->name ?: null,
'updated_custom_fields' => false,
'updated_profile_name' => false,
'updated_profile_photo' => false,
'updated_profile_coverphoto' => DataLayer::i()->includeSSOForMember( $this->member ) ? [ 'repositioned' => $type === 'reposition', 'old' => $originalPhotoUrl, 'new' => $newPhotoUrl ] : true,
);
DataLayer::i()->addEvent( 'social_update', $properties );
}
}
/**
@@ -19,6 +19,7 @@ use IPS\Application;
use IPS\Content\Controller;
use IPS\Content\Search\Mysql\Query;
use IPS\core\Alerts\Alert;
use IPS\core\DataLayer;
use IPS\core\Messenger\Conversation;
use IPS\DateTime;
use IPS\Db;
@@ -31,6 +32,7 @@ use IPS\Notification;
use IPS\Output;
use IPS\Request;
use IPS\Session;
use IPS\Settings;
use IPS\Theme;
use LogicException;
use OutOfRangeException;
@@ -506,6 +508,13 @@ class messenger extends Controller
}
else
{
if ( DataLayer::enabled() and $conversation )
{
foreach ( $conversation->getDataLayerProperties() as $key => $value )
{
DataLayer::i()->addContextProperty( $key, $value );
}
}
Output::i()->output = Theme::i()->getTemplate('messaging')->template( $folder, $folders, $folderCounts, $conversations, $pagination, $conversation, $baseUrl, ( $conversation ? 'messenger_convo' : 'messenger' ), ( Request::i()->sortBy ? htmlspecialchars( Request::i()->sortBy, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE ): 'mt_last_post_time' ), ( Request::i()->filter ? htmlspecialchars( Request::i()->filter, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE ) : '' ), $alert );
}
@@ -53,7 +53,7 @@ class modcp extends Controller
*/
public function __call( string $method, array $args ) : void
{
if ( Settings::i()->core_datalayer_enabled )
if ( DataLayer::enabled() )
{
DataLayer::i()->addContextProperty( 'community_area', Lang::load( Lang::defaultLanguage() )->addToStack('modcp') );
}
@@ -13,6 +13,7 @@ namespace IPS\core\modules\front\search;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DateInterval;
use DateMalformedStringException;
use DateTimeZone;
use IPS\Application;
use IPS\Application\Module;
@@ -92,11 +93,15 @@ class search extends Controller
protected function _checkCached() : void
{
/* Check whether a 304 Not modified response is appropriate */
if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) AND ( new DateTime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )->getTimestamp() > ( time() - $this->_cacheTimeout ) )
try
{
header('HTTP/1.1 304 Not Modified' );
exit;
if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) AND ( new DateTime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )->getTimestamp() > ( time() - $this->_cacheTimeout ) )
{
header('HTTP/1.1 304 Not Modified' );
exit;
}
}
catch( DateMalformedStringException ){}
}
/**
@@ -160,7 +165,7 @@ class search extends Controller
}
/* Data Layer Event */
if ( Settings::i()->core_datalayer_enabled AND !Request::i()->isAjax() AND Request::i()->q )
if ( DataLayer::enabled() AND !Request::i()->isAjax() AND Request::i()->q )
{
DataLayer::i()->addEvent( 'search', array( 'query' => Request::i()->q, ) );
}
@@ -1010,8 +1015,10 @@ class search extends Controller
'source' => Content\Tag::allTags(),
'minimized' => FALSE,
'forceLower' => (bool)Settings::i()->tags_force_lower,
'freeChoice' => false
'freeChoice' => false,
'separator' => ','
) ), NULL, NULL, NULL, 'tags' ), NULL, 'search_tab_content' );
Member::loggedIn()->language()->words['tags_desc'] = Member::loggedIn()->language()->get( 'tags_search_desc' );
$form->add( new Radio( 'eitherTermsOrTags', Request::i()->eitherTermsOrTags, FALSE, array( 'options' => array( 'or' => 'termsortags_or_desc', 'and' => 'termsortags_and_desc' ) ), NULL, NULL, NULL, 'eitherTermsOrTags' ), NULL, 'search_tab_content' );
/* Author */
@@ -12,7 +12,7 @@ namespace IPS\core\modules\front\sitemap;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Content\Controller;
use IPS\Dispatcher\Controller;
use IPS\DateTime;
use IPS\Db;
use IPS\Http\Url;
@@ -22,6 +22,7 @@ use IPS\Settings;
use IPS\Xml\SimpleXML;
use UnderflowException;
use function defined;
use const IPS\CIC;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -58,9 +59,16 @@ class sitemap extends Controller
}
else
{
$sitemapUrl = Url::external( Settings::i()->sitemap_url ? rtrim( Settings::i()->sitemap_url, '/' ) : rtrim( Settings::i()->base_url, '/' ) . '/sitemap.php' );
$content = SimpleXML::create( 'sitemapindex', 'https://www.sitemaps.org/schemas/sitemap/0.9' );
if( CIC )
{
$sitemapUrl = Url::external( rtrim( Settings::i()->base_url, '/' ).'/sitemap.php' );
}
else
{
$sitemapUrl = Url::external( Settings::i()->sitemap_url ? rtrim( Settings::i()->sitemap_url, '/' ) : rtrim( Settings::i()->base_url, '/' ) . '/sitemap.php' );
}
$content = SimpleXML::create( 'sitemapindex', 'http://www.sitemaps.org/schemas/sitemap/0.9' );
foreach ( Db::i()->select( array( 'sitemap', 'updated' ), 'core_sitemap', array( 'data IS NOT NULL' ) ) as $sitemap )
{
$content->addChild( 'sitemap', array( 'loc' => $sitemapUrl->setQueryString( 'file', $sitemap['sitemap'] ), 'lastmod' => DateTime::ts( $sitemap['updated'] )->format('c') ) );
+34 -18
View File
@@ -17,6 +17,7 @@ use Exception;
use IPS\Application;
use IPS\Application\Module;
use IPS\cms\Pages\Page;
use IPS\cms\Records;
use IPS\Content\Item;
use IPS\core\AdminNotification;
use IPS\core\DataLayer;
@@ -445,7 +446,7 @@ class ajax extends Controller
}
/* Render */
$output = Theme::i()->getTemplate( 'widgets' )->topContributorRows( $topContributors, $timeframe, Request::i()->orientation );
$output = Theme::i()->getTemplate( 'widgets' )->topContributorRows( $topContributors, $timeframe, Request::i()->layout, Request::i()->isCarousel );
if ( Request::i()->isAjax() )
{
Output::i()->sendOutput( $output );
@@ -511,7 +512,7 @@ class ajax extends Controller
}
/* Render */
$output = Theme::i()->getTemplate( 'widgets' )->mostSolvedRows( $topSolved, $timeframe, Request::i()->orientation );
$output = Theme::i()->getTemplate( 'widgets' )->mostSolvedRows( $topSolved, $timeframe, Request::i()->layout, Request::i()->isCarousel );
if ( Request::i()->isAjax() )
{
Output::i()->sendOutput( $output );
@@ -827,13 +828,14 @@ class ajax extends Controller
public function getDataLayerEvents() : void
{
$payload = '{}';
if ( Settings::i()->core_datalayer_enabled AND Member::loggedIn()->member_id )
if ( DataLayer::enabled() AND Member::loggedIn()->member_id )
{
$payload = DataLayer::i()->jsonEvents;
DataLayer::i()->clearCache();
}
/* Using this instead of \IPS\Output::i()->json because it's already JSON encoded */
/* We have to set this before invoking sendOutput to prevent the data layer events from being cached all over again in the session */
Output::i()->bypassDataLayer = true;
Output::i()->sendOutput( Member::loggedIn()->language()->stripVLETags( $payload ), 200, 'application/json', Output::i()->httpHeaders );
}
@@ -867,17 +869,19 @@ class ajax extends Controller
Output::i()->redirect( Url::internal( '' ) );
}
/* Is there a widget in the request? */
if ( !is_string( Request::i()->blockID ) or empty( Request::i()->blockID ) )
{
Output::i()->json( [ "No block id present in the request" ], 400 );
}
/* In this case, let's see if the block exists at all */
foreach ( ['pageApp', 'pageModule', 'pageController', 'pageID', 'pageArea'] as $pageParam )
/* Note - pageID is the ID of the CMS page (if there is one) while pageElse is the ID of the content in the page (e.g. records, topics, blogs, etc) */
foreach ( ['pageApp', 'pageModule', 'pageController', 'pageID', 'pageArea', 'pageElse', 'pageContentClass'] as $pageParam )
{
$$pageParam = Request::i()->$pageParam;
}
$item = null;
try
{
// If they're not a member, they definitely don't have permission
@@ -892,18 +896,28 @@ class ajax extends Controller
}
$canEdit = false;
$itemId = $pageID ?: Request::i()->pageElse;
if ( $pageApp and $pageModule and $pageController and is_numeric( $itemId ) )
$itemId = $pageID ?: $pageElse;
if ( is_numeric( $itemId ) )
{
$controllerClass = "\\IPS\\{$pageApp}\\modules\\front\\{$pageModule}\\{$pageController}";
if ( class_exists( $controllerClass ) && is_subclass_of( $controllerClass, ContentController::class ) )
// first, check the content class directly
if ( is_numeric( $pageElse ) and isset( $pageContentClass ) and class_exists( $pageContentClass ) and is_subclass_of( $pageContentClass, Item::class ) )
{
$item = $controllerClass::loadItem( (int) $itemId );
if ( $item instanceof Item and $item->author()?->member_id === Member::loggedIn()->member_id ) /* todo we may want to add a mod permission as well; for now we just do an author check */
$item = $pageContentClass::load( $pageElse );
}
else if ( $pageApp and $pageModule and $pageController ) // Still haven't verified permission? Check permissions to edit the CMS page itself
{
$controllerClass = "\\IPS\\{$pageApp}\\modules\\front\\{$pageModule}\\{$pageController}";
if ( class_exists( $controllerClass ) && is_subclass_of( $controllerClass, ContentController::class ) )
{
$canEdit = true;
$item = $controllerClass::loadItem( (int)$itemId );
}
}
if ( $item instanceof Item and $item->author()?->member_id === Member::loggedIn()->member_id )
{
$canEdit = true;
}
}
if ( !$canEdit && $app->canManageWidgets() )
@@ -959,15 +973,17 @@ class ajax extends Controller
$items = json_decode( Request::i()->items, true ) ?: [];
if ( is_array( $items ) )
{
Db::i()->replace( 'core_table_of_contents', [
$contentId = strval( $pageElse ?: $pageID ) ?: null;
$insert = [
'contents' => json_encode( $items ),
'app' => $pageApp,
'controller' => $pageController,
'module' => $pageModule,
'key' => strval( $pageID ?: Request::i()->pageElse ) ?: null
]);
'key' => $contentId ? ( $item instanceof Records ? $item::class . "_" : '' ) . $contentId : null
];
Db::i()->insert( 'core_table_of_contents', $insert, true );
Widget::deleteCaches( Request::i()->blockID );
Widget::deleteCaches( 'tableofcontents' );
}
else
{
@@ -1006,7 +1022,7 @@ class ajax extends Controller
$timestamps = array_map( "intval", explode( ",", Request::i()->timestamps ) );
if ( count( $timestamps ) )
{
$lang = Lang::defaultLanguage() ? Lang::load( Lang::defaultLanguage() ) : Member::loggedIn()->language();
$lang = Member::loggedIn()->language() ?: Lang::load( Lang::defaultLanguage() );
foreach ( $timestamps as $timestamp )
{
// we cache based on how different the time is compared to the current time
@@ -78,7 +78,6 @@ class announcement extends Controller
if ( ( !Settings::i()->site_online and !Member::loggedIn()->group['g_access_offline'] ) OR ( !Member::loggedIn()->member_id AND !Member::loggedIn()->group['g_view_board'] ) )
{
Output::i()->bodyClasses[] = 'ipsLayout_minimal';
Output::i()->allowDefaultWidgets = FALSE;
Output::i()->sidebar['enabled'] = FALSE;
}
@@ -36,6 +36,7 @@ use IPS\Settings;
use IPS\Text\Parser;
use IPS\Theme;
use OutOfRangeException;
use UnderflowException;
use UnexpectedValueException;
use function class_exists;
use function count;
@@ -338,7 +339,7 @@ class editor extends Controller
{
$categoryName = $categoryNames[ $row['emo_set'] ] ?? Member::loggedIn()->language()->get( 'core_emoticon_group_' . $row['emo_set'] );
}
catch ( OutOfRangeException )
catch ( UnderflowException )
{
$categoryName = '_default';
}
@@ -13,7 +13,7 @@ namespace IPS\core\modules\front\system;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DateInterval;
use IPS\Content\Controller;
use IPS\Dispatcher\Controller;
use IPS\Data\Cache;
use IPS\DateTime;
use IPS\Http\Url;
@@ -55,6 +55,10 @@ class embed extends Controller
{
try
{
if( empty( Request::i()->url ) )
{
throw new UnexpectedValueException;
}
$return = Parser::embeddableMedia( Url::createFromString( Request::i()->url, FALSE, TRUE ), TRUE );
}
catch( UnexpectedValueException $e )
@@ -19,6 +19,7 @@ use IPS\IPS;
use IPS\Member;
use IPS\Output;
use IPS\Request;
use IPS\Settings as SettingsClass;
use IPS\Theme;
use function defined;
@@ -71,7 +72,12 @@ class followed extends Controller
}
/* Don't forget Members - add this on to the end so it never defaults UNLESS we only have apps that do not have followable content */
$types['core'] = array( 'core_member' => "\IPS\Member", 'core_tag' => "IPS\Content\Tag" );
$types['core'] = array( 'core_member' => "\IPS\Member" );
if( SettingsClass::i()->tags_enabled )
{
$types['core']['core_tag'] = "IPS\Content\Tag";
}
/* What type are we looking at? */
$currentAppModule = NULL;
@@ -12,10 +12,13 @@ namespace IPS\core\modules\front\system;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DateInterval;
use IPS\Application;
use IPS\Data\Store;
use IPS\DateTime;
use IPS\Db;
use IPS\Dispatcher\Controller;
use IPS\Email;
use IPS\Extensions\SSOAbstract;
use IPS\Http\Url;
use IPS\Http\Url\Internal;
@@ -29,6 +32,7 @@ use IPS\MFA\MFAHandler;
use IPS\Output;
use IPS\Request;
use IPS\Session;
use IPS\Text\Encrypt;
use IPS\Theme;
use OutOfRangeException;
use UnderflowException;
@@ -157,7 +161,6 @@ class login extends Controller
}
/* Display Login Form */
Output::i()->allowDefaultWidgets = FALSE;
Output::i()->bodyClasses[] = 'ipsLayout_minimal';
Output::i()->sidebar['enabled'] = FALSE;
Output::i()->title = Member::loggedIn()->language()->addToStack('login');
@@ -254,8 +257,58 @@ class login extends Controller
$login->reauthenticateAs = $member;
$error = NULL;
/* Show error if linking cannot be completed */
if( !count( $login->usernamePasswordMethods() ) AND !count( $login->buttonMethods() ) )
{
/**
* Send validation email and show page explaining the next step.
*/
$vid = LoginClass::generateRandomString();
$plainSecurityKey = LoginClass::generateRandomString();
/* Invalidating any existing validating links */
try
{
/* Anything sent in the last 6 minutes? -- keep in mind the email link is only valid for 10 minutes */
Db::i()->select( '*', 'core_validating', [
'member_id=? AND login_link=1 AND email_sent>?',
$member->member_id,
( new DateTime )->sub( new DateInterval( 'PT6M' ) )->getTimestamp()
] )->first();
}
catch( \UnderflowException $e )
{
Db::i()->delete( 'core_validating', [ 'member_id=? AND login_link=1', $member->member_id ] );
Db::i()->insert( 'core_validating', [
'vid' => $vid,
'member_id' => $member->member_id,
'entry_date' => time(),
'email_chg' => false,
'ip_address' => Request::i()->ipAddress(),
'new_email' => '',
'email_sent' => time(),
'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag(),
'login_link' => true,
'extra' => $_SESSION['linkAccounts']
] );
/* send email */
Email::buildFromTemplate( 'core', 'loginLinkValidate', [ $member, $vid, $plainSecurityKey ], Email::TYPE_TRANSACTIONAL )->send( $member );
}
unset( $_SESSION['linkAccounts'] );
/* Otherwise show the reauthenticate form */
Output::i()->bodyClasses[] = 'ipsLayout_minimal';
Output::i()->sidebar['enabled'] = FALSE;
Output::setCacheTime();
Output::i()->title = Member::loggedIn()->language()->addToStack('login');
Output::i()->output = Theme::i()->getTemplate( 'system' )->mergeSocialAccountEmailValidation( $handler );
return;
}
/* Did we submit the merge form? */
if( isset( Request::i()->mergeAccount ) and Request::i()->mergeAccount )
else if( isset( Request::i()->mergeAccount ) and Request::i()->mergeAccount )
{
Session::i()->csrfCheck();
@@ -207,7 +207,8 @@ class metatags extends Controller
$output = array(
'scope' => rtrim( Settings::i()->base_url, '/' ) . '/',
'name' => Settings::i()->board_name
'name' => Settings::i()->board_name,
'display' => 'standalone',
);
foreach( $manifest as $k => $v )
@@ -273,63 +274,4 @@ class metatags extends Controller
Output::i()->sendOutput( json_encode( $output, JSON_PRETTY_PRINT ), 200, 'application/manifest+json', $cacheHeaders );
}
/**
* Output the IE browserconfig.xml file
*
* @return void
*/
protected function iebrowserconfig() : void
{
if( \IPS\IN_DEV === FALSE AND isset( Store::i()->iebrowserconfig ) )
{
$output = Store::i()->iebrowserconfig;
}
else
{
$manifest = json_decode( Settings::i()->manifest_details, TRUE );
/* Init */
$xml = new XMLWriter;
$xml->openMemory();
$xml->setIndent( TRUE );
$xml->startDocument( '1.0', 'UTF-8' );
$xml->startElement( 'browserconfig' );
$xml->startElement( 'msapplication' );
$xml->startElement( 'tile' );
if( isset( $manifest['theme_color'] ) AND $manifest['theme_color'] )
{
$xml->writeElement( 'TileColor', $manifest['theme_color'] );
}
$homeScreen = json_decode( Settings::i()->icons_homescreen, TRUE ) ?? array();
foreach( $homeScreen as $k => $v )
{
if( mb_strpos( $k, 'msapplication' ) !== FALSE AND $k !== 'msapplication-TileImage' )
{
$file = File::get( 'core_Icons', $v['url'] );
$xml->startElement( str_replace( 'msapplication-', '', $k ) );
$xml->startAttribute( 'src' );
$xml->text( (string) $file->url );
$xml->endAttribute();
$xml->endElement();
}
}
$xml->endElement();
$xml->endElement();
$xml->endElement();
$xml->endDocument();
$output = $xml->outputMemory();
Store::i()->iebrowserconfig = $output;
}
$cacheHeaders = ( !\IPS\IN_DEV ) ? Output::getCacheHeaders( time(), 86400 ) : array();
Output::i()->sendOutput( $output, 200, 'application/xml', $cacheHeaders );
}
}
@@ -13,9 +13,12 @@ namespace IPS\core\modules\front\system;
/* To prevent PHP errors (extending class does not exist) revealing path */
use Exception;
use IPS\Api\Webhook;
use IPS\Application;
use IPS\Content;
use IPS\Content\Item;
use IPS\core\DataLayer;
use IPS\core\Followed\Follow;
use IPS\Db;
use IPS\Dispatcher\Controller;
use IPS\Events\Event;
@@ -31,14 +34,17 @@ use IPS\Node\Model;
use IPS\Notification;
use IPS\Notification\Table;
use IPS\Output;
use IPS\Platform\Bridge;
use IPS\Request;
use IPS\Session;
use IPS\Settings;
use IPS\Theme;
use OutOfRangeException;
use UnderflowException;
use function count;
use function defined;
use function in_array;
use function md5;
use const IPS\REBUILD_QUICK;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
@@ -132,6 +138,37 @@ class notifications extends Controller
'encoding' => Request::i()->encoding,
'device' => $device->device_key
) );
/* The expectation is to receive some push notifications, so let's add them to follower_content, new_content and new_comment and they can be removed manually if the user doesn't want them */
$notificationTypes = array( 'follower_content', 'new_content', 'new_comment' );
foreach ( $notificationTypes as $notificationKey )
{
try
{
/* See if we have a row in preferences already */
$currentRow = Db::i()->select( '*', 'core_notification_preferences', array( 'member_id=? AND notification_key=?', Member::loggedIn()->member_id, $notificationKey ) )->first();
/* Does this row have a preference of push? */
$prefs = explode( ',', $currentRow['preference'] );
if ( !in_array( 'push', $prefs ) )
{
$prefs[] = 'push';
}
Db::i()->update( 'core_notification_preferences', [
'preference' => implode( ',', $prefs ),
], array( 'member_id=? AND notification_key=?', Member::loggedIn()->member_id, $notificationKey ) );
}
catch( UnderflowException $e )
{
Db::i()->insert( 'core_notification_preferences', [
'member_id' => Member::loggedIn()->member_id,
'notification_key' => $notificationKey,
'preference' => implode( ',', [ 'inline', 'push' ] )
], true );
}
}
}
Output::i()->json( 'OK' );
@@ -239,13 +276,14 @@ class notifications extends Controller
$form->class = 'ipsForm--vertical ipsForm--notification-options';
}
if ( Request::i()->isAjax() )
if ( Request::i()->isAjax() and ! isset( Request::i()->fromFollowButton ) )
{
$form->actionButtons = array();
Output::i()->sendOutput( Theme::i()->getTemplate('system')->notificationSettingsType( Member::loggedIn()->language()->addToStack("notifications__{$extensionKey}"), $form, TRUE ), 200, 'text/html', Output::i()->httpHeaders );
}
else
{
/* If it's not ajax, or come via the follow button, then we want to show the form with the save button */
Output::i()->output = Theme::i()->getTemplate('system')->notificationSettingsType( Member::loggedIn()->language()->addToStack("notifications__{$extensionKey}"), $form, FALSE );
}
}
@@ -327,34 +365,7 @@ class notifications extends Controller
}
/* Get class */
$class = NULL;
foreach ( $application->extensions( 'core', 'ContentRouter' ) as $ext )
{
foreach ( $ext->classes as $classname )
{
if ( $classname == 'IPS\\' . Request::i()->follow_app . '\\' . IPS::mb_ucfirst( Request::i()->follow_area ) )
{
$class = $classname;
break;
}
if ( isset( $classname::$containerNodeClass ) and $classname::$containerNodeClass == 'IPS\\' . Request::i()->follow_app . '\\' . IPS::mb_ucfirst( Request::i()->follow_area ) )
{
$class = $classname::$containerNodeClass;
break;
}
if( isset( $classname::$containerFollowClasses ) )
{
foreach( $classname::$containerFollowClasses as $followClass )
{
if( $followClass == 'IPS\\' . Request::i()->follow_app . '\\' . IPS::mb_ucfirst( Request::i()->follow_area ) )
{
$class = $followClass;
break;
}
}
}
}
}
$class = Follow::getClassToFollow( Request::i()->follow_app, Request::i()->follow_area );
if( Request::i()->follow_app == 'core' and Request::i()->follow_area == 'member' )
{
@@ -376,18 +387,6 @@ class notifications extends Controller
{
Output::i()->error( 'cant_follow_member', '3C154/8', 403, '' );
}
$class = 'IPS\\Member';
}
if( Request::i()->follow_app == 'core' and Request::i()->follow_area == 'club' )
{
$class = 'IPS\Member\Club';
}
if( Request::i()->follow_app == 'core' and Request::i()->follow_area == 'tag' )
{
$class = 'IPS\Content\Tag';
}
if ( !$class )
@@ -401,7 +400,7 @@ class notifications extends Controller
{
if ( in_array( 'IPS\Node\Model', class_parents( $class ) ) )
{
/* @var Model $thing */
/* @var Model $class */
$thing = $class::loadAndCheckPerms( (int) Request::i()->follow_id );
Output::i()->title = Member::loggedIn()->language()->addToStack( 'follow_thing', FALSE, array( 'sprintf' => array( $thing->_title ) ) );
@@ -418,6 +417,7 @@ class notifications extends Controller
}
elseif ( $class == 'IPS\Member\Club' )
{
/* @var Club $class */
$thing = $class::loadAndCheckPerms( (int) Request::i()->follow_id );
Output::i()->title = Member::loggedIn()->language()->addToStack( 'follow_thing', FALSE, array( 'sprintf' => array( $thing->_title ) ) );
Output::i()->breadcrumb = array(
@@ -432,7 +432,7 @@ class notifications extends Controller
throw new OutOfRangeException;
}
/* @var Item $thing */
/* @var Item $class */
$thing = $class::loadAndCheckPerms( (int) Request::i()->follow_id );
Output::i()->title = Member::loggedIn()->language()->addToStack( 'follow_thing', FALSE, array( 'sprintf' => array( $thing->mapped('title') ) ) );
@@ -553,7 +553,7 @@ class notifications extends Controller
{
foreach ( $options as $k => $v )
{
$form->hiddenValues[ $k ] = $v;
$form->hiddenValues[ 'follow_type' ] = $k;
if ( empty( $notificationConfiguration ) )
{
$type = $type == 'follower_content' ? 'core_Content' : $type;
@@ -581,91 +581,26 @@ class notifications extends Controller
/* Handle submissions */
if ( $values = $form->values() )
{
/* Insert */
$save = array(
'follow_id' => md5( Request::i()->follow_app . ';' . Request::i()->follow_area . ';' . Request::i()->follow_id . ';' . Member::loggedIn()->member_id ),
'follow_app' => Request::i()->follow_app,
'follow_area' => Request::i()->follow_area,
'follow_rel_id' => Request::i()->follow_id,
'follow_member_id' => Member::loggedIn()->member_id,
'follow_is_anon' => !$values['follow_public'],
'follow_added' => time(),
'follow_notify_do' => ( isset( $values['follow_type'] ) AND $values['follow_type'] == 'none' ) ? 0 : 1,
'follow_notify_meta' => '',
'follow_notify_freq' => ( $class == "IPS\Member" ) ? 'immediate' : $values['follow_type'],
'follow_notify_sent' => 0,
'follow_visible' => 1,
);
if ( $current )
{
Db::i()->update( 'core_follow', $save, array( 'follow_id=?', $current['follow_id'] ) );
}
else
{
Db::i()->insert( 'core_follow', $save );
}
$class = Follow::getClassToFollow( Request::i()->follow_app, Request::i()->follow_area );
try
{
$useDataLayer = Member::loggedIn()->member_id and DataLayer::enabled( 'analytics_full' );
$alreadyFollowing = $useDataLayer ? Db::i()->select( 'count(*)', 'core_follow', [
'follow_app=? AND follow_area=? AND follow_rel_id=? AND follow_member_id=?', Request::i()->follow_app, Request::i()->follow_area, Request::i()->follow_id, Member::loggedIn()->member_id
] )->first() : false;
$thing = $class::load( Request::i()->follow_id );
$thing->follow( $values['follow_type'], $values['follow_public'] );
/* Fire event */
Event::fire( 'onFollow', Member::loggedIn(), [ $thing, !$values['follow_public'] ] );
/* Remove cached */
Db::i()->delete( 'core_follow_count_cache', array( 'id=? AND class=?', Request::i()->follow_id, 'IPS\\' . Request::i()->follow_app . '\\' . IPS::mb_ucfirst( Request::i()->follow_area ) ) );
/* Also follow all nodes if following club */
if( $class == "IPS\Member\Club" )
{
foreach ( $thing->nodes() as $node )
if ( $useDataLayer and !$alreadyFollowing )
{
$itemClass = $node['node_class']::$contentItemClass;
$followApp = $itemClass::$application;
$followArea = mb_strtolower( mb_substr( $node['node_class'], mb_strrpos( $node['node_class'], '\\' ) + 1 ) );
$save = array(
'follow_id' => md5( $followApp . ';' . $followArea . ';' . $node['node_id'] . ';' . Member::loggedIn()->member_id ),
'follow_app' => $followApp,
'follow_area' => $followArea,
'follow_rel_id' => $node['node_id'],
'follow_member_id' => Member::loggedIn()->member_id,
'follow_is_anon' => !$values['follow_public'],
'follow_added' => time(),
'follow_notify_do' => ( isset( $values['follow_type'] ) AND $values['follow_type'] == 'none' ) ? 0 : 1,
'follow_notify_meta' => '',
'follow_notify_freq' => $values['follow_type'],
'follow_notify_sent' => 0,
'follow_visible' => 1,
);
Db::i()->insert( 'core_follow', $save, TRUE );
DataLayer::i()->addEvent( 'follow', [
'follow_app' => Request::i()->follow_app,
'follow_area' => Request::i()->follow_area,
'follow_id' => Request::i()->follow_id
]);
}
}
/* Send notification if following member */
if( $class == "IPS\Member" )
{
if( $values['follow_public'] )
{
/* Give points */
$receiver = Member::load( Request::i()->follow_id );
$receiver->achievementAction( 'core', 'FollowMember', [
'giver' => Member::loggedIn()
] );
$notification = new Notification( Application::load( 'core' ), 'member_follow', Member::loggedIn(), array( Member::loggedIn() ) );
$notification->recipients->attach( $thing );
$notification->send();
}
}
else if ( in_array( 'IPS\Node\Model', class_parents( $class ) ) )
{
Member::loggedIn()->achievementAction( 'core', 'FollowNode', $class::load( Request::i()->follow_id ) );
}
else if ( in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$item = $class::load( Request::i()->follow_id );
Member::loggedIn()->achievementAction( 'core', 'FollowContentItem', [
'item' => $item,
'author' => $item->author()
] );
}
}
catch( OutOfRangeException ){}
/* Boink */
if ( Request::i()->isAjax() )
@@ -704,63 +639,33 @@ class notifications extends Controller
try
{
$follow = Db::i()->select( '*', 'core_follow', array( 'follow_id=? AND follow_member_id=?', Request::i()->id, Member::loggedIn()->member_id ) )->first();
$follow = Follow::load( Request::i()->id );
if( $follow->member_id != Member::loggedIn()->member_id )
{
throw new OutOfRangeException;
}
}
catch ( UnderflowException $e )
catch ( OutOfRangeException $e )
{
Output::i()->error( 'cant_find_unfollow', '2C154/D', 404, '' );
}
/* Fire event */
if( $follow['follow_app'] == 'core' AND $follow['follow_area'] == 'member' )
{
$class = 'IPS\\Member';
}
elseif( $follow['follow_app'] == 'core' AND $follow['follow_area'] == 'club' )
{
$class = 'IPS\\Member\\Club';
}
elseif( $follow['follow_app'] == 'core' and $follow['follow_area'] == 'tag' )
{
$class = 'IPS\Content\Tag';
}
else
{
$class = 'IPS\\' . $follow['follow_app'] . '\\' . IPS::mb_ucfirst( $follow['follow_area'] );
}
try
{
$thing = $class::load( $follow['follow_rel_id'] );
Event::fire( 'onUnfollow', Member::loggedIn(), [ $thing ] );
}
catch( OutOfRangeException ){}
Db::i()->delete( 'core_follow', array( 'follow_id=? AND follow_member_id=?', Request::i()->id, Member::loggedIn()->member_id ) );
Db::i()->delete( 'core_follow_count_cache', array( 'id=? AND class=?', $follow['follow_rel_id'], 'IPS\\' . Request::i()->follow_app . '\\' . IPS::mb_ucfirst( Request::i()->follow_area ) ) );
/* If we are unfollowing a club, unfollow all of its nodes */
if( Request::i()->follow_app == 'core' and Request::i()->follow_area == 'club' )
{
$class = 'IPS\\Member\Club';
/* We call the unfollow on the object, not on the follow record.
This is so that we can handle object-specific actions (e.g. unfollow a club unfollow club nodes)
It's a bit circular, but we'll live. */
if( $item = $follow->_item )
{
$item->unfollow( Member::loggedIn(), $follow->id );
/* Get thing */
$thing = NULL;
try
{
$thing = $class::loadAndCheckPerms( (int) $follow['follow_rel_id'] );
foreach ( $thing->nodes() as $node )
{
$itemClass = $node['node_class']::$contentItemClass;
$followApp = $itemClass::$application;
$followArea = mb_strtolower( mb_substr( $node['node_class'], mb_strrpos( $node['node_class'], '\\' ) + 1 ) );
Db::i()->delete( 'core_follow', array( 'follow_id=? AND follow_member_id=?', md5( $followApp . ';' . $followArea . ';' . $node['node_id'] . ';' . Member::loggedIn()->member_id ), Member::loggedIn()->member_id ) );
}
}
catch ( OutOfRangeException $e ) {}
}
if ( DataLayer::enabled( 'analytics_full' ) )
{
DataLayer::i()->addEvent( 'unfollow', [
'follow_app' => $follow->app,
'follow_area' => $follow->area,
'follow_id' => $follow->rel_id,
]);
}
}
if ( Request::i()->isAjax() )
{
@@ -898,7 +803,7 @@ class notifications extends Controller
protected function unfollowFromEmail() : void
{
/* Logged in? */
if ( Member::loggedIn()->member_id )
if ( Member::loggedIn()->member_id and ! isset( Request::i()->listunsubscribe ) )
{
/* Go to the normal page */
Output::i()->redirect( Url::internal( "app=core&module=system&controller=notifications&do=follow&follow_app=". Request::i()->follow_app ."&follow_area=". Request::i()->follow_area ."&follow_id=" . Request::i()->follow_id ) );
@@ -906,12 +811,12 @@ class notifications extends Controller
if ( ! empty( Request::i()->gkey ) )
{
list( $followKey, $memberKey ) = explode( '-', Request::i()->gkey );
[ $followKey, $memberKey ] = explode( '-', Request::i()->gkey );
/* Do we follow it? */
try
{
$current = Db::i()->select( '*', 'core_follow', array( 'MD5( CONCAT_WS( \';\', follow_app, follow_area, follow_rel_id, follow_member_id, follow_added ) )=?', $followKey ) )->first();
/* Already no subs? */
if ( $current['follow_notify_freq'] === 'none' )
{
@@ -929,29 +834,8 @@ class notifications extends Controller
{
throw new Exception;
}
/* Get class */
if( Request::i()->follow_app == 'core' and Request::i()->follow_area == 'member' )
{
$class = 'IPS\\Member';
}
elseif( Request::i()->follow_app == 'core' and Request::i()->follow_area == 'club' )
{
$class = 'IPS\\Member\Club';
}
elseif( Request::i()->follow_app == 'core' and Request::i()->follow_area == 'tag' )
{
$class = 'IPS\Content\Tag';
}
else
{
$class = 'IPS\\' . Request::i()->follow_app . '\\' . IPS::mb_ucfirst( Request::i()->follow_area );
if( !IPS::classUsesTrait( $class, 'IPS\Content\Followable' ) )
{
throw new Exception;
}
}
$class = Follow::getClassToFollow( Request::i()->follow_app, Request::i()->follow_area );
if ( !class_exists( $class ) )
{
throw new Exception;
@@ -987,7 +871,18 @@ class notifications extends Controller
/* Grab a count */
$count = Db::i()->select( 'COUNT(*)', 'core_follow', array( 'follow_member_id=? and follow_notify_freq != ?', $member->member_id, 'none' ) )->first();
if ( isset( Request::i()->listunsubscribe ) and Request::i()->requestMethod() == 'POST' )
{
/* Unsubscribe them now as it's come from clicking the list-unsubscribe header in the email which
contains List-Unsubscribe-Post:List-Unsubscribe=One-Click which sends via POST to prevent links being crawled and activated */
$thing->follow( 'none', !$current['follow_is_anon'], $member );
/* Done */
Output::i()->output = Member::loggedIn()->language()->addToStack('unsubscribed');
return;
}
$form = new Form( 'unfollowFromEmail', 'update_follow' );
$form->class = 'ipsForm--vertical ipsForm--notifications-unfollow-email';
@@ -1014,20 +909,7 @@ class notifications extends Controller
{
if ( $values['guest_unfollow_choice'] == 'single' or isset( $values['guest_unfollow_single'] ) )
{
Db::i()->update( 'core_follow', array( 'follow_notify_freq' => 'none' ), array( 'follow_id=? AND follow_member_id=?', $current['follow_id'], $member->member_id ) );
/* Unfollow club areas */
if ( $class == "IPS\Member\Club" )
{
foreach ( $thing->nodes() as $node )
{
$itemClass = $node['node_class']::$contentItemClass;
$followApp = $itemClass::$application;
$followArea = mb_strtolower( mb_substr( $node['node_class'], mb_strrpos( $node['node_class'], '\\' ) + 1 ) );
Db::i()->update( 'core_follow', array( 'follow_notify_freq' => 'none' ), array( 'follow_id=? AND follow_member_id=?', md5( $followApp . ';' . $followArea . ';' . $node['node_id'] . ';' . $member->member_id ), $member->member_id ) );
}
}
$thing->follow( 'none', !$current['follow_is_anon'], $member );
}
else
{
@@ -1196,9 +1078,15 @@ class notifications extends Controller
Output::i()->json( array( 'error' => 'not_found' ), 404 );
}
/* Is this our notification? */
$data = json_decode( $notification['notification_data'], true );
if ( ! is_array( $data ) )
{
Output::i()->json( array( 'error' => 'invalid_data' ), 400 );
}
$data['unreadCount'] = Member::loggedIn()->notification_cnt;
if( !isset( $data['member'] ) OR $data['member'] != Member::loggedIn()->member_id )
{
Output::i()->json( array( 'error' => 'no_permission' ), 403 );
@@ -141,7 +141,6 @@ class register extends Controller
/* Set Session Location */
Session::i()->setLocation( Url::internal( 'app=core&module=system&controller=register', NULL, 'register' ), array(), 'loc_registering' );
Output::i()->allowDefaultWidgets = FALSE;
/* What's the "log in" link? */
$loginUrl = Url::internal( 'app=core&module=system&controller=login', NULL, 'login' );
@@ -561,7 +560,7 @@ class register extends Controller
$captcha = new Captcha;
/* If PBR request is from the last 5 minutes, don't ask for a captcha again */
if( $postBeforeRegister !== NULL AND DateTime::ts( $postBeforeRegister['timestamp'] )->add( new \DateInterval('PT5M' ) )->getTimestamp() > time() )
if( $postBeforeRegister !== NULL AND DateTime::ts( $postBeforeRegister['timestamp'] )->add( new DateInterval('PT5M' ) )->getTimestamp() > time() )
{
$captcha = '';
}
@@ -901,13 +900,19 @@ class register extends Controller
/* Load record */
try
{
$record = Db::i()->select( '*', 'core_validating', array( 'vid=? AND member_id=? AND ( new_reg=? or email_chg=? )', Request::i()->vid, Request::i()->mid, 1, 1 ) )->first();
$record = Db::i()->select( '*', 'core_validating', array( 'vid=? AND member_id=? AND ( new_reg=? or email_chg=? or login_link=? )', Request::i()->vid, Request::i()->mid, 1, 1, 1 ) )->first();
}
catch ( UnderflowException $e )
{
$this->_performRedirect( NULL, FALSE, 'validate_no_record' );
}
/* If this is a login link, make sure it's recent */
if( $record['login_link'] AND $record['email_sent'] < ( new DateTime )->sub( new DateInterval( 'PT10M' ) )->getTimestamp() )
{
$this->_performRedirect( NULL, FALSE, 'validate_no_record' );
}
/* Check security key is valid. */
if( !Login::compareHashes( Encrypt::fromTag( $record['security_key'] )->decrypt(), Request::i()->security_key ) )
{
@@ -948,7 +953,16 @@ class register extends Controller
{
$member->emailValidationConfirmed( $record );
}
else
elseif( $record['login_link'] )
{
$details = json_decode( $record['extra'], TRUE );
$member = Member::load( $details['member'] );
/* And then handler to link with */
$handler = Handler::load( $details['handler'] );
$handler->completeLink( $member, $details['details'] ?? null );
Db::i()->delete( 'core_validating', array( 'member_id=? AND login_link=?', $member->member_id, 1 ) );
}
elseif( $record['email_chg'] )
{
$oldEmail = $member->email;
$member->changeEmail( $record['new_email'] );
@@ -957,6 +971,9 @@ class register extends Controller
Db::i()->delete( 'core_validating', array( 'member_id=?', $member->member_id ) );
/* Invalidate sessions except this one */
$member->invalidateSessionsAndLogins( Session::i()->id );
/* Send a confirmation email */
Email::buildFromTemplate( 'core', 'email_address_changed', array( $member, $oldEmail ), Email::TYPE_TRANSACTIONAL )->send( $oldEmail, array(), array(), NULL, NULL, array( 'Reply-To' => Settings::i()->email_in ) );
}
@@ -18,6 +18,7 @@ use DOMXpath;
use Exception;
use IPS\Api\OAuthClient;
use IPS\Application;
use IPS\core\DataLayer;
use IPS\core\ShareLinks\Service;
use IPS\DateTime;
use IPS\Db;
@@ -37,12 +38,14 @@ use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\YesNo;
use IPS\Helpers\Wizard;
use IPS\Http\Url;
use IPS\Log;
use IPS\Login;
use IPS\Login\Exception as LoginException;
use IPS\Login\Handler;
use IPS\Login\Success;
use IPS\Member;
use IPS\Member\Device;
use IPS\Member\Group;
use IPS\Member\PrivacyAction;
use IPS\Member\ProfileStep;
use IPS\MFA\MFAHandler;
@@ -61,6 +64,7 @@ use UnderflowException;
use function count;
use function defined;
use function in_array;
use function intval;
use function is_array;
use function is_numeric;
use function is_string;
@@ -194,10 +198,6 @@ class settings extends Controller
'title' => 'email_address',
'url' => Url::internal( "app=core&module=system&controller=settings&area=email", "front", "settings_email" )
];
if( Member::loggedIn()->members_bitoptions['email_messages_bounce'] )
{
$tabs['email']['warning'] = true;
}
}
/* Can change password? */
@@ -424,7 +424,6 @@ class settings extends Controller
$form->class = 'ipsForm_collapseTablet';
$currentEmail = htmlspecialchars( Member::loggedIn()->email, ENT_DISALLOWED, 'UTF-8', FALSE );
$form->addDummy( 'current_email', Member::loggedIn()->members_bitoptions["email_messages_bounce"] ? Theme::i()->getTemplate( 'global', 'core' )->memberEmailBlockedMessage( $currentEmail ) : $currentEmail );
$form->add( new FormEmail(
'new_email',
'',
@@ -989,6 +988,10 @@ class settings extends Controller
$row = Db::i()->select( '*', PrivacyAction::$databaseTable, $where )->first();
PrivacyAction::constructFromData( $row )->delete();
Member::loggedIn()->logHistory( 'core', 'privacy', [ 'type' => 'account_deletion_cancelled' ] );
if ( DataLayer::enabled() )
{
DataLayer::i()->addEvent( 'account_deletion_canceled', [] );
}
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front'), 'account_deletion_cancelled' );
}
catch( UnderflowException $e )
@@ -1010,6 +1013,12 @@ class settings extends Controller
$request = PrivacyAction::getDeletionRequestByMemberAndKey( Member::loggedIn(), $key );
$request->confirmAccountDeletion();
/* Add data layer event */
if ( DataLayer::enabled() )
{
DataLayer::i()->addEvent( 'account_deletion_requested', [] );
}
Output::i()->redirect( $this->url->setQueryString( 'area','mfa'), 'account_deletion_confirmed' );
}
catch ( OutOfRangeException $e )
@@ -1350,6 +1359,30 @@ class settings extends Controller
}
catch( BadMethodCallException $e ){}
}
/* Data Layer Event */
if ( $oldName !== $values['new_username'] and DataLayer::enabled() )
{
try
{
$groupName = Group::load( Member::loggedIn()->member_group_id )->formattedName;
}
catch ( UnderflowException $e )
{
$groupName = null;
}
$properties = array(
'profile_group' => $groupName,
'profile_group_id' => Member::loggedIn()->member_group_id ?: null,
'profile_id' => intval( Member::loggedIn()->member_id ) ?: null,
'profile_name' => Member::loggedIn()->name ?: null,
'updated_custom_fields' => false,
'updated_profile_name' => DataLayer::i()->includeSSOForMember() ? [ 'old' => $oldName, 'new' => $values['new_username'] ] : true,
'updated_profile_photo' => false,
'updated_profile_coverphoto' => false,
);
DataLayer::i()->addEvent( 'social_update', $properties );
}
/* Redirect */
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=username', 'front', 'settings_username' ), 'username_changed' );
@@ -22,8 +22,10 @@ use IPS\Image;
use IPS\Member;
use IPS\Output;
use IPS\Request;
use IPS\Session;
use IPS\Theme;
use IPS\Settings;
use IPS\Theme\Editor\Setting;
use RuntimeException;
use function defined;
use function file_get_contents;
@@ -81,7 +83,7 @@ class themeeditor extends Controller
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'codemirror/diff_match_patch.js', 'core', 'interface' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'codemirror/codemirror.js', 'core', 'interface' ) );
Color::loadJS();
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'themeeditor/themeEditor.js', 'core', 'interface' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'themeEditor.js', 'global', 'components' ) );
/* Flag our theme as being edited */
Theme::i()->editingStart();
@@ -98,22 +100,37 @@ class themeeditor extends Controller
*/
public function save(): void
{
Session::i()->csrfCheck();
/* Has someone ended this session for us? */
if ( ! Member::loggedIn()->isEditingTheme() )
{
Output::i()->error( 'theme_editor_session_expired', '2S164/A', 403, '' );
}
/* Can we use the theme editor? */
if( ! Member::loggedIn()->modPermission( 'can_use_theme_editor' ) )
{
Output::i()->error( 'no_permission', '2S164/D', 403, '' );
}
/* Useful for debugging */
$changed = Theme::i()->getCssVariables( Theme::CUSTOM_ONLY );
$data = [];
$unchanged = [];
$imageKeys = iterator_to_array( Db::i()->select( 'setting_key', 'core_theme_editor_settings', [ 'setting_type=?', Setting::SETTING_IMAGE ] ) );
$rootCssVariables = Theme::master()->getCssVariables( Theme::FORCE_DEFAULT );
$cssVariables = [];
foreach( $rootCssVariables as $key => $value )
{
/* Skip images, we will handle those later */
if( in_array( $key, $imageKeys ) )
{
continue;
}
$cssVariables[ $key ] = $value;
if ( isset( Request::i()->$key ) )
{
@@ -139,11 +156,10 @@ class themeeditor extends Controller
}
}
/* Handle image settings (except logos) */
foreach( Db::i()->select( 'setting_key', 'core_theme_editor_settings', [
[ 'setting_type=?', Theme\Editor\Setting::SETTING_IMAGE ],
//[ Db::i()->in( 'setting_key', [ 'set__logo-light', 'set__logo-dark', 'set__mobile-logo-light', 'set__mobile-logo-dark' ], true ) ]
] ) as $key )
$imageFiles = [];
/* Handle image settings */
foreach( $imageKeys as $key )
{
/* Did we delete the file? */
if( isset( Request::i()->{ 'delete__' . $key } ) and empty( Request::i()->$key ) )
@@ -152,7 +168,8 @@ class themeeditor extends Controller
}
elseif ( isset( $_FILES[ $key ] ) and $_FILES[ $key ]['tmp_name'] )
{
$imageData = $this->_handleImageUpload( $key, false );
$imageData = $this->_handleImageUpload( $key, in_array( $key, [ 'set__logo-light', 'set__logo-dark', 'set__mobile-logo-light', 'set__mobile-logo-dark' ] ) );
$imageFiles[ $key ] = $imageData;
/* If we specifically set the value to null, then we will use the default value */
$newValue = isset( $imageData['fullUrl'] ) ? (string) $imageData['fullUrl'] : null;
@@ -229,7 +246,16 @@ class themeeditor extends Controller
}
elseif ( isset( $_FILES[ 'set__' . $type ] ) and $_FILES[ 'set__' . $type ]['tmp_name'] )
{
$data['logo'][ $logoKey ] = $this->_handleImageUpload( 'set__' . $type );
/* We have most likely already processed the logos with all the other images */
if( isset( $imageFiles['set__' . $type ] ) )
{
$data['logo'][ $logoKey ] = $imageFiles['set__' . $type ];
unset( $data['logo'][ $logoKey ]['fullUrl'] );
}
else
{
$data['logo'][ $logoKey ] = $this->_handleImageUpload( 'set__' . $type );
}
}
}
@@ -277,6 +303,8 @@ class themeeditor extends Controller
] )
] );
Theme::i()->clearEditingSession();
Output::i()->redirect( Url::internal( "" ), ( \IPS\IN_DEV and ! Settings::i()->theme_designer_mode ) ? 'theme_editor_saved_indev' : 'theme_editor_saved' );
}
@@ -330,11 +358,25 @@ class themeeditor extends Controller
catch ( Exception $e ) { }
}
$file = File::create( 'core_Theme', $uniqueFilename, $image, null, true, null, false );
$file = File::create( 'core_Theme', $uniqueFilename, $image, 'set_resources_' . Theme::i()->_id, true, null, false );
$save = array(
'resource_app' => 'core',
'resource_location' => 'front',
'resource_path' => '/custom/',
'resource_set_id' => Theme::i()->_id,
'resource_user_edited' => 1,
'resource_name' => $file->originalFilename,
'resource_added' => time(),
'resource_filename' => (string) $file,
'resource_data' => $file->contents()
);
Db::i()->insert( 'core_theme_resources', $save );
return [
'url' => (string) $file,
'fullUrl' => (string) $file->url,
'filename' => $file->originalFilename,
'fullUrl' => '{resource="custom/' . $file->originalFilename . '" app="core" location="front"}',
'setting_height' => ( isset( Request::i()->$logoHeightSetting ) and $isLogo ) ? Request::i()->$logoHeightSetting : 100,
'img_width' => $width,
'img_height' => $height,
@@ -355,6 +397,14 @@ class themeeditor extends Controller
*/
public function close(): void
{
Session::i()->csrfCheck();
/* Can we use the theme editor? */
if( ! Member::loggedIn()->modPermission( 'can_use_theme_editor' ) )
{
Output::i()->error( 'no_permission', '2S164/E', 403, '' );
}
/* Flag our theme as no longer being edited */
Theme::i()->editingFinish();
@@ -363,6 +413,33 @@ class themeeditor extends Controller
Output::i()->redirect( $redirect );
}
/**
* Store modified theme settings
*
* @return void
*/
protected function store() : void
{
Session::i()->csrfCheck();
/* Has someone ended this session for us? */
if ( ! Member::loggedIn()->isEditingTheme() )
{
Output::i()->error( 'theme_editor_session_expired', '2S164/F', 403, '' );
}
/* Can we use the theme editor? */
if( ! Member::loggedIn()->modPermission( 'can_use_theme_editor' ) )
{
Output::i()->error( 'no_permission', '2S164/G', 403, '' );
}
/* Update the session */
Theme::i()->updateSessionVars( Request::i()->data );
Output::i()->sendOutput( '' );
}
/**
* Parse Custom CSS so that we can properly handle any
* resource tags, etc
@@ -371,7 +448,25 @@ class themeeditor extends Controller
*/
protected function customCss() : void
{
Session::i()->csrfCheck();
/* Has someone ended this session for us? */
if ( ! Member::loggedIn()->isEditingTheme() )
{
Output::i()->error( 'theme_editor_session_expired', '2S164/B', 403, '' );
}
/* Can we use the theme editor? */
if( ! Member::loggedIn()->modPermission( 'can_use_theme_editor' ) )
{
Output::i()->error( 'no_permission', '2S164/C', 403, '' );
}
/* Store the latest CSS */
Theme::i()->updateSessionCss( Request::i()->content );
$content = Theme::compileCustomCss( Request::i()->content );
Output::i()->json( [ 'content' => $content ] );
}
}
@@ -14,6 +14,7 @@ namespace IPS\core\modules\front\system;
use DateInterval;
use IPS\Content\Controller;
use IPS\core\DataLayer;
use IPS\core\Warnings\Reason;
use IPS\core\Warnings\Warning;
use IPS\DateTime;
@@ -22,6 +23,7 @@ use IPS\Helpers\Table\Content;
use IPS\Http\Url;
use IPS\Member;
use IPS\Output;
use IPS\Platform\Bridge;
use IPS\Request;
use IPS\Session;
use IPS\Settings;
@@ -121,6 +123,13 @@ class warnings extends Controller
catch ( OutOfRangeException $e )
{
Output::i()->error( 'node_error', '2C184/3', 404, '' );
return;
}
/* If the member viewing this is the member who was warned, log a data layer event */
if ( Member::loggedIn()->member_id === $warning->member and DataLayer::enabled( "analytics_full" ) )
{
DataLayer::i()->addEvent( 'warning_viewed', $warning->getDataLayerProperties() );
}
/* Show it */
@@ -211,10 +220,17 @@ class warnings extends Controller
}
/* Acknowledge it */
$alreadyAcknowledged = $warning->acknowledged;
$warning->acknowledged = TRUE;
$warning->save();
$member->members_bitoptions['unacknowledged_warnings'] = (bool) Db::i()->select( 'COUNT(*)', 'core_members_warn_logs', array( "wl_member=? AND wl_acknowledged=0", $member->member_id ), NULL, NULL, NULL, NULL, Db::SELECT_FROM_WRITE_SERVER )->first();
$member->save();
/* Data Layer */
if ( !$alreadyAcknowledged and DataLayer::enabled( "analytics_full" ) )
{
DataLayer::i()->addEvent( 'warning_acknowledged', $warning->getDataLayerProperties() );
}
/* Redirect */
if ( $redirectTo = Request::i()->referrer() )
@@ -18,6 +18,7 @@ use IPS\cms\widgets\Blocks;
use IPS\Db;
use IPS\Dispatcher\Controller;
use IPS\Helpers\Form;
use IPS\Log;
use IPS\Member;
use IPS\Output;
use IPS\Patterns\ActiveRecordIterator;
@@ -33,6 +34,7 @@ use function count;
use function defined;
use function explode;
use function in_array;
use function json_encode;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -122,6 +124,11 @@ class widgets extends Controller
continue;
}
if( !$block->isExecutableByPage( $this->pageApp, $this->pageModule, $this->pageController ) )
{
continue;
}
/* Is this widget hidden from the block list? */
if( !$block::$showInBlockList )
{
@@ -527,6 +534,14 @@ class widgets extends Controller
$pageController = 'global';
}
/* Stop Pages widgets from being stored here */
if ( $pageApp === 'cms' )
{
/* Log it */
Log::log( 'Pages widget wants be stored in core_widget_areas: ' . json_encode( $area->toArray( true, false ) ), 'page_builder' );
return;
}
/* If we have no content, clear this out entirely */
if( !$area->hasWidgets() )
{
@@ -7,7 +7,7 @@
* @package Invision Community
* @since 2 Apr 2013
*/
namespace IPS\core\modules\setup\install;
/* To prevent PHP errors (extending class does not exist) revealing path */
@@ -48,7 +48,7 @@ class license extends Controller
public function manage() : void
{
$form = new Form( 'license', 'continue', Url::external( ( Request::i()->isSecure() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '?controller=license' ) );
$form->add( new Text( 'lkey', '', TRUE, array( 'size' => 50 ), function( $val )
$form->add( new Text( 'lkey', 'Nulled by IPBMafia.ru | 5.11.2025', TRUE, array( 'size' => 50 ), function( $val )
{
IPS::checkLicenseKey( $val, ( Request::i()->isSecure() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . mb_substr( $_SERVER['SCRIPT_NAME'], 0, -mb_strlen( 'admin/install/index.php' ) ) );
}, NULL, '<a href="' . Url::ips( 'docs/find_lkey' ) . '" target="_blank" rel="noopener">' . Member::loggedIn()->language()->addToStack('lkey_help') . ' <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h82.7L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V32c0-17.7-14.3-32-32-32H320zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z"/></svg></a>' ) );
@@ -59,18 +59,18 @@ class license extends Controller
throw new InvalidArgumentException('eula_err');
}
}, "<textarea disabled style='width: 100%; height: 250px'>" . file_get_contents( 'eula.txt' ) . "</textarea>" ) );
if ( $values = $form->values() )
{
$values['lkey'] = trim( $values['lkey'] );
if ( mb_substr( $values['lkey'], -12 ) === '-TESTINSTALL' )
{
$values['lkey'] = mb_substr( $values['lkey'], 0, -12 );
}
$toWrite = "<?php\n\n" . '$INFO = ' . var_export( array( 'lkey' => 'LICENSE KEY GOES HERE!-123456789' ), TRUE ) . ';';
try
{
$file = @file_put_contents( \IPS\ROOT_PATH . '/conf_global.php', $toWrite );
@@ -95,17 +95,17 @@ class license extends Controller
$errorform = new Form( 'license', 'continue' );
$errorform->class = '';
$errorform->add( new TextArea( 'conf_global_error', $toWrite, FALSE ) );
foreach( $values as $k => $v )
{
$errorform->hiddenValues[ $k ] = $v;
}
Output::i()->output = Theme::i()->getTemplate( 'global' )->confWriteError( $errorform, \IPS\ROOT_PATH );
return;
}
}
Output::i()->title = Member::loggedIn()->language()->addToStack('license');
Output::i()->output = Theme::i()->getTemplate( 'global' )->block( 'license', $form, TRUE, TRUE );
}
@@ -27,7 +27,6 @@ use UnderflowException;
use function count;
use function defined;
use function is_array;
use const IPS\UPGRADE_MD5_CHECK;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -99,10 +98,6 @@ class systemcheck extends Controller
}
}
}
/* Check we have the latest version and run an md5 check */
$incorrectFiles = array();
/* Or do not check :) */
/* Display */
if ( $canProceed )
@@ -110,6 +105,6 @@ class systemcheck extends Controller
Output::i()->redirect( Url::internal("controller=license&key={$_SESSION['uniqueKey']}") );
}
Output::i()->title = Member::loggedIn()->language()->addToStack('healthcheck');
Output::i()->output = Theme::i()->getTemplate( 'global' )->healthcheck( $requirements, $incorrectFiles );
Output::i()->output = Theme::i()->getTemplate( 'global' )->healthcheck( $requirements );
}
}