Version 5.0.13
This commit is contained in:
1 parent
b89858b2ff
commit
5efe744c5f
1407 files changed
+69690
-30177
No files matched your search
@@ -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
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user