Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+35 -22
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Chart;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -46,7 +46,7 @@ class _Callback extends \IPS\Helpers\Chart\Dynamic
*/
public function __construct( \IPS\Http\Url $url, $callback=NULL, $title='', $options=array(), $defaultType='AreaChart', $defaultTimescale='monthly', $defaultTimes=array( 'start' => 0, 'end' => 0 ), $identifier='', $minimumDate=NULL )
{
$this->identifier = \substr( md5( (string) $url ), 0, 6 ) . $identifier;
$this->identifier = \substr( md5( (string) $url ), 0, 6 ) . $identifier . ( \IPS\Request::i()->chartId ?: '_default' );
$this->callback = $callback;
parent::__construct( $url, $title, $options, $defaultType, $defaultTimescale, $defaultTimes, $identifier, $minimumDate );
@@ -71,7 +71,7 @@ class _Callback extends \IPS\Helpers\Chart\Dynamic
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $name );
$filterKey = $name;
if ( !$filterable or !isset( \IPS\Request::i()->filters[ $this->identifier ] ) or in_array( $filterKey, \IPS\Request::i()->filters[ $this->identifier ] ) )
if ( !$filterable or !isset( \IPS\Request::i()->filters[ $this->identifier ] ) or \in_array( $filterKey, \IPS\Request::i()->filters[ $this->identifier ] ) )
{
if ( $this->type !== 'PieChart' and $this->type !== 'GeoChart' )
{
@@ -96,40 +96,39 @@ class _Callback extends \IPS\Helpers\Chart\Dynamic
$this->availableFilters[ $filterKey ] = $name;
}
}
/**
* Get the chart output
* Compile Data for Output
*
* @return string
* @param array $data The data
* @return void
*/
public function getOutput()
public function compileForOutput(): void
{
/* Init data */
$data = $this->initData();
/* Get rows */
$callbackFunction = $this->callback;
$rows = $callbackFunction( $this );
/* Pie Chart */
if ( $this->type === 'PieChart' or $this->type === 'GeoChart' )
{
$rows = call_user_func( $this->callback, $this );
{
foreach ( $rows as $k => $v )
{
if( count( $this->availableFilters ) and !in_array( $k, $this->currentFilters ) )
if( \count( $this->availableFilters ) and !\in_array( $k, $this->currentFilters ) )
{
continue;
}
$this->addRow( array( 'key' => $this->availableFilters[ $k ], 'value' => $v ) );
}
$output = $this->render( $this->type, $this->options, $this->format );
}
/* Graph */
else
{
$rows = call_user_func( $this->callback, $this );
foreach ( $rows as $row )
{
$result = array();
@@ -205,14 +204,14 @@ class _Callback extends \IPS\Helpers\Chart\Dynamic
}
else
{
$this->addRow( array_merge( array( $datetime ), array_fill( 0, count( $this->series ), 0 ) ) );
$this->addRow( array_merge( array( $datetime ), array_fill( 0, \count( $this->series ), 0 ) ) );
}
}
else
{
if( count($d) < count($this->series) )
if( \count($d) < \count($this->series) )
{
$this->addRow( array_merge( array( $datetime ), $d, array_fill( 0, count($this->series) - count($d), 0 ) ) );
$this->addRow( array_merge( array( $datetime ), $d, array_fill( 0, \count($this->series) - \count($d), 0 ) ) );
}
else
{
@@ -221,14 +220,28 @@ class _Callback extends \IPS\Helpers\Chart\Dynamic
}
}
if ( count( $data ) === 1 )
if ( \count( $data ) === 1 )
{
$this->options['domainAxis']['type'] = 'category';
}
$output = $this->render( $this->type, $this->options, $this->format );
}
}
return $output;
/**
* Get the chart output
*
* @return string
*/
public function getOutput()
{
$this->compileForOutput();
if ( isset( \IPS\Request::i()->download ) )
{
return $this->download();
}
else
{
return $this->render( $this->type, $this->options, $this->format );
}
}
}
+8 -8
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -84,7 +84,7 @@ class _Chart
*/
public function addRow( $values )
{
if ( count( $values ) !== count( $this->headers ) )
if ( \count( $values ) !== \count( $this->headers ) )
{
throw new \LengthException('COLUMN_COUNT_MISMATCH');
}
@@ -96,21 +96,21 @@ class _Chart
switch ( $data['type'] )
{
case 'string':
if ( !is_string( $values[ $i ] ) )
if ( !\is_string( $values[ $i ] ) )
{
throw new \InvalidArgumentException( "VALUE_{$i}_NOT_STRING" );
}
break;
case 'number':
if ( !is_numeric( $values[ $i ] ) and !is_null( $values[ $i ] ) )
if ( !\is_numeric( $values[ $i ] ) and !\is_null( $values[ $i ] ) )
{
throw new \InvalidArgumentException( "VALUE_{$i}_NOT_NUMBER" );
}
break;
case 'bool':
if ( !is_bool( $values[ $i ] ) )
if ( !\is_bool( $values[ $i ] ) )
{
throw new \InvalidArgumentException( "VALUE_{$i}_NOT_BOOL" );
}
@@ -146,11 +146,11 @@ class _Chart
{
if ( !\IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->jsFiles[] = 'https://www.google.com/jsapi';
\IPS\Output::i()->headJs .= "google.load('visualization', '1.0', {'packages':['corechart']}); ";
\IPS\Output::i()->jsFiles[] = 'https://www.gstatic.com/charts/loader.js';
\IPS\Output::i()->headJs .= "google.charts.load( '47', { 'packages':['corechart'] } );";
}
if ( !$this->numbersCanBeNegative and in_array( $type, array( 'LineChart', 'ColumnChart' ) ) and !isset( $options['vAxis']['viewWindow']['min'] ) )
if ( !$this->numbersCanBeNegative and \in_array( $type, array( 'LineChart', 'ColumnChart' ) ) and !isset( $options['vAxis']['viewWindow']['min'] ) )
{
$options['vAxis']['viewWindow']['min'] = 0;
}
+119 -64
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Chart;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -83,7 +83,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
{
$this->table = $table;
$this->dateField = $dateField;
$this->identifier = \substr( md5( $table . $dateField ), 0, 6 ) . ( $identifier ?: ( \IPS\Request::i()->chartId ?: '_default' ) );
$this->identifier = \substr( md5( $table . $dateField ), 0, 6 ) . $identifier . ( \IPS\Request::i()->chartId ?: '_default' );
if ( !empty( $tableInclude ) )
{
@@ -121,7 +121,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
$filterKey = $name;
}
if ( !$filterable or !isset( \IPS\Request::i()->filters[ $this->identifier ] ) or in_array( $filterKey, \IPS\Request::i()->filters[ $this->identifier ] ) )
if ( !$filterable or !isset( \IPS\Request::i()->filters[ $this->identifier ] ) or \in_array( $filterKey, \IPS\Request::i()->filters[ $this->identifier ] ) )
{
if ( $this->type !== 'PieChart' and $this->type !== 'GeoChart' )
{
@@ -153,21 +153,13 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
/**
* Get the chart output
* Compile Data for Output
*
* @return string
* @param array $data The data
* @return void
*/
public function getOutput()
public function compileForOutput(): void
{
/* Auto-support tables where appropriate */
if ( !empty( $this->tableInclude ) )
{
if( !array_search( 'Table', $this->availableTypes ) )
{
$this->availableTypes[] = 'Table';
}
}
/* Init data */
$data = $this->initData();
@@ -195,7 +187,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
break;
case 'weekly':
$timescale = '%Y-%u';
$timescale = '%x-%v';
break;
case 'monthly':
@@ -203,40 +195,6 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
break;
}
/* Table... */
if ( $this->type === 'Table' )
{
/* Are we filtering? */
if( $this->groupBy )
{
if( count( $this->availableFilters ) AND count( $this->currentFilters ) )
{
$where[] = array( \IPS\Db::i()->in( $this->groupBy, $this->currentFilters ) );
}
}
$table = new \IPS\Helpers\Table\Db( $this->table, $this->url, $where );
/* Reformat chart join for the table helper */
if( \is_array( $this->joins ) )
{
array_walk( $this->joins, function ( $item ) use ( $table )
{
$table->joins[] = array('from' => $item[0], 'where' => $item[1], 'type' => isset( $item[2] ) ? $item[2] : 'LEFT');
} );
}
if( count( $this->tableInclude ) )
{
$table->include = $this->tableInclude;
}
$table->parsers = $this->tableParsers;
$table->sortBy = $table->sortBy ?: $this->dateField;
$output = (string) $table;
return $output;
}
/* Pie Chart */
if ( $this->type === 'PieChart' or $this->type === 'GeoChart' )
{
@@ -252,7 +210,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
$this->groupBy
)->setKeyField( $this->groupBy )->setValueField( $key );
if( count( $this->joins ) )
if( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -262,14 +220,12 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
foreach ( $stmt as $k => $v )
{
if( count( $this->availableFilters ) and !in_array( $k, $this->currentFilters ) )
if( \count( $this->availableFilters ) and !\in_array( $k, $this->currentFilters ) )
{
continue;
}
$this->addRow( array( 'key' => $this->availableFilters[ $k ], 'value' => $v ) );
}
$output = $this->render( $this->type, $this->options, $this->format );
}
/* Graph */
@@ -277,7 +233,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
{
/* Fetch */
$fromUnixTime = "FROM_UNIXTIME( IFNULL( {$this->dateField}, 0 ) )";
if ( !$this->timezoneError and \IPS\Member::loggedIn()->timezone and in_array( \IPS\Member::loggedIn()->timezone, \DateTimeZone::listIdentifiers() ) )
if ( !$this->timezoneError and \IPS\Member::loggedIn()->timezone and \in_array( \IPS\Member::loggedIn()->timezone, \DateTimeZone::listIdentifiers() ) )
{
$fromUnixTime = "CONVERT_TZ( {$fromUnixTime}, @@session.time_zone, '" . \IPS\Db::i()->escape_string( \IPS\Member::loggedIn()->timezone ) . "' )";
}
@@ -290,7 +246,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
$this->groupBy ? array( 'time_as_alias', $this->groupBy ) : 'time_as_alias'
);
if( count( $this->joins ) )
if( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -304,7 +260,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
if( $this->groupBy )
{
if( count( $this->availableFilters ) AND !in_array( $row[ $this->groupBy ], $this->currentFilters ) )
if( \count( $this->availableFilters ) AND !\in_array( $row[ $this->groupBy ], $this->currentFilters ) )
{
continue;
}
@@ -396,7 +352,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
else
{
$this->addRow( array_merge( array( $datetime ), array_fill( 0, count( $this->series ), 0 ) ) );
$this->addRow( array_merge( array( $datetime ), array_fill( 0, \count( $this->series ), 0 ) ) );
}
}
else
@@ -413,9 +369,9 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
else
{
if( count($d) < count($this->series) )
if( \count($d) < \count($this->series) )
{
$this->addRow( array_merge( array( $datetime ), $d, array_fill( 0, count($this->series) - count($d), 0 ) ) );
$this->addRow( array_merge( array( $datetime ), $d, array_fill( 0, \count($this->series) - \count($d), 0 ) ) );
}
else
{
@@ -425,14 +381,113 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
}
if ( count( $data ) === 1 )
if ( \count( $data ) === 1 )
{
$this->options['domainAxis']['type'] = 'category';
}
$output = $this->render( $this->type, $this->options, $this->format );
}
}
/**
* Get the chart output
*
* @return string
*/
public function getOutput()
{
/* Auto-support tables where appropriate */
if ( !empty( $this->tableInclude ) )
{
if( !array_search( 'Table', $this->availableTypes ) )
{
$this->availableTypes[] = 'Table';
}
}
/* Work out where clause */
$where = $this->where;
$where[] = array( "{$this->dateField}>?", 0 );
if ( $this->start )
{
$where[] = array( "{$this->dateField}>?", $this->start->getTimestamp() );
}
if ( $this->end )
{
$where[] = array( "{$this->dateField}<?", $this->end->getTimestamp() );
}
/* Table... */
if ( $this->type === 'Table' )
{
/* Are we filtering? */
if( $this->groupBy )
{
if( \count( $this->availableFilters ) AND \count( $this->currentFilters ) )
{
$where[] = array( \IPS\Db::i()->in( $this->groupBy, $this->currentFilters ) );
}
}
return $output;
$table = new \IPS\Helpers\Table\Db( $this->table, $this->url, $where );
/* Reformat chart join for the table helper */
if( \is_array( $this->joins ) )
{
array_walk( $this->joins, function ( $item ) use ( $table )
{
$table->joins[] = array('from' => $item[0], 'where' => $item[1], 'type' => isset( $item[2] ) ? $item[2] : 'LEFT');
} );
}
if( \count( $this->tableInclude ) )
{
$table->include = $this->tableInclude;
}
$table->parsers = $this->tableParsers;
$table->sortBy = $table->sortBy ?: $this->dateField;
if ( isset( \IPS\Request::i()->download ) )
{
$headers = array();
foreach( $table->getHeaders( $table->getAdvancedSearchValues() ) AS $header )
{
if ( $header === '_buttons' )
{
continue;
}
$headers[] = array(
'label' => \IPS\Member::loggedIn()->language()->addToStack( $header ),
'type' => 'string'
);
}
$rows = array();
foreach( $table->getRows( $table->getAdvancedSearchValues() ) AS $k => $v )
{
$rows[$k] = array();
foreach( $v AS $j => $value )
{
$rows[$k][] = trim( strip_tags( (string) $value ) );
}
}
return $this->download( $headers, $rows, \IPS\Output::i()->title );
}
$output = (string) $table;
return $output;
}
$this->compileForOutput();
if ( isset( \IPS\Request::i()->download ) )
{
return $this->download();
}
else
{
return $this->render( $this->type, $this->options, $this->format );
}
}
}
+131 -14
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Chart;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -66,6 +66,11 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
* @brief Type
*/
public $type;
/**
* @brief Search term
*/
public $searchTerm = null;
/**
* @brief Available Types
@@ -102,6 +107,11 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
*/
public $timezoneError = FALSE;
/**
* @brief If we need to show a timezone warning, we usually include a link to learn more - set this to TRUE to hide the link
*/
public $hideTimezoneLink = FALSE;
/**
* @brief If set to an \IPS\DateTime instance, minimum time will be checked against this value
*/
@@ -157,11 +167,11 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
$this->title = $title;
$this->options = $options;
$this->timescale = $defaultTimescale;
$this->start = $defaultTimes['start'];
$this->end = $defaultTimes['end'];
$this->start = $defaultTimes['start'] ?: \IPS\DateTime::create()->sub( new \DateInterval('P6M') );
$this->end = $defaultTimes['end'] ?: \IPS\DateTime::create();
$this->minimumDate = $minimumDate;
if ( isset( \IPS\Request::i()->type[ $this->identifier ] ) and in_array( \IPS\Request::i()->type[ $this->identifier ], $this->availableTypes ) )
if ( isset( \IPS\Request::i()->type[ $this->identifier ] ) and \in_array( \IPS\Request::i()->type[ $this->identifier ], $this->availableTypes ) )
{
$this->type = \IPS\Request::i()->type[ $this->identifier ];
$url = $url->setQueryString( 'type', array( $this->identifier => $this->type ) );
@@ -171,7 +181,14 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
$this->type = $defaultType;
}
if ( isset( \IPS\Request::i()->timescale[ $this->identifier ] ) and in_array( \IPS\Request::i()->timescale[ $this->identifier ], array( 'hourly', 'daily', 'weekly', 'monthly' ) ) )
/* Are we searching? The chart controller should inspect this property if it supports searching to limit the series it adds. */
if( isset( $this->options['limitSearch'] ) AND isset( \IPS\Request::i()->search[ $this->identifier ] ) )
{
$this->searchTerm = \IPS\Request::i()->search[ $this->identifier ];
}
/* Change timescale */
if ( isset( \IPS\Request::i()->timescale[ $this->identifier ] ) and \in_array( \IPS\Request::i()->timescale[ $this->identifier ], array( 'hourly', 'daily', 'weekly', 'monthly' ) ) )
{
if( \IPS\Request::i()->timescale[ $this->identifier ] != 'hourly' OR ( \IPS\Request::i()->timescale[ $this->identifier ] == 'hourly' AND $this->enableHourly === TRUE ) )
{
@@ -190,13 +207,17 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
$this->addHeader( \IPS\Member::loggedIn()->language()->addToStack('date'), ( $this->timescale == 'none' OR $this->timescale == 'hourly' ) ? 'datetime' : 'date' );
}
if ( isset( \IPS\Request::i()->start[ $this->identifier ] ) and \IPS\Request::i()->start[ $this->identifier ] )
if ( isset( \IPS\Request::i()->start[ $this->identifier ] ) )
{
try
{
$originalStart = $this->start;
if ( is_numeric( \IPS\Request::i()->start[ $this->identifier ] ) )
if( !\IPS\Request::i()->start[ $this->identifier ] )
{
$this->start = 0;
}
elseif ( \is_numeric( \IPS\Request::i()->start[ $this->identifier ] ) )
{
$this->start = \IPS\DateTime::ts( \IPS\Request::i()->start[ $this->identifier ] );
}
@@ -223,11 +244,15 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
catch ( \Exception $e ) {}
}
if ( isset( \IPS\Request::i()->end[ $this->identifier ] ) and \IPS\Request::i()->end[ $this->identifier ] )
if ( isset( \IPS\Request::i()->end[ $this->identifier ] ) )
{
try
{
if ( is_numeric( \IPS\Request::i()->end[ $this->identifier ] ) )
if( !\IPS\Request::i()->end[ $this->identifier ] )
{
$this->end = \IPS\DateTime::create();
}
elseif ( \is_numeric( \IPS\Request::i()->end[ $this->identifier ] ) )
{
$this->end = \IPS\DateTime::ts( \IPS\Request::i()->end[ $this->identifier ] );
}
@@ -251,7 +276,7 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
$this->url = $url;
if ( \IPS\Member::loggedIn()->timezone and in_array( \IPS\Member::loggedIn()->timezone, \DateTimeZone::listIdentifiers() ) )
if ( \IPS\Member::loggedIn()->timezone and \in_array( \IPS\Member::loggedIn()->timezone, \DateTimeZone::listIdentifiers() ) )
{
try
{
@@ -280,6 +305,14 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
}
}
/**
* Compile Data for Output
*
* @param array $data The data
* @return void
*/
abstract public function compileForOutput(): void;
/**
* Get the chart output
*
@@ -302,7 +335,7 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
try
{
/* If we have filters, we can save them */
if( count( $this->availableFilters ) > 0 )
if( \count( $this->availableFilters ) > 0 )
{
/* We have an existing chart ID */
if( isset( \IPS\Request::i()->chartId ) AND \IPS\Request::i()->chartId != '_default' )
@@ -404,7 +437,7 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
{
$chartOutput = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->dynamicChart( $this, $output );
if( count( $this->availableFilters ) > 0 AND ( !\IPS\Request::i()->isAjax() OR ( \IPS\Request::i()->tab AND !\IPS\Request::i()->chartId ) ) )
if( \count( $this->availableFilters ) > 0 AND ( !\IPS\Request::i()->isAjax() OR ( \IPS\Request::i()->tab AND !\IPS\Request::i()->chartId ) ) )
{
return \IPS\Theme::i()->getTemplate( 'global', 'core' )->tabs( $this->getChartTabs(), ( isset( \IPS\Request::i()->chartId ) ) ? \IPS\Request::i()->chartId : NULL, $chartOutput, $this->url, 'chartId', 'ipsTabs_small ipsTabs_contained' );
}
@@ -471,7 +504,7 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
{
$filters = $this->currentFilters;
if ( in_array( $filter, $filters ) )
if ( \in_array( $filter, $filters ) )
{
unset( $filters[ array_search( $filter, $filters ) ] );
}
@@ -502,7 +535,7 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
case 'hourly':
$data[ $date->format( 'Y-n-j-h-i-s' ) ] = array();
$date->add( new \DateInterval( 'P1H' ) );
$date->add( new \DateInterval( 'PT1H' ) );
break;
case 'daily':
@@ -530,4 +563,88 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
return $data;
}
/**
* Download as CSV
*
* @param array|NULL $headers Headers, or NULL to use headers defined by this object.
* @param array|NULL $rows Rows, or NULL to use rows defined by this object.
* @param string|NULL $fileName File Name, or NULL to use the title defined by this object.
* @return void
*/
public function download( ?array $rawHeaders = NULL, ?array $rawRows = NULL, ?string $fileName = NULL ): void
{
/* Compile the data */
$file = tempnam( \IPS\TEMP_DIRECTORY, 'IPS' );
$fh = fopen( $file, 'w' );
/* Set headers */
$headers = array();
if ( $rawHeaders === NULL )
{
$rawHeaders = $this->headers;
}
foreach( $rawHeaders AS $data )
{
$headers[] = $data['label'];
}
fputcsv( $fh, $headers );
/* Set Rows */
if ( $rawRows === NULL )
{
$rawRows = $this->rows;
}
foreach( $rawRows AS $row )
{
$i = 0;
$save = array();
foreach( $rawHeaders AS $data )
{
switch( $data['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':
$save[] = ( new \IPS\DateTime( $row[$i] ) )->fullYearLocaleDate();
break;
/* Anything else can just save as-is */
default:
$save[] = $row[$i];
break;
}
$i++;
}
fputcsv( $fh, $save );
}
fclose( $fh );
$csv = \file_get_contents( $file );
if ( $fileName )
{
$name = $fileName;
}
else if ( $this->title )
{
$name = $this->title;
}
else
{
$name = \IPS\Output::i()->title;
}
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $name );
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $csv );
\IPS\Output::i()->sendOutput( $csv, 200, 'text/csv', array( 'Content-Disposition' => \IPS\Output::getContentDisposition( 'attachment', "{$name}.csv" ) ), FALSE, FALSE, FALSE );
}
}
+6 -12
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\CoverPhoto;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -37,7 +37,7 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
$form = new \IPS\Helpers\Form( 'coverPhoto' );
$form->class = 'ipsForm_vertical ipsForm_noLabels';
$form->add( new \IPS\Helpers\Form\Upload( 'cover_photo', NULL, TRUE, array( 'image' => TRUE, 'minimize' => FALSE, 'maxFileSize' => ( $photo->maxSize and $photo->maxSize != -1 ) ? $photo->maxSize / 1024 : NULL, 'storageExtension' => $this->_coverPhotoStorageExtension() ) ) );
$form->add( new \IPS\Helpers\Form\Upload( 'cover_photo', NULL, TRUE, array( 'image' => TRUE, 'allowStockPhotos' => TRUE, 'minimize' => FALSE, 'maxFileSize' => ( $photo->maxSize and $photo->maxSize != -1 ) ? $photo->maxSize / 1024 : NULL, 'storageExtension' => $this->_coverPhotoStorageExtension() ) ) );
if ( $values = $form->values() )
{
try
@@ -56,7 +56,7 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
}
}
@@ -87,7 +87,7 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
}
else
{
\IPS\Output::i()->redirect( $_SERVER['HTTP_REFERER'] );
\IPS\Output::i()->redirect( $this->_coverPhotoReturnUrl() );
}
}
@@ -114,7 +114,7 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
}
else
{
\IPS\Output::i()->redirect( $_SERVER['HTTP_REFERER'] );
\IPS\Output::i()->redirect( $this->_coverPhotoReturnUrl() );
}
}
@@ -129,7 +129,6 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
* Set Cover Photo
*
* @param \IPS\Helpers\CoverPhoto $photo New Photo
* @param string $type 'new', 'remove', 'reposition'
* @return void
*/
abstract protected function _coverPhotoSet( \IPS\Helpers\CoverPhoto $photo );
@@ -148,11 +147,6 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
*/
protected function _coverPhotoReturnUrl()
{
$returnURL = ( isset( $_SERVER['HTTP_REFERER'] ) AND $_SERVER['HTTP_REFERER'] ) ? \IPS\Http\Url::createFromString( $_SERVER['HTTP_REFERER'] ) : \IPS\Request::i()->url()->stripQueryString( 'do' );
if ( !( $returnURL instanceof \IPS\Http\Url\Internal ) )
{
$returnURL = \IPS\Request::i()->url()->stripQueryString( 'do' );
}
return $returnURL;
return \IPS\Request::i()->referrer() ?: \IPS\Request::i()->url()->stripQueryString( 'do' );
}
}
+1 -1
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
+18 -11
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -34,6 +34,7 @@ class _Address extends FormAbstract
protected $defaultOptions = array(
'minimize' => FALSE,
'requireFullAddress' => TRUE,
'preselectCountry' => TRUE,
);
/**
@@ -47,7 +48,7 @@ class _Address extends FormAbstract
if ( !$this->value OR ( $this->value instanceof \IPS\GeoLocation AND !$this->value->country ) )
{
$this->value = ( $this->value instanceof \IPS\GeoLocation ) ? $this->value : new \IPS\GeoLocation;
if ( $this->options['requireFullAddress'] and $defaultCountry = static::calculateDefaultCountry() )
if ( $this->options['preselectCountry'] and $defaultCountry = static::calculateDefaultCountry() )
{
$this->value->country = $defaultCountry;
}
@@ -63,24 +64,30 @@ class _Address extends FormAbstract
* @return string|NULL
*/
public static function calculateDefaultCountry()
{
{
if ( isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) )
{
if( mb_strpos( $_SERVER['HTTP_ACCEPT_LANGUAGE'], ',' ) )
{
$exploded = explode( ',', $_SERVER['HTTP_ACCEPT_LANGUAGE'] );
$exploded = $exploded[0];
$languages = explode( ',', $_SERVER['HTTP_ACCEPT_LANGUAGE'] );
}
else
{
$exploded = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
$languages = array( $_SERVER['HTTP_ACCEPT_LANGUAGE'] );
}
$exploded = explode( '-', $exploded );
if ( isset( $exploded[1] ) )
foreach( $languages as $language )
{
$country = mb_strtoupper( $exploded[1] );
if ( in_array( $country, \IPS\GeoLocation::$countries ) )
/* Remove quotient if it exists */
if( mb_strpos( $language, ';' ) !== FALSE )
{
$language = mb_substr( $language, 0, mb_strpos( $language, ';' ) );
}
$dashPos = mb_strpos( $language, '-' );
$country = mb_strtoupper( $dashPos ? mb_substr( $language, $dashPos + 1 ) : $language );
if ( \in_array( $country, \IPS\GeoLocation::$countries ) )
{
return $country;
}
}
@@ -110,7 +117,7 @@ class _Address extends FormAbstract
/* Create the object */
$input = parent::getValue();
$value = new \IPS\GeoLocation;
$value->addressLines = ( isset( $input['address'] ) and is_array( $input['address'] ) ) ? array_filter( $input['address'] ) : array();
$value->addressLines = ( isset( $input['address'] ) and \is_array( $input['address'] ) ) ? array_filter( $input['address'] ) : array();
if ( empty( $value->addressLines ) )
{
$value->addressLines = array( NULL );
+16 -6
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -35,7 +35,7 @@ class _Captcha extends FormAbstract
*/
public function __construct()
{
$params = func_get_args();
$params = \func_get_args();
if ( !isset( $params[0] ) )
{
$params[0] = 'captcha_field';
@@ -51,7 +51,8 @@ class _Captcha extends FormAbstract
$this->captcha = new $class;
}
call_user_func_array( 'parent::__construct', $params );
parent::__construct( ...$params );
$this->required = TRUE;
}
@@ -86,6 +87,7 @@ class _Captcha extends FormAbstract
/**
* Get HTML
*
* @param \IPS\Helpers\Form|null $form Form helper object
* @return string
*/
public function rowHtml( $form=NULL )
@@ -111,9 +113,17 @@ class _Captcha extends FormAbstract
else
{
/* If we previously did an AJAX validate which is still valid, return true */
if ( isset( $_SESSION[ 'captcha-val-' . $this->name ] ) and $_SESSION[ 'captcha-val-' . $this->name ] > ( time() - 60 ) )
$cached = NULL;
$cacheKey = 'captcha-val-' . $this->name . '-' . \IPS\Member::loggedIn()->ip_address;
try
{
unset( $_SESSION[ 'captcha-val-' . $this->name ] );
$cached = \IPS\Data\Cache::i()->getWithExpire( $cacheKey, TRUE );
}
catch( \Exception $ex ) { }
if ( $cached )
{
unset( \IPS\Data\Cache::i()->$cacheKey );
return TRUE;
}
/* Otherwise, check with service */
@@ -125,7 +135,7 @@ class _Captcha extends FormAbstract
/* If it's valid and we're doing an AJAX validate, save that in the session so the next request doesn't check again */
if ( $return and \IPS\Request::i()->isAjax() )
{
$_SESSION[ 'captcha-val-' . $this->name ] = time();
\IPS\Data\Cache::i()->storeWithExpire( $cacheKey, time(), \IPS\DateTime::create()->add( new \DateInterval( 'PT1M' ) ), TRUE );
}
/* Return */
+2 -2
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form\Captcha;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -44,7 +44,7 @@ class _Invisible
*/
public function rowHtml()
{
return $this->getHtml();
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->captchaInvisible( \IPS\Settings::i()->recaptcha2_public_key, preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short ), TRUE );
}
/**
+1 -1
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form\Captcha;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
+1 -1
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form\Captcha;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
+2 -2
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -92,6 +92,6 @@ class _Checkbox extends FormAbstract
*/
public static function stringValue( $value )
{
return intval( $value );
return \intval( $value );
}
}
+16 -9
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -25,14 +25,21 @@ class _CheckboxSet extends Select
/**
* Constructor
*
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$args = func_get_args();
$args[3]['multiple'] = TRUE;
$options['multiple'] = TRUE;
call_user_func_array( 'parent::__construct', $args );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
if ( !isset( $this->options['descriptions'] ) )
{
@@ -65,7 +72,7 @@ class _CheckboxSet extends Select
if ( $this->options['returnLabels'] )
{
$value = array();
if ( !is_array( $this->value ) )
if ( !\is_array( $this->value ) )
{
$this->value = explode( ',', $this->value );
}
@@ -81,7 +88,7 @@ class _CheckboxSet extends Select
/* If the value is NULL or an empty string, i.e. from a custom field, then we should not convert it into an array because the
value will evaluate to 0 with an == check and the first option in the checkbox set will always be selected erroneously */
if ( $this->options['unlimited'] === NULL and !is_array( $value ) AND $value !== NULL AND $value !== '' )
if ( $this->options['unlimited'] === NULL and !\is_array( $value ) AND $value !== NULL AND $value !== '' )
{
$value = array( $value );
}
@@ -105,10 +112,10 @@ class _CheckboxSet extends Select
/* We need the array keys. For custom fields we will have 0 => 1, 1 => 1 if checkboxes 0 and 1 are checked, while for Content we will have
customKey => 1, customOtherKey => 1 - we always need the array keys */
$value = is_array( $value ) ? array_keys( $value ) : array();
$value = \is_array( $value ) ? array_keys( $value ) : array();
if ( $this->options['returnLabels'] )
{
if ( is_array( $value ) )
if ( \is_array( $value ) )
{
$return = array();
foreach ( $value as $k => $v )
+18 -8
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -34,6 +34,8 @@ class _Codemirror extends TextArea
'mode' => 'php' // Formatting mode. Default is htmlmixed.
'height' => 300 // Height of code mirror editor
'preview' => 'http://...' // A URL where the value can be POSTed (as "value") and will return a preview. Defaults to NULL, which will hide the preview button.
'tagLinks' => array(), // An array of links to display next to the headers for tags.
'tagSource' => \IPS\Http\Url( ... ), // A URL that will fetch tags using AJAX.
);
* @endcode
*/
@@ -46,20 +48,28 @@ class _Codemirror extends TextArea
'mode' => 'htmlmixed',
'nullLang' => NULL,
'height' => 300,
'preview' => NULL
'preview' => NULL,
'tagLinks' => array(),
'tagSource' => NULL
);
/**
* Constructor
* Sets that the field is required if there is a minimum length and vice-versa
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
/* Call parent constructor */
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
/* We don't support this feature */
$this->options['nullLang'] = NULL;
@@ -79,9 +89,9 @@ class _Codemirror extends TextArea
{
if ( $this->options['height'] )
{
$this->options['height'] = is_numeric( $this->options['height'] ) ? $this->options['height'] . 'px' : $this->options['height'];
$this->options['height'] = \is_numeric( $this->options['height'] ) ? $this->options['height'] . 'px' : $this->options['height'];
}
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->codemirror( $this->name, $this->value, $this->required, $this->options['maxLength'], $this->options['disabled'], '', $this->options['placeholder'], $this->options['tags'], $this->options['mode'], $this->htmlId ? "{$this->htmlId}-input" : NULL, $this->options['height'], $this->options['preview'] );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->codemirror( $this->name, $this->value, $this->required, $this->options['maxLength'], $this->options['disabled'], '', $this->options['placeholder'], $this->options['tags'], $this->options['mode'], $this->htmlId ? "{$this->htmlId}-input" : NULL, $this->options['height'], $this->options['preview'], $this->options['tagLinks'], $this->options['tagSource'] );
}
}
+27 -19
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -27,24 +27,16 @@ class _Color extends FormAbstract
* @code
$defaultOptions = array(
'disabled' => FALSE, // Disables input. Default is FALSE.
'swatches' => FALSE // Shows colour swatches
'rgba' => FALSE // Show RGBA mode
);
* @endcode
*/
protected $defaultOptions = array(
'disabled' => FALSE,
'disabled' => FALSE,
'swatches' => FALSE,
'rgba' => FALSE
);
/**
* Constructor
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @return void
*/
public function __construct()
{
/* Call parent constructor */
call_user_func_array( 'parent::__construct', func_get_args() );
}
/**
* Get HTML
@@ -53,7 +45,9 @@ class _Color extends FormAbstract
*/
public function html()
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->color( $this->name, $this->value, $this->required, $this->options['disabled'] );
$swatches = NULL;
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->color( $this->name, $this->value, $this->required, $this->options['disabled'], $this->options['swatches'], $this->options['rgba'] );
}
/**
@@ -75,7 +69,7 @@ class _Color extends FormAbstract
$value = $this->value;
}
if ( mb_substr( $value, 0, 1 ) !== '#' )
if ( ! $this->options['rgba'] and mb_substr( $value, 0, 1 ) !== '#' )
{
$value = '#' . $value;
}
@@ -93,14 +87,28 @@ class _Color extends FormAbstract
{
parent::validate();
if( !$this->required AND ( !$this->value OR $this->value == '#' ) )
if( !$this->required AND ! $this->options['rgba'] AND ( !$this->value OR $this->value == '#' ) )
{
return TRUE;
}
if ( !preg_match( '/^(?:#)?(([a-f0-9]{3})|([a-f0-9]{6}))$/i', $this->value ) )
$hexPass = preg_match( '/^(?:#)?(([a-f0-9]{3})|([a-f0-9]{6}))$/i', $this->value );
$namePass = preg_match( '/^([a-z]*)$/i', $this->value );
$rgbaPass = preg_match( '/^rgba\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/mi', $this->value );
if ( $this->options['rgba'] )
{
throw new \InvalidArgumentException('form_color_bad');
if ( ! $rgbaPass and ! $namePass and ! $hexPass )
{
throw new \InvalidArgumentException('form_color_bad_rgba');
}
}
else
{
if ( ! $namePass and ! $hexPass )
{
throw new \InvalidArgumentException('form_color_bad');
}
}
}
}
+10 -5
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -45,19 +45,22 @@ class _Custom extends FormAbstract
*/
public function html()
{
return call_user_func( $this->options['getHtml'], $this );
$htmlFunction = $this->options['getHtml'];
return $htmlFunction( $this );
}
/**
* Get HTML
*
* @param \IPS\Helpers\Form|null $form Form helper object
* @return string
*/
public function rowHtml( $form=NULL )
{
if ( isset( $this->options['rowHtml'] ) )
{
return call_user_func( $this->options['rowHtml'], $this, $form );
$htmlFunction = $this->options['rowHtml'];
return $htmlFunction( $this, $form );
}
return parent::rowHtml( $form );
}
@@ -71,7 +74,8 @@ class _Custom extends FormAbstract
{
if ( $this->options['formatValue'] !== NULL )
{
return call_user_func( $this->options['formatValue'], $this );
$formatValueFunction = $this->options['formatValue'];
return $formatValueFunction( $this );
}
else
{
@@ -91,7 +95,8 @@ class _Custom extends FormAbstract
if ( $this->options['validate'] )
{
call_user_func( $this->options['validate'], $this );
$validationFunction = $this->options['validate'];
$validationFunction( $this );
}
}
}
+13 -11
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -62,16 +62,18 @@ class _Date extends FormAbstract
/**
* Constructor
* Formats the value appropriately
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool $required Required?
* @param array $options Type-specific options
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array() )
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
/* Work out the key for the time input and unlimited checkboxes */
if ( mb_strpos( $name, '[' ) !== FALSE )
@@ -86,7 +88,7 @@ class _Date extends FormAbstract
}
/* Call parent constructor */
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
/* Set min/max - hardcoded to 32bit values rather than using PHP_INT_MAX because the MySQL server may be 32-bit even if the web server is 64 */
if ( !isset( $this->options['min'] ) or $this->options['min']->getTimestamp() < -2147483648 )
@@ -175,11 +177,11 @@ class _Date extends FormAbstract
}
elseif ( $v )
{
if ( is_int( $v ) or ( $v and is_string( $v ) and preg_match( '/^[0-9]+$/', $v ) ) )
if ( \is_int( $v ) or ( $v and \is_string( $v ) and preg_match( '/^[0-9]+$/', $v ) ) )
{
return \IPS\DateTime::ts( $v, $timezone );
}
else if( is_array( $v ) )
else if( \is_array( $v ) )
{
/* When using pagination and date range, data may come as an array of the datetime object, e.g. field['start']['date']=(date)&field['start']['timezone']=(timezone) */
try
+48 -8
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -29,6 +29,11 @@ class _DateRange extends FormAbstract
$defaultOptions = array(
'start' => array( ... ),
'end' => array( ... ),
'unlimited' => -1, // If any value other than NULL is provided, an "Unlimited" checkbox will be displayed. If checked, the value specified will be sent.
'unlimitedLang' => 'unlimited', // Language string to use for unlimited checkbox label
'unlimitedToggles' => array(...), // Names of other input fields that should show/hide when the "Unlimited" checkbox is toggled.
'unlimitedToggleOn' => TRUE, // Whether the toggles should show on unlimited TRUE or FALSE. Default is TRUE.
'unlimitedChecked' => FALSE, // Whether the unlimited checkbox should be checked by default. Default is FALSE.
);
* @endcode
*/
@@ -53,6 +58,11 @@ class _DateRange extends FormAbstract
'unlimitedToggles' => array(),
'unlimitedToggleOn' => TRUE,
),
'unlimited' => NULL,
'unlimitedLang' => 'no_date_range_restriction',
'unlimitedToggles' => array(),
'unlimitedToggleOn' => TRUE,
'unlimitedChecked' => FALSE
);
/**
@@ -64,20 +74,32 @@ class _DateRange extends FormAbstract
* @brief End Date Object
*/
public $end = NULL;
/**
* @brief Name for unlimited checkbox
*/
public $unlimitedName = '';
/**
* Constructor
* Creates the two date objects
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array() )
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$this->start = new \IPS\Helpers\Form\Date( "{$name}[start]", isset( $defaultValue['start'] ) ? $defaultValue['start'] : NULL, FALSE, isset( $options['start'] ) ? $options['start'] : array() );
$this->end = new \IPS\Helpers\Form\Date( "{$name}[end]", isset( $defaultValue['end'] ) ? $defaultValue['end'] : NULL, FALSE, isset( $options['end'] ) ? $options['end'] : array() );
$this->unlimitedName = "{$name}_unlimited";
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
}
/**
@@ -93,7 +115,8 @@ class _DateRange extends FormAbstract
/**
* Format Value
*
* @return array
* @note Returns an array with keys 'start' and 'end' normally, but if using the unlimited checkbox option will return that value if checked
* @return mixed
*/
public function formatValue()
{
@@ -111,6 +134,13 @@ class _DateRange extends FormAbstract
$end->setTime( 23, 59, 59 );
}
/* Unlimited? */
$unlimitedName = $this->unlimitedName;
if ( $this->options['unlimited'] !== NULL and isset( \IPS\Request::i()->$unlimitedName ) )
{
return $this->options['unlimited'];
}
/* Return */
return array(
'start' => $start,
@@ -125,7 +155,16 @@ class _DateRange extends FormAbstract
*/
public function html()
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->dateRange( $this->start->html(), $this->end->html() );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->dateRange(
$this->start->html(),
$this->end->html(),
$this->options['unlimited'],
$this->options['unlimitedLang'],
$this->unlimitedName,
$this->options['unlimitedChecked'],
$this->options['unlimitedToggles'],
$this->options['unlimitedToggleOn']
);
}
/**
@@ -147,7 +186,8 @@ class _DateRange extends FormAbstract
if( $this->customValidationCode !== NULL )
{
call_user_func( $this->customValidationCode, $this->value );
$validationFunction = $this->customValidationCode;
$validationFunction( $this->value );
}
}
}
+209 -90
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -45,7 +45,6 @@ class _Editor extends FormAbstract
'maxLength' => FALSE, // The maximum length. Note that the content is HTML, so this isn't the maximum number of visible characters, so should only be used for database limits. The database's max allowed packet will override if smaller
'editorId' => NULL, // Passed to editorAttachments. Only necessary if the name may be changed
'allowAttachments => TRUE, // Should the editor show upload options?
'embedLocalOnly' => FALSE, // Allow local embeds only
);
* @endcode
*/
@@ -65,7 +64,7 @@ class _Editor extends FormAbstract
'maxLength' => 16777215, // The default for MEDIUMTEXT */
'editorId' => NULL,
'allowAttachments' => TRUE,
'embedLocalOnly' => FALSE
'ipsPlugins' => "ipsautolink,ipsautosave,ipsctrlenter,ipscode,ipscontextmenu,ipsemoticon,ipsimage,ipslink,ipsmentions,ipspage,ipspaste,ipspreview,ipsquote,ipsspoiler,ipsautogrow,ipssource,removeformat"
);
/**
@@ -85,18 +84,26 @@ class _Editor extends FormAbstract
/**
* Constructor
* Sets that the field is required if there is a minimum length and vice-versa
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @note $name and $htmlId must be different
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
* @throws \OutOfBoundsException
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$args = func_get_args();
$this->postKey = md5( $args[3]['autoSaveKey'] . ':' . session_id() );
$this->postKey = md5( $options['autoSaveKey'] . ':' . session_id() );
if ( \IPS\Settings::i()->giphy_enabled )
{
$this->defaultOptions['ipsPlugins'] .= ',ipsgiphy';
}
if ( isset( \IPS\Request::i()->usingEditor ) and \IPS\Request::i()->isAjax() and \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'front' )
{
\IPS\Session::i()->setUsingEditor();
@@ -106,32 +113,27 @@ class _Editor extends FormAbstract
$this->defaultOptions['controller'] = 'app=core&module=system&controller=editor';
/* Get our extension */
if ( !isset( $args[3]['allButtons'] ) or !$args[3]['allButtons'] )
if ( !isset( $options['allButtons'] ) or !$options['allButtons'] )
{
$extensions = \IPS\Application::load( $args[3]['app'] )->extensions( 'core', 'EditorLocations' );
if ( !isset( $extensions[ $args[3]['key'] ] ) )
$extensions = \IPS\Application::load( $options['app'] )->extensions( 'core', 'EditorLocations' );
if ( !isset( $extensions[ $options['key'] ] ) )
{
throw new \OutOfBoundsException( $args[3]['key'] );
throw new \OutOfBoundsException( $options['key'] );
}
$this->extension = $extensions[ $args[3]['key'] ];
if ( method_exists( $this->extension, 'embedLocalOnly' ) )
{
$args[3]['embedLocalOnly'] = $this->extension->embedLocalOnly();
}
$this->extension = $extensions[ $options['key'] ];
}
/* Don't minimize if we have a value */
$name = isset( $args[3]['editorId'] ) ? $args[3]['editorId'] : $args[0];
if ( ( !isset( $args[3]['minimizeWithContent'] ) or !$args[3]['minimizeWithContent'] ) and ( $args[1] or \IPS\Request::i()->$name or ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] ) ) )
$name = isset( $options['editorId'] ) ? $options['editorId'] : $name;
if ( ( !isset( $options['minimizeWithContent'] ) or !$options['minimizeWithContent'] ) and ( $defaultValue or \IPS\Request::i()->$name or ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] ) ) )
{
$args[3]['minimize'] = NULL;
$options['minimize'] = NULL;
}
/* Create the upload helper - if the form has been submitted, this has to be done before parent::__construct() as we need the uploader present for getValue(), but for views, we won't load until the editor is clicked */
$this->options = array_merge( $this->defaultOptions, $args[3] );
if ( $this->canAttach() )
$this->options = array_merge( $this->defaultOptions, $options );
if ( $this->canAttach() AND $this->options['allowAttachments'] )
{
if ( isset( \IPS\Request::i()->getUploader ) and \IPS\Request::i()->getUploader === $name or ( isset( \IPS\Request::i()->postKey ) and \IPS\Request::i()->postKey === $this->postKey and isset( \IPS\Request::i()->deleteFile ) ) )
{
@@ -166,14 +168,14 @@ class _Editor extends FormAbstract
}
if ( \IPS\Data\Store::i()->maxAllowedPacket )
{
if ( !isset( $args[3]['maxLength'] ) or $args[3]['maxLength'] > \IPS\Data\Store::i()->maxAllowedPacket )
if ( !isset( $options['maxLength'] ) or $options['maxLength'] > \IPS\Data\Store::i()->maxAllowedPacket )
{
$args[3]['maxLength'] = \IPS\Data\Store::i()->maxAllowedPacket;
$options['maxLength'] = \IPS\Data\Store::i()->maxAllowedPacket;
}
}
/* Go */
call_user_func_array( 'parent::__construct', $args );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
/* Preview? */
if ( \IPS\Request::i()->isAjax() and isset( \IPS\Request::i()->_previewField ) and \IPS\Request::i()->_previewField === $this->name )
@@ -181,8 +183,8 @@ class _Editor extends FormAbstract
\IPS\Output::i()->sendOutput( $this->getValue() );
}
/* Include editor JS - but not if loaded via ajax. JS loader will handle that on demand */
if( !\IPS\Request::i()->isAjax() )
/* Include editor JS - but not if this page was loaded via ajax OR if we're a guest and the editor is minimized - because the JS loader will handle that on demand */
if( ( !$this->options['minimize'] or \IPS\Member::loggedIn()->member_id ) and !\IPS\Request::i()->isAjax() )
{
if ( \IPS\IN_DEV )
{
@@ -193,6 +195,23 @@ class _Editor extends FormAbstract
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, array( \IPS\Http\Url::internal( 'applications/core/interface/ckeditor/ckeditor/ckeditor.js', 'none', NULL, array(), \IPS\Http\Url::PROTOCOL_RELATIVE ) ) );
}
}
/* And send a preload header for editor.css if we can, but not if we are using IN_DEV */
if( !\IPS\Request::i()->isAjax() AND !\IPS\IN_DEV )
{
/* If we don't have the 'timestamp' parameter cached we need to fetch it */
if( !isset( \IPS\Data\Store::i()->editorTimestamp ) OR !\IPS\Data\Store::i()->editorTimestamp )
{
$scriptCode = \file_get_contents( \IPS\ROOT_PATH . '/applications/core/interface/ckeditor/ckeditor/ckeditor.js' );
preg_match( "/\{timestamp:\"(.+?)\"/", $scriptCode, $matches );
\IPS\Data\Store::i()->editorTimestamp = $matches[1];
}
$cssUrl = \IPS\Http\Url::external( rtrim( \IPS\Settings::i()->base_url, '/' ) . '/applications/core/interface/ckeditor/ckeditor/skins/' . \IPS\Theme::i()->editor_skin . '/editor.css?t=' . \IPS\Data\Store::i()->editorTimestamp );
\IPS\Output::i()->linkTags[] = array( 'as' => 'style', 'rel' => 'preload', 'href' => (string) $cssUrl );
}
}
/**
@@ -214,7 +233,7 @@ class _Editor extends FormAbstract
{
foreach ( $rows as $rowId => $data )
{
if ( is_array( $data ) )
if ( \is_array( $data ) )
{
$allowed[ $device ][ $rowId ]['name'] = $data['name'];
foreach ( $data['items'] as $k => $v )
@@ -253,10 +272,6 @@ class _Editor extends FormAbstract
/* Clean resources in ACP */
$value = $this->value;
\IPS\Output::i()->parseFileObjectUrls( $value );
if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation == 'admin' )
{
$value = \IPS\Text\Parser::mungeResources( $value );
}
/* Fix Emoji */
$value = \IPS\Output::i()->replaceEmojiWithImages( $value );
@@ -289,7 +304,7 @@ class _Editor extends FormAbstract
/* Or if the user can't attach, just show a bar */
else
{
$attachmentArea = \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->editorAttachmentsPlaceholder( $this->name, $this->postKey, $this->noUploaderError );
$attachmentArea = \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->editorAttachmentsPlaceholder( $this->name, $this->postKey, $this->noUploaderError, $this->canUseMediaExtension() );
}
/* Display */
@@ -345,7 +360,7 @@ class _Editor extends FormAbstract
{
$value = preg_replace( '/[\x{200B}\x{2063}]/u', '', $value );
}
/* Parse value */
if ( $value )
{
@@ -357,38 +372,58 @@ class _Editor extends FormAbstract
if ( $this->uploader )
{
$inserts = array();
$fileAttachments = array();
foreach ( $this->getAttachments() as $attachment )
{
if ( !isset( $parser ) or !in_array( $attachment['attach_id'], $parser->mappedAttachments ) or array_key_exists( $attachment['attach_id'], $parser->existingAttachments ) )
if ( !isset( $parser ) or !\in_array( $attachment['attach_id'], $parser->mappedAttachments ) or array_key_exists( $attachment['attach_id'], $parser->existingAttachments ) )
{
$ext = mb_substr( $attachment['attach_file'], mb_strrpos( $attachment['attach_file'], '.' ) + 1 );
if ( in_array( mb_strtolower( $ext ), \IPS\File::$videoExtensions ) )
if ( \in_array( mb_strtolower( $ext ), \IPS\File::$videoExtensions ) )
{
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedVideo( $attachment['attach_location'], \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ) . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'], $attachment['attach_file'], \IPS\File::getMimeType( $attachment['attach_file'] ), $attachment['attach_id'] );
}
elseif ( $attachment['attach_is_image'] )
{
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedImage( $attachment['attach_location'], $attachment['attach_thumb_location'] ? $attachment['attach_thumb_location'] : $attachment['attach_location'], $attachment['attach_file'], $attachment['attach_id'] );
if ( $attachment['attach_thumb_location'] )
{
$ratio = round( ( $attachment['attach_thumb_height'] / $attachment['attach_thumb_width'] ) * 100, 2 );
$width = $attachment['attach_thumb_width'];
}
else
{
$ratio = round( ( $attachment['attach_img_height'] / $attachment['attach_img_width'] ) * 100, 2 );
$width = $attachment['attach_img_width'];
}
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedImage( $attachment['attach_location'], $attachment['attach_thumb_location'] ? $attachment['attach_thumb_location'] : $attachment['attach_location'], $attachment['attach_file'], $attachment['attach_id'], $width, $ratio );
}
else
{
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedFile( \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ) . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'], $attachment['attach_file'] );
$fileAttachments[] = \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedFile( \IPS\Http\Url::baseUrl() . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'], $attachment['attach_file'], FALSE, $attachment['attach_ext'], $attachment['attach_id'] );
}
if ( !isset( $parser ) or !in_array( $attachment['attach_id'], $parser->mappedAttachments ) )
if ( !isset( $parser ) or !\in_array( $attachment['attach_id'], $parser->mappedAttachments ) )
{
$inserts[] = array(
'attachment_id' => $attachment['attach_id'],
'location_key' => "{$this->options['app']}_{$this->options['key']}",
'id1' => ( is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][0] ) ) ? $this->options['attachIds'][0] : NULL,
'id2' => ( is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][1] ) ) ? $this->options['attachIds'][1] : NULL,
'id3' => ( is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][2] ) ) ? $this->options['attachIds'][2] : NULL,
'temp' => is_string( $this->options['attachIds'] ) ? $this->options['attachIds'] : ( $this->options['attachIds'] === NULL ? md5( $this->options['autoSaveKey'] ) : $this->options['attachIds'] )
'id1' => ( \is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][0] ) ) ? $this->options['attachIds'][0] : NULL,
'id2' => ( \is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][1] ) ) ? $this->options['attachIds'][1] : NULL,
'id3' => ( \is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][2] ) ) ? $this->options['attachIds'][2] : NULL,
'temp' => \is_string( $this->options['attachIds'] ) ? $this->options['attachIds'] : ( $this->options['attachIds'] === NULL ? md5( $this->options['autoSaveKey'] ) : $this->options['attachIds'] )
);
}
}
}
if( count( $inserts ) and !isset( \IPS\Request::i()->_previewField ) )
/* Add any file attachments on a single line */
if( \count( $fileAttachments ) )
{
$value .= str_replace( \IPS\Http\Url::baseUrl(), '<___base_url___>/', "<p>" . implode( ' ', $fileAttachments ) . "</p>" );
}
if( \count( $inserts ) and !isset( \IPS\Request::i()->_previewField ) )
{
\IPS\Db::i()->insert( 'core_attachments_map', $inserts, TRUE );
}
@@ -494,6 +529,33 @@ class _Editor extends FormAbstract
}
return $canAttach;
}
/**
* Can the member attach any of his existing media uploads?
*
* @return bool
*/
protected function canUseMediaExtension()
{
/* If we have an extension and it implicit disallows any attaachments, don't allow media extensions too , i.e. contact us form needs this */
if ( $this->extension )
{
$extensionCanAttach = $this->extension->canAttach( \IPS\Member::loggedIn(), $this );
if ( $extensionCanAttach === FALSE )
{
return FALSE;
}
}
foreach ( \IPS\Application::allExtensions( 'core', 'EditorMedia' ) as $k => $class )
{
if ( $class->count( \IPS\Member::loggedIn(), '' ) )
{
return TRUE;
}
}
return FALSE;
}
/**
* @brief Error message to display if we're not showing the uploader
@@ -521,7 +583,7 @@ class _Editor extends FormAbstract
{
try
{
$file = \IPS\File::get( 'core_Attachment', $attachment['attach_location'] );
$file = \IPS\File::get( 'core_Attachment', $attachment['attach_location'], $attachment['attach_filesize'] );
$file->attachmentThumbnailUrl = $attachment['attach_thumb_location'] ? \IPS\File::get( 'core_Attachment', $attachment['attach_thumb_location'] )->url : $file->url;
/* Reset the original filename based on the attachment record as filenames can be renamed if they contain special characters as AWS being the lowest common denominator cannot
@@ -536,49 +598,37 @@ class _Editor extends FormAbstract
}
/* Can we upload more? */
$maxTotalSize = array();
if ( \IPS\Member::loggedIn()->group['g_attach_max'] > 0 and \IPS\Member::loggedIn()->member_id )
$error = NULL;
$maxTotalSize = NULL;
if ( $maxTotalSize = static::maxTotalAttachmentSize( \IPS\Member::loggedIn(), $currentPostUsage, $error ) )
{
/* Only query logged in member's usage once per page load, even if there are more than one editor */
static $loggedInMembersAttachmentUsage = NULL;
if( $loggedInMembersAttachmentUsage === NULL )
{
$loggedInMembersAttachmentUsage = \IPS\Db::i()->select( 'SUM(attach_filesize)', 'core_attachments', array( 'attach_member_id=?', \IPS\Member::loggedIn()->member_id ) )->first();
}
$globalSpaceRemaining = ( ( \IPS\Member::loggedIn()->group['g_attach_max'] * 1024 ) - $loggedInMembersAttachmentUsage ) / 1048576;
$maxTotalSize[] = $globalSpaceRemaining;
if ( $globalSpaceRemaining <= 0 )
{
$this->noUploaderError = 'editor_used_global_space';
}
$maxTotalSize = $maxTotalSize / 1048576;
$this->noUploaderError = $error;
}
if ( \IPS\Member::loggedIn()->group['g_attach_per_post'] )
{
$perPostSpaceRemaining = ( ( \IPS\Member::loggedIn()->group['g_attach_per_post'] * 1024 ) - $currentPostUsage ) / 1048576;
$maxTotalSize[] = $perPostSpaceRemaining;
if ( $perPostSpaceRemaining <= 0 )
{
$this->noUploaderError = 'editor_used_post_space';
}
}
$maxTotalSize = count( $maxTotalSize ) ? min( $maxTotalSize ) : NULL;
/* Create the uploader */
if ( $maxTotalSize === NULL or $maxTotalSize > 0 )
{
$maxTotalSize = ( !is_null( $maxTotalSize ) ) ? round( $maxTotalSize, 2 ) : NULL;
$maxTotalSize = ( !\is_null( $maxTotalSize ) ) ? $maxTotalSize : NULL;
$postKey = $this->postKey;
$allowStockPhotos = FALSE;
if ( ( \IPS\Settings::i()->pixabay_enabled ) and ( \IPS\Settings::i()->pixabay_editor_permissions == '*' OR \IPS\Member::loggedIn()->inGroup( explode( ',', \IPS\Settings::i()->pixabay_editor_permissions ) ) ) )
{
$allowStockPhotos = TRUE;
}
$options = array(
'allowedFileTypes' => static::allowedFileExtensions(),
'template' => 'core.attachments.fileItem',
'multiple' => TRUE,
'postKey' => $this->postKey,
'storageExtension' => 'core_Attachment',
'retainDeleted' => TRUE,
'totalMaxSize' => $maxTotalSize,
'maxFileSize' => \IPS\Member::loggedIn()->group['g_attach_per_post'] ? ( \IPS\Member::loggedIn()->group['g_attach_per_post'] * 1024 ) / 1048576 : NULL,
'allowStockPhotos' => $allowStockPhotos,
'callback' => function( $file ) use ( $postKey )
{
\IPS\Db::i()->delete( 'core_files_temp', array( 'contents=?', (string) $file ) );
@@ -586,16 +636,7 @@ class _Editor extends FormAbstract
return $attachment['attach_id'];
}
);
if ( \IPS\Settings::i()->attach_allowed_types == 'all' and \IPS\Settings::i()->attach_allowed_extensions )
{
$options['allowedFileTypes'] = explode( ',', \IPS\Settings::i()->attach_allowed_extensions );
}
elseif ( \IPS\Settings::i()->attach_allowed_types == 'media' )
{
$options['allowedFileTypes'] = array_merge( \IPS\Image::$imageExtensions, \IPS\File::$videoExtensions );
}
if ( \IPS\Settings::i()->attachment_resample_size )
{
$maxImageSizes = explode( 'x', \IPS\Settings::i()->attachment_resample_size );
@@ -639,7 +680,7 @@ class _Editor extends FormAbstract
}
/* Delete the maps - Only do this for attachments that have actually been saved (if they haven't been saved, there's nothing in core_attachments_map for us to delete */
if ( isset( $this->options['attachIds'] ) and is_array( $this->options['attachIds'] ) and count( $this->options['attachIds'] ) )
if ( isset( $this->options['attachIds'] ) and \is_array( $this->options['attachIds'] ) and \count( $this->options['attachIds'] ) )
{
$where = array( array( 'location_key=?', "{$this->options['app']}_{$this->options['key']}" ), array( 'attachment_id=?', $attachment['attach_id'] ) );
$i = 1;
@@ -710,7 +751,7 @@ class _Editor extends FormAbstract
{
$existingAttachments = '';
$where = array();
if ( isset( $this->options['attachIds'] ) and is_array( $this->options['attachIds'] ) and count( $this->options['attachIds'] ) )
if ( isset( $this->options['attachIds'] ) and \is_array( $this->options['attachIds'] ) and \count( $this->options['attachIds'] ) )
{
$where = array( array( 'location_key=?', "{$this->options['app']}_{$this->options['key']}" ) );
$i = 1;
@@ -777,4 +818,82 @@ class _Editor extends FormAbstract
return TRUE;
}
/**
* Get allowed extension file types (NULL for any, empty array for none)
*
* @return array|NULL
*/
public static function allowedFileExtensions()
{
if ( \IPS\Settings::i()->attach_allowed_types == 'none' )
{
return array();
}
elseif ( \IPS\Settings::i()->attach_allowed_types == 'all' and \IPS\Settings::i()->attach_allowed_extensions )
{
return explode( ',', \IPS\Settings::i()->attach_allowed_extensions );
}
elseif ( \IPS\Settings::i()->attach_allowed_types == 'media' )
{
return array_merge( \IPS\Image::$imageExtensions, \IPS\File::$videoExtensions );
}
elseif ( \IPS\Settings::i()->attach_allowed_types == 'images' )
{
return \IPS\Image::$imageExtensions;
}
else
{
return NULL;
}
}
/**
* @var Cache used within maxTotalAttachmentSize()
*/
protected static $membersAttachmentUsage = [];
/**
* Get the maximum combined size of attachments that can be used or NULL for no limit
*
* @param \IPS\Member $member The member
* @param int $currentPostUsage Size, in bytes, of current attachments in the post
* @param string $error If a value is passed by reference, will be set to an error string if the value is 0
* @return int|NULL
*/
public static function maxTotalAttachmentSize( \IPS\Member $member, $currentPostUsage, &$error = NULL )
{
$maxTotalSize = array();
/* Get the global limit */
if ( $member->member_id and $member->group['g_attach_max'] > 0 )
{
if( !isset( $membersAttachmentUsage[ $member->member_id ] ) )
{
$membersAttachmentUsage[ $member->member_id ] = \IPS\Db::i()->select( 'SUM(attach_filesize)', 'core_attachments', array( 'attach_member_id=?', \IPS\Member::loggedIn()->member_id ) )->first();
}
$globalSpaceRemaining = ( ( $member->group['g_attach_max'] * 1024 ) - $membersAttachmentUsage[ $member->member_id ] );
$maxTotalSize[] = $globalSpaceRemaining;
if ( $globalSpaceRemaining <= 0 )
{
$error = 'editor_used_global_space';
}
}
/* Get the per post limit */
if ( $member->group['g_attach_per_post'] )
{
$perPostSpaceRemaining = ( ( $member->group['g_attach_per_post'] * 1024 ) - $currentPostUsage );
$maxTotalSize[] = $perPostSpaceRemaining;
if ( $perPostSpaceRemaining <= 0 )
{
$error = 'editor_used_post_space';
}
}
/* Return whichever is lower */
return $maxTotalSize ? min( $maxTotalSize ) : NULL;
}
}
+63 -48
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -32,8 +32,69 @@ class _Email extends Text
*/
public $childDefaultOptions = array(
'accountEmail' => FALSE,
'htmlAutocomplete' => "email",
);
/**
* Validate
*
* @param string $value The provided value (an email address)
* @param bool|\IPS\Member $accountEmail If TRUE, additional checks will be performed to ensure provided email address is acceptable for use on a user's account. If an \IPS\Member object, that member will be excluded
* @throws \InvalidArgumentException
* @throws \DomainException
* @return TRUE
*/
public static function validateEmail( $value, $accountEmail )
{
/* Check it's generally an acceptable email */
if ( $value !== '' and filter_var( $value, FILTER_VALIDATE_EMAIL ) === FALSE )
{
throw new \InvalidArgumentException('form_email_bad');
}
/* If it's for a user account, do additional checks */
if ( $accountEmail )
{
/* Check if it exists */
if ( $error = \IPS\Login::emailIsInUse( $value, ( $accountEmail instanceof \IPS\Member ) ? $accountEmail : NULL, \IPS\Member::loggedIn()->isAdmin() ) )
{
throw new \DomainException( $error );
}
/* Check Banned and Allowed Emails only if the data are not coming from an administrator */
if ( !\IPS\Member::loggedIn()->isAdmin() )
{
/* Check it's not a banned address */
foreach ( \IPS\Db::i()->select( 'ban_content', 'core_banfilters', array( "ban_type=?", 'email' ) ) as $bannedEmail )
{
if ( preg_match( '/^' . str_replace( '\*', '.*', preg_quote( $bannedEmail, '/' ) ) . '$/i', $value ) )
{
throw new \DomainException( 'form_email_banned' );
}
}
/* Check it's an allowed domain */
if ( \IPS\Settings::i()->allowed_reg_email !== '' AND $allowedEmailDomains = explode( ',', \IPS\Settings::i()->allowed_reg_email ) )
{
$matched = FALSE;
foreach ( $allowedEmailDomains AS $domain )
{
if( \mb_stripos( $value, "@" . $domain ) !== FALSE )
{
$matched = TRUE;
}
}
if ( \count( $allowedEmailDomains ) AND !$matched )
{
throw new \DomainException( 'form_email_banned' );
}
}
}
}
}
/**
* Validate
*
@@ -45,52 +106,6 @@ class _Email extends Text
{
parent::validate();
/* Check it's generally an acceptable email */
if ( $this->value !== '' and filter_var( $this->value, FILTER_VALIDATE_EMAIL ) === FALSE )
{
throw new \InvalidArgumentException('form_email_bad');
}
/* If it's for a user account, do additional checks */
if ( $this->options['accountEmail'] )
{
/* Check if it exists */
if ( $error = \IPS\Login::emailIsInUse( $this->value, ( $this->options['accountEmail'] instanceof \IPS\Member ) ? $this->options['accountEmail'] : NULL, \IPS\Member::loggedIn()->isAdmin() ) )
{
throw new \DomainException( $error );
}
/* Check Banned and Allowed Emails only if the data are not coming from an administrator */
if ( !\IPS\Member::loggedIn()->isAdmin() )
{
/* Check it's not a banned address */
foreach ( \IPS\Db::i()->select( 'ban_content', 'core_banfilters', array( "ban_type=?", 'email' ) ) as $bannedEmail )
{
if ( preg_match( '/^' . str_replace( '\*', '.*', preg_quote( $bannedEmail, '/' ) ) . '$/i', $this->value ) )
{
throw new \DomainException( 'form_email_banned' );
}
}
/* Check it's an allowed domain */
if ( \IPS\Settings::i()->allowed_reg_email !== '' AND $allowedEmailDomains = explode( ',', \IPS\Settings::i()->allowed_reg_email ) )
{
$matched = FALSE;
foreach ( $allowedEmailDomains AS $domain )
{
if( \mb_stripos( $this->value, "@" . $domain ) !== FALSE )
{
$matched = TRUE;
}
}
if ( count( $allowedEmailDomains ) AND !$matched )
{
throw new \DomainException( 'form_email_banned' );
}
}
}
}
static::validateEmail( $this->value, $this->options['accountEmail'] );
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
/**
* @brief Enumeration class for Form Builder
* @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 26 Feb 2013
*/
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Enumeration class for Form Builder
*/
class _Enum extends FormAbstract
{
/**
* @brief Default Options
@code
array(
'threshold' => 25, // Number of options before switching to a Multi-Select
)
@encode
*/
protected $defaultOptions = array(
'threshold' => 25
);
/**
* @brief Form Class
*/
protected $class;
/**
* @brief Threshold
*/
protected $threshold;
/**
* Constructor
*
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$options['multiple'] = TRUE;
$this->threshold = $options['threshold'] ?? $this->defaultOptions['threshold'];
if ( \count( $options['options'] ) >= $this->threshold )
{
$this->class = new \IPS\Helpers\Form\Select( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
}
else
{
$this->class = new \IPS\Helpers\Form\CheckboxSet( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
}
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
}
/**
* Get HTML
*
* @return string
*/
public function html()
{
return $this->class->html();
}
/**
* Get value
*
* @return array
*/
public function getValue()
{
return $this->class->getValue();
}
/**
* Validate
*
* @throws \OutOfRangeException
* @return bool
*/
public function validate()
{
return $this->class->validate();
}
}
+106 -64
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -78,7 +78,7 @@ class _Form
public $hiddenValues = array();
/**
* @brief Extra attributes for <form> tag
* @brief Extra attributes for `<form>` tag
*/
public $attributes = array();
@@ -89,7 +89,7 @@ class _Form
/**
* @brief If form has upload field, the maximum size (Needed to add enctype="multipart/form-data")
* @ntoe Only actually affects no-JS uploads, Plupload does it's own thing
* @note Only actually affects no-JS uploads, Plupload does it's own thing
*/
protected $uploadField = FALSE;
@@ -124,7 +124,7 @@ class _Form
* @param string $id Form ID
* @param string $submitLang Language string for submit button
* @param \IPS\Http\Url|NULL $action Action URL
* @param array $attributes Extra attributes for <form> tag
* @param array $attributes Extra attributes for `<form>` tag
* @return void
*/
public function __construct( $id='form', $submitLang='save', $action=NULL, $attributes=array() )
@@ -167,7 +167,10 @@ class _Form
/**
* Add Tab
*
* @param string $lang Language key
* @param string $lang Language key
* @param string|null $icon Icon to use
* @param string|null $blurbLang Language to use for the blurb
* @param string|null $css CSS class to use for the tab
* @return void
*/
public function addTab( $lang, $icon=NULL, $blurbLang=NULL, $css=NULL )
@@ -199,34 +202,42 @@ class _Form
/**
* Add Header
*
* @param string $lang Language key
* @param string $lang Language key
* @param string|NULL $after The key of element to insert after
* @param string|NULL $tab The tab to insert onto
* @return void
*/
public function addHeader( $lang )
public function addHeader( $lang, $after=NULL, $tab=NULL )
{
$this->elements[ $this->currentTab ][] = \IPS\Theme::i()->getTemplate( 'forms', 'core' )->header( $lang, "{$this->id}_header_{$lang}" );
/* Place the input into the correct position */
$this->_insert( \IPS\Theme::i()->getTemplate( 'forms', 'core' )->header( $lang, "{$this->id}_header_{$lang}" ), NULL, $tab, $after );
}
/**
* Add Seperator
*
* @param string|NULL $after The key of element to insert after
* @param string|NULL $tab The tab to insert onto
* @return void
*/
public function addSeparator()
public function addSeparator( $after=NULL, $tab=NULL )
{
$this->elements[ $this->currentTab ][] = \IPS\Theme::i()->getTemplate( 'forms', 'core', 'front' )->seperator();
/* Place the input into the correct position */
$this->_insert( \IPS\Theme::i()->getTemplate( 'forms', 'core', 'front' )->seperator(), NULL, $tab, $after );
}
/**
* Add Message Row
*
* @param string $lang Language key or formatted string to display
* @param string $css Custom CSS class(es) to apply
* @param bool $parse Set this to false if the language string passed is already formatted for display
* @param string $_id HTML ID
* @param string $lang Language key or formatted string to display
* @param string $css Custom CSS class(es) to apply
* @param bool $parse Set this to false if the language string passed is already formatted for display
* @param string $_id HTML ID
* @param string|NULL $after The key of element to insert after
* @param string|NULL $tab The tab to insert onto
* @return void
*/
public function addMessage( $lang, $css='', $parse=TRUE, $_id=NULL )
public function addMessage( $lang, $css='', $parse=TRUE, $_id=NULL, $after=NULL, $tab=NULL )
{
if ( !$_id )
{
@@ -237,18 +248,22 @@ class _Form
}
}
$this->elements[ $this->currentTab ][] = \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->message( $lang, $_id, $css, $parse );
/* Place the input into the correct position */
$this->_insert( \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->message( $lang, $_id, $css, $parse ), NULL, $tab, $after );
}
/**
* Add Html
*
* @param string $html HTML to add
* @param string $html HTML to add
* @param string|NULL $after The key of element to insert after
* @param string|NULL $tab The tab to insert onto
* @return void
*/
public function addHtml( $html )
public function addHtml( $html, $after=NULL, $tab=NULL )
{
$this->elements[ $this->currentTab ][] = $html;
/* Place the input into the correct position */
$this->_insert( $html, NULL, $tab, $after );
}
/**
@@ -267,13 +282,17 @@ class _Form
*
* @param mixed $name Name to identify matrix
* @param \IPS\Helpers\Form\Matrix $matrix The Matrix
* @param string|NULL $after The key of element to insert after
* @param string|NULL $tab The tab to insert onto
* @return void
*/
public function addMatrix( $name, $matrix )
public function addMatrix( $name, $matrix, $after=NULL, $tab=NULL )
{
$matrix->formId = $this->id;
$this->tabClasses[ $this->currentTab ] = 'ipsMatrix';
$this->elements[ $this->currentTab ][ $name ] = $matrix;
/* Place the input into the correct position */
$this->_insert( $matrix, $name, $tab, $after );
}
/**
@@ -294,47 +313,33 @@ class _Form
/**
* Add Dummy Row
*
* @param string $langKey Language key
* @param string $value Value
* @param string $desc Field description
* @param string $warning Field warning
* @param string $id Element ID
* @param string $langKey Language key
* @param string $value Value
* @param string $desc Field description
* @param string $warning Field warning
* @param string $id Element ID
* @param string|NULL $after The key of element to insert after
* @param string|NULL $tab The tab to insert onto
* @return void
*/
public function addDummy( $langKey, $value, $desc='', $warning='', $id='' )
public function addDummy( $langKey, $value, $desc='', $warning='', $id='', $after=NULL, $tab=NULL )
{
$this->elements[ $this->currentTab ][] = \IPS\Theme::i()->getTemplate( 'forms', 'core' )->row( \IPS\Member::loggedIn()->language()->addToStack( $langKey ), $value, $desc, $warning, FALSE, NULL, NULL, NULL, $id );
/* Place the input into the correct position */
$this->_insert( \IPS\Theme::i()->getTemplate( 'forms', 'core' )->row( \IPS\Member::loggedIn()->language()->addToStack( $langKey ), $value, $desc, $warning, FALSE, NULL, NULL, NULL, $id ), NULL, $tab, $after );
}
/**
* Add Input
*
* @param \IPS\Helpers\Form\FormAbstract $input Form element to add
* @param string|NULL $after The key of element to insert after
* @param string|NULL $tab The tab to insert onto
* @param string|NULL $after The key of element to insert after
* @param string|NULL $tab The tab to insert onto
* @return void
*/
public function add( $input, $after=NULL, $tab=NULL )
{
$tab = $tab ?: $this->currentTab;
if ( $after )
{
$elements = array();
foreach ( $this->elements[ $tab ] as $key => $element )
{
$elements[ $key ] = $element;
if ( $key === $after )
{
$elements[ $input->name ] = $input;
}
}
$this->elements[ $tab ] = $elements;
}
else
{
$this->elements[ $tab ][ $input->name ] = $input;
}
/* Place the input into the correct position */
$this->_insert( $input, $input->name, $tab, $after );
/* If it's a captcha field, we need to add a hidden value */
if ( $input instanceof Form\Captcha )
@@ -354,7 +359,7 @@ class _Form
$this->languageKeys[] = $input->name . '_desc';
$this->languageKeys[] = $input->name . '_warning';
if ( isset( $input->options['options'] ) and count( $input->options['options'] ) )
if ( isset( $input->options['options'] ) and \count( $input->options['options'] ) )
{
$this->languageKeys = array_merge( $this->languageKeys, array_map(
function ($v )
@@ -376,6 +381,42 @@ class _Form
}
}
}
/**
* Actually place the element in the correct position
*
* @param mixed $element Thing we are adding (could be a form input, message, etc.)
* @param string|NULL $elementKey The key of the element
* @param string|NULL $tab The tab to insert this thing into
* @param string|NULL $after The key of the element we want to insert this thing after
* @return void
*/
protected function _insert( $element, $elementKey=NULL, $tab=NULL, $after=NULL )
{
$tab = $tab ?: $this->currentTab;
if ( $after )
{
$elements = array();
foreach ( $this->elements[ $tab ] as $key => $_element )
{
$elements[ $key ] = $_element;
if ( $key === $after )
{
$elements[ $elementKey ] = $element;
}
}
$this->elements[ $tab ] = $elements;
}
elseif( $elementKey )
{
$this->elements[ $tab ][ $elementKey ] = $element;
}
else
{
$this->elements[ $tab ][] = $element;
}
}
/**
* Get HTML
@@ -385,7 +426,7 @@ class _Form
public function __toString()
{
/* Preload languages */
if ( count( $this->languageKeys ) and !( \IPS\Member::loggedIn()->language() instanceof \IPS\Lang\Setup\Lang ) )
if ( \count( $this->languageKeys ) and !( \IPS\Member::loggedIn()->language() instanceof \IPS\Lang\Setup\Lang ) )
{
try
{
@@ -408,7 +449,7 @@ class _Form
$html[ $tab ] .= \IPS\Theme::i()->getTemplate( 'forms', 'core' )->emptyRow( $element->nested(), $k );
continue;
}
if ( !is_string( $element ) and $element->error )
if ( !\is_string( $element ) and $element->error )
{
$errorTabs[] = $tab;
}
@@ -441,9 +482,9 @@ class _Form
*/
public function customTemplate( $template )
{
$args = func_get_args();
$args = \func_get_args();
if ( count( $args ) > 1 )
if ( \count( $args ) > 1 )
{
array_shift( $args );
}
@@ -453,7 +494,7 @@ class _Form
}
/* Preload languages */
if ( count( $this->languageKeys ) )
if ( \count( $this->languageKeys ) )
{
try
{
@@ -473,7 +514,7 @@ class _Form
$html[ $tab ] .= \IPS\Theme::i()->getTemplate( 'forms', 'core' )->emptyRow( $element->nested(), $k );
continue;
}
if ( !is_string( $element ) and $element->error )
if ( !\is_string( $element ) and $element->error )
{
$errorTabs[] = $tab;
}
@@ -481,7 +522,8 @@ class _Form
}
}
return call_user_func_array( $template, array_merge( $args, array( $this->id, $this->action, $this->elements, $this->hiddenValues, $this->actionButtons, $this->uploadField, $this->class, $this->attributes, $this->sidebar, $this, $errorTabs ) ) );
$templateArguments = array_merge( $args, array( $this->id, $this->action, $this->elements, $this->hiddenValues, $this->actionButtons, $this->uploadField, $this->class, $this->attributes, $this->sidebar, $this, $errorTabs ) );
return $template( ...$templateArguments );
}
/**
@@ -515,7 +557,7 @@ class _Form
$uploadCurrentFiles = array();
/* Did we submit the form? */
if( isset( \IPS\Request::i()->$name ) and \IPS\Request::i()->csrfKey === \IPS\Session::i()->csrfKey )
if( isset( \IPS\Request::i()->$name ) and \IPS\Login::compareHashes( (string) \IPS\Session::i()->csrfKey, (string) \IPS\Request::i()->csrfKey ) )
{
/* Work out which fields are being toggled by other fields */
$htmlIdsToIgnoreBecauseTheyAreHiddenByToggles = array();
@@ -561,7 +603,7 @@ class _Form
{
foreach ( $element->options['toggles'] as $toggleValue => $toggleHtmlIds )
{
$match = is_array( $element->value ) ? in_array( $toggleValue, $element->value ) : $toggleValue == $element->value;
$match = \is_array( $element->value ) ? \in_array( $toggleValue, $element->value ) : $toggleValue == $element->value;
if ( !$match )
{
$htmlIdsToIgnoreBecauseTheyAreHiddenByToggles = array_merge( $htmlIdsToIgnoreBecauseTheyAreHiddenByToggles, $toggleHtmlIds );
@@ -597,7 +639,7 @@ class _Form
/* If this is dependant on a toggle which isn't set, don't return a value so that it doesn't
trigger an error we cannot see */
if ( $element->htmlId and in_array( $element->htmlId, $htmlIdsToIgnore ) )
if ( $element->htmlId and \in_array( $element->htmlId, $htmlIdsToIgnore ) )
{
$values[ $_name ] = $stringValues ? $element::stringValue( $element->defaultValue ) : $element->defaultValue;
continue;
@@ -616,14 +658,14 @@ class _Form
if ( $element->options['retainDeleted'] )
{
$uploadRetainDeleted[] = $element->name;
if ( is_array( $element->value ) )
if ( \is_array( $element->value ) )
{
foreach( $element->value AS $value )
{
$uploadCurrentFiles[] = $value->originalFilename;
}
}
else
elseif( $element->value !== NULL )
{
$uploadCurrentFiles[] = $element->value->originalFilename;
}
@@ -685,13 +727,13 @@ class _Form
/* At this point we are about to return the values. Any uploaded files are now the responsibility of the controller, so release the hold on them */
foreach ( $uploadFieldNames as $name )
{
if ( !in_array( $name, $uploadRetainDeleted ) )
if ( !\in_array( $name, $uploadRetainDeleted ) )
{
\IPS\Db::i()->delete( 'core_files_temp', array( 'upload_key=?', md5( $name . session_id() ) ) );
}
else
{
/* If we're retaining deleted files, then remove any files that are actually are a part of the valye */
/* If we're retaining deleted files, then remove any files that are actually are a part of the value */
\IPS\Db::i()->delete( 'core_files_temp', array( "upload_key=? AND " . \IPS\Db::i()->in( 'filename', $uploadCurrentFiles ), md5( $name . session_id() ) ) );
}
}
+22 -15
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -61,6 +61,11 @@ abstract class _FormAbstract
* @brief Appears Required?
*/
public $appearRequired = FALSE;
/**
* @brief Additional CSS classnames to use on the row
*/
public $rowClasses = array();
/**
* @brief Type-Specific Options
@@ -130,14 +135,14 @@ abstract class _FormAbstract
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$this->_name = $name;
$this->required = is_null( $required ) ? FALSE : $required;
$this->appearRequired = is_null( $required ) ? TRUE : $required;
$this->required = \is_null( $required ) ? FALSE : $required;
$this->appearRequired = \is_null( $required ) ? TRUE : $required;
$this->options = array_merge( $this->defaultOptions, $options );
$this->customValidationCode = $customValidationCode;
$this->prefix = $prefix;
$this->suffix = $suffix;
$this->defaultValue = $defaultValue;
$this->htmlId = preg_replace( "/[^a-zA-Z0-9\-_]/", "_", $id );
$this->htmlId = $id ? preg_replace( "/[^a-zA-Z0-9\-_]/", "_", $id ) : NULL;
$this->setValue( TRUE );
}
@@ -234,6 +239,7 @@ abstract class _FormAbstract
/**
* Get HTML
*
* @param \IPS\Helpers\Form|null $form Form helper object
* @return string
*/
public function rowHtml( $form=NULL )
@@ -291,12 +297,12 @@ abstract class _FormAbstract
/* Some elements support an array for suffix, such as Number which supports preUnlimited and postUnlimited. We need to wipe out
the suffix here before calling the row() template, however, which only supports a string and throws an Array to string conversion error.
By this point, the element template has already ran and used the suffix if designed to */
if( is_array( $this->suffix ) )
if( \is_array( $this->suffix ) )
{
$this->suffix = '';
}
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->row( $label, $html, $desc, $warning, $this->appearRequired, $this->error, $this->prefix, $this->suffix, $this->htmlId ?: ( $form ? "{$form->id}_{$this->name}" : NULL ), $this, $form );
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->row( $label, $html, $desc, $warning, $this->appearRequired, $this->error, $this->prefix, $this->suffix, $this->htmlId ?: ( $form ? "{$form->id}_{$this->name}" : NULL ), $this, $form, $this->rowClasses );
}
catch ( \Exception $e )
{
@@ -318,7 +324,7 @@ abstract class _FormAbstract
*/
public function getLabelForAttribute()
{
return $this->name;
return $this->htmlId ?? $this->name;
}
/**
@@ -357,7 +363,7 @@ abstract class _FormAbstract
*/
public function validate()
{
if( ( $this->value === '' OR ( is_array( $this->value ) AND empty( $this->value ) ) ) and $this->required )
if( ( $this->value === '' OR ( \is_array( $this->value ) AND empty( $this->value ) ) ) and $this->required )
{
throw new \InvalidArgumentException('form_required');
}
@@ -366,13 +372,14 @@ abstract class _FormAbstract
{
if ( !static::utf8mb4Check( $this->value ) )
{
throw new \DomainException( \IPS\Member::loggedIn()->isAdmin() ? 'form_multibyte_unicode_admin' : 'form_multibyte_unicode' );
throw new \DomainException( \IPS\Member::loggedIn()->isAdmin() ? ( \IPS\CIC ? 'form_multibyte_unicode_admin_cic' : 'form_multibyte_unicode_admin' ) : 'form_multibyte_unicode' );
}
}
if( $this->customValidationCode !== NULL )
{
call_user_func( $this->customValidationCode, $this->value );
$validationFunction = $this->customValidationCode;
$validationFunction( $this->value );
}
return TRUE;
@@ -384,9 +391,9 @@ abstract class _FormAbstract
* @param mixed $value The value
* @return bool
*/
protected static function utf8mb4Check( $value )
public static function utf8mb4Check( $value )
{
if ( is_array( $value ) )
if ( \is_array( $value ) )
{
foreach ( $value as $_value )
{
@@ -396,7 +403,7 @@ abstract class _FormAbstract
}
}
}
elseif ( is_string( $value ) )
elseif ( \is_string( $value ) )
{
return (bool) !preg_match( '/[\x{10000}-\x{10FFFF}]/u', $value );
}
@@ -411,11 +418,11 @@ abstract class _FormAbstract
*/
public static function stringValue( $value )
{
if ( is_array( $value ) )
if ( \is_array( $value ) )
{
return implode( ',', array_map( function( $v )
{
if ( is_object( $v ) )
if ( \is_object( $v ) )
{
return (string) $v;
}
+8 -7
View File
@@ -12,7 +12,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -47,8 +47,8 @@ class _Ftp extends \IPS\Helpers\Form\FormAbstract
{
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_forms.js', 'nexus', 'global' ) );
$value = is_array( $this->value ) ? $this->value : json_decode( \IPS\Text\Encrypt::fromTag( $this->value )->decrypt(), TRUE );
$defaultValue = is_array( $this->defaultValue ) ? $this->defaultValue : json_decode( \IPS\Text\Encrypt::fromTag( $this->defaultValue )->decrypt(), TRUE );
$value = \is_array( $this->value ) ? $this->value : json_decode( \IPS\Text\Encrypt::fromTag( $this->value )->decrypt(), TRUE );
$defaultValue = \is_array( $this->defaultValue ) ? $this->defaultValue : json_decode( \IPS\Text\Encrypt::fromTag( $this->defaultValue )->decrypt(), TRUE );
if ( isset( $value['pw'] ) and isset( $defaultValue['pw'] ) and $value['pw'] and $value['pw'] === $defaultValue['pw'] and !$this->error )
{
$value['pw'] = '********';
@@ -68,7 +68,7 @@ class _Ftp extends \IPS\Helpers\Form\FormAbstract
if ( isset( $value['pw'] ) and $value['pw'] === '********' )
{
$defaultValue = is_array( $this->defaultValue ) ? $this->defaultValue : json_decode( \IPS\Text\Encrypt::fromTag( $this->defaultValue )->decrypt(), TRUE );
$defaultValue = \is_array( $this->defaultValue ) ? $this->defaultValue : json_decode( \IPS\Text\Encrypt::fromTag( $this->defaultValue )->decrypt(), TRUE );
$value['pw'] = $defaultValue['pw'];
}
@@ -108,8 +108,8 @@ class _Ftp extends \IPS\Helpers\Form\FormAbstract
if ( $this->value['server'] or $this->value['un'] or $this->value['pw'] )
{
/* And is it different to what it was originally, or do we need to establish the connection for custom validation? */
$defaultValue = is_array( $this->defaultValue ) ? $this->defaultValue : json_decode( \IPS\Text\Encrypt::fromTag( $this->defaultValue )->decrypt(), TRUE );
if ( $defaultValue['protocol'] != $this->value['protocol'] or $defaultValue['server'] != $this->value['server'] or $defaultValue['port'] != $this->value['port'] or $defaultValue['un'] != $this->value['un'] or $defaultValue['pw'] != $this->value['pw'] or $defaultValue['path'] != $this->value['path'] or $this->customValidationCode !== NULL )
$defaultValue = \is_array( $this->defaultValue ) ? $this->defaultValue : json_decode( \IPS\Text\Encrypt::fromTag( $this->defaultValue )->decrypt(), TRUE );
if ( !isset( $defaultValue['protocol'] ) or $defaultValue['protocol'] != $this->value['protocol'] or $defaultValue['server'] != $this->value['server'] or $defaultValue['port'] != $this->value['port'] or $defaultValue['un'] != $this->value['un'] or $defaultValue['pw'] != $this->value['pw'] or $defaultValue['path'] != $this->value['path'] or $this->customValidationCode !== NULL )
{
/* And are we supposed to be validating? */
if ( $this->options['validate'] and ( !$this->options['allowBypassValidation'] or !isset( $this->value['bypassValidation'] ) ) )
@@ -137,7 +137,8 @@ class _Ftp extends \IPS\Helpers\Form\FormAbstract
/* Do any custom validation */
if( $this->customValidationCode !== NULL )
{
call_user_func( $this->customValidationCode, $ftp );
$validationFunction = $this->customValidationCode;
$validationFunction( $ftp );
}
}
/* If not, should we? */
+271
View File
@@ -0,0 +1,271 @@
<?php
/**
* @brief Interval input class for Form Builder
* @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 3 Aug 2018
*/
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Interval input class for Form Builder
*/
class _Interval extends FormAbstract
{
/**
* @brief Seconds
*/
const SECONDS = 's';
/**
* @brief Minutes
*/
const MINUTES = 'i';
/**
* @brief Hours
*/
const HOURS = 'h';
/**
* @brief Days
*/
const DAYS = 'd';
/**
* @brief Weeks
*/
const WEEKS = 'w';
/**
* @brief A DateInterval object
*/
const INTERVAL = 'o';
/**
* @brief Default Options
* @code
$defaultOptions = array(
'valueAs' => 's', // The format that the value will be provided and returned as (see constants) - will only return whole numbers so smaller units won't be available
'min' => 0, // Minimum value in the valueAs format. NULL is no minimum. Default is 0.
'max' => 100, // Maximum number in the valueAs format. NULL is no maximum. Default is NULL.
'unlimited' => -1, // If any value other than NULL is provided, an "Unlimited" checkbox will be displayed. If checked, the value specified will be sent.
'unlimitedLang' => 'unlimited', // Language string to use for unlimited checkbox label
'unlimitedToggles' => array(...), // Names of other input fields that should show/hide when the "Unlimited" checkbox is toggled.
'unlimitedToggleOn' => TRUE, // Whether the toggles should show on unlimited checked (TRUE) or unchecked(FALSE). Default is TRUE
'valueToggles' => array(...), // Names of other input fields that should show/hide if the value is/isn't 0.
'disabled' => FALSE, // Disables input. Default is FALSE.
'endSuffix' => '...', // A suffix to go after the unlimited checkbox
);
* @endcode
*/
protected $defaultOptions = array(
'valueAs' => self::SECONDS,
'min' => 0,
'max' => NULL,
'unlimited' => NULL,
'unlimitedLang' => 'unlimited',
'unlimitedToggles' => array(),
'unlimitedToggleOn' => TRUE,
'valueToggles' => array(),
'disabled' => FALSE,
'endSuffix' => NULL,
);
/**
* Get HTML
*
* @return string
*/
public function html()
{
$valueNumber = '';
$selectedUnit = $this->options['valueAs'];
if ( $this->value !== NULL )
{
if ( $this->options['unlimited'] !== NULL and $this->value == $this->options['unlimited'] )
{
$valueNumber = $this->value;
}
else
{
if ( $this->value )
{
$valueNumber = static::convertValue( $this->value, $this->options['valueAs'], static::SECONDS );
$selectedUnit = static::bestUnit( $valueNumber );
}
else
{
$valueNumber = 0;
}
}
}
$minimum = NULL;
if ( $this->options['min'] === 0 )
{
switch ( $this->options['valueAs'] )
{
case static::WEEKS:
$minimum = 86400 * 7;
break;
case static::DAYS:
$minimum = 86400;
break;
case static::HOURS:
$minimum = 3600;
break;
case static::MINUTES:
$minimum = 60;
break;
}
}
elseif( $this->options['min'] !== NULL )
{
$minimum = static::convertValue( $this->options['min'], $this->options['valueAs'], static::SECONDS );
}
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->interval( $this->name, $valueNumber, $selectedUnit, $this->required, $this->options['unlimited'], $this->options['unlimitedLang'], $this->options['unlimitedToggles'], $this->options['unlimitedToggleOn'], $this->options['valueToggles'], $minimum, $this->options['max'] === NULL ? NULL : static::convertValue( $this->options['max'], $this->options['valueAs'], static::SECONDS ), $this->options['disabled'], $this->suffix );
}
/**
* Get Value
*
* @return mixed
*/
public function getValue()
{
$rawValue = parent::getValue();
if ( $this->options['unlimited'] !== NULL and isset( $rawValue['unlimited'] ) )
{
return $this->options['unlimited'];
}
else
{
$rawValue['val'] = str_replace( trim( ',' ), '.', $rawValue['val'] );
if ( !\is_numeric( $rawValue['val'] ) )
{
throw new \InvalidArgumentException( 'form_number_bad' );
}
return static::convertValue( $rawValue['val'], $rawValue['unit'], $this->options['valueAs'] );
}
}
/**
* Convert values
*
* @param int $value The value
* @param string $fromUnit The unit that value is in (see constants)
* @param string $toUnit The unit to convert to (see constants)
* @return int
*/
public static function convertValue( $value, $fromUnit, $toUnit )
{
/* Convert from $fromUnit to seconds */
$value = \floatval( $value );
switch ( $fromUnit )
{
case static::WEEKS:
$value *= 7;
case static::DAYS:
$value *= 24;
case static::HOURS:
$value *= 60;
case static::MINUTES:
$value *= 60;
}
/* Covert to $toUnit */
switch ( $toUnit )
{
case static::WEEKS:
$value /= 7;
case static::DAYS:
$value /= 24;
case static::HOURS:
$value /= 60;
case static::MINUTES:
$value /= 60;
}
return $value;
}
/**
* Get best unit
*
* @param int $value Number of seconds (modified by reference to the returned unit)
* @return string
*/
public static function bestUnit( &$value )
{
if ( $value >= 604800 and ( $value % 604800 ) === 0 )
{
$value /= 604800;
return static::WEEKS;
}
elseif ( $value >= 86400 and ( $value % 86400 ) === 0 )
{
$value /= 86400;
return static::DAYS;
}
elseif ( $value >= 3600 and ( $value % 3600 ) === 0 )
{
$value /= 3600;
return static::HOURS;
}
elseif ( $value >= 60 and ( $value % 60 ) === 0 )
{
$value /= 60;
return static::MINUTES;
}
else
{
return static::SECONDS;
}
}
/**
* Validate
*
* @throws \LengthException
* @return TRUE
*/
public function validate()
{
parent::validate();
if ( $this->options['unlimited'] === NULL or $this->value != $this->options['unlimited'] )
{
if ( $this->value === 0 and $this->required )
{
throw new \InvalidArgumentException('form_required');
}
if ( $this->options['min'] !== NULL and $this->value < $this->options['min'] )
{
$minValue = static::convertValue( $this->options['min'], $this->options['valueAs'], static::SECONDS );
$minUnit = static::bestUnit( $minValue );
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_interval_min_' . $minUnit, FALSE, array( 'pluralize' => array( $minValue ) ) ) );
}
if ( $this->options['max'] !== NULL and $this->value > $this->options['max'] )
{
$maxValue = static::convertValue( $this->options['max'], $this->options['valueAs'], static::SECONDS );
$maxUnit = static::bestUnit( $maxValue );
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_interval_max_' . $maxUnit, FALSE, array( 'pluralize' => array( $maxValue ) ) ) );
}
}
}
}
+19 -21
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -37,7 +37,7 @@ class _Item extends FormAbstract
'minAjaxLength' => int // Number of characters to type before selector appears
* 'where' => array // Array of additional where array( array( 'foo=?', 1 ) )
* );
* @encode
* @endcode
*/
protected $defaultOptions = array(
'class' => NULL,
@@ -49,17 +49,7 @@ class _Item extends FormAbstract
'minAjaxLength' => 3,
'where' => array()
);
/**
* Constructor
*
* @return void
*/
public function __construct()
{
call_user_func_array( 'parent::__construct', func_get_args() );
}
/**
* Get HTML
*
@@ -88,12 +78,12 @@ class _Item extends FormAbstract
$field = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['title'];
$where = array( array( $field . " LIKE CONCAT('%', ?, '%')", \IPS\Request::i()->q ) );
$idField = $class::$databaseColumnId;
if ( isset( $class::$databaseColumnMap['container'] ) and is_array( $this->options['containerIds'] ) and count( $this->options['containerIds'] ) )
if ( isset( $class::$databaseColumnMap['container'] ) and \is_array( $this->options['containerIds'] ) and \count( $this->options['containerIds'] ) )
{
$where[] = array( \IPS\Db::i()->in( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['container'], $this->options['containerIds'] ) );
}
if ( isset( $this->options['where'] ) and count( $this->options['where'] ) )
if ( isset( $this->options['where'] ) and \count( $this->options['where'] ) )
{
$where = array_merge( $where, $this->options['where'] );
}
@@ -102,7 +92,7 @@ class _Item extends FormAbstract
{
$results[] = array(
'id' => $item->$idField,
'html' => call_user_func( $template, $item )
'html' => $template( $item )
);
}
@@ -147,12 +137,20 @@ class _Item extends FormAbstract
}
else
{
$this->value = ( is_string( $this->value ) ) ? explode( ',', $this->value ) : $this->value;
/* $this->value may be a single, numeric, ID, so look for that to avoid a Countable error */
if ( \is_numeric( $this->value ) )
{
$this->value = array( $this->value );
}
else
{
$this->value = ( \is_string( $this->value ) ) ? explode( ',', $this->value ) : $this->value;
}
}
if ( count( $this->value ) )
if ( \count( $this->value ) )
{
if ( is_array( $this->options['orderResults'] ) )
if ( \is_array( $this->options['orderResults'] ) )
{
if ( isset( $itemClass::$databaseColumnMap[ $this->options['orderResults'][0] ] ) )
{
@@ -180,11 +178,11 @@ class _Item extends FormAbstract
*/
public static function stringValue( $value )
{
if ( is_array( $value ) )
if ( \is_array( $value ) )
{
return implode( ',', array_keys( $value ) );
}
elseif ( is_object( $value ) )
elseif ( \is_object( $value ) )
{
return $value->_id;
}
+13 -6
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -71,19 +71,25 @@ class _KeyValue extends FormAbstract
/**
* Constructor
* Creates the two date objects
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array() )
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$options = array_merge( $this->defaultOptions, $options );
$this->keyField = new \IPS\Helpers\Form\Text( "{$name}[key]", isset( $defaultValue['key'] ) ? $defaultValue['key'] : NULL, FALSE, isset( $options['key'] ) ? $options['key'] : array() );
$this->valueField = new \IPS\Helpers\Form\Text( "{$name}[value]", isset( $defaultValue['value'] ) ? $defaultValue['value'] : NULL, FALSE, isset( $options['value'] ) ? $options['value'] : array() );
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
}
/**
@@ -123,7 +129,8 @@ class _KeyValue extends FormAbstract
if( $this->customValidationCode !== NULL )
{
call_user_func( $this->customValidationCode, $this->value );
$validationFunction = $this->customValidationCode;
$validationFunction( $this->value );
}
}
}
+47 -15
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -82,6 +82,11 @@ class _Matrix extends \IPS\Helpers\Form
* @brief Manageable? (Rows can be added and deleted)
*/
public $manageable = TRUE;
/**
* @brief Sortable?
*/
public $sortable = FALSE;
/**
* @brief Squash fields? (values in the matrix are json_encode'd as a single value to get around max_post_vars limits)
@@ -135,7 +140,7 @@ class _Matrix extends \IPS\Helpers\Form
{
try
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->matrix( $this->id, array_keys( $this->columns ), $this->elements(), $this->action, $this->hiddenValues, $this->actionButtons, $this->langPrefix, $this->calculateWidths(), $this->manageable, $this->checkAlls, $this->checkAllRows, $this->classes, $this->showTooltips, $this->squashFields );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->matrix( $this->id, array_keys( $this->columns ), $this->elements(), $this->action, $this->hiddenValues, $this->actionButtons, $this->langPrefix, $this->calculateWidths(), $this->manageable, $this->checkAlls, $this->checkAllRows, $this->classes, $this->showTooltips, $this->squashFields, $this->sortable );
}
catch ( \Exception $e )
{
@@ -154,7 +159,7 @@ class _Matrix extends \IPS\Helpers\Form
*/
public function nested()
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->matrixNested( $this->id, array_keys( $this->columns ), $this->elements(), $this->action, $this->hiddenValues, $this->actionButtons, $this->langPrefix, $this->calculateWidths(), $this->manageable, $this->checkAlls, $this->checkAllRows, $this->classes, $this->showTooltips, $this->squashFields );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->matrixNested( $this->id, array_keys( $this->columns ), $this->elements(), $this->action, $this->hiddenValues, $this->actionButtons, $this->langPrefix, $this->calculateWidths(), $this->manageable, $this->checkAlls, $this->checkAllRows, $this->classes, $this->showTooltips, $this->squashFields, $this->sortable );
}
/**
@@ -165,12 +170,12 @@ class _Matrix extends \IPS\Helpers\Form
protected function calculateWidths()
{
$widths = $this->widths;
$width = ( ( 100 - array_sum( $this->widths ) ) / count( $this->columns ) );
$width = ( ( 100 - array_sum( $this->widths ) ) / \count( $this->columns ) );
foreach ( array_keys( $this->columns ) as $c )
{
if ( !isset( $widths[ $c ] ) )
{
$widths[ $c ] = $width;
$widths[ $c ] = number_format( $width, 2, '.', '' );
}
}
@@ -180,7 +185,7 @@ class _Matrix extends \IPS\Helpers\Form
/**
* Get elements
*
* @param $bool Get values?
* @param bool $getValues Get values?
* @return array
*/
public function elements( $getValues=TRUE )
@@ -246,7 +251,7 @@ class _Matrix extends \IPS\Helpers\Form
{
if( $pos = mb_strpos( $inputKey, '_new_' ) AND $inputKey !== '_new_' )
{
if( is_array( $inputValue ) AND isset( $inputValue[ $newId ] ) )
if( \is_array( $inputValue ) AND isset( $inputValue[ $newId ] ) )
{
/* @note This previously used an array_merge() however this was causing elements to be overwritten with a blank value in some cases */
foreach( $inputValue[ $newId ] AS $key => $value )
@@ -260,8 +265,8 @@ class _Matrix extends \IPS\Helpers\Form
}
}
$this->elements["_new_[{$i}]"] = $this->buildRow( "_new_[{$i}]", $data );
$this->addedRows[] = "_new_[{$i}]";
$this->elements["_new_[{$newId}]"] = $this->buildRow( "_new_[{$newId}]", $data );
$this->addedRows[] = "_new_[{$newId}]";
$i++;
}
}
@@ -272,6 +277,24 @@ class _Matrix extends \IPS\Helpers\Form
{
$this->elements["_new_[x]"] = $blankRow;
}
/* Do we need to order it? */
if ( $this->sortable )
{
$matrixOrderName = "{$this->id}_matrixOrder";
if ( isset( \IPS\Request::i()->$matrixOrderName ) )
{
$matrixOrderValues = \IPS\Request::i()->$matrixOrderName;
uksort( $this->elements, function( $a, $b ) use ( $matrixOrderValues ) {
if ( \in_array( $a, $matrixOrderValues ) and \in_array( $b, $matrixOrderValues ) )
{
return array_search( $a, $matrixOrderValues ) - array_search( $b, $matrixOrderValues );
}
return 0;
} );
}
}
}
return $this->elements;
@@ -287,7 +310,7 @@ class _Matrix extends \IPS\Helpers\Form
protected function buildRow( $rowId, $data )
{
$row = array();
if ( is_string( $data ) )
if ( \is_string( $data ) )
{
return $data;
}
@@ -296,7 +319,7 @@ class _Matrix extends \IPS\Helpers\Form
$inputName = "{$rowId}[{$columnName}]";
/* Create using array */
if ( is_array( $columnData ) )
if ( \is_array( $columnData ) )
{
$classname = '\IPS\Helpers\Form\\' . $columnData[0];
$row[ $columnName ] = new $classname(
@@ -309,7 +332,7 @@ class _Matrix extends \IPS\Helpers\Form
/* Create using callback function */
else
{
$row[ $columnName ] = call_user_func( $columnData, $inputName, isset( $data[ $columnName ] ) ? $data[ $columnName ] : NULL, $data );
$row[ $columnName ] = $columnData( $inputName, isset( $data[ $columnName ] ) ? $data[ $columnName ] : NULL, $data );
}
}
if ( isset( $data['_level'] ) )
@@ -331,7 +354,7 @@ class _Matrix extends \IPS\Helpers\Form
$values = array();
$name = "{$this->id}_submitted";
if( $force or ( isset( \IPS\Request::i()->$name ) and \IPS\Request::i()->csrfKey === \IPS\Session::i()->csrfKey ) )
if( $force or ( isset( \IPS\Request::i()->$name ) and \IPS\Login::compareHashes( (string) \IPS\Session::i()->csrfKey, (string) \IPS\Request::i()->csrfKey ) ) )
{
// Do we need to unsquash any values?
// Squashed values are json_encoded by javascript to prevent us exceeding max_post_vars
@@ -353,12 +376,12 @@ class _Matrix extends \IPS\Helpers\Form
foreach ( $this->elements( FALSE ) as $rowId => $columns )
{
if ( is_array( $columns ) )
if ( \is_array( $columns ) )
{
foreach ( $columns as $columnName => $element )
{
/* If this was a ehader or something, skip it */
if ( !is_object( $element ) )
if ( !\is_object( $element ) )
{
continue;
}
@@ -384,6 +407,15 @@ class _Matrix extends \IPS\Helpers\Form
$element->value = $value;
}
}
if ( isset( $element->options['nullLang'] ) )
{
$nullKey = "{$rowId}[{$columnName}_null]";
if ( $value = \IPS\Request::i()->valueFromArray( $nullKey ) )
{
$element->value = NULL;
}
}
/* Set value */
$values[ $rowId ][ $columnName ] = $element->value;
+42 -26
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -37,33 +37,38 @@ class _Member extends Text
/**
* Constructor
* Adds 'confirm' to the available options
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$args = func_get_args();
$this->defaultOptions['autocomplete'] = array(
'source' => 'app=core&module=system&controller=ajax&do=findMember',
'source' => 'app=core&module=system&controller=ajax&do=findMember',
'resultItemTemplate' => 'core.autocomplete.memberItem',
'commaTrigger' => false,
'unique' => true,
'minAjaxLength' => 3,
'disallowedCharacters' => array()
'disallowedCharacters' => array(),
'lang' => 'mem_optional',
);
if( isset( $args[3] ) and array_key_exists( 'multiple', $args[3] ) and $args[3]['multiple'] > 0 )
if( \count( $options ) and array_key_exists( 'multiple', $options ) and $options['multiple'] > 0 )
{
$this->defaultOptions['autocomplete']['maxItems'] = $args[3]['multiple'];
$this->defaultOptions['autocomplete']['maxItems'] = $options['multiple'];
}
elseif ( !isset( $args[3] ) or !array_key_exists( 'multiple', $args[3] ) )
elseif ( !array_key_exists( 'multiple', $options ) )
{
$this->defaultOptions['autocomplete']['maxItems'] = $this->childDefaultOptions['multiple'];
}
call_user_func_array( 'parent::__construct', $args );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
}
/**
@@ -74,7 +79,7 @@ class _Member extends Text
public function html()
{
$value = $this->value;
if ( is_array( $this->value ) )
if ( \is_array( $this->value ) )
{
$value = array();
foreach ( $this->value as $v )
@@ -89,7 +94,7 @@ class _Member extends Text
}
/* This value is decoded by the JS widget before use. */
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, 'text', ( $this->options['autocomplete'] AND !is_null( $value ) ) ? htmlspecialchars( $value, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE ) : $value, $this->required, $this->options['maxLength'], $this->options['size'], $this->options['disabled'], $this->options['autocomplete'], $this->options['placeholder'], NULL, $this->options['nullLang'] );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, 'text', ( $this->options['autocomplete'] AND !\is_null( $value ) ) ? htmlspecialchars( $value, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE ) : $value, $this->required, $this->options['maxLength'], $this->options['size'], $this->options['disabled'], $this->options['autocomplete'], $this->options['placeholder'], NULL, $this->options['nullLang'] );
}
/**
@@ -103,7 +108,7 @@ class _Member extends Text
{
$return = array();
foreach ( is_array( $this->value ) ? $this->value : explode( "\n", $this->value ) as $v )
foreach ( \is_array( $this->value ) ? $this->value : explode( "\n", $this->value ) as $v )
{
if ( $v instanceof \IPS\Member )
{
@@ -127,7 +132,7 @@ class _Member extends Text
if ( !empty( $return ) )
{
return ( $this->options['multiple'] === NULL or $this->options['multiple'] == 0 ) ? $return : array_slice( $return, 0, $this->options['multiple'] );
return ( $this->options['multiple'] === NULL or $this->options['multiple'] == 0 ) ? $return : \array_slice( $return, 0, $this->options['multiple'] );
}
}
@@ -144,11 +149,11 @@ class _Member extends Text
{
parent::validate();
if ( $this->value !== '' and !( $this->value instanceof \IPS\Member ) and !is_array( $this->value ) )
if ( $this->value !== '' and !( $this->value instanceof \IPS\Member ) and !\is_array( $this->value ) )
{
throw new \InvalidArgumentException('form_member_bad');
}
else if ( is_array( $this->value ) )
else if ( \is_array( $this->value ) )
{
foreach( $this->value AS $value )
{
@@ -169,19 +174,30 @@ class _Member extends Text
*/
public static function stringValue( $value )
{
if ( is_array( $value ) )
if( !\is_array( $value ) )
{
if ( !count( $value ) )
if( $value instanceof \IPS\Member )
{
return NULL;
$value = array( $value );
}
return implode( "\n", array_map( function( $v )
elseif( $value )
{
return $v->member_id;
}, $value ) );
$value = explode( "\n", $value );
}
else
{
$value = array();
}
}
if ( !\count( $value ) )
{
return NULL;
}
return $value ? (string) $value->member_id : NULL;
return implode( "\n", array_map( function( $v )
{
return $v->member_id;
}, $value ) );
}
}
+171 -57
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -29,19 +29,23 @@ class _Node extends FormAbstract
'url' => \IPS\Http\Url::internal(...) // The URL that this form element will be displayed on
'class' => '\IPS\core\Foo', // The node class
'permissionCheck' => 'add', // If a permission key is provided, only nodes that the member has that permission for will be available. Alternatively, can be a callback to return if node can be selected.
'showAllNodes' => FALSE, // Normally all nodes are shown in the AdminCP, but only nodes the current user has permission to view (regardless of permissionCheck) are shown on the front end. This option forces all nodes to be returned even on the front end. Useful in areas like the widget manager where you may wish to select a block that guests can view but moderators cannot.
'zeroVal' => 'none', // If provided, a checkbox allowing you to receive a value of 0 will be shown with the label given. Default is NULL.
'zeroValTogglesOn' => array( ... ), // Element IDs to toggle on when the zero val checkbox is checked
'zeroValTogglesOff' => array( ... ), // Element IDs to toggle on when the zero val checkbox is unchecked
'multiple' => TRUE, // If multiple values are supported. Defaults to FALSE
'subnodes' => TRUE, // Controls if subnodes should be included. Defaults to TRUE
'togglePerm' => 'edit', // If a permission key is provided, nodes that have this permission will toggle the element IDs in toggleIds on/off
'toggleIds' => array(), // Element IDs to toggle on/off when togglePerm is reached
'togglePermPBR' => FALSE, // Determines value of $considerPostBeforeRegistering when performing toggle permission check
'toggleIds' => array(), // Element IDs to toggle on when a node with 'togglePerm' permission IS selected - or, if togglePerm is NULL, an associtive array of elements to toggle when particular node IDs are selected
'toggleIdsOff' => array(), // Element IDs to toggle on when a node with 'togglePerm' permission IS NOT selected
'forceOwner' => \IPS\Member::load(), // If nodes are 'owned', the owner. NULL for currently logged in member, FALSE to not limit by owner
'where' => array(), // where clause to control which results to display
'disabledIds' => array(), // Array of disabled IDs
'noParentNodes' => 'Custom' // If a value is provided, subnodes of this class which have no parent node will be added into a pseudo-group with the title provided. e.g. custom packages in Nexus do not belong to any package group
'autoPopulate' => FALSE // Whether or not to autopopulate children of root nodes (defaults to TRUE which means the children are loaded, use FALSE to only show the parent nodes by default until they are clicked on)
'clubs' => TRUE, // If TRUE, will also show nodes inside clubs that the user can access. Defaults to FALSE.
'maxResults' => 200, // Maximum number of nodes to load. NULL to load all nodes or FALSE to respect node class definition. Defaults to FALSE.
);
* @endcode
*/
@@ -49,11 +53,14 @@ class _Node extends FormAbstract
'url' => NULL,
'class' => NULL,
'permissionCheck' => NULL,
'showAllNodes' => FALSE,
'zeroVal' => NULL,
'multiple' => FALSE,
'subnodes' => TRUE,
'togglePerm' => NULL,
'togglePermPBR' => TRUE,
'toggleIds' => array(),
'toggleIdsOff' => array(),
'zeroValTogglesOn' => array(),
'zeroValTogglesOff' => array(),
'forceOwner' => NULL,
@@ -61,17 +68,26 @@ class _Node extends FormAbstract
'disabledIds' => array(),
'noParentNodes' => NULL,
'autoPopulate' => TRUE,
'clubs' => FALSE
'clubs' => FALSE,
'maxResults' => FALSE,
);
/**
* Constructor
*
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
if ( !$this->options['url'] )
{
@@ -83,24 +99,39 @@ class _Node extends FormAbstract
{
$this->options['clubs'] = FALSE;
}
/* Do we need to respect the node class's defined max results (which is the default)? */
if( $this->options['maxResults'] === FALSE )
{
$nodeClass = $this->options['class'];
$this->options['maxResults'] = $nodeClass::$maxFormHelperResults;
}
}
/**
* Get HTML
*
* @return string
* @note $selectable controls whether the node is selectable, not whether it is provided as an option or not.
* @note $showable controls whether or not the node should be displayed at all. On the front end this is based on view permissions, and on the backend we display all nodes regardless of view permissions.
*/
public function html()
{
$nodeClass = $this->options['class'];
$permCheck = is_string( $this->options['permissionCheck'] ) ? $this->options['permissionCheck'] : NULL;
$disabledCallback = is_callable( $this->options['permissionCheck'] ) ? $this->options['permissionCheck'] : NULL;
$selectable = \is_string( $this->options['permissionCheck'] ) ? $this->options['permissionCheck'] : NULL;
$showable = $this->_getShowable();
$disabledCallback = \is_callable( $this->options['permissionCheck'] ) ? $this->options['permissionCheck'] : NULL;
if ( !$permCheck and \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation === 'front' )
if ( !$selectable and $showable and \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation === 'front' )
{
$permCheck = 'view';
$selectable = 'view';
}
/* Determine if we need a limit clause */
$limit = $this->options['maxResults'] ?
( ( isset( \IPS\Request::i()->_nodeSelectOffset ) ) ? array( (int) \IPS\Request::i()->_nodeSelectOffset, $this->options['maxResults'] ) : array( 0, $this->options['maxResults'] ) ) :
NULL;
/* Are we getting some AJAX stuff? */
if ( isset( \IPS\Request::i()->_nodeSelectName ) and \IPS\Request::i()->_nodeSelectName === $this->name )
{
@@ -117,19 +148,19 @@ class _Node extends FormAbstract
case 'children':
try
{
$node = $permCheck ? $nodeClass::loadAndCheckPerms( \IPS\Request::i()->_nodeId, 'view' ) : $nodeClass::load( \IPS\Request::i()->_nodeId );
$node = $showable ? $nodeClass::loadAndCheckPerms( \IPS\Request::i()->_nodeId, $showable ) : $nodeClass::load( \IPS\Request::i()->_nodeId );
/* Note - we must check 'view' permissions here, so that the list can properly populate even if we do not have the original permission
* - subsequent children may eventually have those permissions, so if the actual permCheck fails, add the node to the disabled list
* - where $permCheck is null, we need to keep it null for areas such as the AdminCP where view permissions should not be set.
* - where $showable is null, we need to keep it null for areas such as the AdminCP where view permissions should not be set.
*/
$children = $node->children( $permCheck === NULL ? NULL : 'view', NULL, $this->options['subnodes'], $disabled, $this->options['where'] );
$children = $node->children( $showable, NULL, $this->options['subnodes'], $disabled, $this->options['where'] );
if( $permCheck !== NULL )
if( $selectable !== NULL )
{
foreach( $children AS $child )
{
if ( !$child->can( $permCheck ) )
if ( !$child->can( $selectable ) )
{
$this->options['disabledIds'][] = $child->_id;
}
@@ -141,23 +172,34 @@ class _Node extends FormAbstract
\IPS\Output::i()->json( NULL, 404 );
}
\IPS\Output::i()->json( array( 'viewing' => $node->_id, 'title' => $node->_title, 'output' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $children, FALSE, $permCheck, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabledIds'] ) ) );
\IPS\Output::i()->json( array( 'viewing' => $node->_id, 'title' => $node->_title, 'output' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $children, FALSE, $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabledIds'], NULL, array(), NULL, $this->options['togglePermPBR'], $this->options['toggleIdsOff'] ) ) );
break;
case 'parent':
try
{
$node = $permCheck ? $nodeClass::loadAndCheckPerms( \IPS\Request::i()->_nodeId, 'view' ) : $nodeClass::load( \IPS\Request::i()->_nodeId );
$node = $showable ? $nodeClass::loadAndCheckPerms( \IPS\Request::i()->_nodeId, $showable ) : $nodeClass::load( \IPS\Request::i()->_nodeId );
$parent = $node->parent();
$children = $parent ? $parent->children( $permCheck, NULL, $this->options['subnodes'], $disabled, $this->options['where'] ) : $nodeClass::roots( NULL, NULL, $this->options['where'] );
$children = $parent ? $parent->children( $showable, NULL, $this->options['subnodes'], $disabled, $this->options['where'] ) : $nodeClass::roots( $showable, NULL, $this->options['where'] );
if( $selectable !== NULL )
{
foreach( $children AS $child )
{
if ( !$child->can( $selectable ) )
{
$this->options['disabledIds'][] = $child->_id;
}
}
}
}
catch ( \Exception $e )
{
\IPS\Output::i()->json( $e->getMessage(), 404 );
}
\IPS\Output::i()->json( array( 'viewing' => $parent ? $parent->_id : 0, 'title' => $parent ? $parent->_title : \IPS\Member::loggedIn()->language()->addToStack( $nodeClass::$nodeTitle ), 'output' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $children, FALSE, $permCheck, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabledIds'] ) ) );
\IPS\Output::i()->json( array( 'viewing' => $parent ? $parent->_id : 0, 'title' => $parent ? $parent->_title : \IPS\Member::loggedIn()->language()->addToStack( $nodeClass::$nodeTitle ), 'output' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $children, FALSE, $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabledIds'], NULL, array(), NULL, $this->options['togglePermPBR'], $this->options['toggleIdsOff'] ) ) );
break;
case 'search':
@@ -166,14 +208,14 @@ class _Node extends FormAbstract
$_results = $nodeClass::search( '_title', \IPS\Request::i()->q, '_title' );
foreach ( $_results as $node )
{
if ( ( !$permCheck or $node->can( $permCheck ) ) AND ! in_array( $node->_id, $disabled ) )
if ( ( !$showable or $node->can( $showable ) ) AND ! \in_array( $node->_id, $disabled ) )
{
$id = ( $node instanceof $nodeClass ? $node->_id : "s.{$node->_id}" );
$results[ $id ] = $node;
}
}
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $results, TRUE, $permCheck, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabled'] ) );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $results, TRUE, $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabled'], NULL, array(), NULL, $this->options['togglePermPBR'], $this->options['toggleIdsOff'] ) );
break;
}
}
@@ -187,23 +229,32 @@ class _Node extends FormAbstract
$nodes = $nodeClass::loadByOwner( $this->options['forceOwner'] ?: \IPS\Member::loggedIn(), $this->options['where'] );
if ( $this->options['clubs'] )
{
$nodes = array_merge( $nodes, $nodeClass::clubNodes( $permCheck, NULL, $this->options['where'] ) );
$nodes = array_merge( $nodes, $nodeClass::clubNodes( $showable, NULL, $this->options['where'] ) );
}
}
else
{
/* Recurse three levels in to allow for discoverability */
/* Get roots */
if ( $this->options['clubs'] )
{
$nodes = $nodeClass::rootsWithClubs( 'view', NULL, $this->options['where'] );
$nodes = $nodeClass::rootsWithClubs( $showable, NULL, $this->options['where'], $limit );
}
else
{
$nodes = $nodeClass::roots( 'view', NULL, $this->options['where'] );
$nodes = $nodeClass::roots( $showable, NULL, $this->options['where'], $limit );
}
foreach( $nodes AS $node )
/* We want to recurse to a certain amount of depth for discoverability, but if we go too crazy not only will it actually
make the control harder to use, it will have a bad performance impact. So we'll go 3 levels deep or until 300 nodes are
showing, whichever happens first */
$totalLimit = 300;
$currentCount = \count( $nodes );
if ( $currentCount < $totalLimit )
{
$this->_populate( $nodes, $disabled, 0, $node, $children, 3 );
foreach( $nodes AS $node )
{
$this->_populate( $nodes, $disabled, 0, $node, $children, 3, NULL, FALSE, $totalLimit, $currentCount );
}
}
if ( $this->options['noParentNodes'] )
{
@@ -212,7 +263,15 @@ class _Node extends FormAbstract
}
}
if ( isset( $this->options['disabled'] ) and is_array( $this->options['disabled'] ) )
/* Do we need to show a "load more" link? */
$loadMoreLink = FALSE;
if( $this->options['maxResults'] AND \count( $nodes ) == $this->options['maxResults'] )
{
$loadMoreLink = (int) \IPS\Request::i()->_nodeSelectOffset + $this->options['maxResults'];
}
if ( isset( $this->options['disabled'] ) and \is_array( $this->options['disabled'] ) )
{
$this->options['url'] = $this->options['url']->setQueryString( '_disabled', json_encode( $this->options['disabled'] ) );
@@ -234,22 +293,36 @@ class _Node extends FormAbstract
{
$selected = array();
if ( is_array( $this->value ) )
if ( \is_array( $this->value ) )
{
foreach ( $this->value as $node )
{
$title = isset( $node::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $node::$titleLangPrefix . $node->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : $node->_title;
$selected[ !( $node instanceof $nodeClass ) ? "{$node->_id}.s" : $node->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
return isset( $val::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $val::$titleLangPrefix . $val->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : $val->_title;
$selected[ !( $node instanceof $nodeClass ) ? "{$node->_id}.s" : $node->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
if( isset( $val::$titleLangPrefix ) )
{
return \IPS\Member::loggedIn()->language()->addToStack( $val::$titleLangPrefix . $val->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) );
}
else
{
return str_replace( "&#039;", "&apos;", htmlspecialchars( $val->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
}
}, iterator_to_array( $node->parents() ) ) ) );
}
}
elseif ( $this->value instanceof \IPS\Node\Model )
{
$class = get_class( $this->value );
$title = isset( $nodeClass::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $class::$titleLangPrefix . $this->value->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : $this->value->_title;
$class = \get_class( $this->value );
$title = isset( $nodeClass::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $class::$titleLangPrefix . $this->value->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : str_replace( "&#039;", "&apos;", htmlspecialchars( $this->value->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
$selected[ !( $this->value instanceof $nodeClass ) ? "{$this->value->_id}.s" : $this->value->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
return isset( $val::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $val::$titleLangPrefix . $val->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : $val->_title;
if( isset( $val::$titleLangPrefix ) )
{
return \IPS\Member::loggedIn()->language()->addToStack( $val::$titleLangPrefix . $val->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) );
}
else
{
return str_replace( "&#039;", "&apos;", htmlspecialchars( $val->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
}
}, iterator_to_array( $this->value->parents() ) ) ) );
}
@@ -262,10 +335,11 @@ class _Node extends FormAbstract
{
$options = array();
$_children = array();
$disabled = ( isset( $this->options['disabled'] ) and is_array( $this->options['disabled'] ) ) ? $this->options['disabled'] : array();
foreach ( $nodeClass::roots( 'view', NULL, $this->options['where'] ) as $root )
$disabled = ( isset( $this->options['disabled'] ) and \is_array( $this->options['disabled'] ) ) ? $this->options['disabled'] : array();
$currentCount = NULL;
foreach ( $nodeClass::roots( $showable, NULL, $this->options['where'] ) as $root )
{
$this->_populate( $options, $disabled, 0, $root, $_children, NULL, NULL, TRUE );
$this->_populate( $options, $disabled, 0, $root, $_children, NULL, NULL, TRUE, NULL, $currentCount );
}
$value = NULL;
@@ -274,22 +348,22 @@ class _Node extends FormAbstract
if ( $this->options['multiple'] )
{
$value = array();
if ( is_array( $this->value ) )
if ( \is_array( $this->value ) )
{
$value = array_keys( $this->value );
}
elseif ( is_object( $this->value ) )
elseif ( \is_object( $this->value ) )
{
$value = array( $this->value->_id );
}
}
else
{
if ( is_numeric( $this->value ) )
if ( \is_numeric( $this->value ) )
{
$value = $this->value;
}
elseif ( is_object( $this->value ) )
elseif ( \is_object( $this->value ) )
{
$value = array( $this->value->_id );
}
@@ -299,24 +373,41 @@ class _Node extends FormAbstract
$noJS = \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( $this->name . ( $this->options['multiple'] ? '[]' : '' ), $value, $this->required, $options, $this->options['multiple'], '', $disabled );
}
/* Were we just loading more? */
if( isset( \IPS\Request::i()->_nodeSelectName ) and \IPS\Request::i()->_nodeSelectName === $this->name AND isset( \IPS\Request::i()->_nodeSelect ) AND \IPS\Request::i()->_nodeSelect == 'loadMore' )
{
if ( $this->options['clubs'] )
{
\IPS\Output::i()->json( array( 'loadMore' => $loadMoreLink, 'globalOutput' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $nodes, FALSE, $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabledIds'], NULL, array(), FALSE, $this->options['togglePermPBR'], $this->options['toggleIdsOff'] ), 'clubsOutput' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $nodes, FALSE, $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabledIds'], NULL, array(), TRUE, $this->options['togglePermPBR'], $this->options['toggleIdsOff'] ) ) );
}
else
{
\IPS\Output::i()->json( array( 'loadMore' => $loadMoreLink, 'output' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $nodes, FALSE, $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabledIds'], NULL, array(), NULL, $this->options['togglePermPBR'], $this->options['toggleIdsOff'] ) ) );
}
}
/* Display */
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->node( $this->name, $selected, $this->options['multiple'], $this->options['url'], $nodeClass::$nodeTitle, $nodes, $this->options['zeroVal'], $noJS, $permCheck, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, $this->options['zeroValTogglesOn'], $this->options['zeroValTogglesOff'], $this->options['autoPopulate'], $children, $this->options['class'], $this->options['where'], is_array( $this->options['disabledIds'] ) ? $this->options['disabledIds'] : array(), $this->options['noParentNodes'], $noParentNodes, $this->options['clubs'] );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->node( $this->name, $selected, $this->options['multiple'], $this->options['url'], $nodeClass::$nodeTitle, $nodes, $this->options['zeroVal'], $noJS, $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, $this->options['zeroValTogglesOn'], $this->options['zeroValTogglesOff'], $this->options['autoPopulate'], $children, $this->options['class'], $this->options['where'], \is_array( $this->options['disabledIds'] ) ? $this->options['disabledIds'] : array(), $this->options['noParentNodes'], $noParentNodes, $this->options['clubs'], $this->options['togglePermPBR'], $this->options['toggleIdsOff'], $loadMoreLink );
}
/**
* Populate array options
*
* @param array $options The list of nodes
* @param array $nodes The list of nodes
* @param array|boolean $disabled Disabled options
* @param int $depth The current depth
* @param \IPS\Node\Model $node The node
* @param array $children Children of the node
* @param int|NULL $depthLimit How deep the recursion should go
* @param \IPS\Node\Model|NULL $parent If we are recursing on a child, this should be the parent node. Otherwise NULL if in the root.
* @param bool $noJS No-javascript fallback
* @return void
*/
protected function _populate( &$nodes, &$disabled, $depth, $node, &$children, $depthLimit = NULL, $parent = NULL, $noJS = FALSE )
protected function _populate( &$nodes, &$disabled, $depth, $node, &$children, $depthLimit = NULL, $parent = NULL, $noJS = FALSE, $totalLimit = NULL, &$currentCount )
{
if ( ( !$this->options['permissionCheck'] OR $node->can('view') ) and ( empty( $this->options['disabled'] ) or !in_array( $node->_id, $this->options['disabled'] ) ) and !$node->deleteOrMoveQueued() )
$showable = $this->_getShowable();
if ( ( !$showable OR $node->can( $showable ) ) and ( empty( $this->options['disabled'] ) or !\in_array( $node->_id, $this->options['disabled'] ) ) and !$node->deleteOrMoveQueued() )
{
if ( $noJS )
{
@@ -336,16 +427,17 @@ class _Node extends FormAbstract
if ( $this->options['permissionCheck'] )
{
if ( is_string( $this->options['permissionCheck'] ) )
if ( \is_string( $this->options['permissionCheck'] ) )
{
if ( !$node->can( $this->options['permissionCheck'] ) )
{
$disabled[] = $node->_id;
}
}
elseif ( is_callable( $this->options['permissionCheck'] ) )
elseif ( \is_callable( $this->options['permissionCheck'] ) )
{
if ( !call_user_func( $this->options['permissionCheck'], $node ) )
$permissionCheck = $this->options['permissionCheck'];
if ( !$permissionCheck( $node ) )
{
$disabled[] = $node->_id;
}
@@ -354,13 +446,33 @@ class _Node extends FormAbstract
if ( $depthLimit === NULL OR $depth < (int) $depthLimit )
{
foreach( $node->children( 'view', NULL, $this->options['subnodes'], NULL, $this->options['where'] ) AS $child )
if ( !$totalLimit or ( ( $currentCount += $node->childrenCount( $showable, NULL, $this->options['subnodes'], $this->options['where'] ) ) < $totalLimit ) )
{
$this->_populate( $nodes, $disabled, $depth + 1, $child, $children, $depthLimit, $node, $noJS );
foreach( $node->children( $showable, NULL, $this->options['subnodes'], NULL, $this->options['where'] ) AS $child )
{
$this->_populate( $nodes, $disabled, $depth + 1, $child, $children, $depthLimit, $node, $noJS, $totalLimit, $currentCount );
}
}
}
}
}
/**
* Get the "should I show this option" value
*
* @return string|NULL
*/
protected function _getShowable()
{
if( $this->options['showAllNodes'] === TRUE )
{
return NULL;
}
else
{
return ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation === 'front' ) ? 'view' : NULL;
}
}
/**
* Get Value
@@ -387,13 +499,14 @@ class _Node extends FormAbstract
*/
public function formatValue()
{
$nodeClass = $this->options['class'];
$permCheck = $this->options['permissionCheck'];
$nodeClass = $this->options['class'];
$selectable = $this->options['permissionCheck'];
$showable = $this->_getShowable();
if ( $this->value and !( $this->value instanceof \IPS\Node\Model ) )
{
$return = array();
foreach ( is_array( $this->value ) ? $this->value : explode( ',', $this->value ) as $v )
foreach ( \is_array( $this->value ) ? $this->value : explode( ',', $this->value ) as $v )
{
if ( $v instanceof \IPS\Node\Model )
{
@@ -404,9 +517,10 @@ class _Node extends FormAbstract
try
{
$exploded = explode( '.', $v );
$node = call_user_func( array( ( isset( $exploded[1] ) and $exploded[1] === 's' ) ? $nodeClass::$subnodeClass : $nodeClass, 'load' ), $exploded[0] );
$classToUse = ( isset( $exploded[1] ) and $exploded[1] === 's' ) ? $nodeClass::$subnodeClass : $nodeClass;
$node = $classToUse::load( $exploded[0] );
if ( !$permCheck or ( is_string( $permCheck ) and $node->can( $permCheck ) ) or ( is_callable( $permCheck ) and call_user_func( $permCheck, $node ) ) )
if ( ( !$showable or $node->can( $showable ) ) and !$selectable or ( \is_string( $selectable ) and $node->can( $selectable ) ) or ( \is_callable( $selectable ) and $selectable( $node ) ) )
{
$return[ isset( $exploded[1] ) ? "s{$node->_id}" : $node->_id ] = $node;
}
@@ -437,7 +551,7 @@ class _Node extends FormAbstract
public function validate()
{
/* We return a NULL value instead of an empty string, so we need to check that if field is required */
if( ( $this->value === NULL OR ( is_array( $this->value ) AND empty( $this->value ) ) ) and $this->required )
if( ( $this->value === NULL OR ( \is_array( $this->value ) AND empty( $this->value ) ) ) and $this->required )
{
throw new \InvalidArgumentException('form_required');
}
@@ -453,11 +567,11 @@ class _Node extends FormAbstract
*/
public static function stringValue( $value )
{
if ( is_array( $value ) )
if ( \is_array( $value ) )
{
return implode( ',', array_keys( $value ) );
}
elseif ( is_object( $value ) )
elseif ( \is_object( $value ) )
{
return $value->_id;
}
+17 -16
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -58,16 +58,18 @@ class _Number extends FormAbstract
/**
* Constructor
* Formats the value appropriately
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool $required Required?
* @param array $options Type-specific options
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array() )
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
/* Specify there's a value if the unlimited element contains a value */
$unlimitedName = "{$name}_unlimited";
@@ -77,21 +79,20 @@ class _Number extends FormAbstract
}
/* Work out the step */
$args = func_get_args();
if ( !isset( $options['step'] ) and isset( $options['decimals'] ) )
{
if ( $options['decimals'] === TRUE )
{
$args[3]['step'] = 'any';
$options['step'] = 'any';
}
else
{
$args[3]['step'] = 1 / ( pow( 10, $options['decimals'] ) );
$options['step'] = 1 / ( pow( 10, $options['decimals'] ) );
}
}
/* Call parent constructor */
call_user_func_array( 'parent::__construct', $args );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
}
/**
@@ -185,7 +186,7 @@ class _Number extends FormAbstract
$value = str_replace( trim( \IPS\Member::loggedIn()->language()->locale['decimal_point'] ), '.', $value );
/* If it's not numeric, throw an exception */
if ( ( !is_numeric( $value ) and $value !== '' and $this->required === FALSE ) or ( !is_numeric( $value ) and $this->required === TRUE ) )
if ( ( !\is_numeric( $value ) and $value !== '' and $this->required === FALSE ) or ( !\is_numeric( $value ) and $this->required === TRUE ) )
{
throw new \InvalidArgumentException( 'form_number_bad' );
}
@@ -193,15 +194,15 @@ class _Number extends FormAbstract
/* Convert to int/float */
if ( $this->options['decimals'] )
{
$value = floatval( $value );
if ( is_int( $this->options['decimals'] ) )
$value = \floatval( $value );
if ( \is_int( $this->options['decimals'] ) )
{
$value = round( $value, $this->options['decimals'] );
}
}
else
{
$value = intval( $value );
$value = \intval( $value );
}
/* Return */
+53 -9
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -26,19 +26,28 @@ class _Password extends Text
* @brief Default Options
* @code
$childDefaultOptions = array(
'protect' => FALSE, // If true, will return value as an object that can be cast to a string rather than the string itself. This should be used for user passwords to avoid them showing in logs.
'validateFor' => \IPS\Member::loggedIn(), // If an \IPS\Member object is provided, the password will be checked if it is valid for that account. Default is NULL. Note that it is possible a user may not have any available password-based login handler, so this should only be used when *changing* a password.
'confirm' => 'password1', // If the name of another element in the form is provided, will check is the values match. Default is NULL.
'showMeter' => FALSE, // Show a "strength" meter
'minimumStrength => 3 // If a strength is provided validation will fail for any lesser value.
'minimumStrength => 3, // If a strength is provided validation will fail for any lesser value
'checkStrength' => FALSE, // Whether or not to verify the password strength
'strengthMember' => NULL, // If an \IPS\Member object is provided and minimumStrength is set, the password will be checked against the member's email address and username to ensure it does not match
'strengthRequest' => array() // If minimumStrength is set, request keys can be supplied in this array and the password will be checked against the request to ensure it does not match any of the values
);
* @endcode
*/
public $childDefaultOptions = array(
'protect' => FALSE,
'validateFor' => NULL,
'confirm' => NULL,
'showMeter' => FALSE,
'minimumStrength' => NULL,
'checkStrength' => FALSE
'checkStrength' => FALSE,
'strengthMember' => NULL,
'strengthRequest' => array(),
'htmlAutocomplete' => "current-password",
);
/**
@@ -48,8 +57,12 @@ class _Password extends Text
*/
public function getValue()
{
$value = parent::getValue();
if ( $this->options['protect'] )
{
return \IPS\Request::i()->protect( $this->name );
}
$value = parent::getValue();
if ( $value === '********' and $this->defaultValue )
{
$value = $this->defaultValue;
@@ -69,11 +82,16 @@ class _Password extends Text
parent::validate();
/* Password length */
if ( mb_strlen( $this->value ) < 3 AND ( $this->required OR $this->value ) )
if ( mb_strlen( $this->value ) < 3 AND ( $this->required OR (string) $this->value ) )
{
throw new \InvalidArgumentException( 'err_password_length' );
}
if ( mb_strlen( $this->value ) > 72 )
{
throw new \InvalidArgumentException( 'err_password_toolong' );
}
/* Does the password meet the minimum required strength? */
if ( $this->options['checkStrength'] === TRUE )
{
@@ -82,7 +100,33 @@ class _Password extends Text
require_once \IPS\ROOT_PATH . "/system/3rd_party/phpass/phpass.php";
$phpass = new \PasswordStrength();
if ( $this->options['showMeter'] and \IPS\Settings::i()->password_strength_meter_enforce and $phpass->classify( $this->value ) < $this->options['minimumStrength'] )
$score = NULL;
if( $this->options['strengthMember'] AND $this->options['strengthMember'] instanceof \IPS\Member )
{
if( (string) $this->value == $this->options['strengthMember']->name OR (string) $this->value == $this->options['strengthMember']->email )
{
$score = $phpass::STRENGTH_VERY_WEAK;
}
}
if( \count( $this->options['strengthRequest'] ) )
{
foreach( $this->options['strengthRequest'] as $requestKey )
{
if( isset( \IPS\Request::i()->$requestKey ) AND (string) $this->value == \IPS\Request::i()->$requestKey )
{
$score = $phpass::STRENGTH_VERY_WEAK;
}
}
}
if( $score === NULL AND $this->options['showMeter'] and \IPS\Settings::i()->password_strength_meter_enforce )
{
$score = $phpass->classify( (string) $this->value );
}
if ( $this->options['showMeter'] and \IPS\Settings::i()->password_strength_meter_enforce and $score < $this->options['minimumStrength'] )
{
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('err_password_strength', FALSE, array( 'sprintf' => \IPS\Member::loggedIn()->language()->addToStack( 'strength_' . \IPS\Settings::i()->password_strength_option ) ) ) );
}
@@ -113,7 +157,7 @@ class _Password extends Text
if ( $this->options['confirm'] !== NULL )
{
$confirmKey = $this->options['confirm'];
if ( $this->value !== \IPS\Request::i()->$confirmKey )
if ( (string) $this->value !== (string) \IPS\Request::i()->$confirmKey )
{
throw new \InvalidArgumentException( 'form_password_confirm' );
}
@@ -132,7 +176,7 @@ class _Password extends Text
{
/* 10/19/15 - adding htmlspecialchars around value if autocomplete is enabled so that html tag characters can be used (e.g. for members) */
/* This value is decoded by the JS widget before use. */
if( $this->options['autocomplete'] and !empty( $this->value ) and is_array( $this->value ) )
if( $this->options['autocomplete'] and !empty( $this->value ) and \is_array( $this->value ) )
{
foreach( $this->value as $key => $value )
{
@@ -140,6 +184,6 @@ class _Password extends Text
}
}
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, $this->formType, ( $this->value and $this->value === $this->defaultValue and !$this->error ) ? '********' : $this->value, $this->required, $this->options['maxLength'], $this->options['size'], $this->options['disabled'], $this->options['autocomplete'], $this->options['placeholder'], NULL, $this->options['nullLang'], $this->htmlId, $this->options['showMeter'] );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, $this->formType, ( $this->value and $this->value === $this->defaultValue and !$this->error ) ? '********' : $this->value, $this->required, $this->options['maxLength'], $this->options['size'], $this->options['disabled'], $this->options['autocomplete'], $this->options['placeholder'], NULL, $this->options['nullLang'], $this->htmlId, $this->options['showMeter'], $this->options['htmlAutocomplete'], $this->options['strengthMember'], $this->options['strengthRequest'] );
}
}
+6 -6
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -48,7 +48,7 @@ class _Poll extends FormAbstract
{
$json = array();
if ( $this->value AND is_array( $this->value->choices ) AND count( $this->value->choices ) )
if ( $this->value AND \is_array( $this->value->choices ) AND \count( $this->value->choices ) )
{
foreach ( $this->value->choices as $k => $choice )
{
@@ -158,11 +158,11 @@ EOF;
}
/* Do we have at least one question? */
if ( !count( $this->value->choices ) )
if ( !\count( $this->value->choices ) )
{
throw new \InvalidArgumentException('form_poll_no_questions');
}
if ( count( $this->value->choices ) > \IPS\Settings::i()->max_poll_questions )
if ( \count( $this->value->choices ) > \IPS\Settings::i()->max_poll_questions )
{
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('form_poll_too_many_questions', FALSE, array( 'pluralize' => array( \IPS\Settings::i()->max_poll_questions ) ) ) );
}
@@ -170,11 +170,11 @@ EOF;
/* Do all the questions have at least 2 options? */
foreach ( $this->value->choices as $question )
{
if ( count( $question['choice'] ) < 2 )
if ( \count( $question['choice'] ) < 2 )
{
throw new \InvalidArgumentException('form_poll_too_few_answers');
}
if ( count( $question['choice'] ) > \IPS\Settings::i()->max_poll_choices )
if ( \count( $question['choice'] ) > \IPS\Settings::i()->max_poll_choices )
{
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('form_poll_too_many_answers', FALSE, array( 'sprintf' => array( \IPS\Settings::i()->max_poll_choices ) ) ) );
}
+16 -15
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -25,11 +25,20 @@ class _Radio extends Select
/**
* Constructor
*
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
if ( !isset( $this->options['descriptions'] ) )
{
$this->options['descriptions'] = array();
@@ -71,17 +80,9 @@ class _Radio extends Select
{
foreach ( $this->options['options'] as $k => $v )
{
$key = "{$v}_desc";
if ( \IPS\Member::loggedIn()->language()->checkKeyExists( $key ) )
{
$descriptions[ $k ] = \IPS\Member::loggedIn()->language()->addToStack( $key );
}
$descriptions[ $k ] = \IPS\Member::loggedIn()->language()->addToStack( "{$v}_desc", FALSE, array( 'returnBlank' => TRUE, 'returnInto' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->rowDesc( NULL, NULL ), $this, NULL ) );
$key = "{$v}_warning";
if ( \IPS\Member::loggedIn()->language()->checkKeyExists( $key ) )
{
$warnings[ $k ] = \IPS\Member::loggedIn()->language()->addToStack( $key );
}
$warnings[ $k ] = \IPS\Member::loggedIn()->language()->addToStack( "{$v}_warning", FALSE, array( 'returnBlank' => TRUE, 'returnInto' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->rowWarning( NULL, NULL, FALSE, NULL, NULL, NULL, $this->name . '_' . $k ), $this, NULL ) );
}
}
@@ -101,7 +102,7 @@ class _Radio extends Select
if ( $this->options['parse'] === 'image' )
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->radioImages( $this->name, $value, $this->required, $this->options['options'], $this->options['disabled'], $this->options['toggles'], $descriptions, $warnings, $this->options['userSuppliedInput'], $this->options['unlimited'], $this->options['unlimitedLang'], $this->htmlId );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->radioImages( $this->name, $value, $this->required, $this->options['options'], $this->options['disabled'], $this->options['toggles'], $descriptions, $warnings, $this->options['userSuppliedInput'], $this->options['unlimited'], $this->options['unlimitedLang'], $this->htmlId, $this->options['gridspan'] );
}
else
{
@@ -140,7 +141,7 @@ class _Radio extends Select
$value = parent::getValue();
/* Disabled radio fields do not submit a value to the server */
if( $this->options['disabled'] === TRUE or ( is_array( $this->options['disabled'] ) and in_array( $value, $this->options['disabled'] ) ) )
if( $this->options['disabled'] === TRUE or ( \is_array( $this->options['disabled'] ) and \in_array( $value, $this->options['disabled'] ) ) )
{
return $this->defaultValue;
}
+2 -2
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -55,7 +55,7 @@ class _Rating extends FormAbstract
*/
public function formatValue()
{
return $this->value ? intval( $this->value ) : NULL;
return $this->value ? \intval( $this->value ) : NULL;
}
/**
+1 -1
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
+24 -22
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -32,6 +32,7 @@ class _Select extends FormAbstract
'class' => '', // CSS class
'disabled' => FALSE, // Disables input. Default is FALSE.
'parse' => 'lang', // Sets how the values for options should be parsed. Acceptable values are "lang" (language keys), "normal" (htmlentities), "raw" (no parsing) or "image" (image URLs, only for Radio fields). Default is "lang".
'gridspan' => 3, // If 'parse' is set to 'image', this controls the gridspan to use when creating the option layout
'unlimited' => -1, // If any value other than NULL is provided, an "Unlimited" checkbox will be displayed. If checked, the value specified will be sent.
'unlimitedLang' => 'unlimited', // Language string to use for unlimited checkbox label
'unlimitedToggles' => array(...), // Names of other input fields that should show/hide when the "Unlimited" checkbox is toggled.
@@ -50,6 +51,7 @@ class _Select extends FormAbstract
'class' => '',
'disabled' => FALSE,
'parse' => 'lang',
'gridspan' => 3,
'unlimited' => NULL,
'unlimitedLang' => 'all',
'unlimitedToggles' => array(),
@@ -63,28 +65,28 @@ class _Select extends FormAbstract
/**
* Constructor
*
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool $required Required?
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
/* Set the default value to the first option if one isn't provided */
if ( $defaultValue === NULL and ( !isset( $options['noDefault'] ) OR !$options['noDefault'] ) and is_array( $options['options'] ) )
if ( $defaultValue === NULL and ( !isset( $options['noDefault'] ) OR !$options['noDefault'] ) and \is_array( $options['options'] ) )
{
foreach ( $options['options'] as $k => $v )
{
if ( is_array( $v ) )
if ( \is_array( $v ) )
{
foreach ( $v as $_k => $_v )
{
if ( !isset( $options['disabled'] ) OR ( is_array( $options['disabled'] ) AND !in_array( $_k, $options['disabled'] ) ) )
if ( !isset( $options['disabled'] ) OR ( \is_array( $options['disabled'] ) AND !\in_array( $_k, $options['disabled'] ) ) )
{
$defaultValue = $_k;
break;
@@ -93,7 +95,7 @@ class _Select extends FormAbstract
}
else
{
if ( !isset( $options['disabled'] ) OR ( is_array( $options['disabled'] ) AND !in_array( $k, $options['disabled'] ) ) )
if ( !isset( $options['disabled'] ) OR ( \is_array( $options['disabled'] ) AND !\in_array( $k, $options['disabled'] ) ) )
{
$defaultValue = $k;
break;
@@ -108,7 +110,7 @@ class _Select extends FormAbstract
}
/* Call parent constructor */
call_user_func( 'parent::__construct', $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
}
/**
@@ -129,7 +131,7 @@ class _Select extends FormAbstract
if ( $this->options['returnLabels'] and ( $this->options['unlimited'] === NULL or $this->value !== $this->options['unlimited'] ) )
{
$value = array();
if ( is_array( $this->value ) )
if ( \is_array( $this->value ) )
{
foreach ( $this->value as $v )
{
@@ -162,7 +164,7 @@ class _Select extends FormAbstract
case 'lang':
foreach ( $this->options['options'] as $k => $v )
{
if ( is_array( $v ) )
if ( \is_array( $v ) )
{
foreach ( $v as $x => $y )
{
@@ -179,7 +181,7 @@ class _Select extends FormAbstract
case 'normal':
foreach ( $this->options['options'] as $k => $v )
{
if ( is_array( $v ) )
if ( \is_array( $v ) )
{
foreach ( $v as $x => $y )
{
@@ -223,14 +225,14 @@ class _Select extends FormAbstract
}
}
if ( isset( $value ) AND is_array( $value ) AND $this->options['multiple'] AND array_search( '__EMPTY', $value ) !== FALSE )
if ( isset( $value ) AND \is_array( $value ) AND $this->options['multiple'] AND array_search( '__EMPTY', $value ) !== FALSE )
{
unset( $value[ array_search( '__EMPTY', $value ) ] );
}
if ( $this->options['returnLabels'] )
{
if ( is_array( $value ) )
if ( \is_array( $value ) )
{
$return = array();
foreach ( $value as $k => $v )
@@ -276,7 +278,7 @@ class _Select extends FormAbstract
}
foreach ( $this->options['options'] as $k => $v )
{
if ( is_array( $v ) )
if ( \is_array( $v ) )
{
$acceptableValues = array_merge( $acceptableValues, $this->options['returnLabels'] ? $v : array_keys( $v ) );
}
@@ -286,10 +288,10 @@ class _Select extends FormAbstract
}
}
$disabledValues = ( is_array( $this->options['disabled'] ) ) ? $this->options['disabled'] : array( $this->options['disabled'] );
$disabledValues = ( \is_array( $this->options['disabled'] ) ) ? $this->options['disabled'] : ( $this->options['disabled'] ? array( $this->options['disabled'] ) : array() );
$acceptableValues = array_diff( $acceptableValues, $disabledValues );
$diff = array_diff( ( is_array( $this->value ) ? $this->value : array( $this->value ) ), $acceptableValues );
$diff = array_diff( ( \is_array( $this->value ) ? $this->value : array( $this->value ) ), $acceptableValues );
if ( !empty( $diff ) )
{
/* Is the entire thing disabled? If so, just return any default value set (if one has been set)*/
+14 -8
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -43,18 +43,24 @@ class _SocialGroup extends Member
/**
* Constructor
* Sets that the field is required if there is a minimum length and vice-versa
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
/* Call parent constructor */
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
/* If we are editing, the value will be a group ID we need to load */
if( is_int( $this->value ) )
if( \is_int( $this->value ) )
{
$this->groupId = $this->value;
@@ -73,7 +79,7 @@ class _SocialGroup extends Member
{
$this->options['owner'] = \IPS\Member::loggedIn();
}
else if( !$this->options['owner'] instanceof \IPS\Member AND is_int( $this->options['owner'] ) )
else if( !$this->options['owner'] instanceof \IPS\Member AND \is_int( $this->options['owner'] ) )
{
$this->options['owner'] = \IPS\Member::load( $this->options['owner'] );
}
@@ -104,7 +110,7 @@ class _SocialGroup extends Member
$inserts[] = array( 'group_id' => $this->groupId, 'member_id' => $member->member_id );
}
if( count( $inserts ) )
if( \count( $inserts ) )
{
\IPS\Db::i()->insert( 'core_sys_social_group_members', $inserts );
}
+34 -23
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -23,28 +23,12 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
class _Sort extends FormAbstract
{
/**
* Constructor
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool $required Required?
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
* @brief Default Options
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
call_user_func_array( 'parent::__construct', func_get_args() );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-ui.js', 'core', 'interface' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-touchpunch.js', 'core', 'interface' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery.menuaim.js', 'core', 'interface' ) );
}
protected $defaultOptions = array(
'checkboxes' => NULL
);
/**
* Get HTML
*
@@ -52,7 +36,11 @@ class _Sort extends FormAbstract
*/
public function html()
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->sort( $this->name, $this->value );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-ui.js', 'core', 'interface' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-touchpunch.js', 'core', 'interface' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery.menuaim.js', 'core', 'interface' ) );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->sort( $this->name, $this->value, $this->options );
}
/**
@@ -70,4 +58,27 @@ class _Sort extends FormAbstract
parent::validate();
}
/**
* Get Value
*
* @return mixed
*/
public function getValue()
{
$value = parent::getValue();
$checkboxName = preg_replace( '/^(.+?\[?.+?)(\])?$/', '$1_checkboxes$2', $this->name );
if ( $this->options['checkboxes'] and \IPS\Request::i()->$checkboxName )
{
foreach ( $value as $k => $v )
{
$value[ $k ] = isset( \IPS\Request::i()->$checkboxName[ $k ] );
}
return $value;
}
else
{
return $value;
}
}
}
+15 -16
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -33,12 +33,11 @@ class _Stack extends FormAbstract
);
/**
* Constructor: Store the form element to stack
* Constructor
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool $required Required?
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
@@ -47,8 +46,8 @@ class _Stack extends FormAbstract
* @return void
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
call_user_func_array( 'parent::__construct', func_get_args() );
{
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-ui.js', 'core', 'interface' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-touchpunch.js', 'core', 'interface' ) );
@@ -57,7 +56,7 @@ class _Stack extends FormAbstract
/* Test for javascript disabled add stack */
if ( $_SERVER['REQUEST_METHOD'] === 'POST' )
{
if ( ( \IPS\Request::i()->valueFromArray('form_remove_stack') !== NULL OR isset( \IPS\Request::i()->form_add_stack ) ) and \IPS\Request::i()->csrfKey === \IPS\Session::i()->csrfKey )
if ( ( \IPS\Request::i()->valueFromArray('form_remove_stack') !== NULL OR isset( \IPS\Request::i()->form_add_stack ) ) and \IPS\Login::compareHashes( (string) \IPS\Session::i()->csrfKey, (string) \IPS\Request::i()->csrfKey ) )
{
$this->reloadForm = true;
}
@@ -76,13 +75,13 @@ class _Stack extends FormAbstract
$remove = \IPS\Request::i()->valueFromArray('form_remove_stack');
/* The JS fallback needs a unique key for the field to remove */
$remove = ( is_array( $remove ) ) ? key( $remove ) : null;
$remove = ( \is_array( $remove ) ) ? key( $remove ) : null;
if( count($this->value) )
if( \count($this->value) )
{
foreach( $this->value as $k => $v )
{
$class = ( new $classType( $this->name . '[' . count($fields) . ']', $v, FALSE, $this->options, $this->customValidationCode, $this->prefix, $this->suffix, $this->htmlId . '_' . count($fields) ) );
$class = ( new $classType( $this->name . '[' . \count($fields) . ']', $v, FALSE, $this->options, $this->customValidationCode, $this->prefix, $this->suffix, $this->htmlId . '_' . \count($fields) ) );
$class->setValue( TRUE );
$html = $class->html();
@@ -94,7 +93,7 @@ class _Stack extends FormAbstract
}
else
{
$class = ( new $classType( $this->name . '[0]', NULL, FALSE, $this->options, $this->customValidationCode, $this->prefix, $this->suffix, $this->htmlId . '_' . count($fields) ) );
$class = ( new $classType( $this->name . '[0]', NULL, FALSE, $this->options, $this->customValidationCode, $this->prefix, $this->suffix, $this->htmlId . '_' . \count($fields) ) );
$class->setValue( TRUE );
$html = $class->html();
@@ -107,7 +106,7 @@ class _Stack extends FormAbstract
/* We hit the add stack button with JS disabled */
if ( $this->reloadForm === TRUE AND $remove === NULL )
{
$class = ( new $classType( $this->name . '[]', NULL, FALSE, $this->options, $this->customValidationCode, $this->prefix, $this->suffix, $this->htmlId . '_' . count($fields) ) );
$class = ( new $classType( $this->name . '[]', NULL, FALSE, $this->options, $this->customValidationCode, $this->prefix, $this->suffix, $this->htmlId . '_' . \count($fields) ) );
$class->setValue( TRUE );
$html = $class->html();
$fields[] = $html;
@@ -150,7 +149,7 @@ class _Stack extends FormAbstract
if( mb_substr( $name, 0, 8 ) !== '_new_[x]' and isset( \IPS\Request::i()->$name ) )
{
if( is_array( \IPS\Request::i()->$name ) AND count( \IPS\Request::i()->$name ) )
if( \is_array( \IPS\Request::i()->$name ) AND \count( \IPS\Request::i()->$name ) )
{
foreach( \IPS\Request::i()->$name as $k => $v )
{
@@ -162,7 +161,7 @@ class _Stack extends FormAbstract
return ( $this->options['removeEmptyValues'] ) ? array_filter( $values ) : $values;
}
else if ( is_array( $this->value ) AND count( $this->value ) )
else if ( \is_array( $this->value ) AND \count( $this->value ) )
{
foreach( $this->value as $k => $v )
{
@@ -192,12 +191,12 @@ class _Stack extends FormAbstract
throw new \InvalidArgumentException('form_required');
}
if ( $this->options['maxItems'] !== NULL and count( $this->value ) > $this->options['maxItems'] )
if ( $this->options['maxItems'] !== NULL and \count( $this->value ) > $this->options['maxItems'] )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_items_max', FALSE, array( 'pluralize' => array( $this->options['maxItems'] ) ) ) );
}
if ( $this->options['minItems'] !== NULL and count( $this->value ) < $this->options['minItems'] )
if ( $this->options['minItems'] !== NULL and \count( $this->value ) < $this->options['minItems'] )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_items_min', FALSE, array( 'pluralize' => array( $this->options['minItems'] ) ) ) );
}
+6 -1
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -22,6 +22,11 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class _Tel extends Text
{
/**
* @brief Child default Options
*/
protected $childDefaultOptions = array( 'htmlAutocomplete' => "tel" );
/**
* @brief Dialling Codes
*/
+90 -43
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -50,8 +50,9 @@ class _Text extends TextArea
'nullLang' => 'no_value', // If provided, an "or X" checkbox will appear with X being the value of this language key. When checked, NULL will be returned as the value.
'accountUsername' => TRUE, // If TRUE or an \IPS\Member, additional checks will be performed to ensure provided value is acceptable for use as a username. Pass an \IPS\Member object to exclude that member from the duplicate checks
'trim' => TRUE, // If TRUE (which is the default), whitespace will be stripped from the start and end of the value
'bypassProfanity' => FALSE, // If TRUE, profanity filter will be bypassed when it is not appropriate to do so (ex: on the login form). Defaults to FALSE.
);
'bypassProfanity' => 0, // Profanity filter bypass, see BYPASS_PROFANITY_* constants below.
'htmlAutocomplete' => NULL, // an HTML autocomplete attribute If Null no autocomplete is set otherwise use a value from https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-url
);
* @endcode
*/
protected $defaultOptions = array(
@@ -65,7 +66,8 @@ class _Text extends TextArea
'nullLang' => NULL,
'accountUsername' => FALSE,
'trim' => TRUE,
'bypassProfanity' => FALSE,
'bypassProfanity' => 0,
'htmlAutocomplete' => NULL,
);
/**
@@ -77,34 +79,49 @@ class _Text extends TextArea
* @brief Form type
*/
public $formType = 'text';
/**
* @brief Do not bypass profanity filters
*/
const BYPASS_PROFANITY_NONE = 0;
/**
* @brief Perform profanity swaps only
*/
const BYPASS_PROFANITY_SWAP = 1;
/**
* @brief Bypass profanity entirely
*/
const BYPASS_PROFANITY_ALL = 2;
/**
* Constructor
* Sets that the field is required if there is a minimum length and vice-versa
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
/* Pull in default options from child class */
$this->defaultOptions = array_merge( $this->defaultOptions, $this->childDefaultOptions );
/* Set username regex */
$args = func_get_args();
if ( isset( $args[3]['accountUsername'] ) and $args[3]['accountUsername'] !== FALSE )
if ( isset( $options['accountUsername'] ) and $options['accountUsername'] !== FALSE )
{
$args[3]['minLength'] = \IPS\Settings::i()->min_user_name_length;
$args[3]['maxLength'] = \IPS\Settings::i()->max_user_name_length;
if ( \IPS\Settings::i()->username_characters )
{
$args[3]['regex'] = '/^[' . str_replace( '\-', '-', preg_quote( \IPS\Settings::i()->username_characters, '/' ) ) . ']*$/iu';
}
$options['minLength'] = \IPS\Settings::i()->min_user_name_length;
$options['maxLength'] = \IPS\Settings::i()->max_user_name_length;
}
/* Call parent constructor */
call_user_func_array( 'parent::__construct', $args );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
/* Add JS */
if ( isset( $this->options['autocomplete']['prefix'] ) and $this->options['autocomplete']['prefix'] )
@@ -113,7 +130,7 @@ class _Text extends TextArea
}
/* Set the form type */
$this->formType = mb_strtolower( mb_substr( get_called_class(), mb_strrpos( get_called_class(), '\\' ) + 1 ) );
$this->formType = mb_strtolower( mb_substr( \get_called_class(), mb_strrpos( \get_called_class(), '\\' ) + 1 ) );
}
/**
@@ -128,7 +145,7 @@ class _Text extends TextArea
{
/* 10/19/15 - adding htmlspecialchars around value if autocomplete is enabled so that html tag characters can be used (e.g. for members) */
/* This value is decoded by the JS widget before use */
if( $this->options['autocomplete'] and !empty( $this->value ) and is_array( $this->value ) )
if( $this->options['autocomplete'] and !empty( $this->value ) and \is_array( $this->value ) )
{
foreach( $this->value as $key => $value )
{
@@ -136,7 +153,7 @@ class _Text extends TextArea
}
}
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, $this->formType, $this->value, $this->required, $this->options['maxLength'], $this->options['size'], $this->options['disabled'], $this->options['autocomplete'], $this->options['placeholder'], NULL, $this->options['nullLang'], $this->htmlId );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, $this->formType, $this->value, $this->required, $this->options['maxLength'], $this->options['size'], $this->options['disabled'], $this->options['autocomplete'], $this->options['placeholder'], NULL, $this->options['nullLang'], $this->htmlId, FALSE, $this->options['htmlAutocomplete'] );
}
/**
@@ -158,7 +175,7 @@ class _Text extends TextArea
if ( $this->options['trim'] )
{
if ( is_array( $return ) )
if ( \is_array( $return ) )
{
$return = array_map( 'trim', $return );
}
@@ -170,19 +187,27 @@ class _Text extends TextArea
if ( isset( $this->options['autocomplete'] ) )
{
$return = htmlspecialchars_decode( $return, ENT_QUOTES );
if ( !is_array( $return ) and ( !isset( $this->options['autocomplete']['maxItems'] ) or $this->options['autocomplete']['maxItems'] != 1 ) )
if ( \is_array( $return ) )
{
$return = array_map( function( $val ) {
return htmlspecialchars_decode( $val, ENT_QUOTES );
}, $return );
}
else
{
$return = htmlspecialchars_decode( $return, ENT_QUOTES );
}
if ( !\is_array( $return ) and ( !isset( $this->options['autocomplete']['maxItems'] ) or $this->options['autocomplete']['maxItems'] != 1 ) )
{
$return = array_filter( array_map( 'trim', explode( "\n", $return ) ) );
}
}
/* Remove all invisible characters if this is a username */
if( $this->options['accountUsername'] )
{
if ( !is_array( $return ) )
if ( !\is_array( $return ) )
{
$return = preg_replace( '/\p{C}+/u', '', $return );
}
@@ -197,8 +222,8 @@ class _Text extends TextArea
if ( isset( $this->options['autocomplete']['prefix'] ) and $this->options['autocomplete']['prefix'] )
{
$return = is_array( $return ) ? $return : ( $return ? array( $return ) : array() );
$return = \is_array( $return ) ? $return : ( $return ? array( $return ) : array() );
$firstAsPrefix = $this->name . '_first_as_prefix';
$freechoicePrefixCheckbox = $this->name . '_freechoice_prefix';
$freechoicePrefix = $this->name . '_prefix';
@@ -237,13 +262,23 @@ class _Text extends TextArea
*/
public function formatValue()
{
if ( $this->options['autocomplete'] !== NULL and ( !isset( $this->options['autocomplete']['maxItems'] ) or $this->options['autocomplete']['maxItems'] != 1 ) and !is_array( $this->value ) and $this->value !== NULL )
if ( $this->options['autocomplete'] !== NULL and ( !isset( $this->options['autocomplete']['maxItems'] ) or $this->options['autocomplete']['maxItems'] != 1 ) and !\is_array( $this->value ) and $this->value !== NULL )
{
return array_filter( array_map( 'trim', explode( "\n", $this->value ) ) );
}
return $this->value;
}
/**
* Get the value to use in the label 'for' attribute
*
* @return mixed
*/
public function getLabelForAttribute()
{
return 'elInput_' . parent::getLabelForAttribute();
}
/**
* Validate
@@ -257,7 +292,7 @@ class _Text extends TextArea
parent::validate();
/* Check it isn't just invisible characters (we don't strip them, because things like zero-width-joiners when in between other characters have a special meaning */
if ( $this->required and is_string( $this->value ) and mb_strlen( preg_replace( '/\p{C}+/u', '', $this->value ) ) === 0 )
if ( $this->required and \is_string( $this->value ) and mb_strlen( preg_replace( '/\p{C}+/u', '', $this->value ) ) === 0 )
{
throw new \DomainException('form_required');
}
@@ -293,7 +328,7 @@ class _Text extends TextArea
/* Construct break points */
$exactProfanityBreakpoints = array();
if( count( $exactProfanity ) )
if( \count( $exactProfanity ) )
{
$array = array();
foreach( array_keys( $exactProfanity ) as $entry )
@@ -314,6 +349,12 @@ class _Text extends TextArea
/* Username */
if ( $this->options['accountUsername'] )
{
/* Check it is valid */
if ( !\IPS\Login::usernameIsAllowed( $this->value ) )
{
throw new \DomainException( 'form_name_banned' );
}
/* Check if it exists */
if ( !( $this->options['accountUsername'] instanceof \IPS\Member ) or mb_strtolower( $this->options['accountUsername']->name ) !== mb_strtolower( $this->value ) )
{
@@ -338,18 +379,18 @@ class _Text extends TextArea
{
if( $this->value )
{
$values = ( is_array( $this->value ) ) ? $this->value : array( $this->value );
$values = ( \is_array( $this->value ) ) ? $this->value : array( $this->value );
}
else
{
$values = array();
}
if ( isset( $this->options['autocomplete']['maxItems'] ) and count( $values ) > $this->options['autocomplete']['maxItems'] )
if ( isset( $this->options['autocomplete']['maxItems'] ) and \count( $values ) > $this->options['autocomplete']['maxItems'] )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_max', FALSE, array( 'pluralize' => array( $this->options['autocomplete']['maxItems'] ) ) ) );
}
if ( isset( $this->options['autocomplete']['minItems'] ) and ( $this->required or count( $values ) > 0 ) and count( $values ) < $this->options['autocomplete']['minItems'] )
if ( isset( $this->options['autocomplete']['minItems'] ) and ( $this->required or \count( $values ) > 0 ) and \count( $values ) < $this->options['autocomplete']['minItems'] )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_min', FALSE, array( 'pluralize' => array( $this->options['autocomplete']['minItems'] ) ) ) );
}
@@ -374,7 +415,7 @@ class _Text extends TextArea
}
}
}
if ( isset( $this->options['autocomplete']['filterProfanity'] ) AND is_array( $this->value ) AND count( $this->value ) and \IPS\Dispatcher::i() instanceof \IPS\Dispatcher\Front and !\IPS\Member::loggedIn()->group['g_bypass_badwords'] )
if ( isset( $this->options['autocomplete']['filterProfanity'] ) AND \is_array( $this->value ) AND \count( $this->value ) and \IPS\Dispatcher::i() instanceof \IPS\Dispatcher\Front and !\IPS\Member::loggedIn()->group['g_bypass_badwords'] )
{
foreach ( $values as $k => $v )
{
@@ -413,7 +454,7 @@ class _Text extends TextArea
}
}
if ( isset( $this->options['autocomplete']['unique'] ) AND $this->options['autocomplete']['unique'] AND is_array( $this->value ) AND count( $this->value ) )
if ( isset( $this->options['autocomplete']['unique'] ) AND $this->options['autocomplete']['unique'] AND \is_array( $this->value ) AND \count( $this->value ) )
{
foreach ( $this->value as $v )
{
@@ -425,7 +466,7 @@ class _Text extends TextArea
}
}
if ( is_array ( $this->value ) AND isset( $this->options['autocomplete']['source'] ) AND is_array( $this->options['autocomplete']['source'] ) AND ( !isset( $this->options['autocomplete']['freeChoice'] ) OR !$this->options['autocomplete']['freeChoice'] ) )
if ( \is_array( $this->value ) AND isset( $this->options['autocomplete']['source'] ) AND \is_array( $this->options['autocomplete']['source'] ) AND ( !isset( $this->options['autocomplete']['freeChoice'] ) OR !$this->options['autocomplete']['freeChoice'] ) )
{
if( isset( $this->options['autocomplete']['forceLower'] ) AND $this->options['autocomplete']['forceLower'] )
{
@@ -439,15 +480,15 @@ class _Text extends TextArea
}
/* Split on profanity */
if( $this->options['bypassProfanity'] === FALSE and is_string( $this->value ) and \IPS\Dispatcher::i() instanceof \IPS\Dispatcher\Front and !\IPS\Member::loggedIn()->group['g_bypass_badwords'] AND !$this->options['accountUsername'] )
if( \in_array( $this->options['bypassProfanity'], array( static::BYPASS_PROFANITY_NONE, static::BYPASS_PROFANITY_SWAP ) ) and \is_string( $this->value ) and \IPS\Dispatcher::i() instanceof \IPS\Dispatcher\Front and !\IPS\Member::loggedIn()->group['g_bypass_badwords'] AND !$this->options['accountUsername'] )
{
$newVal = NULL;
if ( count( $exactProfanityBreakpoints ) )
if ( \count( $exactProfanityBreakpoints ) )
{
/* preg_split can return boolean false*/
$split = preg_split( '/' . implode( '|', $exactProfanityBreakpoints ) . '/iu', $this->value, null, PREG_SPLIT_DELIM_CAPTURE );
if( is_array( $split ) )
if( \is_array( $split ) )
{
foreach ( $split as $section )
{
@@ -459,7 +500,10 @@ class _Text extends TextArea
}
else
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( $section ) ) ) );
if ( $this->options['bypassProfanity'] === static::BYPASS_PROFANITY_NONE )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( $section ) ) ) );
}
}
}
else
@@ -472,7 +516,7 @@ class _Text extends TextArea
$value = $newVal ?: $this->value;
if ( count( $looseProfanity ) )
if ( \count( $looseProfanity ) )
{
$swaps = array();
foreach( $looseProfanity AS $type => $row )
@@ -485,7 +529,10 @@ class _Text extends TextArea
{
if ( mb_stristr( $value, $type ) )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( $row->type ) ) ) );
if ( $this->options['bypassProfanity'] === static::BYPASS_PROFANITY_NONE )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( $row->type ) ) ) );
}
}
}
}
+12 -6
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -49,15 +49,21 @@ class _TextArea extends FormAbstract
/**
* Constructor
* Sets that the field is required if there is a minimum length and vice-versa
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
/* Call parent constructor */
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
/* If we have a minimum length, the field is required */
if( $this->options['minLength'] >= 1 )
@@ -113,7 +119,7 @@ class _TextArea extends FormAbstract
parent::validate();
/* Tags are stored as an array so we can't do things like mb_strlen() against them */
if( is_array( $this->value ) )
if( \is_array( $this->value ) )
{
return TRUE;
}
+9 -1
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -25,6 +25,14 @@ class _Timezone extends Select
/**
* Constructor
*
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
+13 -7
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -49,15 +49,21 @@ class _Translatable extends FormAbstract
/**
* Constructor
* Sets that the field is required if there is a minimum length and vice-versa
*
* @see \IPS\Helpers\Form\FormAbstract::__construct
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
/* Call parent constructor */
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
/* Get current values */
if ( $this->value === NULL )
@@ -70,7 +76,7 @@ class _Translatable extends FormAbstract
$v = $v['word_custom'] ?: $v['word_default'];
if ( $v or !isset( $values[ $k ] ) )
{
if ( is_array( $this->options['sprintf'] ) )
if ( \is_array( $this->options['sprintf'] ) )
{
$values[ $k ] = vsprintf( $v, $this->options['sprintf'] );
}
@@ -84,7 +90,7 @@ class _Translatable extends FormAbstract
$this->value = $values;
}
elseif ( is_string( $this->value ) )
elseif ( \is_string( $this->value ) )
{
$values = array();
foreach ( \IPS\Lang::getEnabledLanguages() as $lang )
+78
View File
@@ -0,0 +1,78 @@
<?php
/**
* @brief T/R/B/L input class for Form Builder
* @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 07 Feb 2020
*/
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* T/R/B/L input class for Form Builder
*/
class _Trbl extends FormAbstract
{
/**
* @brief Default Options
* @code
$defaultOptions = array(
);
* @endcode
*/
protected $defaultOptions = array(
);
/**
* Constructor
*
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
if ( !$this->value )
{
$this->value = array( 0, 0, 0, 0 );
}
}
/**
* Get HTML
*
* @return string
*/
public function html()
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->trbl( $this->name, $this->value[0], $this->value[1], $this->value[2], $this->value[3] );
}
/**
* Get Value
*
* @return mixed
*/
public function getValue()
{
$name = $this->name;
return \IPS\Request::i()->$name;
}
}
+184 -60
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -41,8 +41,10 @@ class _Upload extends FormAbstract
'callback' => function() { ... }, // A callback function to run against submitted files
'minimize' => TRUE, // Default is minimized. Pass FALSE to show the maximized field. Cannot be used in conjunction with temporary uploads
'retainDeleted' => FALSE, // By default, if you specify a default value and the user deletes the files specified, the files will be physically deleted. This option overrides this behaviour.
'template' => 'core.attachments.fileItem' // The javascript template key to use when rendering uploaded items
'obscure' => TRUE, // Controls if an md5 hash should be added to the filename. *Must* be TRUE unless the uploaded files are public to all users (like emoticons)
'template' => 'core.attachments.fileItem', // The javascript template key to use when rendering uploaded items. e.g. core.attachments.fileItem
'obscure' => TRUE, // Controls if an md5 hash should be added to the filename. *Must* be TRUE unless the uploaded files are public to all users (like emoticons),
'supportsDelete' => TRUE // Should the uploaded file be direct deletable from the upload field
'allowStockPhotos' => FALSE // If Pixabay enhancement is enabled, this will allow users to select stock art from Pixabay to upload
);
* @endcode
*/
@@ -63,7 +65,9 @@ class _Upload extends FormAbstract
'retainDeleted' => FALSE,
'template' => 'core.attachments.fileItem',
'default' => NULL,
'obscure' => TRUE
'obscure' => TRUE,
'supportsDelete' => TRUE,
'allowStockPhotos' => FALSE
);
/**
@@ -94,21 +98,20 @@ class _Upload extends FormAbstract
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
/* What's PHP's upload limit */
$potentialValues = array();
if( (float) ini_get('upload_max_filesize') > 0 )
if ( $maxChunkSize = static::maxChunkSize() )
{
$potentialValues[] = \IPS\File::returnBytes( ini_get('upload_max_filesize') );
$this->maxChunkSize = $maxChunkSize / 1048576;
}
if( (float) ini_get('post_max_size') > 0 )
/* Can we really use stock photos? */
if ( ! empty( $options['allowStockPhotos'] ) )
{
$potentialValues[] = \IPS\File::returnBytes( ini_get('post_max_size') ) - 1048576;
if ( ! \IPS\Settings::i()->pixabay_enabled )
{
$options['allowStockPhotos'] = FALSE;
}
}
if( (float) ini_get('memory_limit') > 0 )
{
$potentialValues[] = \IPS\File::returnBytes( ini_get('memory_limit') );
}
$this->maxChunkSize = min( $potentialValues ) / 1048576;
/* Work out storage extension */
if ( isset( $options['postKey'] ) and $options['postKey'] and ( !isset( $options['storageExtension'] ) or !$options['storageExtension'] ) )
{
@@ -123,9 +126,31 @@ class _Upload extends FormAbstract
if ( isset( $options['storageExtension'] ) and $options['storageExtension'] )
{
$storageClass = \IPS\File::getClass( $options['storageExtension'] );
if( !$storageClass::$supportsChunking and ( !isset( $options['maxFileSize'] ) or $this->maxChunkSize < $options['maxFileSize'] ) )
/* If the storage engine has a minimum chunk size, but our server can't handle uploads that large, we can't support chunking */
$supportsChunking = $storageClass::$supportsChunking;
if ( $storageClass::$supportsChunking and isset( $storageClass::$minChunkSize ) and $storageClass::$minChunkSize > $this->maxChunkSize )
{
$options['maxFileSize'] = $this->maxChunkSize;
$supportsChunking = FALSE;
}
/* If we support chunking... */
if ( $supportsChunking )
{
/* Just need to make sure the storage engine's maximum chunk size is bigger than what our server can handle */
if ( isset( $storageClass::$maxChunkSize ) and $storageClass::$maxChunkSize < $this->maxChunkSize )
{
$this->maxChunkSize = $storageClass::$maxChunkSize;
}
}
/* If we don't... */
elseif ( !$supportsChunking )
{
/* Then we need to set the maximum file size to what our server can handle */
if ( !isset( $options['maxFileSize'] ) or $this->maxChunkSize < $options['maxFileSize'] )
{
$options['maxFileSize'] = $this->maxChunkSize;
}
}
}
@@ -185,22 +210,31 @@ class _Upload extends FormAbstract
{
try
{
if ( $storageClass::$supportsChunking and isset( \IPS\Request::i()->chunks ) and \IPS\Request::i()->chunks > 1 )
/* Chunked */
if ( $storageClass::$supportsChunking and method_exists( $storageClass, 'chunkInit' ) and isset( \IPS\Request::i()->chunks ) and \IPS\Request::i()->chunks > 1 )
{
$chunkName = 'chunk-' . md5( \IPS\Request::i()->name ) . '-' . $this->name . '-' . session_id() . '.txt';
$lastChunk = ( \IPS\Request::i()->chunk == \IPS\Request::i()->chunks - 1 );
/* We need to be able to set data into sessions */
\IPS\Request::i()->setCookie( 'noCache', 1 );
/* If this is the FIRST chunk, start the process */
if ( \IPS\Request::i()->chunk == 0 )
{
$url = \IPS\File::create( $this->options['storageExtension'], $chunkName, NULL, $this->options['storageContainer'], FALSE, $_FILES[ $this->name ]['tmp_name'], FALSE );
$ref = $storageClass->chunkInit( \IPS\Request::i()->name, $this->options['storageContainer'], $this->options['obscure'] );
}
else
{
$url = $storageClass->chunkAppend( $chunkName, $_FILES[ $this->name ]['tmp_name'], $lastChunk ? \IPS\Request::i()->name : NULL, $this->options['storageContainer'] );
$ref = $_SESSION['chunkUploads'][ md5( \IPS\Request::i()->name ) . '-' . $this->name ];
}
if ( $lastChunk )
/* Process this chunk */
$ref = $storageClass->chunkProcess( $ref, $_FILES[ $this->name ]['tmp_name'], \IPS\Request::i()->chunk );
$_SESSION['chunkUploads'][ md5( \IPS\Request::i()->name ) . '-' . $this->name ] = $ref;
/* If this is the LAST chunk, finish the process */
if ( \IPS\Request::i()->chunk == \IPS\Request::i()->chunks - 1 )
{
$url = $storageClass->chunkFinish( $ref );
$fileObj = \IPS\File::get( $this->options['storageExtension'], $url );
$fileArray = array(
@@ -210,11 +244,11 @@ class _Upload extends FormAbstract
'name' => $fileObj->originalFilename,
'tmp_name' => $_FILES[ $this->name ]['tmp_name']
);
/* If there is an error, an exception will be thrown and will be caught below like normal */
\IPS\File::validateUpload( $fileArray, $this->options['allowedFileTypes'], $this->options['maxFileSize'] );
if ( $fileObj->isImage() AND ( $this->options['checkImage'] OR is_array( $this->options['image'] ) ) )
if ( $fileObj->isImage() AND ( $this->options['checkImage'] OR \is_array( $this->options['image'] ) ) )
{
try
{
@@ -225,12 +259,16 @@ class _Upload extends FormAbstract
$image->setExifData( $fileObj->contents() );
}
if( is_array( $this->options['image'] ) )
$hasBeenResized = FALSE;
if( \is_array( $this->options['image'] ) )
{
$image->resizeToMax( $this->options['image']['maxWidth'] ?: NULL, $this->options['image']['maxHeight'] ?: NULL );
$hasBeenResized = $image->resizeToMax( $this->options['image']['maxWidth'] ?: NULL, $this->options['image']['maxHeight'] ?: NULL );
}
if ( $hasBeenResized or $image->hasBeenRotated )
{
$fileObj->replace( (string) $image );
}
$fileObj->replace( (string) $image );
}
catch ( \Exception $e ) { }
}
@@ -246,7 +284,8 @@ class _Upload extends FormAbstract
if ( $this->options['callback'] )
{
$r = call_user_func( $this->options['callback'], $fileObj );
$callbackFunction = $this->options['callback'];
$r = $callbackFunction( $fileObj );
if ( $r !== NULL )
{
$insertId = $r;
@@ -258,7 +297,7 @@ class _Upload extends FormAbstract
'key' => $_SERVER['HTTP_X_PLUPLOAD'],
'imagesrc' => $fileObj->isImage() ? (string) $fileObj->url : NULL,
'videosrc' => $fileObj->isVideo() ? (string) $fileObj->url : NULL,
'thumbnail' => ( $fileObj->isImage() AND $fileObj->attachmentThumbnailUrl !== NULL and is_string( $fileObj->attachmentThumbnailUrl ) ) ? \IPS\File::get( $this->options['storageExtension'], $fileObj->attachmentThumbnailUrl )->url : NULL,
'thumbnail' => ( $fileObj->isImage() AND $fileObj->attachmentThumbnailUrl !== NULL and \is_string( $fileObj->attachmentThumbnailUrl ) ) ? \IPS\File::get( $this->options['storageExtension'], $fileObj->attachmentThumbnailUrl )->url : NULL,
) );
}
else
@@ -266,6 +305,7 @@ class _Upload extends FormAbstract
\IPS\Output::i()->json( array( 'chunk' => 'OK' ) );
}
}
/* Not chunked */
else
{
foreach ( $this->processUploads() as $insertId => $fileObj )
@@ -275,7 +315,7 @@ class _Upload extends FormAbstract
'key' => $_SERVER['HTTP_X_PLUPLOAD'],
'imagesrc' => $fileObj->isImage() ? (string) $fileObj->url : NULL,
'videosrc' => $fileObj->isVideo() ? (string) $fileObj->url : NULL,
'thumbnail' => ( $fileObj->isImage() AND $fileObj->attachmentThumbnailUrl !== NULL and is_string( $fileObj->attachmentThumbnailUrl ) ) ? \IPS\File::get( $this->options['storageExtension'], $fileObj->attachmentThumbnailUrl )->url : NULL,
'thumbnail' => ( $fileObj->isImage() AND $fileObj->attachmentThumbnailUrl !== NULL and \is_string( $fileObj->attachmentThumbnailUrl ) ) ? \IPS\File::get( $this->options['storageExtension'], $fileObj->attachmentThumbnailUrl )->url : NULL,
) );
}
}
@@ -285,22 +325,45 @@ class _Upload extends FormAbstract
\IPS\Log::log( $e, 'upload_failure' );
$message = $e->getMessage();
if ( \IPS\Member::loggedIn()->isAdmin() and \IPS\Member::loggedIn()->language()->checkKeyExists("uploaderr_{$e->getMessage()}_admin") )
if( $e instanceof \IPS\File\Exception )
{
$message = \IPS\Member::loggedIn()->language()->addToStack("uploaderr_{$e->getMessage()}_admin");
$message = \IPS\Member::loggedIn()->language()->addToStack("files-{$e->getCode()}", FALSE, array( 'sprintf' => array( $e->originalFilename ?: $e->file ) ) );
}
elseif ( \IPS\Member::loggedIn()->language()->checkKeyExists("uploaderr_{$e->getMessage()}") )
$subMessage = NULL;
if( $e instanceof \IPS\File\Exception AND $extra = $e->extraErrorMessage() )
{
$message = \IPS\Member::loggedIn()->language()->addToStack("uploaderr_{$e->getMessage()}");
if ( \IPS\Member::loggedIn()->isAdmin() and \IPS\Member::loggedIn()->language()->checkKeyExists( $extra . "_admin" ) )
{
$subMessage = \IPS\Member::loggedIn()->language()->addToStack( $extra . "_admin" );
}
else
{
$subMessage = \IPS\Member::loggedIn()->language()->addToStack( $extra );
}
}
else
{
$message = \IPS\Member::loggedIn()->language()->addToStack("uploaderr_unspecified");
if ( \IPS\Member::loggedIn()->isAdmin() and \IPS\Member::loggedIn()->language()->checkKeyExists("uploaderr_{$e->getMessage()}_admin") )
{
$subMessage = \IPS\Member::loggedIn()->language()->addToStack( "uploaderr_{$e->getMessage()}_admin" );
}
elseif ( \IPS\Member::loggedIn()->language()->checkKeyExists("uploaderr_{$e->getMessage()}") )
{
$subMessage = \IPS\Member::loggedIn()->language()->addToStack( "uploaderr_{$e->getMessage()}" );
}
else
{
$subMessage = \IPS\Member::loggedIn()->language()->addToStack("uploaderr_unspecified" );
}
}
\IPS\Output::i()->json( array(
'error' => $message,
'extra' => $e->getCode()
'extra' => $e->getCode(),
'sub' => $subMessage
) );
}
}
@@ -331,7 +394,7 @@ class _Upload extends FormAbstract
/* Put the value in an array if we need to */
if ( $this->value !== NULL and ! empty( $this->value ) )
{
if ( $this->options['multiple'] !== TRUE and !is_array( $this->value ) )
if ( $this->options['multiple'] !== TRUE and !\is_array( $this->value ) )
{
$this->value = array( $this->value );
}
@@ -340,12 +403,12 @@ class _Upload extends FormAbstract
/* Build JSON version of the existing value, which allows the widget to build the interface */
$existing = array();
if( $this->value and count( $this->value ) )
if( $this->value and \count( $this->value ) )
{
foreach( $this->value as $id => $file )
{
/* If this was attachment data, expand */
if( is_array( $file ) )
if( \is_array( $file ) )
{
$attachment = $file;
$file = $file['fileurl'];
@@ -366,6 +429,7 @@ class _Upload extends FormAbstract
'hasThumb' => false,
'originalFileName' => $file,
'size' => \IPS\Output\Plugin\Filesize::humanReadableFilesize( $file, FALSE, TRUE ),
'sizeRaw' => $file,
'custom' => NULL
);
}
@@ -380,6 +444,7 @@ class _Upload extends FormAbstract
'originalFileName' => $file->originalFilename,
'thumbnail' => ( isset( $attachment ) AND isset( $attachment['attach_is_image'] ) AND $attachment['attach_is_image'] AND $attachment['attach_thumb_location'] ) ? ( (string) \IPS\File::get( $this->options['storageExtension'], $attachment['attach_thumb_location'] )->url ) : ( $file->isImage() ? (string) $file->url : NULL ),
'size' => \IPS\Output\Plugin\Filesize::humanReadableFilesize( $file->filesize(), FALSE, TRUE ),
'sizeRaw' => $file->filesize(),
'default' => ( isset( $attachment ) AND $attachment['default'] ) ? $attachment['default'] : NULL,
);
}
@@ -389,15 +454,23 @@ class _Upload extends FormAbstract
foreach( $this->value as $id => $file )
{
$this->value[ $id ] = ( is_array( $file ) ) ? $file['fileurl'] : $file;
$this->value[ $id ] = ( \is_array( $file ) ) ? $file['fileurl'] : $file;
}
}
/* We want this to use decimals even if locale wants commas for decimal separator, i.e. for uploader */
$maxFileSize = $this->options['maxFileSize'] ? number_format( $this->options['maxFileSize'], 4, '.', '' ) : $this->options['maxFileSize'];
$maxFileSize = $this->options['maxFileSize'] ? number_format( $this->options['maxFileSize'], 2, '.', '' ) : $this->options['maxFileSize'];
/* Do we have maximum image dimensions which we can ask plupload to handle for us client-side? */
$maxImageDims = NULL;
if ( $this->options['image'] and isset( $this->options['image']['maxWidth'] ) and isset( $this->options['image']['maxHeight'] ) )
{
$maxImageDims = "{$this->options['image']['maxWidth']}x{$this->options['image']['maxHeight']}";
}
/* The html() method is called more than once, however $this->value is wiped out so if it was an attachments array all of the array properties are lost */
$this->builtHtml = call_user_func( $this->template, $this->name, $this->value, $this->options['minimize'], $maxFileSize, $this->options['maxFiles'], $this->maxChunkSize, $this->options['totalMaxSize'], $this->options['allowedFileTypes'], $uploadKey, $this->options['multiple'], $this->options['postKey'], $this->options['temporary'] or ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] ), $this->options['template'], $existing, $this->options['default'] );
$templateFunction = $this->template;
$this->builtHtml = $templateFunction( $this->name, $this->value, $this->options['minimize'], $maxFileSize, $this->options['maxFiles'], $this->maxChunkSize, $this->options['totalMaxSize'], $this->options['allowedFileTypes'], $uploadKey, $this->options['multiple'], $this->options['postKey'], $this->options['temporary'] or ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] ), $this->options['template'], $existing, $this->options['default'], $this->options['supportsDelete'], $maxImageDims, $this->options['allowStockPhotos'] );
return $this->builtHtml;
}
@@ -413,13 +486,13 @@ class _Upload extends FormAbstract
{
/* Get the files we had already */
$return = array();
$tempFiles = iterator_to_array( \IPS\Db::i()->select( 'id,contents,filename', 'core_files_temp', array( 'upload_key=?', md5( $this->name . session_id() ) ) )->setKeyField('id') );
$tempFiles = iterator_to_array( \IPS\Db::i()->select( 'id,contents,filename,exif', 'core_files_temp', array( 'upload_key=?', md5( $this->name . session_id() ) ) )->setKeyField('id') );
if ( $this->options['storageExtension'] )
{
$existingName = $this->name . '_existing';
$keepName = $this->name . '_keep';
$keep = \IPS\Request::i()->$keepName;
if ( isset( \IPS\Request::i()->$existingName ) and is_array( \IPS\Request::i()->$existingName ) )
if ( isset( \IPS\Request::i()->$existingName ) and \is_array( \IPS\Request::i()->$existingName ) )
{
foreach ( \IPS\Request::i()->$existingName as $id => $tempId )
{
@@ -431,6 +504,7 @@ class _Upload extends FormAbstract
$file->tempId = $tempId;
/* Reset the original filename to the real file name as $this->originalFilename is generated from the now AWS-safe $this->filename */
$file->originalFilename = $tempFiles[ $tempId ]['filename'];
$file->exifData = $tempFiles[ $tempId ]['exif'] ? json_decode( $tempFiles[ $tempId ]['exif'], TRUE ) : NULL;
$return[ $id ] = $file;
}
else
@@ -492,7 +566,7 @@ class _Upload extends FormAbstract
{
try
{
if ( is_array( $this->defaultValue ) )
if ( \is_array( $this->defaultValue ) )
{
foreach( $this->defaultValue as $file )
{
@@ -557,7 +631,7 @@ class _Upload extends FormAbstract
{
$message = \IPS\Member::loggedIn()->language()->get( 'pluploaderr_' . $e->getMessage() );
$message = str_replace( '{{max_file_size}}', $this->maxChunkSize, $message );
if ( is_array( $this->options['allowedFileTypes'] ) )
if ( \is_array( $this->options['allowedFileTypes'] ) )
{
$message = str_replace( '{{allowed_extensions}}', implode( ', ', $this->options['allowedFileTypes'] ), $message );
}
@@ -587,7 +661,7 @@ class _Upload extends FormAbstract
}
if ( $total > $this->options['totalMaxSize'] * 1048576 )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack('uploaderr_total_size', FALSE, array( 'sprintf' => array( $this->options['totalMaxSize'] ) ) ) );
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack('uploaderr_total_size', FALSE, array( 'sprintf' => array( \IPS\Output\Plugin\Filesize::humanReadableFilesize( $this->options['totalMaxSize'] * 1048576 ) ) ) ) );
}
}
@@ -596,7 +670,7 @@ class _Upload extends FormAbstract
foreach( $return as $k => $v )
{
$toReturn[ $k ] = ( is_array( $v ) AND isset( $v['fileurl'] ) ) ? $v['fileurl'] : $v;
$toReturn[ $k ] = ( \is_array( $v ) AND isset( $v['fileurl'] ) ) ? $v['fileurl'] : $v;
}
$return = $toReturn;
@@ -631,7 +705,7 @@ class _Upload extends FormAbstract
$ext = mb_strtolower( mb_substr( $file['name'], ( mb_strrpos( $file['name'], '.' ) + 1 ) ) );
/* Don't allow "XSS" in images */
if( in_array( $ext, \IPS\File::$safeFileExtensions ) AND in_array( $ext, \IPS\Image::$imageExtensions ) )
if( \in_array( $ext, \IPS\File::$safeFileExtensions ) AND \in_array( $ext, \IPS\Image::$imageExtensions ) )
{
if( \IPS\File::checkXssInFile( file_get_contents( $file['tmp_name'] ) ) )
{
@@ -653,18 +727,22 @@ class _Upload extends FormAbstract
$ext = mb_strtolower( mb_substr( $filename, mb_strrpos( $filename, '.' ) + 1 ) );
/* Resize images */
if ( in_array( $ext, \IPS\Image::$imageExtensions ) AND ( is_array( $options['image'] ) OR $options['checkImage'] ) )
if ( \in_array( $ext, \IPS\Image::$imageExtensions ) AND ( \is_array( $options['image'] ) OR $options['checkImage'] ) )
{
try
{
$image = \IPS\Image::create( $contents );
if( is_array( $options['image'] ) )
$hasBeenResized = FALSE;
if( \is_array( $options['image'] ) )
{
$image->resizeToMax( $options['image']['maxWidth'] ?: NULL, $options['image']['maxHeight'] ?: NULL );
}
return (string) $image;
if ( $hasBeenResized or $image->hasBeenRotated )
{
return (string) $image;
}
}
catch ( \Exception $e ) {}
}
@@ -674,18 +752,27 @@ class _Upload extends FormAbstract
foreach ( $fileObjects as $fileObj )
{
$exif = NULL;
if( \IPS\Image::exifSupported() and $fileObj->isImage() and $fileObj->exifData )
{
$exif = json_encode( $fileObj->exifData );
}
$insertId = \IPS\Db::i()->insert( 'core_files_temp', array(
'upload_key' => md5( $this->name . session_id() ),
'filename' => $fileObj->originalFilename,
'mime' => \IPS\File::getMimeType( $fileObj->originalFilename ),
'contents' => (string) $fileObj,
'time' => time(),
'storage_extension' => $this->options['storageExtension']
'storage_extension' => $this->options['storageExtension'],
'exif' => $exif
) );
if ( $this->options['callback'] )
{
$r = call_user_func( $this->options['callback'], $fileObj );
$callbackFunction = $this->options['callback'];
$r = $callbackFunction( $fileObj );
if ( $r !== NULL )
{
@@ -724,11 +811,11 @@ class _Upload extends FormAbstract
*/
public static function stringValue( $value )
{
if ( is_array( $value ) )
if ( \is_array( $value ) )
{
return implode( ',', array_map( function( $v )
{
if ( is_object( $v ) )
if ( \is_object( $v ) )
{
return (string) $v->url;
}
@@ -737,4 +824,41 @@ class _Upload extends FormAbstract
return ( $value ) ? (string) $value->url : NULL;
}
/**
* Get the maximum chunk size (i.e. PHP's limit) in bytes
*
* @return int|NULL
*/
public static function maxChunkSize()
{
if ( $potentialValues = static::maxChunkSizeValues() )
{
return min( $potentialValues );
}
return NULL;
}
/**
* Get the maximum chunk size (i.e. PHP's limit) in bytes
*
* @return int|NULL
*/
public static function maxChunkSizeValues()
{
$potentialValues = array();
if( (float) ini_get('upload_max_filesize') > 0 )
{
$potentialValues['upload_max_filesize'] = \IPS\File::returnBytes( ini_get('upload_max_filesize') );
}
if( (float) ini_get('post_max_size') > 0 )
{
$potentialValues['post_max_size'] = \IPS\File::returnBytes( ini_get('post_max_size') ) - 1048576;
}
if( (float) ini_get('memory_limit') > 0 )
{
$potentialValues['memory_limit'] = \IPS\File::returnBytes( ini_get('memory_limit') );
}
return $potentialValues;
}
}
+87 -21
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -27,14 +27,15 @@ class _Url extends Text
* @code
$childDefaultOptions = array(
'allowedProtocols' => array( 'http', 'https' ), // Allowed protocols. Default is http and https. Be careful changing this not to introduce security issues.
'image' => TRUE, // If TRUE, will check the URL is for an image. Note, setting this will not allow localhost URLs.
'allowedMimes' => \IPS\Image::$imageMimes, // Sets the allowed mimetype(s). Can be string or array. * is a wildcard. Default is NULL, which allows any mimetypes. Note, setting this will not allow localhost URLs. If the 'image' option is set, it will already be restricted to images
'file' => 'Profile', // If provided, the contents of the URL will be fetched and written as a file, an \IPS\File object will then be returned rather than a string. Provide the extension name which specifies the storage location to use. Note, setting this will not allow localhost URLs.
'image' => TRUE, // If TRUE, will check the URL is for an image.
'allowedMimes' => \IPS\Image::$imageMimes, // Sets the allowed mimetype(s). Can be string or array. * is a wildcard. Default is NULL, which allows any mimetypes. If the 'image' option is set, it will already be restricted to images
'file' => 'Profile', // If provided, the contents of the URL will be fetched and written as a file, an \IPS\File object will then be returned rather than a string. Provide the extension name which specifies the storage location to use.
'maxFileSize' => NULL, // If provided along with 'file', the resulting file that is written to disk cannot be greater than this size in megabytes. NULL for no limit (default is NULL).
'maxDimensions' => NULL, // If supplied as an array with keys 'width' and 'height' and the resulting file is an image, the image will be scaled down to these maximum dimensions. NULL for no limits (default is NULL).
'rateLimit' => 20, // Rate-limit requests to prevent the ability to DOS a remote server - time between allowed attempts in seconds (defaults to 20). NULL to disable rate limiting.
);
* @endcode
* @note You should NOT use file/image/allowedMimes options for public-facing forms. These options will result in the file being imported, and exposing such a form to end users opens up the community to potential SSRF concerns.
*/
public $childDefaultOptions = array(
'allowedProtocols' => array( 'http', 'https' ),
@@ -52,11 +53,22 @@ class _Url extends Text
* @throws \InvalidArgumentException
* @throws \DomainException
* @return TRUE
* @note Custom validation is performed AFTER all standard validations to ensure consistent error messages in places like importing profile photos (if an attacker was attempting to supply a local address). This means that if the form helper's 'file' childoption is set, the value supplied to the custom helper will be an instance of \IPS\File instead of \IPS\Http\Url. Also, a second param $response is made available if you need to check the value of the URL's response and file, allowedMimes, or image was set (preventing a second unnecessary HTTP request).
*/
public function validate()
{
/* If we have a custom validation function, store it now and we'll run it manually afterwards */
$validationFunction = NULL;
if( $this->customValidationCode !== NULL )
{
$validationFunction = $this->customValidationCode;
$this->customValidationCode = NULL;
}
parent::validate();
$response = NULL;
if ( $this->value )
{
$value = $this->formatValue();
@@ -68,7 +80,7 @@ class _Url extends Text
}
/* And that it's an allowed protocol */
if ( $this->options['allowedProtocols'] and !in_array( mb_strtolower( $value->data['scheme'] ), $this->options['allowedProtocols'] ) )
if ( $this->options['allowedProtocols'] and !\in_array( mb_strtolower( $value->data['scheme'] ), $this->options['allowedProtocols'] ) )
{
throw new \DomainException('form_url_bad_protocol');
}
@@ -76,46 +88,46 @@ class _Url extends Text
/* Try to fetch it, if necessary */
if ( $this->options['file'] or $this->options['allowedMimes'] or $this->options['image'] )
{
/* Localhost is not allowed as sending a HTTP request to the local server will cause scripts which are relying
on only being accessible to the local machine to execute */
if ( $value->isLocalhost() )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_url_localhost', FALSE, array( 'sprintf' => array( \IPS\Http\Url::internal('')->data['host'] ) ) ) );
}
/* Is rate limiting enabled (the default)? */
if( $this->options['rateLimit'] !== NULL AND intval( $this->options['rateLimit'] ) > 0 )
if( $this->options['rateLimit'] !== NULL AND \intval( $this->options['rateLimit'] ) > 0 )
{
/* We need to rate limit URL fetch requests - have we tried to fetch this field yet? */
$_key = 'url_fetch_' . $this->htmlId;
if( isset( $_SESSION[ $_key ] ) AND $_SESSION[ $_key ] )
if( $rateLimit = $this->getRateLimitValue() )
{
/* Was it less than 20 seconds ago? If so, make the user wait */
$timeLeft = $_SESSION[ $_key ] - ( time() - intval( $this->options['rateLimit'] ) );
$timeLeft = $rateLimit - ( time() - \intval( $this->options['rateLimit'] ) );
if( $timeLeft > 0 )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_url_too_soon', FALSE, array( 'sprintf' => array( $this->options['rateLimit'], $timeLeft ) ) ) );
}
}
$_SESSION[ $_key ] = time();
$this->setRateLimitValue( time() );
}
/* Make the request */
try
{
$response = $value->request()->get();
$response = $value->request( null, null, 5, $this->options['allowedProtocols'] )->get();
}
catch ( \IPS\Http\Request\Exception $e )
{
if( $e->getMessage() === 'localhost_url_not_followed' )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_url_localhost', FALSE, array( 'sprintf' => array( \IPS\Http\Url::internal('')->data['host'] ) ) ) );
}
if( $e->getMessage() === 'protocol_not_followed' )
{
throw new \DomainException('form_url_bad_protocol');
}
throw new \DomainException( 'form_url_error' );
}
/* Check MIME */
if ( $this->options['allowedMimes'] or $this->options['image'] )
{
$allowedMimes = $this->options['allowedMimes'] ? ( is_array( $this->options['allowedMimes'] ) ? $this->options['allowedMimes'] : array( $this->options['allowedMimes'] ) ): \IPS\Image::$imageMimes;
$allowedMimes = $this->options['allowedMimes'] ? ( \is_array( $this->options['allowedMimes'] ) ? $this->options['allowedMimes'] : array( $this->options['allowedMimes'] ) ): \IPS\Image::$imageMimes;
$match = FALSE;
$contentType = ( isset( $response->httpHeaders['Content-Type'] ) ) ? $response->httpHeaders['Content-Type'] : ( ( isset( $response->httpHeaders['content-type'] ) ) ? $response->httpHeaders['content-type'] : NULL );
@@ -187,6 +199,60 @@ class _Url extends Text
}
}
}
/* Now run our validation */
if( $validationFunction !== NULL )
{
$validationFunction( $this->value, $response );
$this->customValidationCode = $validationFunction;
}
return TRUE;
}
/**
* Get the stored rate limit for this field and member
* We use sessions if we're a member to avoid adding data to cache_store
*
* @return int
*/
protected function getRateLimitValue()
{
if ( \IPS\Member::loggedIn()->member_id )
{
return ( isset( $_SESSION[ 'url_fetch_' . $this->htmlId ] ) ? $_SESSION[ 'url_fetch_' . $this->htmlId ] : NULL );
}
else
{
$cached = NULL;
try
{
$cached = \IPS\Data\Cache::i()->getWithExpire( 'url_fetch_' . $this->htmlId . '-' . \IPS\Member::loggedIn()->ip_address, TRUE );
}
catch( \Exception $ex ) { }
return $cached;
}
}
/**
* Set the stored rate limit for this field and member
* We use sessions if we're a member to avoid adding data to cache_store
*
* @param mixed $value The value to store
*
* @return void
*/
protected function setRateLimitValue( $value )
{
if ( !\IPS\Member::loggedIn()->member_id )
{
$_SESSION[ 'url_fetch_' . $this->htmlId ] = $value;
}
else
{
\IPS\Data\Cache::i()->storeWithExpire( 'url_fetch_' . $this->htmlId . '-' . \IPS\Member::loggedIn()->ip_address, $value, \IPS\DateTime::create()->add( new \DateInterval( 'PT60M' ) ), TRUE );
}
}
/**
+12 -5
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -42,13 +42,20 @@ class _WidthHeight extends FormAbstract
/**
* Constructor
* Sets an appropriate default value
*
* @param string $name Name
* @param mixed $defaultValue Default value
* @param bool|NULL $required Required? (NULL for not required, but appears to be so)
* @param array $options Type-specific options
* @param callback $customValidationCode Custom validation code
* @param string $prefix HTML to show before input field
* @param string $suffix HTML to show after input field
* @param string $id The ID to add to the row
* @return void
*/
public function __construct()
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
call_user_func_array( 'parent::__construct', func_get_args() );
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
if ( $this->value === NULL )
{
@@ -71,7 +78,7 @@ class _WidthHeight extends FormAbstract
*/
public function html()
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->widthheight( $this->name, $this->value[0], $this->value[1], $this->options['unlimited'], $this->options['unlimitedLang'], $this->options['image'] ? $this->options['image'] : NULL, $this->options['resizableDiv'] );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->widthheight( $this->name, $this->value[0], $this->value[1], $this->options['unlimited'], $this->options['unlimitedLang'], $this->options['image'] ? $this->options['image'] : NULL, $this->options['resizableDiv'] );
}
/**
+1 -1
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -11,7 +11,7 @@
namespace IPS\Helpers;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -66,9 +66,9 @@ class _MultipleRedirect
elseif ( isset( $_SESSION[ $key ] ) )
{
$data = json_decode( $_SESSION[ $key ], TRUE );
$iteration = intval( \IPS\Request::i()->mr ) + 1;
$iteration = \intval( \IPS\Request::i()->mr ) + 1;
}
elseif ( !is_numeric( \IPS\Request::i()->mr ) and $base64Decoded = @base64_decode( \IPS\Request::i()->mr ) )
elseif ( !\is_numeric( \IPS\Request::i()->mr ) and $base64Decoded = @base64_decode( \IPS\Request::i()->mr ) )
{
$data = json_decode( urldecode( $base64Decoded ), TRUE );
$iteration = 1;
@@ -83,20 +83,20 @@ class _MultipleRedirect
{
unset( $_SESSION[ $key ] );
call_user_func( $finished );
$finished();
return;
}
try
{
$response = call_user_func( $callback, $data );
$response = $callback( $data );
if ( $response === NULL )
{
unset( $_SESSION[ $key ] );
if ( !\IPS\Request::i()->isAjax() )
{
call_user_func( $finished );
$finished();
return;
}
else
@@ -105,7 +105,7 @@ class _MultipleRedirect
\IPS\Output::i()->json( $finalRedirect ? array( 'done' => true ) : array( 'close' => true ) );
}
}
elseif ( is_string( $response[0] ) )
elseif ( \is_string( $response[0] ) )
{
$this->output = $response[0];
if ( \IPS\Request::i()->isAjax() )
@@ -147,6 +147,9 @@ class _MultipleRedirect
*/
public function __toString()
{
return $this->output ?: \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->multipleRedirect( $this->url, ( ( isset( \IPS\Request::i()->mr_continue ) and isset( \IPS\Request::i()->mr ) ) ? \IPS\Request::i()->mr : 0 ) );
/* Don't cache for a short while to ensure sessions work */
\IPS\Request::i()->setCookie( 'noCache', 1 );
return $this->output ?: \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->multipleRedirect( $this->url, ( ( isset( \IPS\Request::i()->mr_continue ) and isset( \IPS\Request::i()->mr ) ) ? \IPS\Request::i()->mr : 0 ), isset( \IPS\Request::i()->_wizardHeight ) ? \intval( \IPS\Request::i()->_wizardHeight ) : NULL );
}
}
+194 -43
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -101,7 +101,27 @@ class _Content extends Table
* @brief Array of item IDs the current $member has posted in
*/
public $contentPostedIn = array();
/**
* @brief Callback method to adjust rows
*/
protected $callback = NULL;
/**
* @brief Get first comment
*/
protected $getFirstComment = FALSE;
/**
* @brief Get follower count
*/
protected $getFollowerCount = FALSE;
/**
* @brief Get reactions for content item if supported
*/
protected $getReactions = FALSE;
/**
* Constructor
*
@@ -113,9 +133,12 @@ class _Content extends Table
* @param string|NULL $permCheck Permission key to check
* @param bool $honorPinned Show pinned topics at the top of the table
* @param bool $showMovedLinks Show moved links in the result set.
* @param NULL|callable $callback Method to call to prepare the returned rows
* @param bool $getFirstComment Get the first comment for this item
* @param bool $getFollowerCount Get the follower counts for this item
* @return void
*/
public function __construct( $class, \IPS\Http\Url $baseUrl, $where=NULL, \IPS\Node\Model $container=NULL, $includeHidden=\IPS\Content\Hideable::FILTER_AUTOMATIC, $permCheck='view', $honorPinned=TRUE, $showMovedLinks=FALSE )
public function __construct( $class, \IPS\Http\Url $baseUrl, $where=NULL, \IPS\Node\Model $container=NULL, $includeHidden=\IPS\Content\Hideable::FILTER_AUTOMATIC, $permCheck='view', $honorPinned=TRUE, $showMovedLinks=FALSE, $callback=NULL, $getFirstComment=FALSE, $getFollowerCount=FALSE, $getReactions=FALSE )
{
/* Init */
$this->include = array();
@@ -126,6 +149,10 @@ class _Content extends Table
$this->honorPinned = $honorPinned;
$this->showMovedLinks = $showMovedLinks;
$this->permCheck = $permCheck;
$this->callback = $callback;
$this->getFirstComment = $getFirstComment;
$this->getFollowerCount = $getFollowerCount;
$this->getReactions = $getReactions;
/* Init */
parent::__construct( $baseUrl );
@@ -155,15 +182,7 @@ class _Content extends Table
/* Set breadcrumb */
if ( \IPS\IPS::classUsesTrait( $container, 'IPS\Content\ClubContainer' ) and $club = $container->club() )
{
\IPS\core\FrontNavigation::$clubTabActive = TRUE;
\IPS\Output::i()->breadcrumb = array();
\IPS\Output::i()->breadcrumb[] = array( \IPS\Http\Url::internal( 'app=core&module=clubs&controller=directory', 'front', 'clubs_list' ), \IPS\Member::loggedIn()->language()->addToStack('module__core_clubs') );
\IPS\Output::i()->breadcrumb[] = array( $club->url(), $club->name );
if ( \IPS\Settings::i()->clubs_header == 'sidebar' )
{
\IPS\Output::i()->sidebar['contextual'] = \IPS\Theme::i()->getTemplate( 'clubs', 'core' )->header( $club, $container, 'sidebar' );
}
$club->setBreadcrumbs( $container );
}
else
{
@@ -171,16 +190,25 @@ class _Content extends Table
{
\IPS\Output::i()->breadcrumb[] = array( $parent->url(), $parent->_title );
}
\IPS\Output::i()->breadcrumb[] = array( NULL, $container->_title );
}
\IPS\Output::i()->breadcrumb[] = array( NULL, $container->_title );
/* We do want the page in the canonical link otherwise Google won't index past page 1 */
$canonicalUrl = $baseUrl;
if ( $this->page > 1 )
{
$canonicalUrl = $canonicalUrl->setPage( $this->paginationKey, $this->page );
}
/* Meta tags */
\IPS\Output::i()->title = ( $this->page > 1 ) ? \IPS\Member::loggedIn()->language()->addToStack( 'title_with_page_number', FALSE, array( 'sprintf' => array( $container->metaTitle(), $this->page ) ) ) : $container->metaTitle();
\IPS\Output::i()->metaTags['title'] = $container->metaTitle();
\IPS\Output::i()->metaTags['description'] = $container->metaDescription();
\IPS\Output::i()->metaTags['og:title'] = $container->metaTitle();
\IPS\Output::i()->metaTags['og:description'] = $container->metaDescription();
\IPS\Output::i()->linkTags['canonical'] = (string) $baseUrl;
\IPS\Output::i()->linkTags['canonical'] = (string) $canonicalUrl;
}
/* Set available sort options */
@@ -188,9 +216,13 @@ class _Content extends Table
{
if ( isset( $class::$databaseColumnMap[ $k ] ) and !isset( $this->sortOptions[ $k ] ) )
{
$column = is_array( $class::$databaseColumnMap[ $k ] ) ? array_shift( $class::$databaseColumnMap[ $k ] ) : $class::$databaseColumnMap[ $k ];
$this->sortOptions[ $k ] = $class::$databasePrefix . $column;
$column = \is_array( $class::$databaseColumnMap[ $k ] ) ? $class::$databaseColumnMap[ $k ][0] : $class::$databaseColumnMap[ $k ];
/* In some circumstances `updated` and `last_comment` may be the same column, but we don't want two sort options */
if( !\in_array( $class::$databasePrefix . $column, $this->sortOptions ) )
{
$this->sortOptions[$k] = $class::$databasePrefix . $column;
}
}
}
if ( !$this->sortBy )
@@ -201,7 +233,7 @@ class _Content extends Table
}
elseif ( isset( $class::$databaseColumnMap['last_comment'] ) )
{
$this->sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . ( is_array( $class::$databaseColumnMap['last_comment'] ) ? $class::$databaseColumnMap['last_comment'][0] : $class::$databaseColumnMap['last_comment'] );
$this->sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . ( \is_array( $class::$databaseColumnMap['last_comment'] ) ? $class::$databaseColumnMap['last_comment'][0] : $class::$databaseColumnMap['last_comment'] );
}
else
{
@@ -233,7 +265,7 @@ class _Content extends Table
{
if ( isset( $class::$databaseColumnMap[ $k ] ) )
{
if ( is_array( $class::$databaseColumnMap[ $k ] ) )
if ( \is_array( $class::$databaseColumnMap[ $k ] ) )
{
$cols = $class::$databaseColumnMap[ $k ];
$defaultSort = $class::$databaseTable . '.' . $class::$databasePrefix . array_pop( $cols );
@@ -246,6 +278,24 @@ class _Content extends Table
}
}
/* If we are limiting to a single container, check to see if this user can view them */
if ( $this->container and $this->permCheck )
{
try
{
if ( ! $this->container->can( $this->permCheck ) )
{
$this->count = 0;
return array();
}
}
catch( \OutOfBoundsException $e )
{
$this->count = 0;
return array();
}
}
$compareSortBy = $this->sortBy;
$lookFor = $class::$databaseTable . '.' . $class::$databasePrefix;
@@ -253,38 +303,134 @@ class _Content extends Table
{
$compareSortBy = mb_substr( $this->sortBy, mb_strlen( $lookFor ) );
}
$this->sortBy = in_array( $compareSortBy, $this->sortOptions ) ? $this->sortBy :
if( $this->sortBy AND \in_array( $this->sortBy, $class::$databaseColumnMap ) )
{
$len = mb_strlen( $class::$databaseTable );
$this->sortBy = ( mb_substr( $this->sortBy, 0, $len ) == $class::$databaseTable ) ? $this->sortBy : $class::$databaseTable . '.' . $this->sortBy;
}
$this->sortBy = \in_array( $compareSortBy, $this->sortOptions ) ? $this->sortBy :
( array_key_exists( $compareSortBy, $this->sortOptions ) ? $this->sortOptions[ $compareSortBy ] : $defaultSort );
/* Callback? */
if ( $this->advancedSearchCallback and !empty( $advancedSearchValues ) )
{
$obj = $this;
call_user_func( $this->advancedSearchCallback, $obj, $advancedSearchValues );
$advancedSearchCallback = $this->advancedSearchCallback;
$advancedSearchCallback( $obj, $advancedSearchValues );
}
/* What are we sorting by? */
$sortBy = $this->sortBy . ' ' . ( mb_strtolower( $this->sortDirection ) == 'asc' ? 'asc' : 'desc' );
if ( in_array( 'IPS\Content\Pinnable', class_implements( $class ) ) and $this->honorPinned )
if ( \in_array( 'IPS\Content\Pinnable', class_implements( $class ) ) and $this->honorPinned )
{
$column = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['pinned'];
$sortBy = "{$column} DESC, {$sortBy}";
}
/* Specify filter in where clause */
$where = $this->where ? is_array( $this->where ) ? $this->where : array( $this->where ) : array();
$where = $this->where ? \is_array( $this->where ) ? $this->where : array( $this->where ) : array();
if ( $this->filter and isset( $this->filters[ $this->filter ] ) )
{
$where[] = is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
$where[] = \is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
}
/* Get results */
$this->count = $class::getItemsWithPermission( $where, $sortBy, NULL, $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, TRUE, $this->joins, FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$it = $class::getItemsWithPermission( $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ), $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, FALSE, $this->joins, FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$this->count = $class::getItemsWithPermission( $where, $sortBy, NULL, $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, TRUE, $this->joins, ( $this->container ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$it = $class::getItemsWithPermission( $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ), $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, FALSE, $this->joins, ( $this->container ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$this->pages = ceil( $this->count / $this->limit );
$rows = iterator_to_array( $it );
$idField = $class::$databaseColumnId;
if ( isset( $this->getFirstComment ) and isset( $class::$databaseColumnMap['first_comment_id'] ) and $class::$firstCommentRequired )
{
$commentIds = array();
$firstCommentField = $class::$databaseColumnMap['first_comment_id'];
foreach( $rows as $item )
{
$commentIds[ $item->$idField ] = $item->$firstCommentField;
}
if ( \count( $commentIds ) )
{
$commentClass = $class::$commentClass;
$commentField = $commentClass::$databaseColumnId;
$comments = array();
foreach( $commentClass::getItemsWithPermission( array( \IPS\Db::i()->in( $commentClass::$databasePrefix . $commentField, array_values( $commentIds ) ) ), NULL, $this->limit ) as $cObj )
{
$comments[ $cObj->$commentField ] = $cObj;
}
foreach( $rows as $id => $item )
{
if ( isset( $commentIds[ $item->$idField ] ) and isset( $comments[ $commentIds[ $item->$idField ] ] ) )
{
$rows[ $id ]->firstComment = $comments[ $commentIds[ $item->$idField ] ];
}
}
}
}
if ( $this->getFollowerCount and \in_array( 'IPS\Content\Followable', class_implements( $class ) ) )
{
$followers = array();
foreach( $class::followersCounts( $rows ) as $follow )
{
$followers[ $follow['follow_rel_id'] ] = $follow['count'];
}
foreach( $rows as $id => $item )
{
if ( isset( $followers[ $item->$idField ] ) )
{
$rows[ $id ]->followerCount = $followers[ $item->$idField ];
}
else
{
$rows[ $id ]->followerCount = 0;
}
}
}
if ( $this->getReactions and \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) )
{
/* Get enabled reactions */
$reactions = array();
$itemIds = array();
foreach( $rows as $item )
{
$itemIds[] = $item->$idField;
}
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'rep_class=? AND ' . \IPS\Db::i()->in( 'item_id', $itemIds ), $class::$commentClass ) ) as $react )
{
$reactions[ $react['item_id'] ][] = $react;
}
foreach( $rows as $id => $item )
{
if ( isset( $reactions[ $item->$idField ] ) )
{
$rows[ $id ]->reputation = array();
foreach( $reactions[ $item->$idField ] as $itemRow )
{
$reputation[ $itemRow['member_id'] ] = $itemRow['reaction'];
}
$rows[ $id ]->reputation = $reputation;
}
else
{
$rows[ $id ]->reputation = array();
}
}
}
/* Pre load the contentPostedIn data to save separate DISTINCT queries later and a query for each unread item */
if ( isset( $class::$databaseColumnMap['container'] ) )
{
@@ -309,13 +455,18 @@ class _Content extends Table
catch( \OutOfRangeException $ex ) {}
}
}
/* Pull in extra data */
if ( method_exists( $class, 'tableGetRows' ) )
{
$class::tableGetRows( $rows );
}
if ( $this->callback != NULL and \is_callable( $this->callback ) )
{
$rows = \call_user_func( $this->callback, $rows );
}
/* Return */
return $rows;
}
@@ -513,7 +664,7 @@ class _Content extends Table
$params = array();
/* Permission check for the items we have specific actions for here, modActions will take care of permissions for the rest */
if( in_array( \IPS\Request::i()->modaction, array( 'hide', 'move', 'merge' ) ) )
if( \in_array( \IPS\Request::i()->modaction, array( 'hide', 'move', 'merge' ) ) )
{
$options = array();
$ids = array_keys( \IPS\Request::i()->moderate );
@@ -530,7 +681,7 @@ class _Content extends Table
}
/* The user doesn't have permission to perform the action on any of the content */
if ( !count( $options ) )
if ( !\count( $options ) )
{
throw new \OutOfRangeException;
}
@@ -543,7 +694,7 @@ class _Content extends Table
$params = $this->getMoveForm();
/* This is the form instead */
if( !is_array( $params ) )
if( !\is_array( $params ) )
{
return $params;
}
@@ -563,7 +714,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
@@ -571,7 +722,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html' );
}
return;
}
@@ -580,7 +731,7 @@ class _Content extends Table
/* Merge: what's the master? */
if ( \IPS\Request::i()->modaction == 'merge' )
{
if ( count( $options ) === 1 )
if ( \count( $options ) === 1 )
{
foreach ( $options as $id => $title )
{
@@ -594,7 +745,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
@@ -602,7 +753,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html' );
}
}
}
@@ -613,7 +764,7 @@ class _Content extends Table
$form->class = 'ipsForm_vertical';
$form->hiddenValues['modaction'] = 'merge';
$form->hiddenValues['moderate'] = \IPS\Request::i()->moderate;
$form->add( new \IPS\Helpers\Form\Radio( 'merge_master', NULL, TRUE, array( 'options' => $options, 'descriptions' => $descriptions ) ) );
$form->add( new \IPS\Helpers\Form\Radio( 'merge_master', NULL, TRUE, array( 'options' => $options, 'descriptions' => $descriptions, 'parse' => 'normal' ) ) );
if ( isset( $class::$databaseColumnMap['moved_to'] ) )
{
$form->add( new \IPS\Helpers\Form\Checkbox( 'move_keep_link' ) );
@@ -638,7 +789,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
@@ -646,7 +797,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html' );
}
return;
}
@@ -704,7 +855,7 @@ class _Content extends Table
'url' => \IPS\Request::i()->url()->setQueryString( 'modaction', 'move' ),
'permissionCheck' => function( $node ) use ( $currentContainer, $class )
{
if( $currentContainer->id != $node->id )
if( !$currentContainer or $currentContainer->id != $node->id )
{
try
{
@@ -746,7 +897,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
@@ -754,7 +905,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html' );
}
return;
}
+13 -8
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -51,7 +51,7 @@ class _Custom extends Table
public function __construct( $dataSource, \IPS\Http\Url $baseUrl )
{
$this->dataSource = $dataSource;
$this->count = count( $this->dataSource );
$this->count = \count( $this->dataSource );
return parent::__construct( $baseUrl );
}
@@ -74,13 +74,13 @@ class _Custom extends Table
$rows = array();
foreach ( $this->dataSource as $i => $data )
{
$row = $this->include ? array_combine( $this->include, array_fill( 0, count( $this->include ), NULL ) ) : array();
$row = $this->include ? array_combine( $this->include, array_fill( 0, \count( $this->include ), NULL ) ) : array();
/* Get the columns from the XML */
foreach ( $data as $k => $v )
{
/* Are we including this one? */
if( ( $this->include !== NULL and !in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and in_array( $k, $this->exclude ) ) )
if( ( $this->include !== NULL and !\in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and \in_array( $k, $this->exclude ) ) )
{
continue;
}
@@ -97,10 +97,13 @@ class _Custom extends Table
if ( $this->quickSearch !== NULL and \IPS\Request::i()->quicksearch )
{
$quickSearchColumn = $this->quickSearch;
$rows = array_filter( $rows, is_callable( $this->quickSearch ) ? $this->quickSearch : function( $row ) use ( $quickSearchColumn )
$rows = array_filter( $rows, \is_callable( $this->quickSearch ) ? $this->quickSearch : function( $row ) use ( $quickSearchColumn )
{
return mb_strpos( mb_strtolower( $row[ $quickSearchColumn ] ), mb_strtolower( trim( \IPS\Request::i()->quicksearch ) ) ) !== FALSE;
} );
$this->count = \count( $rows );
$this->pages = ceil( $this->count / $this->limit );
}
/* Do we need to sort? */
@@ -124,7 +127,7 @@ class _Custom extends Table
/* Limit */
if ( $this->limit and $this->count > $this->limit )
{
$rows = array_slice( $rows, ( $this->limit * ( $this->page - 1 ) ), $this->limit, TRUE );
$rows = \array_slice( $rows, ( $this->limit * ( $this->page - 1 ) ), $this->limit, TRUE );
}
/* Apply parsers */
@@ -135,7 +138,8 @@ class _Custom extends Table
/* Parse if necessary */
if( isset( $this->parsers[ $k ] ) )
{
$v = call_user_func( $this->parsers[ $k ], $v, $this->dataSource[ $i ] );
$parserFunction = $this->parsers[ $k ];
$v = $parserFunction( $v, $this->dataSource[ $i ] );
}
else
{
@@ -148,7 +152,8 @@ class _Custom extends Table
/* Add in some buttons if necessary */
if( $this->rowButtons !== NULL )
{
$rows[ $i ]['_buttons'] = call_user_func( $this->rowButtons, $row, $i );
$rowButtons = $this->rowButtons;
$rows[ $i ]['_buttons'] = $rowButtons( $row, $i );
}
}
+69 -45
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -62,6 +62,11 @@ class _Db extends Table
*/
public $keyField = NULL;
/**
* @brief The database we will query against
*/
public $db;
/**
* Constructor
*
@@ -69,19 +74,19 @@ class _Db extends Table
* @param \IPS\Http\Url $baseUrl Base URL
* @param array|null $where WHERE clause
* @param array|null $forceIndex Index to force
* @param \IPS\Db|null $database An instance of \IPS\Db to run the queries against (defaults to current connection)
* @return void
*/
public function __construct( $table, \IPS\Http\Url $baseUrl, $where=NULL, $forceIndex=NULL )
public function __construct( $table, \IPS\Http\Url $baseUrl, $where=NULL, $forceIndex=NULL, $database=NULL )
{
$this->table = $table;
$this->where = $where;
$this->index = $forceIndex;
$this->db = $database ?? \IPS\Db::i();
return parent::__construct( $baseUrl );
}
protected $onlySelected = false;
/**
* Get rows
*
@@ -91,24 +96,25 @@ class _Db extends Table
public function getRows( $advancedSearchValues )
{
/* Specify filter in where clause */
$where = $this->where ? is_array( $this->where ) ? $this->where : array( $this->where ) : array();
$where = $this->where ? \is_array( $this->where ) ? $this->where : array( $this->where ) : array();
if ( $this->filter and isset( $this->filters[ $this->filter ] ) )
{
$where[] = is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
$where[] = \is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
}
/* Add quick search term to where clause if necessary */
if ( $this->quickSearch !== NULL and \IPS\Request::i()->quicksearch )
{
if ( is_callable( $this->quickSearch ) )
if ( \is_callable( $this->quickSearch ) )
{
$where[] = call_user_func( $this->quickSearch, trim( \IPS\Request::i()->quicksearch ) );
$quickSearchFunc = $this->quickSearch;
$where[] = $quickSearchFunc( trim( \IPS\Request::i()->quicksearch ) );
}
else
{
$columns = is_array( $this->quickSearch ) ? $this->quickSearch[0] : $this->quickSearch;
$columns = is_array( $columns ) ? $columns : array( $columns );
$columns = \is_array( $this->quickSearch ) ? $this->quickSearch[0] : $this->quickSearch;
$columns = \is_array( $columns ) ? $columns : array( $columns );
$_where = array();
foreach ( $columns as $c )
@@ -116,7 +122,7 @@ class _Db extends Table
$_where[] = "LOWER(`{$c}`) LIKE CONCAT( '%', ?, '%' )";
}
$where[] = array_merge( array( '(' . implode( ' OR ', $_where ) . ')' ), array_fill( 0, count( $_where ), mb_strtolower( trim( \IPS\Request::i()->quicksearch ) ) ) );
$where[] = array_merge( array( '(' . implode( ' OR ', $_where ) . ')' ), array_fill( 0, \count( $_where ), mb_strtolower( trim( \IPS\Request::i()->quicksearch ) ) ) );
}
}
@@ -125,11 +131,11 @@ class _Db extends Table
{
foreach ( $advancedSearchValues as $k => $v )
{
if ( isset( $this->advancedSearch[ $k ] ) AND $v !== '' )
if ( isset( $this->advancedSearch[ $k ] ) AND $v !== '' AND ( !\is_array( $v ) OR !empty( $v ) ) )
{
$type = $this->advancedSearch[ $k ];
if ( is_array( $type ) )
if ( \is_array( $type ) )
{
if ( isset( $type[2] ) )
{
@@ -146,7 +152,7 @@ class _Db extends Table
switch ( $type )
{
case SEARCH_CUSTOM:
if ( $clause = call_user_func( $lambda, $v ) )
if ( $clause = $lambda( $v ) )
{
$where[] = $clause;
}
@@ -160,17 +166,24 @@ class _Db extends Table
$where[] = array( "{$k} LIKE ?", '%' . $v . '%' );
break;
case SEARCH_QUERY_TEXT:
switch ( $v[0] )
if ( !empty( $v[1] ) )
{
case 'c':
$where[] = array( "{$k} LIKE ?", '%' . $v[1] . '%' );
break;
case 'bw':
$where[] = array( "{$k} LIKE ?", $v[1] . '%' );
break;
case 'eq':
$where[] = array( "{$k}=?", $v[1] );
break;
switch ( $v[0] )
{
case 'c':
$where[] = array( "{$k} LIKE ?", '%' . $v[1] . '%' );
break;
case 'bw':
$where[] = array( "{$k} LIKE ?", $v[1] . '%' );
break;
case 'eq':
$where[] = array( "{$k}=?", $v[1] );
break;
}
}
else
{
unset( $advancedSearchValues[ $k ] );
}
break;
case SEARCH_DATE_RANGE:
@@ -204,7 +217,7 @@ class _Db extends Table
case SEARCH_SELECT:
if ( isset( $options['multiple'] ) AND $options['multiple'] === TRUE )
{
$where[] = array( \IPS\Db::i()->in( $k, $v ) );
$where[] = array( $this->db->findInSet( $k, $v ) );
break;
}
// No break so we fall through to radio
@@ -227,7 +240,7 @@ class _Db extends Table
case SEARCH_NODE:
$nodeClass = $options['class'];
$prop = isset( $options['searchProp'] ) ? $options['searchProp'] : '_id';
if ( !is_array( $v ) )
if ( !\is_array( $v ) )
{
$v = array( $v );
}
@@ -235,7 +248,7 @@ class _Db extends Table
$values = array();
foreach ( $v as $_v )
{
if ( !is_object( $_v ) )
if ( !\is_object( $_v ) )
{
if ( mb_substr( $_v, 0, 2 ) === 's.' )
{
@@ -253,7 +266,7 @@ class _Db extends Table
}
$values[] = $_v->$prop;
}
$where[] = array( \IPS\Db::i()->in( $k, $values ) );
$where[] = array( $this->db->in( $k, $values ) );
break;
case SEARCH_NUMERIC:
@@ -283,10 +296,10 @@ class _Db extends Table
}
}
}
$selects = $this->selects;
if ( count( $this->joins ) )
if ( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -298,8 +311,8 @@ class _Db extends Table
}
/* Count results (for pagination) */
$count = \IPS\Db::i()->select( 'count(*)', $this->table, $where );
if ( count( $this->joins ) )
$count = $this->db->select( 'count(*)', $this->table, $where );
if ( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -309,9 +322,9 @@ class _Db extends Table
$count = $count->first();
/* Now get column headers */
$query = \IPS\Db::i()->select( ( count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ) );
$query = $this->db->select( ( \count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ) );
if ( count( $this->joins ) )
if ( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -363,8 +376,8 @@ class _Db extends Table
/* Run query */
$rows = array();
$select = \IPS\Db::i()->select(
( count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*',
$select = $this->db->select(
( \count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*',
$this->table,
$where,
$orderBy,
@@ -376,7 +389,7 @@ class _Db extends Table
$select->forceIndex( $this->index );
}
if ( count( $this->joins ) )
if ( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -415,7 +428,8 @@ class _Db extends Table
/* Parse if necessary (NB: deliberately do this before removing the row in case we need to do some processing, but don't want the column to actually show) */
if( isset( $this->parsers[ $k ] ) )
{
$v = call_user_func( $this->parsers[ $k ], $v, $_row );
$thisParser = $this->parsers[ $k ];
$v = $thisParser( $v, $_row );
}
else
{
@@ -423,7 +437,7 @@ class _Db extends Table
}
/* Are we including this one? */
if( ( ( $this->include !== NULL and !in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and in_array( $k, $this->exclude ) ) ) and !array_key_exists( $k, $advancedSearchValues ) )
if( ( ( $this->include !== NULL and !\in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and \in_array( $k, $this->exclude ) ) ) and !array_key_exists( $k, $advancedSearchValues ) )
{
unset( $row[ $k ] );
continue;
@@ -436,13 +450,18 @@ class _Db extends Table
/* Add in some buttons if necessary */
if( $this->rowButtons !== NULL )
{
$row['_buttons'] = call_user_func( $this->rowButtons, $_row );
$rowButtons = $this->rowButtons;
$row['_buttons'] = $rowButtons( $_row );
}
/* Highlighting? */
if ( isset( $this->parsers['_highlight'] ) and $class = call_user_func( $this->parsers['_highlight'], $_row ) )
if ( isset( $this->parsers['_highlight'] ) )
{
$this->highlightRows[ $rowId ] = $class;
$class = $this->parsers['_highlight'];
if( $class = $class( $_row ) )
{
$this->highlightRows[ $rowId ] = $class;
}
}
$rows[ $rowId ] = $row;
@@ -455,6 +474,11 @@ class _Db extends Table
$sortDirection = $this->sortDirection;
uasort( $rows, function( $a, $b ) use ( $sortBy, $sortDirection )
{
if( !isset( $a[ $sortBy ] ) )
{
return 0;
}
if( $sortDirection === 'asc' )
{
return strnatcasecmp( mb_strtolower( $a[ $sortBy ] ), mb_strtolower( $b[ $sortBy ] ) );
@@ -472,7 +496,7 @@ class _Db extends Table
/**
* User set sortBy is suitable for an SQL sort operation
* @param array $count Result of COUNT(*) query with field names included
* @param array $count Result of count(*) query with field names included
* @return boolean
*/
protected function _isSqlSort( $count )
@@ -482,7 +506,7 @@ class _Db extends Table
return false;
}
if( !is_array( $count ) )
if( !\is_array( $count ) )
{
$count = array( $count );
}
+53 -29
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -29,6 +29,8 @@ const HEADER = 8;
const SEARCH_RADIO = 9;
const SEARCH_NUMERIC_TEXT = 10;
const SEARCH_QUERY_TEXT = 11;
const SEARCH_CHECKBOX = 12;
/**
* List Table Builder
*/
@@ -198,7 +200,7 @@ abstract class _Table
{
return array( ... ); // Same format as IPS\Helpers\Table::$rootButtons
}
* @code
* @endcode
*/
public $rowButtons = NULL;
@@ -255,7 +257,8 @@ abstract class _Table
if ( \IPS\Request::i()->$parameter )
{
$this->page = intval( \IPS\Request::i()->$parameter );
$this->page = \intval( \IPS\Request::i()->$parameter );
if ( !$this->page OR $this->page < 1 )
{
$this->page = 1;
@@ -279,7 +282,7 @@ abstract class _Table
}
/* Set base URL */
$this->baseUrl = $baseUrl->setQueryString( array( 'filter' => $this->filter, 'sortby' => $this->sortBy, 'sortdirection' => $this->sortDirection, $this->paginationKey => $this->page ) );
$this->baseUrl = $baseUrl->setQueryString( array( 'filter' => $this->filter, 'sortby' => $this->sortBy, 'sortdirection' => $this->sortDirection ) )->setPage( $this->paginationKey, $this->page );
/* Templates */
$this->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'tables', 'core' ), 'table' );
@@ -315,7 +318,7 @@ abstract class _Table
if ( \IPS\Request::i()->$value )
{
$this->page = intval( \IPS\Request::i()->$value );
$this->page = \intval( \IPS\Request::i()->$value );
if ( !$this->page OR $this->page < 1 )
{
$this->page = 1;
@@ -331,9 +334,7 @@ abstract class _Table
* For example, we might allow array( 'last_post', 'author' ) but the $this->sortBy property is often set as "app_table.last_post" meaning
* that the filter drop down doesn't show the selected value correctly.
*
* @param string $property Property we are updating
* @param mixed $value Value being set
* @return void
* @return string
*/
public function getSortByColumn()
{
@@ -363,7 +364,7 @@ abstract class _Table
foreach ( $this->advancedSearch as $k => $type )
{
$options = array();
if ( is_array( $type ) )
if ( \is_array( $type ) )
{
$options = $type[1];
$type = $type[0];
@@ -383,13 +384,13 @@ abstract class _Table
$form->add( new \IPS\Helpers\Form\Custom( $this->langPrefix . $k, NULL, FALSE, array(
'getHtml' => function( $element )
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", $element->value[0], $element->required, array(
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", ( \is_array( $element->value ) AND isset( $element->value[0] ) ) ? $element->value[0] : NULL, $element->required, array(
'c' => \IPS\Member::loggedIn()->language()->addToStack('contains'),
'bw' => \IPS\Member::loggedIn()->language()->addToStack('begins_with'),
'eq' => \IPS\Member::loggedIn()->language()->addToStack('exactly'),
) )
. ' '
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( "{$element->name}[1]", 'text', $element->value[1], $element->required, NULL, FALSE, NULL, NULL, NULL, '', NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( "{$element->name}[1]", 'text', ( \is_array( $element->value ) AND isset( $element->value[1] ) ) ? $element->value[1] : NULL, $element->required, NULL, FALSE, NULL, NULL, NULL, '', NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
}
) ) );
break;
@@ -419,7 +420,7 @@ abstract class _Table
$form->add( new \IPS\Helpers\Form\Custom( $this->langPrefix . $k, NULL, FALSE, array(
'getHtml' => function( $element )
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", $element->value[0], $element->required, array(
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", ( \is_array( $element->value ) AND isset( $element->value[0] ) ) ? $element->value[0] : NULL, $element->required, array(
'any' => \IPS\Member::loggedIn()->language()->addToStack('any'),
'gt' => \IPS\Member::loggedIn()->language()->addToStack('gt'),
'lt' => \IPS\Member::loggedIn()->language()->addToStack('lt'),
@@ -435,11 +436,14 @@ abstract class _Table
'eq' => array( $element->name . '-qty' ),
) )
. ' '
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->number( "{$element->name}[1]", $element->value[1], $element->required, NULL, FALSE, NULL, NULL, NULL, 0, NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->number( "{$element->name}[1]", ( \is_array( $element->value ) AND isset( $element->value[1] ) ) ? $element->value[1] : NULL, $element->required, NULL, FALSE, NULL, NULL, NULL, 0, NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
}
) ) );
break;
case SEARCH_CHECKBOX:
$form->add( new \IPS\Helpers\Form\Checkbox( $this->langPrefix . $k, NULL, FALSE, $options ) );
break;
case SEARCH_BOOL:
$form->add( new \IPS\Helpers\Form\YesNo( $this->langPrefix . $k, TRUE, FALSE, $options ) );
break;
@@ -448,7 +452,7 @@ abstract class _Table
break;
}
}
return $form;
}
@@ -482,36 +486,46 @@ abstract class _Table
}
/* Get rows */
$rows = $this->getRows( $advancedSearchValues );
$rows = $this->getRows( $advancedSearchValues ) ?? array();
/* Check we're on a valid page (must come after getRows() as this is where $this->pages is set) */
if ( $this->page )
{
if ( $this->pages and $this->page > $this->pages )
{
\IPS\Output::i()->redirect( $this->baseUrl->setQueryString( $this->paginationKey, 1 ), NULL, 303 );
\IPS\Output::i()->redirect( $this->baseUrl->setPage( $this->paginationKey, 1 ), NULL, 303 );
}
}
/* Add link tags */
if ( $this->page != 1 )
{
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, $this->page );
\IPS\Output::i()->linkTags['first'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, 1 );
\IPS\Output::i()->linkTags['prev'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, $this->page - 1 );
if( !isset( \IPS\Output::i()->linkTags['canonical'] ) )
{
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page );
}
\IPS\Output::i()->linkTags['first'] = (string) $this->baseUrl->setPage( $this->paginationKey, 1 );
\IPS\Output::i()->linkTags['prev'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page - 1 );
}
/* If we literally requested ?page=1 add canonical tag to get rid of the page query string param */
elseif( isset( $this->baseUrl->data[ \IPS\Http\Url::COMPONENT_QUERY ][ $this->paginationKey ] ) )
{
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->stripQueryString( $this->paginationKey );
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setPage();
}
if ( $this->pages > $this->page )
{
\IPS\Output::i()->linkTags['next'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, $this->page + 1 );
\IPS\Output::i()->linkTags['next'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page + 1 );
}
if ( $this->pages != $this->page )
{
\IPS\Output::i()->linkTags['last'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, $this->pages );
\IPS\Output::i()->linkTags['last'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->pages );
}
/* No rows to show? Add a noindex but follow for crawling later to check if content exists */
if ( ! \count( $rows ) )
{
\IPS\Output::i()->metaTags['robots'] = 'noindex, follow';
}
/* Get table headers */
@@ -521,9 +535,10 @@ abstract class _Table
$resortKey = $this->resortKey;
if( \IPS\Request::i()->isAjax() and \IPS\Request::i()->$resortKey )
{
if ( count( $rows ) )
if ( \count( $rows ) )
{
$rowHtml = call_user_func( $this->rowsTemplate, $this, $headers, $rows, $this->mainColumn, $this->rootButtons, array() );
$rowsTemplate = $this->rowsTemplate;
$rowHtml = $rowsTemplate( $this, $headers, $rows, $this->mainColumn, $this->rootButtons, array() );
}
else
{
@@ -545,7 +560,8 @@ abstract class _Table
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_core.js', 'core', 'global' ) );
/* Build table */
return call_user_func( $this->tableTemplate, $this, $headers, $rows, ( is_array( $this->quickSearch ) ? $this->quickSearch[1] : $this->quickSearch ), !empty( $this->advancedSearch ) );
$tableTemplate = $this->tableTemplate;
return $tableTemplate( $this, $headers, $rows, ( \is_array( $this->quickSearch ) ? $this->quickSearch[1] : $this->quickSearch ), !empty( $this->advancedSearch ) );
}
}
catch ( \Exception $e )
@@ -569,7 +585,7 @@ abstract class _Table
$return = array();
foreach( $values as $k => $v )
{
if ( is_array( $v ) )
if ( \is_array( $v ) )
{
$return[ $k ] = $this->_convertSearchValuesForQueryString( $v );
}
@@ -690,6 +706,15 @@ abstract class _Table
$advancedSearchValues = array();
$advancedSearchValuesQuery = array();
/* Store these to prevent URL from being rebuilt on subsequent calls */
$storeSearchValues = FALSE;
if ( $this->advancedSearchValues === NULL )
{
$storeSearchValues = TRUE;
$this->advancedSearchValues = array();
}
if ( !empty( $this->advancedSearch ) )
{
/* Are we displaying the advanced search form? */
@@ -703,9 +728,8 @@ abstract class _Table
$form = $this->advancedSearch();
/* Store these to prevent URL from being rebuilt on subsequent calls */
if ( $this->advancedSearchValues === NULL )
if ( $storeSearchValues === TRUE )
{
$this->advancedSearchValues = array();
foreach ( $values as $k => $v )
{
if( array_key_exists( $k, $form->hiddenValues ) )
+27 -19
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Tree;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -80,17 +80,18 @@ class _Tree
/**
* Constructor
*
* @param string $url URL where the tree table is displayed
* @param string $title Tree Table title
* @param callback $getRoots Callback function to get the root rows
* @param callback $getRow Callback function to get a single row by ID
* @param callback $getRowParentId Callback function to get the parent ID for a row
* @param callback $getChildren Callback function to get the child rows for a row
* @param callback $searchable Show the search bar?
* @param bool $lockParents If true, will prevent any item from being moved out of its current parent, only allowing them to be reordered within their current parent
* @param bool $protectRoots If true, root cannot be turned into sub-items, and other items cannot be turned into roots
* @param int|null $rootsPerPage Number of roots to show per page (NULL for unlimited). In most cases this doesn't make sense, since it makes re-ordering impossible. But for trees which are not orderable and which may contain a lot of roots, you can set this value
* @param callback $getTotalRoots If using $rootsPerPage, a callback function that returns the total number of roots
* @param string $url URL where the tree table is displayed
* @param string $title Tree Table title
* @param callback $getRoots Callback function to get the root rows
* @param callback $getRow Callback function to get a single row by ID
* @param callback $getRowParentId Callback function to get the parent ID for a row
* @param callback $getChildren Callback function to get the child rows for a row
* @param callback|null $getRootButtons Callback function to get the root buttons
* @param callback $searchable Show the search bar?
* @param bool $lockParents If true, will prevent any item from being moved out of its current parent, only allowing them to be reordered within their current parent
* @param bool $protectRoots If true, root cannot be turned into sub-items, and other items cannot be turned into roots
* @param int|null $rootsPerPage Number of roots to show per page (NULL for unlimited). In most cases this doesn't make sense, since it makes re-ordering impossible. But for trees which are not orderable and which may contain a lot of roots, you can set this value
* @param callback $getTotalRoots If using $rootsPerPage, a callback function that returns the total number of roots
* @return void
*/
public function __construct( $url, $title, $getRoots, $getRow, $getRowParentId, $getChildren, $getRootButtons=NULL, $searchable=FALSE, $lockParents=FALSE, $protectRoots=FALSE, $rootsPerPage = NULL, $getTotalRoots = NULL )
@@ -119,31 +120,37 @@ class _Tree
try
{
/* Get rows */
$page = isset( \IPS\Request::i()->page ) ? intval( \IPS\Request::i()->page ) : 1;
$page = isset( \IPS\Request::i()->page ) ? \intval( \IPS\Request::i()->page ) : 1;
$root = NULL;
$rootParent = NULL;
if( !\IPS\Request::i()->root )
{
$rows = call_user_func( $this->getRoots, $this->rootsPerPage ? array( ( $page - 1 ) * $this->rootsPerPage, $this->rootsPerPage ) : NULL );
$getRootsFunction = $this->getRoots;
$rows = $getRootsFunction( $this->rootsPerPage ? array( ( $page - 1 ) * $this->rootsPerPage, $this->rootsPerPage ) : NULL );
}
else
{
$rows = call_user_func( $this->getChildren, \IPS\Request::i()->root );
$getChildrenFunction = $this->getChildren;
$rows = $getChildrenFunction( \IPS\Request::i()->root );
if ( \IPS\Request::i()->isAjax() )
{
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->rows( $rows, mt_rand() );
}
$root = call_user_func( $this->getRow, \IPS\Request::i()->root, TRUE );
$rootParent = call_user_func( $this->getRowParentId, \IPS\Request::i()->root );
$getRowFunction = $this->getRow;
$root = $getRowFunction( \IPS\Request::i()->root, TRUE );
$getRowParentIdFunction = $this->getRowParentId;
$rootParent = $getRowParentIdFunction( \IPS\Request::i()->root );
}
/* Pagination? */
$pagination = '';
if ( $this->rootsPerPage )
{
$totalNumber = call_user_func( $this->getTotalRoots );
$getTotalRootsFunction = $this->getTotalRoots;
$totalNumber = $getTotalRootsFunction();
if ( $totalNumber )
{
$pagination = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->pagination( $this->url, ceil( $totalNumber / $this->rootsPerPage ), $page, $this->rootsPerPage );
@@ -151,7 +158,8 @@ class _Tree
}
/* Display */
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->template( $this->url, $this->title, $root, $rootParent, $rows, call_user_func( $this->getRootButtons ), $this->lockParents, $this->protectRoots, $this->searchable, $pagination );
$getRootButtonsFunction = $this->getRootButtons;
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->template( $this->url, $this->title, $root, $rootParent, $rows, $getRootButtonsFunction(), $this->lockParents, $this->protectRoots, $this->searchable, $pagination );
}
catch ( \Exception $e )
{
+14 -8
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -100,7 +100,7 @@ class _Wizard
unset( $_SESSION[ 'wizard-' . md5( $this->baseUrl ) . '-step' ] );
unset( $_SESSION[ 'wizard-' . md5( $this->baseUrl ) . '-data' ] );
if ( !is_null( $initialData ) )
if ( !\is_null( $initialData ) )
{
$_SESSION[ 'wizard-' . md5( $this->baseUrl ) . '-data' ] = $initialData;
}
@@ -119,6 +119,9 @@ class _Wizard
*/
public function __toString()
{
/* Don't cache for a short while to ensure sessions work */
\IPS\Request::i()->setCookie( 'noCache', 1 );
try
{
$stepKeys = array_keys( $this->steps );
@@ -136,7 +139,7 @@ class _Wizard
{
$activeStep = $_SESSION[ 'wizard-' . md5( $this->baseUrl ) . '-step' ];
if ( isset( \IPS\Request::i()->_step ) and in_array( \IPS\Request::i()->_step, $stepKeys ) )
if ( isset( \IPS\Request::i()->_step ) and \in_array( \IPS\Request::i()->_step, $stepKeys ) )
{
foreach ( $stepKeys as $key )
{
@@ -161,7 +164,7 @@ class _Wizard
}
}
if ( isset( \IPS\Request::i()->_moveToStep ) and in_array( \IPS\Request::i()->_moveToStep, $stepKeys ) )
if ( isset( \IPS\Request::i()->_moveToStep ) and \in_array( \IPS\Request::i()->_moveToStep, $stepKeys ) )
{
foreach ( $stepKeys as $key )
{
@@ -174,8 +177,9 @@ class _Wizard
}
/* Get it's output */
$output = call_user_func( $this->steps[ $activeStep ], $data );
while ( is_array( $output ) )
$activeStepFunction = $this->steps[ $activeStep ];
$output = $activeStepFunction( $data );
while ( \is_array( $output ) )
{
$data = array_merge( $data, $output );
@@ -210,11 +214,13 @@ class _Wizard
unset( $_SESSION[ 'wizard-' . md5( $this->baseUrl ) . '-data' ] );
}
$output = call_user_func( $this->steps[ $activeStep ], $data );
$activeStepFunction = $this->steps[ $activeStep ];
$output = $activeStepFunction( $data );
}
/* Display */
return call_user_func( $this->template, $stepKeys, $activeStep, $output, $this->baseUrl, $this->showSteps );
$htmlTemplate = $this->template;
return $htmlTemplate( $stepKeys, $activeStep, $output, $this->baseUrl, $this->showSteps );
}
catch ( \Exception $e )
{