Version 4.3.0

This commit is contained in:
Neo committed 2025-12-19 05:47:12 -08:00
1 parent 96997ddd8e
commit fe1acf984a
1349 files changed
+116159 -67711

No files matched your search

+28 -19
View File
@@ -166,26 +166,35 @@ class _Callback extends \IPS\Helpers\Chart\Dynamic
$datetime->setTime( 0, 0, 0 );
$exploded = explode( '-', $time );
switch ( $this->timescale )
{
case 'none':
$datetime = \IPS\DateTime::ts( $time );
break;
case 'daily':
$datetime->setDate( (float) $exploded[0], $exploded[1], $exploded[2] );
//$datetime = $datetime->localeDate();
break;
case 'weekly':
$datetime->setISODate( (float) $exploded[0], $exploded[1] );
//$datetime = $datetime->localeDate();
break;
case 'monthly':
$datetime->setDate( (float) $exploded[0], $exploded[1], 1 );
//$datetime = $datetime->format( 'F Y' );
break;
if( $this->enableHourly === TRUE AND $this->timescale == 'hourly' )
{
$datetime->setDate( (float) $exploded[0], $exploded[1], $exploded[2] );
$datetime->setTime( $exploded[3], $exploded[4], $exploded[5] );
}
else
{
switch ( $this->timescale )
{
case 'none':
$datetime = \IPS\DateTime::ts( $time );
break;
case 'daily':
$datetime->setDate( (float) $exploded[0], $exploded[1], $exploded[2] );
//$datetime = $datetime->localeDate();
break;
case 'weekly':
$datetime->setISODate( (float) $exploded[0], $exploded[1] );
//$datetime = $datetime->localeDate();
break;
case 'monthly':
$datetime->setDate( (float) $exploded[0], $exploded[1], 1 );
//$datetime = $datetime->format( 'F Y' );
break;
}
}
if ( empty( $d ) )
+80 -22
View File
@@ -36,6 +36,11 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
* @brief Where clauses
*/
public $where = array();
/**
* @brief Query joins
*/
public $joins = array();
/**
* @brief Extra column to group by (useful for multi-data line charts)
@@ -78,12 +83,11 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
{
$this->table = $table;
$this->dateField = $dateField;
$this->identifier = \substr( md5( $table . $dateField ), 0, 6 ) . $identifier;
$this->identifier = \substr( md5( $table . $dateField ), 0, 6 ) . ( $identifier ?: ( \IPS\Request::i()->chartId ?: '_default' ) );
if ( !empty( $tableInclude ) )
{
$this->tableInclude = $tableInclude;
$this->availableTypes[] = 'Table';
}
parent::__construct( $url, $title, $options, $defaultType, $defaultTimescale, $defaultTimes, $identifier, $minimumDate );
@@ -155,6 +159,15 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
*/
public function getOutput()
{
/* Auto-support tables where appropriate */
if ( !empty( $this->tableInclude ) )
{
if( !array_search( 'Table', $this->availableTypes ) )
{
$this->availableTypes[] = 'Table';
}
}
/* Init data */
$data = $this->initData();
@@ -173,6 +186,10 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
/* What's our SQL time? */
switch ( $this->timescale )
{
case 'hourly':
$timescale = '%Y-%c-%e-%H-%i-%s';
break;
case 'daily':
$timescale = '%Y-%c-%e';
break;
@@ -189,11 +206,27 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
/* 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 );
$table->include = $this->tableInclude;
$table->joins = $this->joins;
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 */
@@ -210,6 +243,14 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
NULL,
$this->groupBy
)->setKeyField( $this->groupBy )->setValueField( $key );
if( count( $this->joins ) )
{
foreach( $this->joins as $join )
{
$stmt = $stmt->join( $join[0], $join[1], ( isset( $join[2] ) ? $join[2] : 'LEFT' ), ( isset( $join[3] ) ? $join[3] : FALSE ) );
}
}
foreach ( $stmt as $k => $v )
{
@@ -241,6 +282,14 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
$this->groupBy ? array( 'time_as_alias', $this->groupBy ) : 'time_as_alias'
);
if( count( $this->joins ) )
{
foreach( $this->joins as $join )
{
$stmt = $stmt->join( $join[0], $join[1], ( isset( $join[2] ) ? $join[2] : 'LEFT' ), ( isset( $join[3] ) ? $join[3] : FALSE ) );
}
}
foreach ( $stmt as $row )
{
$result = array();
@@ -300,26 +349,35 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
$datetime->setTime( 0, 0, 0 );
$exploded = explode( '-', $time );
switch ( $this->timescale )
{
case 'none':
$datetime = \IPS\DateTime::ts( $time );
break;
case 'daily':
$datetime->setDate( (float) $exploded[0], $exploded[1], $exploded[2] );
//$datetime = $datetime->localeDate();
break;
case 'weekly':
$datetime->setISODate( (float) $exploded[0], $exploded[1] );
//$datetime = $datetime->localeDate();
break;
case 'monthly':
$datetime->setDate( (float) $exploded[0], $exploded[1], 1 );
//$datetime = $datetime->format( 'F Y' );
break;
if( $this->enableHourly === TRUE AND $this->timescale == 'hourly' )
{
$datetime->setDate( (float) $exploded[0], $exploded[1], $exploded[2] );
$datetime->setTime( $exploded[3], $exploded[4], $exploded[5] );
}
else
{
switch ( $this->timescale )
{
case 'none':
$datetime = \IPS\DateTime::ts( $time );
break;
case 'daily':
$datetime->setDate( (float) $exploded[0], $exploded[1], $exploded[2] );
//$datetime = $datetime->localeDate();
break;
case 'weekly':
$datetime->setISODate( (float) $exploded[0], $exploded[1] );
//$datetime = $datetime->localeDate();
break;
case 'monthly':
$datetime->setDate( (float) $exploded[0], $exploded[1], 1 );
//$datetime = $datetime->format( 'F Y' );
break;
}
}
if ( empty( $d ) )
+179 -7
View File
@@ -107,6 +107,11 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
*/
public $minimumDate = NULL;
/**
* @brief Enable hourly filtering. USE WITH CAUTION.
*/
public $enableHourly = FALSE;
/**
* @brief Error(s) to show on chart UI
*/
@@ -128,6 +133,13 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
*/
public function __construct( \IPS\Http\Url $url, $title='', $options=array(), $defaultType='AreaChart', $defaultTimescale='monthly', $defaultTimes=array( 'start' => 0, 'end' => 0 ), $identifier='', $minimumDate=NULL )
{
/* If we are deleting a chart, just do that now and redirect */
if( isset( \IPS\Request::i()->deleteChart ) )
{
\IPS\Db::i()->delete( 'core_saved_charts', array( 'chart_id=? and chart_member=?', \IPS\Request::i()->deleteChart, \IPS\Member::loggedIn()->member_id ) );
\IPS\Output::i()->redirect( \IPS\Request::i()->url()->stripQueryString( array( 'deleteChart', 'chartId' ) ), 'chart_deleted' );
}
if ( !isset( $options['chartArea'] ) )
{
$options['chartArea'] = array(
@@ -135,6 +147,11 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
'width' => '75%'
);
}
if( isset( \IPS\Request::i()->chartId ) AND \IPS\Request::i()->chartId != '_default' )
{
$url = $url->setQueryString( 'chartId', \IPS\Request::i()->chartId );
}
$this->baseURL = $url;
$this->title = $title;
@@ -154,10 +171,13 @@ 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( 'daily', 'weekly', 'monthly' ) ) )
if ( isset( \IPS\Request::i()->timescale[ $this->identifier ] ) and in_array( \IPS\Request::i()->timescale[ $this->identifier ], array( 'hourly', 'daily', 'weekly', 'monthly' ) ) )
{
$this->timescale = \IPS\Request::i()->timescale[ $this->identifier ];
$url = $url->setQueryString( 'timescale', array( $this->identifier => \IPS\Request::i()->timescale[ $this->identifier ] ) );
if( \IPS\Request::i()->timescale[ $this->identifier ] != 'hourly' OR ( \IPS\Request::i()->timescale[ $this->identifier ] == 'hourly' AND $this->enableHourly === TRUE ) )
{
$this->timescale = \IPS\Request::i()->timescale[ $this->identifier ];
$url = $url->setQueryString( 'timescale', array( $this->identifier => \IPS\Request::i()->timescale[ $this->identifier ] ) );
}
}
if ( $this->type === 'PieChart' or $this->type === 'GeoChart' )
@@ -167,7 +187,7 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
}
else
{
$this->addHeader( \IPS\Member::loggedIn()->language()->addToStack('date'), ( $this->timescale == 'none' ) ? 'datetime' : 'date' );
$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 ] )
@@ -182,7 +202,7 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
}
else
{
$this->start = new \IPS\DateTime( \IPS\Request::i()->start[ $this->identifier ], new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) );
$this->start = new \IPS\DateTime( \IPS\Helpers\Form\Date::_convertDateFormat( \IPS\Request::i()->start[ $this->identifier ] ), new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) );
}
if( $this->minimumDate > $this->start )
@@ -213,7 +233,7 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
}
else
{
$this->end = new \IPS\DateTime( \IPS\Request::i()->end[ $this->identifier ], new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) );
$this->end = new \IPS\DateTime( \IPS\Helpers\Form\Date::_convertDateFormat( \IPS\Request::i()->end[ $this->identifier ] ), new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) );
}
/* The end date should include items to the end of the day */
@@ -246,6 +266,18 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
$this->timezoneError = TRUE;
}
}
/* If we have requested a saved chart, load its filters */
if( isset( \IPS\Request::i()->chartId ) AND \IPS\Request::i()->chartId != '_default' AND !isset( \IPS\Request::i()->filters ) )
{
foreach( $this->loadAvailableChartTabs() as $chart )
{
if( $chart['chart_id'] == \IPS\Request::i()->chartId )
{
\IPS\Request::i()->filters = array( $this->identifier => json_decode( $chart['chart_configuration'], true ) );
}
}
}
}
/**
@@ -255,6 +287,11 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
*/
abstract public function getOutput();
/**
* @brief Form to save filters
*/
public $form = NULL;
/**
* HTML
*
@@ -264,6 +301,89 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
{
try
{
/* If we have filters, we can save them */
if( count( $this->availableFilters ) > 0 )
{
/* We have an existing chart ID */
if( isset( \IPS\Request::i()->chartId ) AND \IPS\Request::i()->chartId != '_default' )
{
$title = '';
foreach( $this->loadAvailableChartTabs() as $chart )
{
if( $chart['chart_id'] == \IPS\Request::i()->chartId )
{
$title = $chart['chart_title'];
break;
}
}
/* Generate a form so we can save our filters as a new saved chart */
$this->form = new \IPS\Helpers\Form;
$this->form->class = 'ipsForm_vertical';
$this->form->add( new \IPS\Helpers\Form\Text( 'custom_chart_title', $title, TRUE ) );
if( $values = $this->form->values() )
{
\IPS\Db::i()->update( 'core_saved_charts', array( 'chart_title' => $values['custom_chart_title'] ), array( 'chart_id=? AND chart_member=?', \IPS\Request::i()->chartId, \IPS\Member::loggedIn()->member_id ) );
/* And then return the output we need */
\IPS\Output::i()->json( array(
'title' => $values['custom_chart_title']
) );
}
/* And we want to save our filter updates */
if( isset( \IPS\Request::i()->saveFilters ) )
{
\IPS\Db::i()->update( 'core_saved_charts', array( 'chart_configuration' => json_encode( \IPS\Request::i()->chartFilters ) ), array( 'chart_id=? AND chart_member=?', \IPS\Request::i()->chartId, \IPS\Member::loggedIn()->member_id ) );
}
}
/* We are not viewing a saved chart */
else
{
/* Generate a form so we can save our filters as a new saved chart */
$this->form = new \IPS\Helpers\Form;
$this->form->class = 'ipsForm_vertical';
$this->form->add( new \IPS\Helpers\Form\Text( 'custom_chart_title', NULL, TRUE ) );
if( $values = $this->form->values() )
{
/* Store the new chart */
$id = \IPS\Db::i()->insert( 'core_saved_charts', array(
'chart_member' => \IPS\Member::loggedIn()->member_id,
'chart_controller' => \IPS\Request::i()->app . '_' . \IPS\Request::i()->module . '_' . \IPS\Request::i()->controller . ( \IPS\Request::i()->tab ? '_' . \IPS\Request::i()->tab : '' ),
'chart_configuration' => json_encode( \IPS\Request::i()->chartFilters ),
'chart_title' => $values['custom_chart_title'],
) );
/* Set some input parameters */
$this->url = $this->url->setQueryString( 'chartId', $id );
\IPS\Request::i()->chartId = $id;
\IPS\Request::i()->filters = array( $this->identifier => \IPS\Request::i()->chartFilters );
$this->currentFilters = \IPS\Request::i()->filters;
if ( isset( \IPS\Request::i()->filters[ $this->identifier ] ) )
{
$this->url = $this->url->setQueryString( 'filters', array( $this->identifier => $this->currentFilters ) );
}
/* Reset form, since template looks for it and it should not be set for a saved chart */
$this->form = new \IPS\Helpers\Form;
$this->form->class = 'ipsForm_vertical';
$this->form->add( new \IPS\Helpers\Form\Text( 'custom_chart_title', $values['custom_chart_title'], TRUE ) );
/* And then return the output we need */
\IPS\Output::i()->json( array(
'tabHref' => $this->url->stripQueryString( 'filters' ),
'chartId' => $id,
'tabId' => md5( $this->url->acpQueryString() ),
) );
}
}
}
/* Get data */
$output = '';
if ( !empty( $this->series ) )
@@ -282,7 +402,16 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
}
else
{
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->dynamicChart( $this, $output );
$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 ) ) )
{
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' );
}
else
{
return $chartOutput;
}
}
}
catch ( \Exception $e )
@@ -294,6 +423,43 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
\IPS\IPS::exceptionHandler( $e );
}
}
/**
* @brief Cached tab data
*/
protected $availableChartTabs = NULL;
/**
* Retrieve tabs based on saved charts
*
* @return array
*/
protected function getChartTabs()
{
$tabs = array( '' => 'dynamic_chart_overview' );
foreach( $this->loadAvailableChartTabs() as $chart )
{
$tabs[ $chart['chart_id'] ] = $chart['chart_title'];
}
return $tabs;
}
/**
* Load and return available chart tabs
*
* @return array
*/
protected function loadAvailableChartTabs()
{
if( $this->availableChartTabs === NULL )
{
$this->availableChartTabs = iterator_to_array( \IPS\Db::i()->select( '*', 'core_saved_charts', array( 'chart_member=? AND chart_controller=?', \IPS\Member::loggedIn()->member_id, \IPS\Request::i()->app . '_' . \IPS\Request::i()->module . '_' . \IPS\Request::i()->controller . ( \IPS\Request::i()->tab ? '_' . \IPS\Request::i()->tab : '' ) ) ) );
}
return $this->availableChartTabs;
}
/**
* Flip URL Filter
@@ -333,6 +499,12 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
{
switch ( $this->timescale )
{
case 'hourly':
$data[ $date->format( 'Y-n-j-h-i-s' ) ] = array();
$date->add( new \DateInterval( 'P1H' ) );
break;
case 'daily':
$data[ $date->format( 'Y-n-j' ) ] = array();
+29 -11
View File
@@ -21,7 +21,7 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
* Cover Photo Controller
*/
abstract class _Controller extends \IPS\Dispatcher\Controller
{
{
/**
* Upload Cover Photo
*
@@ -46,16 +46,18 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
}
catch ( \Exception $e ) { }
$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' );
}
$this->_coverPhotoSet( new \IPS\Helpers\CoverPhoto( $values['cover_photo'], 0 ) );
\IPS\Output::i()->redirect( $returnURL->setQueryString( array( '_position' => 1 ) ) );
$this->_coverPhotoSet( new \IPS\Helpers\CoverPhoto( $values['cover_photo'], 0 ), 'new' );
\IPS\Output::i()->redirect( $this->_coverPhotoReturnUrl()->setQueryString( array( '_position' => 1 ) ) );
}
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'admin' )
{
\IPS\Output::i()->output = $form;
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
}
}
/**
@@ -78,7 +80,7 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
}
catch ( \Exception $e ) { }
$this->_coverPhotoSet( new \IPS\Helpers\CoverPhoto( NULL, 0 ) );
$this->_coverPhotoSet( new \IPS\Helpers\CoverPhoto( NULL, 0 ), 'remove' );
if ( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->json( 'OK' );
@@ -104,7 +106,7 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
}
$photo->offset = \IPS\Request::i()->offset;
$this->_coverPhotoSet( $photo );
$this->_coverPhotoSet( $photo, 'reposition' );
if ( \IPS\Request::i()->isAjax() )
{
@@ -127,6 +129,7 @@ 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 );
@@ -137,4 +140,19 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
* @return \IPS\Helpers\CoverPhoto
*/
abstract protected function _coverPhotoGet();
/**
* Get URL to return to after editing cover photo
*
* @return \IPS\Http\Url
*/
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;
}
}
+15 -1
View File
@@ -43,7 +43,7 @@ class _Captcha extends FormAbstract
if ( \IPS\Settings::i()->bot_antispam_type != 'none' )
{
$class = '\IPS\Helpers\Form\Captcha\\' . ucfirst( \IPS\Settings::i()->bot_antispam_type );
$class = '\IPS\Helpers\Form\Captcha\\' . mb_ucfirst( \IPS\Settings::i()->bot_antispam_type );
if ( !class_exists( $class ) )
{
\IPS\Output::i()->error( 'unexpected_captcha', '4S262/1', 500, 'unexpected_captcha_admin' );
@@ -83,6 +83,20 @@ class _Captcha extends FormAbstract
return $this->captcha->getHtml();
}
/**
* Get HTML
*
* @return string
*/
public function rowHtml( $form=NULL )
{
if ( $this->captcha === NULL )
{
return '';
}
return ( method_exists( $this->captcha, 'rowHtml' ) and !$this->error ) ? $this->captcha->rowHtml() : parent::rowHtml( $form );
}
/**
* Get Value
*
+80
View File
@@ -0,0 +1,80 @@
<?php
/**
* @brief Invisible reCAPTCHA
* @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 15 June 2017
*/
namespace IPS\Helpers\Form\Captcha;
/* 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;
}
/**
* Invisible reCAPTCHA
*/
class _Invisible
{
/**
* @brief Error
*/
protected $error;
/**
* Display
*
* @return string
*/
public function getHtml()
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->captchaInvisible( \IPS\Settings::i()->recaptcha2_public_key, preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short ) );
}
/**
* Display
*
* @return string
*/
public function rowHtml()
{
return $this->getHtml();
}
/**
* Verify
*
* @return bool|null TRUE/FALSE indicate if the test passed or not. NULL indicates the test failed, but the captcha system will display an error so we don't have to.
*/
public function verify()
{
try
{
$response = \IPS\Http\Url::external( 'https://www.google.com/recaptcha/api/siteverify' )->request()->post( array(
'secret' => \IPS\Settings::i()->recaptcha2_private_key,
'response' => trim( \IPS\Request::i()->__get('g-recaptcha-response') ),
'remoteip' => \IPS\Request::i()->ipAddress(),
) )->decodeJson( TRUE );
return ( ( (bool) $response['success'] ) and ( $response['hostname'] === \IPS\Http\Url::internal('')->data[ \IPS\Http\Url::COMPONENT_HOST ] ) );
}
catch( \RuntimeException $e )
{
if( $e->getMessage() == 'BAD_JSON' )
{
return FALSE;
}
else
{
throw $e;
}
}
}
}
+3 -15
View File
@@ -35,23 +35,11 @@ class _Keycaptcha
public function getHtml()
{
$explodedKey = explode( '0', \IPS\Settings::i()->keycaptcha_privatekey, 2 );
$uniq = md5( uniqid() );
$uniq = md5( mt_rand() );
$sign = md5( $uniq . \IPS\Request::i()->ipAddress . $explodedKey[0] );
$sign2 = md5( $uniq . $explodedKey[0] );
return <<<HTML
<input type='hidden' id='capcode' name='keycaptcha'>
<script type="text/javascript">
// required
var s_s_c_user_id = '{$explodedKey[1]}';
var s_s_c_session_id = '{$uniq}';
var s_s_c_captcha_field_id = 'capcode';
var s_s_c_submit_button_id ='sbutton-#-r';
var s_s_c_web_server_sign = '{$sign}';
var s_s_c_web_server_sign2 = '{$sign2}';
</script>
<div data-ipsCaptcha data-ipsCaptcha-service='keycaptcha' id='div_for_keycaptcha'></div>
HTML;
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->captchaKeycaptcha( $explodedKey[1], $uniq, $sign, $sign2 );
}
/**
-87
View File
@@ -1,87 +0,0 @@
<?php
/**
* @brief reCAPTCHA
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 18 Apr 2013
*/
namespace IPS\Helpers\Form\Captcha;
/* 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;
}
/**
* reCAPTCHA
*/
class _Recaptcha
{
/**
* @brief Error
*/
protected $error;
/**
* Display
*
* @return string
*/
public function getHtml()
{
$publicKey = \IPS\Settings::i()->recaptcha_public_key;
$theme = \IPS\Theme::i()->settings['recaptcha_theme'];
$lang = preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short );
return <<<HTML
<div data-ipsCaptcha data-ipsCaptcha-service='recaptcha' data-ipsCaptcha-key="{$publicKey}" data-ipsCaptcha-lang="{$lang}" data-ipsCaptcha-theme="{$theme}">
<noscript>
<iframe src="//www.google.com/recaptcha/api/noscript?k={$publicKey}&hl={$lang}&error={$this->error}"
height="300" width="500"></iframe><br>
<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
<input type="hidden" name="recaptcha_response_field" value="manual_challenge">
</noscript>
</div>
HTML;
}
/**
* Verify
*
* @return bool|null TRUE/FALSE indicate if the test passed or not. NULL indicates the test failed, but the captcha system will display an error so we don't have to.
*/
public function verify()
{
try
{
$response = \IPS\Http\Url::external( 'http://www.google.com/recaptcha/api/verify' )->request()->post( array(
'privatekey' => \IPS\Settings::i()->recaptcha_private_key,
'remoteip' => \IPS\Request::i()->ipAddress(),
'challenge' => \IPS\Request::i()->recaptcha_challenge_field,
'response' => \IPS\Request::i()->recaptcha_response_field
) );
}
catch ( \IPS\Http\Request\Exception $e )
{
return FALSE;
}
$response = explode( "\n", $response );
if ( $response[0] === 'true' )
{
return true;
}
else
{
$this->error = $response[1];
return null;
}
}
}
+8 -23
View File
@@ -34,28 +34,7 @@ class _Recaptcha2
*/
public function getHtml()
{
$publicKey = \IPS\Settings::i()->recaptcha2_public_key;
$theme = \IPS\Theme::i()->settings['recaptcha2_theme'];
$lang = preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short );
return <<<HTML
<div data-ipsCaptcha data-ipsCaptcha-service='recaptcha2' data-ipsCaptcha-key="{$publicKey}" data-ipsCaptcha-lang="{$lang}" data-ipsCaptcha-theme="{$theme}">
<noscript>
<div style="width: 302px; height: 352px;">
<div style="width: 302px; height: 352px; position: relative;">
<div style="width: 302px; height: 352px; position: absolute;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k={$publicKey}" style="width: 302px; height:352px; border-style: none;">
</iframe>
</div>
<div style="width: 250px; height: 80px; position: absolute; border-style: none; bottom: 21px; left: 25px; margin: 0px; padding: 0px; right: 25px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 80px; border: 1px solid #c1c1c1; margin: 0px; padding: 0px; resize: none;" value="">
</textarea>
</div>
</div>
</div>
</noscript>
</div>
HTML;
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->captchaRecaptcha2( \IPS\Settings::i()->recaptcha2_public_key, preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short ), \IPS\Theme::i()->settings['recaptcha2_theme'] );
}
/**
@@ -73,7 +52,13 @@ HTML;
'remoteip' => \IPS\Request::i()->ipAddress(),
) )->decodeJson( TRUE );
return (bool) $response['success'];
return ( ( (bool) $response['success'] ) and ( $response['hostname'] === \IPS\Http\Url::internal('')->data[ \IPS\Http\Url::COMPONENT_HOST ] ) );
}
catch( \IPS\Http\Request\Exception $e )
{
\IPS\Log::log( $e, 'recaptcha2_exception' );
return FALSE;
}
catch( \RuntimeException $e )
{
+5 -3
View File
@@ -32,7 +32,8 @@ class _Codemirror extends TextArea
'placeholder' => 'e.g. ...', // A placeholder (NB: Will only work on compatible browsers)
'tags' => array(), // An array of extra insertable tags in key => value pair with key being what is inserted and value serving as a description
'mode' => 'php' // Formatting mode. Default is htmlmixed.
'height' => 300 // Height of code mirror editor
'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.
);
* @endcode
*/
@@ -44,7 +45,8 @@ class _Codemirror extends TextArea
'tags' => array(),
'mode' => 'htmlmixed',
'nullLang' => NULL,
'height' => 300
'height' => 300,
'preview' => NULL
);
/**
@@ -80,6 +82,6 @@ class _Codemirror extends TextArea
$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'] );
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'] );
}
}
+6 -6
View File
@@ -268,6 +268,12 @@ class _Date extends FormAbstract
{
throw new \InvalidArgumentException('form_required');
}
/* Check it's valid */
if ( !( $this->value instanceof \IPS\DateTime ) and $this->value !== NULL AND ( $this->options['unlimited'] === NULL OR $this->value !== $this->options['unlimited'] ) )
{
throw new \InvalidArgumentException( 'form_date_bad' );
}
parent::validate();
@@ -276,12 +282,6 @@ class _Date extends FormAbstract
{
return TRUE;
}
/* Check it's valid */
if ( !( $this->value instanceof \IPS\DateTime ) and $this->value !== NULL )
{
throw new \InvalidArgumentException( 'form_date_bad' );
}
/* Check minimum */
if ( $this->value and $this->options['min'] !== NULL and $this->options['min'] > $this->value )
+10
View File
@@ -79,6 +79,16 @@ class _DateRange extends FormAbstract
call_user_func_array( 'parent::__construct', func_get_args() );
}
/**
* Get the value to use in the label 'for' attribute
*
* @return mixed
*/
public function getLabelForAttribute()
{
return NULL;
}
/**
* Format Value
+56 -39
View File
@@ -94,7 +94,13 @@ class _Editor extends FormAbstract
{
$args = func_get_args();
$this->postKey = md5( $args[3]['autoSaveKey'] . ':' . session_id() );
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();
\IPS\Output::i()->json( array( true ) );
}
$this->defaultOptions['controller'] = 'app=core&module=system&controller=editor';
/* Get our extension */
@@ -245,6 +251,9 @@ class _Editor extends FormAbstract
$value = \IPS\Text\Parser::mungeResources( $value );
}
/* Fix Emoji */
\IPS\Output::i()->replaceEmojiWithImages( $value );
/* CKEditor will replace <p></p> (which doesn't display anything in a normal post) with <p><br></p> (which does)
which creates a discrepency between wheat displays in a post and what displays in the editor */
$value = preg_replace( '/<p>\s*<\/p>/', '', $value );
@@ -278,7 +287,7 @@ class _Editor extends FormAbstract
/* Display */
$template = $raw ? 'editorRaw' : 'editor';
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->$template( $this->name, $value, $this->options, $allowed, md5( $this->options['autoSaveKey'] . ':' . session_id() ), $attachmentArea, json_encode( static::getEmoticons() ), $this->options['tags'] );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->$template( $this->name, $value, $this->options, $allowed, md5( $this->options['autoSaveKey'] . ':' . session_id() ), $attachmentArea, json_encode( array() ), $this->options['tags'] );
}
/**
@@ -345,7 +354,12 @@ class _Editor extends FormAbstract
{
if ( !isset( $parser ) or !in_array( $attachment['attach_id'], $parser->mappedAttachments ) or array_key_exists( $attachment['attach_id'], $parser->existingAttachments ) )
{
if ( $attachment['attach_is_image'] )
$ext = mb_substr( $attachment['attach_file'], mb_strrpos( $attachment['attach_file'], '.' ) + 1 );
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'] );
}
@@ -380,7 +394,7 @@ class _Editor extends FormAbstract
}
/* Remove abandoned attachments */
foreach ( \IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id NOT IN(?)', \IPS\Db::i()->select( 'DISTINCT attachment_id', 'core_attachments_map' ) ), array( 'attach_member_id=? AND attach_date<?', \IPS\Member::loggedIn()->member_id, time() - 86400 ) ) ) as $attachment )
foreach ( \IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id NOT IN(?)', \IPS\Db::i()->select( 'DISTINCT attachment_id', 'core_attachments_map', NULL, NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER ) ), array( 'attach_member_id=? AND attach_date<?', \IPS\Member::loggedIn()->member_id, time() - 86400 ) ) ) as $attachment )
{
try
{
@@ -404,6 +418,26 @@ class _Editor extends FormAbstract
/* Return */
return $value;
}
/**
* Set the value of the element
*
* @param bool $initial Whether this is the initial call or not. Do not reset default values on subsequent calls.
* @param bool $force Set the value even if one was not submitted (done on the final validation when getting values)?
* @return void
*/
public function setValue( $initial=FALSE, $force=FALSE )
{
// @todo extend this to all form elements. See commit notes.
if ( !isset( \IPS\Request::i()->csrfKey ) or !\IPS\Login::compareHashes( (string) \IPS\Session::i()->csrfKey, (string) \IPS\Request::i()->csrfKey ) )
{
$name = $this->name;
unset( \IPS\Request::i()->$name );
}
return parent::setValue( $initial, $force );
}
/**
* Get parser object
@@ -475,6 +509,7 @@ class _Editor extends FormAbstract
/* Load existing attachments */
$existing = array();
$currentPostUsage = 0;
foreach ( $this->getAttachments() as $attachment )
{
try
@@ -487,6 +522,8 @@ class _Editor extends FormAbstract
$file->originalFilename = $attachment['attach_file'];
$existing[ $attachment['attach_id'] ] = $file;
$currentPostUsage += $attachment['attach_filesize'];
}
catch ( \Exception $e ) { }
}
@@ -495,8 +532,17 @@ class _Editor extends FormAbstract
$maxTotalSize = array();
if ( \IPS\Member::loggedIn()->group['g_attach_max'] > 0 and \IPS\Member::loggedIn()->member_id )
{
$globalSpaceRemaining = ( ( \IPS\Member::loggedIn()->group['g_attach_max'] * 1024 ) - \IPS\Db::i()->select( 'SUM(attach_filesize)', 'core_attachments', array( 'attach_member_id=?', \IPS\Member::loggedIn()->member_id ) )->first() ) / 1048576;
/* 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';
@@ -504,7 +550,7 @@ class _Editor extends FormAbstract
}
if ( \IPS\Member::loggedIn()->group['g_attach_per_post'] )
{
$perPostSpaceRemaining = \IPS\Member::loggedIn()->group['g_attach_per_post'] / 1024;
$perPostSpaceRemaining = ( ( \IPS\Member::loggedIn()->group['g_attach_per_post'] * 1024 ) - $currentPostUsage ) / 1048576;
$maxTotalSize[] = $perPostSpaceRemaining;
if ( $perPostSpaceRemaining <= 0 )
{
@@ -538,6 +584,10 @@ class _Editor extends FormAbstract
{
$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 )
{
@@ -633,39 +683,6 @@ class _Editor extends FormAbstract
/* Return */
return $uploader;
}
/**
* Fetch the emoticons - abstracted to a static method for caching
*
* @return array
*/
public static function getEmoticons()
{
if( !isset( \IPS\Data\Store::i()->emoticons ) )
{
$emoticons = iterator_to_array( \IPS\Db::i()->select( '*', 'core_emoticons', array( 'typed<>?', '' ) )->setKeyField('typed') );
foreach( $emoticons as $key => $emoticon )
{
try
{
$emoticons[ $key ] = array(
'image' => (string) \IPS\File::get( "core_Emoticons", $emoticon['image'] )->url,
'image_2x' => ( isset( $emoticon['image_2x'] ) and $emoticon['image_2x'] ) ? (string) \IPS\File::get( "core_Emoticons", $emoticon['image_2x'] )->url . ' 2x' : FALSE,
'height' => $emoticon['height'] ?: FALSE,
'width' => $emoticon['width'] ?: FALSE,
);
}
catch ( \Exception $e )
{
\IPS\Log::log( $e, 'bad_emoticon' );
}
}
\IPS\Data\Store::i()->emoticons = $emoticons;
}
return \IPS\Data\Store::i()->emoticons;
}
/**
* Attachments
+24 -29
View File
@@ -55,45 +55,40 @@ class _Email extends Text
if ( $this->options['accountEmail'] )
{
/* Check if it exists */
foreach ( \IPS\Login::handlers( TRUE ) as $k => $handler )
if ( $error = \IPS\Login::emailIsInUse( $this->value, ( $this->options['accountEmail'] instanceof \IPS\Member ) ? $this->options['accountEmail'] : NULL, \IPS\Member::loggedIn()->isAdmin() ) )
{
if ( $handler->emailIsInUse( $this->value, ( $this->options['accountEmail'] instanceof \IPS\Member ) ? $this->options['accountEmail'] : NULL ) === TRUE )
{
if ( \IPS\Member::loggedIn()->isAdmin() )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack('member_email_exists_admin', FALSE, array( 'sprintf' => array( $k ) ) ) );
}
else
{
throw new \DomainException( 'member_email_exists' );
}
}
throw new \DomainException( $error );
}
/* Check it's not banned */
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\Member::loggedIn()->isAdmin() and \IPS\Settings::i()->allowed_reg_email !== '' AND $allowedEmailDomains = explode( ',', \IPS\Settings::i()->allowed_reg_email ) )
/* Check Banned and Allowed Emails only if the data are not coming from an administrator */
if ( !\IPS\Member::loggedIn()->isAdmin() )
{
$matched = FALSE;
foreach ( $allowedEmailDomains AS $domain )
/* Check it's not a banned address */
foreach ( \IPS\Db::i()->select( 'ban_content', 'core_banfilters', array( "ban_type=?", 'email' ) ) as $bannedEmail )
{
if( \mb_stripos( $this->value, "@" . $domain ) !== FALSE )
if ( preg_match( '/^' . str_replace( '\*', '.*', preg_quote( $bannedEmail, '/' ) ) . '$/i', $this->value ) )
{
$matched = TRUE;
throw new \DomainException( 'form_email_banned' );
}
}
if ( count( $allowedEmailDomains ) AND !$matched )
/* Check it's an allowed domain */
if ( \IPS\Settings::i()->allowed_reg_email !== '' AND $allowedEmailDomains = explode( ',', \IPS\Settings::i()->allowed_reg_email ) )
{
throw new \DomainException( 'form_email_banned' );
$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' );
}
}
}
}
+29 -11
View File
@@ -511,6 +511,8 @@ class _Form
$values = array();
$name = "{$this->id}_submitted";
$uploadFieldNames = array();
$uploadRetainDeleted = array();
$uploadCurrentFiles = array();
/* Did we submit the form? */
if( isset( \IPS\Request::i()->$name ) and \IPS\Request::i()->csrfKey === \IPS\Session::i()->csrfKey )
@@ -596,8 +598,8 @@ 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 ) )
{
$values[ $_name ] = $element->defaultValue;
{
$values[ $_name ] = $stringValues ? $element::stringValue( $element->defaultValue ) : $element->defaultValue;
continue;
}
@@ -611,6 +613,21 @@ class _Form
if ( ( $element instanceof Form\Upload ) )
{
$uploadFieldNames[] = $element->name;
if ( $element->options['retainDeleted'] )
{
$uploadRetainDeleted[] = $element->name;
if ( is_array( $element->value ) )
{
foreach( $element->value AS $value )
{
$uploadCurrentFiles[] = $value->originalFilename;
}
}
else
{
$uploadCurrentFiles[] = $element->value->originalFilename;
}
}
}
/* If it has an error, set it and return */
@@ -668,7 +685,15 @@ 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 )
{
\IPS\Db::i()->delete( 'core_files_temp', array( 'upload_key=?', md5( $name . session_id() ) ) );
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 */
\IPS\Db::i()->delete( 'core_files_temp', array( "upload_key=? AND " . \IPS\Db::i()->in( 'filename', $uploadCurrentFiles ), md5( $name . session_id() ) ) );
}
}
/* And return */
@@ -696,14 +721,7 @@ class _Form
if ( $values )
{
foreach ( $values as $k => $v )
{
\IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => $v ), array( 'conf_key=?', $k ) );
\IPS\Settings::i()->$k = $v;
}
unset( \IPS\Data\Store::i()->settings );
\IPS\Settings::i()->changeValues( $values );
return TRUE;
}
+11 -1
View File
@@ -303,13 +303,23 @@ abstract class _FormAbstract
if ( \IPS\IN_DEV )
{
echo '<pre>';
print_r( $e );
var_dump( $e );
exit;
}
throw $e;
}
}
/**
* Get the value to use in the label 'for' attribute
*
* @return mixed
*/
public function getLabelForAttribute()
{
return $this->name;
}
/**
* Get Value
+5 -1
View File
@@ -75,6 +75,10 @@ class _Item extends FormAbstract
{
$template = array( \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' ), 'itemResult' );
}
else
{
$template = $this->options['itemTemplate'];
}
/* Are we getting some AJAX stuff? */
if ( isset( \IPS\Request::i()->_itemSelectName ) and \IPS\Request::i()->_itemSelectName === $this->name )
@@ -152,7 +156,7 @@ class _Item extends FormAbstract
{
if ( isset( $itemClass::$databaseColumnMap[ $this->options['orderResults'][0] ] ) )
{
$order = $itemClass::$databaseColumnMap[ $this->options['orderResults'][0] ] . ' ' . $this->options['orderResults'][1];
$order = $itemClass::$databaseTable . '.' . $itemClass::$databaseColumnMap[ $this->options['orderResults'][0] ] . ' ' . $this->options['orderResults'][1];
}
}
+13 -2
View File
@@ -240,7 +240,19 @@ class _Matrix extends \IPS\Helpers\Form
}
if ( $added )
{
{
/* Select lists can have user-supplied input, so look for that too */
foreach( \IPS\Request::i() as $inputKey => $inputValue )
{
if( $pos = mb_strpos( $inputKey, '_new_' ) AND $inputKey !== '_new_' )
{
if( is_array( $inputValue ) AND isset( $inputValue[ $newId ] ) )
{
$data = array_merge( $data, $inputValue[ $newId ] );
}
}
}
$this->elements["_new_[{$i}]"] = $this->buildRow( "_new_[{$i}]", $data );
$this->addedRows[] = "_new_[{$i}]";
$i++;
@@ -314,7 +326,6 @@ class _Matrix extends \IPS\Helpers\Form
$name = "{$this->id}_submitted";
if( $force or ( isset( \IPS\Request::i()->$name ) and \IPS\Request::i()->csrfKey === \IPS\Session::i()->csrfKey ) )
{
// Do we need to unsquash any values?
// Squashed values are json_encoded by javascript to prevent us exceeding max_post_vars
$squashedField = $this->id . '_squashed';
+1 -2
View File
@@ -88,9 +88,8 @@ class _Member extends Text
$value = $value->name;
}
/* 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. */
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, 'text', ( $this->options['autocomplete'] AND !is_null( $value ) ) ? htmlspecialchars( $value, ENT_QUOTES | \IPS\HTMLENTITIES, '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'] );
}
/**
+42 -5
View File
@@ -118,7 +118,23 @@ class _Node extends FormAbstract
try
{
$node = $permCheck ? $nodeClass::loadAndCheckPerms( \IPS\Request::i()->_nodeId, 'view' ) : $nodeClass::load( \IPS\Request::i()->_nodeId );
$children = $node->children( $permCheck, NULL, $this->options['subnodes'], $disabled, $this->options['where'] );
/* 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.
*/
$children = $node->children( $permCheck === NULL ? NULL : 'view', NULL, $this->options['subnodes'], $disabled, $this->options['where'] );
if( $permCheck !== NULL )
{
foreach( $children AS $child )
{
if ( !$child->can( $permCheck ) )
{
$this->options['disabledIds'][] = $child->_id;
}
}
}
}
catch ( \Exception $e )
{
@@ -169,6 +185,10 @@ class _Node extends FormAbstract
if( isset( $nodeClass::$ownerTypes ) and $nodeClass::$ownerTypes !== NULL and $this->options['forceOwner'] !== FALSE )
{
$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'] ) );
}
}
else
{
@@ -218,18 +238,18 @@ class _Node extends FormAbstract
{
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->_stripTagsTitle ? $node->_stripTagsTitle : $node->_title );
$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->_stripTagsTitle ? $val->_stripTagsTitle : $val->_title );
return isset( $val::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $val::$titleLangPrefix . $val->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : $val->_title;
}, 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->_stripTagsTitle ? $this->value->_stripTagsTitle : $this->value->_title );
$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;
$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->_stripTagsTitle ? $val->_stripTagsTitle : $val->_title );
return isset( $val::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $val::$titleLangPrefix . $val->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : $val->_title;
}, iterator_to_array( $this->value->parents() ) ) ) );
}
@@ -407,6 +427,23 @@ class _Node extends FormAbstract
return $this->value;
}
/**
* Validate
*
* @throws \InvalidArgumentException
* @return TRUE
*/
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 )
{
throw new \InvalidArgumentException('form_required');
}
return parent::validate();
}
/**
* String Value
+8 -20
View File
@@ -26,7 +26,7 @@ class _Password extends Text
* @brief Default Options
* @code
$childDefaultOptions = array(
'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.
'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.
@@ -50,7 +50,7 @@ class _Password extends Text
{
$value = parent::getValue();
if ( $value === '********' )
if ( $value === '********' and $this->defaultValue )
{
$value = $this->defaultValue;
}
@@ -93,25 +93,13 @@ class _Password extends Text
{
$valid = FALSE;
foreach( \IPS\Login::handlers( TRUE ) as $k => $handler )
$login = new \IPS\Login();
foreach ( $login->usernamePasswordMethods() as $method )
{
if ( $handler->authTypes & \IPS\Login::AUTH_TYPE_USERNAME )
if ( $method->authenticatePasswordForMember( $this->options['validateFor'], $this->value ) )
{
try
{
$handler->authenticate( array( 'auth' => $this->options['validateFor']->name, 'password' => $this->value ) );
$valid = TRUE;
}
catch ( \Exception $e ) {}
}
if ( $handler->authTypes & \IPS\Login::AUTH_TYPE_EMAIL )
{
try
{
$handler->authenticate( array( 'auth' => $this->options['validateFor']->email, 'password' => $this->value ) );
$valid = TRUE;
}
catch ( \Exception $e ) {}
$valid = TRUE;
break;
}
}
@@ -148,7 +136,7 @@ class _Password extends Text
{
foreach( $this->value as $key => $value )
{
$this->value[ $key ] = htmlspecialchars( $value, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
$this->value[ $key ] = htmlspecialchars( $value, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
}
}
+2 -2
View File
@@ -183,12 +183,12 @@ class _Select extends FormAbstract
{
foreach ( $v as $x => $y )
{
$options[ $k ][ $x ] = htmlspecialchars( $y, \IPS\HTMLENTITIES, 'UTF-8', FALSE );
$options[ $k ][ $x ] = htmlspecialchars( $y, ENT_DISALLOWED, 'UTF-8', FALSE );
}
}
else
{
$options[ $k ] = htmlspecialchars( $v, \IPS\HTMLENTITIES, 'UTF-8', FALSE );
$options[ $k ] = htmlspecialchars( $v, ENT_DISALLOWED, 'UTF-8', FALSE );
}
}
break;
+1 -1
View File
@@ -27,7 +27,7 @@ class _SocialGroup extends Member
* @code
$childDefaultOptions = array(
'owner' => 1, // \IPS\Member object or member ID who "owns" this group
'multiple' => 1, // Maximum number of members. NULL for any. Default is 1.
'multiple' => 1, // Maximum number of members. NULL for any. Default is NULL.
);
* @endcode
*/
+2 -2
View File
@@ -168,12 +168,12 @@ class _Stack extends FormAbstract
if ( $this->options['maxItems'] !== NULL and count( $this->value ) > $this->options['maxItems'] )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_max', FALSE, array( 'pluralize' => array( $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'] )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_min', FALSE, array( 'pluralize' => array( $this->options['minItems'] ) ) ) );
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_items_min', FALSE, array( 'pluralize' => array( $this->options['minItems'] ) ) ) );
}
$classType = mb_strpos( $this->options['stackFieldType'], '\\' ) === FALSE ? ( "\\IPS\\Helpers\\Form\\" . $this->options['stackFieldType'] ) : $this->options['stackFieldType'];
+30 -29
View File
@@ -42,7 +42,8 @@ class _Text extends TextArea
'prefix' => TRUE, // If TRUE, user will have option to specify one tag as a prefix
'resultItemTemplate'=> 'core.foo.bar', // Can be used to specify a custom JavaScript template to use for the result
'minAjaxLength' => 3, // Minimum length of value before sending AJAX lookup call
'disallowedCharacters' => array() // An array of disallowed characters (default < > ' ")
'disallowedCharacters' => array(), // An array of disallowed characters (default < > ' ")
'minimized' => TRUE // Whether the autocomplete shows a 'choose' link to activate. Existing values or required = true will always override this.
),
'placeholder' => 'e.g. ...', // A placeholder (NB: Will only work on compatible browsers)
'regex' => '/[A-Z]+/i', // RegEx of acceptable value
@@ -131,7 +132,7 @@ class _Text extends TextArea
{
foreach( $this->value as $key => $value )
{
$this->value[ $key ] = htmlspecialchars( $value, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
$this->value[ $key ] = htmlspecialchars( $value, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
}
}
@@ -171,23 +172,27 @@ class _Text extends TextArea
{
$return = htmlspecialchars_decode( $return, ENT_QUOTES );
if ( !is_array( $return ) )
if ( !is_array( $return ) and ( !isset( $this->options['autocomplete']['maxItems'] ) or $this->options['autocomplete']['maxItems'] != 1 ) )
{
$return = array_filter( explode( "\n", $return ) );
$return = array_filter( array_map( 'trim', explode( "\n", $return ) ) );
}
}
/* Remove all invisible characters */
if ( !is_array( $return ) )
/* Remove all invisible characters if this is a username */
if( $this->options['accountUsername'] )
{
$return = preg_replace( '/\p{C}+/u', '', $return );
}
else
{
foreach( $return as $k => $v )
if ( !is_array( $return ) )
{
$return = preg_replace( '/\p{C}+/u', '', $return );
}
else
{
foreach( $return as $k => $v )
{
$return = preg_replace( '/\p{C}+/u', '', $return );
}
}
}
if ( isset( $this->options['autocomplete']['prefix'] ) and $this->options['autocomplete']['prefix'] )
@@ -229,7 +234,7 @@ class _Text extends TextArea
* @return mixed
*/
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 )
{
return array_filter( array_map( 'trim', explode( "\n", $this->value ) ) );
@@ -249,6 +254,12 @@ 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 )
{
throw new \DomainException('form_required');
}
/* skip validation if it's an username field and if the name wasn't changed */
if ( $this->options['accountUsername'] AND $this->options['accountUsername'] instanceOf \IPS\Member AND $this->options['accountUsername']->name == $this->value )
{
@@ -304,21 +315,11 @@ class _Text extends TextArea
/* Check if it exists */
if ( !( $this->options['accountUsername'] instanceof \IPS\Member ) or mb_strtolower( $this->options['accountUsername']->name ) !== mb_strtolower( $this->value ) )
{
foreach( \IPS\Login::handlers( TRUE ) as $k => $handler )
if ( $error = \IPS\Login::usernameIsInUse( $this->value, ( $this->options['accountUsername'] instanceof \IPS\Member ) ? $this->options['accountUsername'] : NULL, \IPS\Member::loggedIn()->isAdmin() ) )
{
if( $handler->usernameIsInUse( $this->value, $this->options['accountUsername'] ) === TRUE )
{
if( \IPS\Member::loggedIn()->isAdmin() )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'member_name_exists_admin', FALSE, array('sprintf' => array($k)) ) );
}
else
{
throw new \DomainException( 'member_name_exists' );
}
}
throw new \DomainException( $error );
}
/* Check it's not banned */
foreach( \IPS\Db::i()->select( 'ban_content', 'core_banfilters', array("ban_type=?", 'name') ) as $bannedName )
{
@@ -383,7 +384,7 @@ class _Text extends TextArea
}
else
{
throw new \DomainException( 'Profanity Filter' ); // @todo proper lang string
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( $v ) ) ) );
}
}
else
@@ -399,7 +400,7 @@ class _Text extends TextArea
{
if ( mb_stristr( $value, $type ) )
{
throw new \DomainException( 'Profanity Filter' );
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( $v ) ) ) );
}
}
}
@@ -426,11 +427,11 @@ class _Text extends TextArea
{
if( isset( $this->options['autocomplete']['forceLower'] ) AND $this->options['autocomplete']['forceLower'] )
{
$this->value = array_uintersect( array_map( 'mb_strtolower', $this->value ), array_map( 'mb_strtolower', $this->options['autocomplete']['source'] ), 'strcasecmp' );
$this->value = array_uintersect( array_map( 'mb_strtolower', $this->value ), array_map( 'mb_strtolower', array_map( 'trim', $this->options['autocomplete']['source'] ) ), 'strcasecmp' );
}
else
{
$this->value = array_uintersect( $this->value, $this->options['autocomplete']['source'], 'strcasecmp' );
$this->value = array_uintersect( $this->value, array_map( 'trim', $this->options['autocomplete']['source'] ), 'strcasecmp' );
}
}
}
+36 -24
View File
@@ -30,6 +30,7 @@ class _Upload extends FormAbstract
'storageContainer' => NULL, // The file storage container to use.
'multiple' => TRUE, // Specifies if the field should allow multiple file uploads. Default is FALSE.
'image' => array( 'maxWidth' => 100, 'maxHeight' => 100 ), // If the upload must be an image, can pass TRUE or an array with max width and height (in which case, image will be resized appropriately). Default is NULL. Max width/height cannot be used in conjunction with temporary uploads. If it can be, but doesn't have to an image, and you still want to specify max width and height, add an "optional" property set to true
'checkImage' => TRUE, // If TRUE, will load uploaded images into the image processing engine to automatically reorient if needed
'allowedFileTypes' => array( 'pdf', 'txt' ), // Allowed file extensions. NULL allows any. Default is NULL.
'maxFileSize' => 100, // Maximum file size in megabytes. NULL is no limit. Default is NULL. Note that there *may* be server limitations regardless of this value which are calculated automatically.
'totalMaxSize' => 100, // If this is a "multiple" upload field, the maximum storage space allowed in total in megabytes.
@@ -48,6 +49,7 @@ class _Upload extends FormAbstract
protected $defaultOptions = array(
'multiple' => FALSE,
'image' => NULL,
'checkImage' => TRUE,
'allowedFileTypes' => NULL,
'maxFileSize' => NULL,
'totalMaxSize' => NULL,
@@ -212,10 +214,8 @@ class _Upload extends FormAbstract
/* 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 ( is_array( $this->options['image'] ) and $fileObj->isImage() )
if ( $fileObj->isImage() AND ( $this->options['checkImage'] OR is_array( $this->options['image'] ) ) )
{
$options = $this->options;
try
{
$image = \IPS\Image::create( $fileObj->contents() );
@@ -225,7 +225,10 @@ class _Upload extends FormAbstract
$image->setExifData( $fileObj->contents() );
}
$image->resizeToMax( $options['image']['maxWidth'] ?: NULL, $options['image']['maxHeight'] ?: NULL );
if( is_array( $this->options['image'] ) )
{
$image->resizeToMax( $this->options['image']['maxWidth'] ?: NULL, $this->options['image']['maxHeight'] ?: NULL );
}
$fileObj->replace( (string) $image );
}
@@ -254,6 +257,7 @@ class _Upload extends FormAbstract
'id' => $insertId,
'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,
) );
}
@@ -270,6 +274,7 @@ class _Upload extends FormAbstract
'id' => $insertId,
'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,
) );
}
@@ -583,6 +588,16 @@ class _Upload extends FormAbstract
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack('uploaderr_total_size', FALSE, array( 'sprintf' => array( $this->options['totalMaxSize'] ) ) ) );
}
}
/* Now fix the array in case we are using the defaultValue which was array( fileurl =>, default => ) */
$toReturn = array();
foreach( $return as $k => $v )
{
$toReturn[ $k ] = ( is_array( $v ) AND isset( $v['fileurl'] ) ) ? $v['fileurl'] : $v;
}
$return = $toReturn;
/* Return */
if ( !$this->options['multiple'] )
@@ -630,33 +645,30 @@ class _Upload extends FormAbstract
/* Normal - send to storage extension */
else
{
if ( is_array( $this->options['image'] ) )
$options = $this->options;
$fileObjects = \IPS\File::createFromUploads( $this->options['storageExtension'], $fieldName, $this->options['allowedFileTypes'], $this->options['maxFileSize'], $this->options['totalMaxSize'], 0, function( $contents, $filename ) use ( $options )
{
$options = $this->options;
$fileObjects = \IPS\File::createFromUploads( $this->options['storageExtension'], $fieldName, $this->options['allowedFileTypes'], $this->options['maxFileSize'], $this->options['totalMaxSize'], 0, function( $contents, $filename ) use ( $options )
{
$ext = mb_strtolower( mb_substr( $filename, mb_strrpos( $filename, '.' ) + 1 ) );
$ext = mb_strtolower( mb_substr( $filename, mb_strrpos( $filename, '.' ) + 1 ) );
/* Resize images */
if ( in_array( $ext, \IPS\Image::$imageExtensions ) )
/* Resize images */
if ( in_array( $ext, \IPS\Image::$imageExtensions ) AND ( is_array( $options['image'] ) OR $options['checkImage'] ) )
{
try
{
try
$image = \IPS\Image::create( $contents );
if( is_array( $options['image'] ) )
{
$image = \IPS\Image::create( $contents );
$image->resizeToMax( $options['image']['maxWidth'] ?: NULL, $options['image']['maxHeight'] ?: NULL );
return (string) $image;
}
catch ( \Exception $e ) {}
return (string) $image;
}
return $contents;
}, $this->options['storageContainer'], $this->options['obscure'] );
}
else
{
$fileObjects = \IPS\File::createFromUploads( $this->options['storageExtension'], $fieldName, $this->options['allowedFileTypes'], $this->options['maxFileSize'], $this->options['totalMaxSize'], 0, NULL, $this->options['storageContainer'], $this->options['obscure'] );
}
catch ( \Exception $e ) {}
}
return $contents;
}, $this->options['storageContainer'], $this->options['obscure'] );
foreach ( $fileObjects as $fileObj )
{
+1 -1
View File
@@ -68,7 +68,7 @@ class _Url extends Text
}
/* And that it's an allowed protocol */
if ( !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');
}
+152 -74
View File
@@ -197,15 +197,15 @@ class _Content extends Table
{
if ( isset( $class::$databaseColumnMap['updated'] ) )
{
$this->sortBy = $class::$databasePrefix . $class::$databaseColumnMap['updated'];
$this->sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['updated'];
}
elseif ( isset( $class::$databaseColumnMap['last_comment'] ) )
{
$this->sortBy = $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
{
$this->sortBy = $class::$databasePrefix . $class::$databaseColumnMap['date'];
$this->sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['date'];
}
}
@@ -246,20 +246,17 @@ class _Content extends Table
}
}
if ( ! mb_stristr( $this->sortBy, '`' ) )
$compareSortBy = $this->sortBy;
$lookFor = $class::$databaseTable . '.' . $class::$databasePrefix;
if ( mb_substr( $this->sortBy, 0, mb_strlen( $lookFor ) ) === $lookFor )
{
$compareSortBy = $this->sortBy;
$lookFor = $class::$databaseTable . '.' . $class::$databasePrefix;
if ( mb_substr( $this->sortBy, 0, mb_strlen( $lookFor ) ) === $lookFor )
{
$compareSortBy = mb_substr( $this->sortBy, mb_strlen( $lookFor ) );
}
$this->sortBy = in_array( $compareSortBy, $this->sortOptions ) ? $this->sortBy :
( array_key_exists( $compareSortBy, $this->sortOptions ) ? $this->sortOptions[ $compareSortBy ] : $defaultSort );
$compareSortBy = mb_substr( $this->sortBy, mb_strlen( $lookFor ) );
}
$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 ) )
{
@@ -514,60 +511,41 @@ class _Content extends Table
$class = $this->class;
$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' ) ) )
{
$options = array();
$ids = array_keys( \IPS\Request::i()->moderate );
foreach ( $ids as $id )
{
$item = $class::load( $id );
$action = 'can' . ucwords( \IPS\Request::i()->modaction );
if( $item->$action() )
{
$options[ $id ] = $item->mapped('title');
$descriptions[ $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'byline', FALSE, array( 'sprintf' => array( $item->author()->name ) ) ) . \IPS\DateTime::ts( $item->mapped('date') )->html();
}
}
/* The user doesn't have permission to perform the action on any of the content */
if ( !count( $options ) )
{
throw new \OutOfRangeException;
}
}
/* Move: to where? */
if ( \IPS\Request::i()->modaction == 'move' )
{
$form = new \IPS\Helpers\Form( 'form', 'move' );
$form->class = 'ipsForm_vertical';
$form->hiddenValues['modaction'] = 'move';
$form->hiddenValues['moderate'] = \IPS\Request::i()->moderate;
$form->add( new \IPS\Helpers\Form\Node( 'move_to', NULL, TRUE, array(
'class' => $class::$containerNodeClass,
'url' => \IPS\Request::i()->url()->setQueryString( 'modaction', 'move' ),
'permissionCheck' => function( $node )
{
try
{
if ( $node->can( 'add' ) )
{
return true;
}
}
catch( \OutOfBoundsException $e ) { }
return false;
},
'clubs' => TRUE
) ) );
if ( isset( $class::$databaseColumnMap['moved_to'] ) )
/* The method will return an HTML string, or an array of parameters to pass to modAction */
$params = $this->getMoveForm();
/* This is the form instead */
if( !is_array( $params ) )
{
$form->add( new \IPS\Helpers\Form\Checkbox( 'move_keep_link' ) );
if ( \IPS\Settings::i()->topic_redirect_prune )
{
\IPS\Member::loggedIn()->language()->words['move_keep_link_desc'] = \IPS\Member::loggedIn()->language()->addToStack('_move_keep_link_desc', FALSE, array( 'pluralize' => array( \IPS\Settings::i()->topic_redirect_prune ) ) );
}
}
if ( $values = $form->values() )
{
$params[] = $values['move_to'];
$params[] = ( isset( $values['move_keep_link'] ) and $values['move_keep_link'] );
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->sendOutput( \IPS\Output::i()->output );
}
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 );
}
return;
return $params;
}
}
@@ -602,21 +580,31 @@ class _Content extends Table
/* Merge: what's the master? */
if ( \IPS\Request::i()->modaction == 'merge' )
{
$ids = array_keys( \IPS\Request::i()->moderate );
$options = array();
$descriptions = array();
foreach ( $ids as $id )
{
$item = $class::load( $id );
$options[ $id ] = $item->mapped('title');
$descriptions[ $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'byline', FALSE, array( 'sprintf' => array( $item->author()->name ) ) ) . \IPS\DateTime::ts( $item->mapped('date') )->html();
}
if ( count( $options ) === 1 )
{
foreach ( $options as $id => $title )
{
\IPS\Output::i()->redirect( $class::load( $id )->url()->setQueryString( 'do', 'merge' ) );
$item = $class::load( $id );
$form = $item->mergeForm();
if ( $values = $form->values() )
{
$item->mergeIn( array( $class::loadFromUrl( $values['merge_with'] ) ), isset( $values['move_keep_link'] ) ? $values['move_keep_link'] : FALSE );
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->sendOutput( \IPS\Output::i()->output );
}
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 );
}
}
}
}
else
@@ -673,7 +661,20 @@ class _Content extends Table
try
{
$object = $class::loadAndCheckPerms( $id );
/* If this item is read, we need to re-mark it as such after moving */
if( $object instanceof \IPS\Content\ReadMarkers )
{
$unread = $object->unread();
}
$object->modAction( \IPS\Request::i()->modaction, \IPS\Member::loggedIn(), $params );
/* Mark it as read */
if( $object instanceof \IPS\Content\ReadMarkers and \IPS\Request::i()->modaction == 'move' AND $unread == 0 )
{
$object->markRead();
}
}
catch ( \Exception $e ) {}
}
@@ -681,6 +682,83 @@ class _Content extends Table
\IPS\Output::i()->redirect( $this->baseUrl );
}
/**
* Get the form to move items
*
* @return string|array
*/
protected function getMoveForm()
{
$class = $this->class;
$params = array();
$form = new \IPS\Helpers\Form( 'form', 'move' );
$form->class = 'ipsForm_vertical';
$form->hiddenValues['modaction'] = 'move';
$form->hiddenValues['moderate'] = \IPS\Request::i()->moderate;
$currentContainer = $this->container;
$form->add( new \IPS\Helpers\Form\Node( 'move_to', NULL, TRUE, array(
'class' => $class::$containerNodeClass,
'url' => \IPS\Request::i()->url()->setQueryString( 'modaction', 'move' ),
'permissionCheck' => function( $node ) use ( $currentContainer, $class )
{
if( $currentContainer->id != $node->id )
{
try
{
/* If the item is in a club, only allow moving to other clubs that you moderate */
if ( $currentContainer and \IPS\IPS::classUsesTrait( $currentContainer, 'IPS\Content\ClubContainer' ) and $currentContainer->club() )
{
return $class::modPermission( 'move', \IPS\Member::loggedIn(), $node ) and $node->can( 'add' ) ;
}
if ( $node->can( 'add' ) )
{
return true;
}
}
catch( \OutOfBoundsException $e ) { }
}
return false;
},
'clubs' => TRUE
) ) );
if ( isset( $class::$databaseColumnMap['moved_to'] ) )
{
$form->add( new \IPS\Helpers\Form\Checkbox( 'move_keep_link' ) );
if ( \IPS\Settings::i()->topic_redirect_prune )
{
\IPS\Member::loggedIn()->language()->words['move_keep_link_desc'] = \IPS\Member::loggedIn()->language()->addToStack('_move_keep_link_desc', FALSE, array( 'pluralize' => array( \IPS\Settings::i()->topic_redirect_prune ) ) );
}
}
if ( $values = $form->values() )
{
$params[] = $values['move_to'];
$params[] = ( isset( $values['move_keep_link'] ) and $values['move_keep_link'] );
return $params;
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->sendOutput( \IPS\Output::i()->output );
}
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 );
}
return;
}
}
/**
* Return the table headers
+1 -1
View File
@@ -139,7 +139,7 @@ class _Custom extends Table
}
else
{
$v = htmlspecialchars( $v, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
$v = htmlspecialchars( $v, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
}
$rows[ $i ][ $k ] = $v;
+41 -41
View File
@@ -36,16 +36,21 @@ class _Db extends Table
* @brief Initial WHERE clause
*/
public $where;
/**
* @brief Initial GROUP BY clause
*/
protected $group;
/**
* @brief Force index clause
*/
protected $index;
/**
* @brief Primary sort column
*/
public $primarySortBy;
/**
* @brief Direction of primary sort
*/
public $primarySortDirection;
/**
* @brief Joins
@@ -57,31 +62,26 @@ class _Db extends Table
*/
public $keyField = NULL;
/**
* @brief Restrict select columns to only those listed - useful when grouping
*/
public $onlySelected = NULL;
/**
* Constructor
*
* @param array|string $table Database table
* @param string $table Database table
* @param \IPS\Http\Url $baseUrl Base URL
* @param array|null $where WHERE clause
* @param string|null $group GROUP clause
* @param array|null $forceIndex Index to force
* @return void
*/
public function __construct( $table, \IPS\Http\Url $baseUrl, $where=NULL, $group=NULL, $forceIndex=NULL )
public function __construct( $table, \IPS\Http\Url $baseUrl, $where=NULL, $forceIndex=NULL )
{
$this->table = $table;
$this->where = $where;
$this->group = $group;
$this->index = $forceIndex;
return parent::__construct( $baseUrl );
}
protected $onlySelected = false;
/**
* Get rows
*
@@ -285,29 +285,13 @@ class _Db extends Table
$selects = $this->selects;
if( $this->onlySelected !== NULL )
if ( count( $this->joins ) )
{
foreach( $this->onlySelected as $column )
foreach( $this->joins as $join )
{
$selects[] = $column;
}
if( $this->group !== NULL )
{
$this->group = is_array( $this->group ) ? $this->group : array( $this->group );
$this->group = array_unique( array_merge( $this->group, $this->onlySelected ) );
}
}
else
{
if ( count( $this->joins ) )
{
foreach( $this->joins as $join )
if ( isset( $join['select'] ) )
{
if ( isset( $join['select'] ) )
{
$selects[] = $join['select'];
}
$selects[] = $join['select'];
}
}
}
@@ -324,8 +308,8 @@ class _Db extends Table
$count = $count->first();
/* Now get column headers */
$query = \IPS\Db::i()->select( $this->onlySelected ? implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ) );
$query = \IPS\Db::i()->select( '*', $this->table, NULL, NULL, array( 0, 1 ) );
if ( count( $this->joins ) )
{
foreach( $this->joins as $join )
@@ -368,17 +352,22 @@ class _Db extends Table
}, explode( ',', $this->sortBy ) ) );
$orderBy .= $this->sortDirection == 'asc' ? ' asc' : ' desc';
/* Primary sorting effectively creates a way to 'pin' records regardless of the user selected sort */
if( $this->primarySortBy !== NULL AND $this->primarySortDirection !== NULL )
{
$orderBy = "{$this->primarySortBy} {$this->primarySortDirection}, {$orderBy}";
}
}
/* Run query */
$rows = array();
$select = \IPS\Db::i()->select(
$this->onlySelected ? implode( ', ', $selects ) : ( ( count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*' ),
( count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*',
$this->table,
$where,
$orderBy,
array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ),
$this->group
array( ( $this->limit * ( $this->page - 1 ) ), $this->limit )
);
if ( $this->index )
@@ -429,7 +418,7 @@ class _Db extends Table
}
else
{
$v = htmlspecialchars( $v, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
$v = htmlspecialchars( $v, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
}
/* Are we including this one? */
@@ -449,6 +438,12 @@ class _Db extends Table
$row['_buttons'] = call_user_func( $this->rowButtons, $_row );
}
/* Highlighting? */
if ( isset( $this->parsers['_highlight'] ) and $class = call_user_func( $this->parsers['_highlight'], $_row ) )
{
$this->highlightRows[ $rowId ] = $class;
}
$rows[ $rowId ] = $row;
}
@@ -485,6 +480,11 @@ class _Db extends Table
{
return false;
}
if( !is_array( $count ) )
{
$count = array( $count );
}
if ( mb_strstr( $this->sortBy, ',' ) )
{
+25 -23
View File
@@ -167,6 +167,11 @@ abstract class _Table
*/
public $rowClasses = array();
/**
* @brief Rows to highlight
*/
public $highlightRows = array();
/**
* @brief Buttons to show on the "root row"
* @code
@@ -281,7 +286,7 @@ abstract class _Table
$this->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'tables', 'core' ), 'rows' );
/* Create a unique id used by the template - can be overriden manually if desired */
$this->uniqueId = md5( uniqid() );
$this->uniqueId = md5( mt_rand() );
}
/**
@@ -478,7 +483,7 @@ abstract class _Table
/* Get rows */
$rows = $this->getRows( $advancedSearchValues );
/* Check we're on a valid page (must come after getRows() as this is where $this->pages is set) */
if ( $this->page )
{
@@ -700,6 +705,7 @@ abstract class _Table
/* Store these to prevent URL from being rebuilt on subsequent calls */
if ( $this->advancedSearchValues === NULL )
{
$this->advancedSearchValues = array();
foreach ( $values as $k => $v )
{
if( array_key_exists( $k, $form->hiddenValues ) )
@@ -709,26 +715,6 @@ abstract class _Table
if ( $v !== NULL )
{
if( \IPS\Request::i()->isAjax() )
{
if ( is_array( $v ) )
{
$newVal = array();
foreach( $v as $key => $val )
{
$newVal[ $key ] = urldecode( $val );
}
$v = $newVal;
}
else
{
if ( ! is_object( $v ) )
{
$v = urldecode( $v );
}
}
}
$advancedSearchValuesQuery[ $k ] = $v;
$this->advancedSearchValues[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ] = $v;
}
@@ -744,5 +730,21 @@ abstract class _Table
return $this->advancedSearchValues;
}
}
/**
* Return the sort direction to use for links
*
* @note Abstracted so other table helper instances can adjust as needed
* @param string $column Sort by string
* @return string [asc|desc]
*/
public function getSortDirection( $column )
{
if( $column == 'title' )
{
return 'asc';
}
return 'desc';
}
}
+1 -1
View File
@@ -132,7 +132,7 @@ class _Tree
if ( \IPS\Request::i()->isAjax() )
{
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->rows( $rows, md5( uniqid() ) );
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->rows( $rows, mt_rand() );
}
$root = call_user_func( $this->getRow, \IPS\Request::i()->root, TRUE );