Version 4.7.0

This commit is contained in:
Neo committed 2025-12-19 16:00:35 -08:00
1 parent 517a5e1f70
commit 8ba7d43898
1933 files changed
+65613 -11332

No files matched your search

+79 -5
View File
@@ -61,6 +61,16 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
* @brief Table Column Formatters
*/
public $tableParsers = array();
/**
* @brief Along with the other series, show one of the sum of all
*/
public $allRecordsSeries = FALSE;
/**
* @brief The lang string used to label the 'all records' series
*/
public $allRecordsLang = 'database_chart_all_series';
/**
* @brief Custom form
@@ -182,7 +192,10 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
$this->groupBy = $this->customFiltersForm['groupBy'];
$this->where = array( array( $whereFunction( $values ) ) );
$whereFunctionResult = $whereFunction( $values );
$this->where[] = \is_array( $whereFunctionResult ) ? $whereFunctionResult : array( $whereFunctionResult );
foreach( $seriesFunction( $values ) as $series )
{
@@ -195,7 +208,9 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
$seriesFunction = $this->customFiltersForm['series'];
$this->groupBy = $this->customFiltersForm['groupBy'];
$this->where = array( array( $whereFunction( $this->savedCustomFilters ) ) );
$whereFunctionResult = $whereFunction( $this->savedCustomFilters );
$this->where[] = \is_array( $whereFunctionResult ) ? $whereFunctionResult : array( $whereFunctionResult );
foreach( $seriesFunction( $this->savedCustomFilters ) as $series )
{
@@ -212,7 +227,65 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
}
}
if ( $this->allRecordsSeries )
{
/* 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() );
}
/* 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;
case 'weekly':
$timescale = '%x-%v';
break;
case 'monthly':
$timescale = '%Y-%c';
break;
}
$fromUnixTime = "FROM_UNIXTIME( IFNULL( {$this->dateField}, 0 ) )";
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 ) . "' )";
}
$allRecordsStatement = \IPS\Db::i()->select(
"SQL_BIG_RESULT DATE_FORMAT( {$fromUnixTime}, '{$timescale}' ) AS time_as_alias, COUNT(*) as $this->allRecordsLang",
$this->table,
$where,
'time_as_alias ASC',
NULL,
'time_as_alias'
);
foreach ( $allRecordsStatement as $allRecordsRow )
{
$data[ $allRecordsRow['time_as_alias'] ] = $data[ $allRecordsRow['time_as_alias'] ] ?? array();
$data[ $allRecordsRow['time_as_alias'] ][$this->allRecordsLang] = $allRecordsRow[$this->allRecordsLang];
}
$this->addSeries( \IPS\Member::loggedIn()->language()->addToStack($this->allRecordsLang), 'number', 'COUNT(*)', FALSE, $this->allRecordsLang );
}
return $data;
}
@@ -303,7 +376,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
$stmt = \IPS\Db::i()->select(
"SQL_BIG_RESULT DATE_FORMAT( {$fromUnixTime}, '{$timescale}' ) AS time_as_alias, " . implode( ', ', array_unique( $this->series ) ) . ( $this->groupBy ? ", " . $this->groupBy : '' ),
"SQL_BIG_RESULT DATE_FORMAT( {$fromUnixTime}, '{$timescale}' ) AS time_as_alias" . ( \count( $this->series ) ? "," : "" ) . implode( ', ', array_unique( $this->series ) ) . ( $this->groupBy ? ", " . $this->groupBy : '' ),
$this->table,
$where,
'time_as_alias ASC',
@@ -330,7 +403,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
continue;
}
}
foreach( $this->series as $column )
{
if( $this->groupBy )
@@ -546,6 +619,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
$this->compileForOutput();
if ( isset( \IPS\Request::i()->download ) )
{
return $this->download();
+2 -1
View File
@@ -151,8 +151,9 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
/* If we are deleting a chart, just do that now and redirect */
if( isset( \IPS\Request::i()->deleteChart ) )
{
\IPS\Session::i()->csrfCheck();
\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' );
\IPS\Output::i()->redirect( \IPS\Request::i()->url()->stripQueryString( array( 'deleteChart', 'chartId', 'csrfKey' ) ), 'chart_deleted' );
}
if ( !isset( $options['chartArea'] ) )
@@ -0,0 +1,39 @@
<?php
/**
* @brief Invisible captcha Interface
* @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 10 Mai 2022
*/
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;
}
/**
* captcha Interface
*/
interface CaptchaInterface
{
/**
* Display
*
* @return string
*/
public function 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();
}
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* @brief hCaptcha
* @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 10 Mai 2022
*/
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;
}
/**
* hCaptcha
*/
class _Hcaptcha implements CaptchaInterface
{
/**
* Does this CAPTCHA service support being added in a modal?
*/
public static $supportsModal = FALSE;
/**
* @brief Error
*/
protected $error;
/**
* Display
*
* @return string
*/
public function getHtml()
{
\IPS\Output::i()->jsFilesAsync[] = "https://js.hcaptcha.com/1/api.js?hl=" . \IPS\Member::loggedIn()->language()->short ;
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->hCaptcha( \IPS\Settings::i()->hcaptcha_sitekey, preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short ) );
}
/**
* Display
*
* @return string
*/
public function rowHtml()
{
\IPS\Output::i()->jsFilesAsync[] = "https://js.hcaptcha.com/1/api.js?hl=" . \IPS\Member::loggedIn()->language()->short ;
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->hCaptcha( \IPS\Settings::i()->hcaptcha_sitekey, preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short ) );
}
/**
* 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://hcaptcha.com/siteverify' )->request()->post( array(
'secret' => \IPS\Settings::i()->hcaptcha_secret,
'response' => trim( \IPS\Request::i()->__get('h-captcha-response') ),
'remoteip' => \IPS\Request::i()->ipAddress(),
) )->decodeJson( TRUE );
$hostname = \IPS\Http\Url::internal('')->data[ \IPS\Http\Url::COMPONENT_HOST ];
return ( ( (bool) $response['success'] ) and ( $response['hostname'] === mb_substr( $hostname, \mb_strlen( $hostname ) - mb_strlen( $response['hostname'] ) ) ) );
}
catch( \RuntimeException $e )
{
if( $e->getMessage() == 'BAD_JSON' )
{
return FALSE;
}
else
{
throw $e;
}
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* Invisible reCAPTCHA
*/
class _Invisible
class _Invisible implements CaptchaInterface
{
/**
* Does this CAPTCHA service support being added in a modal?
+1 -1
View File
@@ -20,7 +20,7 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* keyCAPTCHA
*/
class _Keycaptcha
class _Keycaptcha implements CaptchaInterface
{
/**
* Does this CAPTCHA service support being added in a modal?
+1 -1
View File
@@ -20,7 +20,7 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* reCAPTCHA
*/
class _Recaptcha2
class _Recaptcha2 implements CaptchaInterface
{
/**
* Does this CAPTCHA service support being added in a modal?
+3 -3
View File
@@ -38,9 +38,9 @@ class _CheckboxSet extends Select
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$options['multiple'] = TRUE;
$options['showAllNone'] = $options['showAllNone'] ?? TRUE;
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
if ( !isset( $this->options['descriptions'] ) )
{
$this->options['descriptions'] = array();
@@ -93,7 +93,7 @@ class _CheckboxSet extends Select
$value = array( $value );
}
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->checkboxset( $this->name, $value, $this->required, $this->parseOptions(), $this->options['multiple'], $this->options['class'], $this->options['disabled'], $this->options['toggles'], NULL, $this->options['unlimited'], $this->options['unlimitedLang'], $this->options['unlimitedToggles'], $this->options['unlimitedToggleOn'], $descriptions, $this->options['impliedUnlimited'] );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->checkboxset( $this->name, $value, $this->required, $this->parseOptions(), $this->options['multiple'], $this->options['class'], $this->options['disabled'], $this->options['toggles'], NULL, $this->options['unlimited'], $this->options['unlimitedLang'], $this->options['unlimitedToggles'], $this->options['unlimitedToggleOn'], $descriptions, $this->options['impliedUnlimited'], $this->options['showAllNone'] );
}
/**
+1 -1
View File
@@ -179,7 +179,7 @@ class _Date extends FormAbstract
{
if ( \is_int( $v ) or ( $v and \is_string( $v ) and preg_match( '/^[0-9]+$/', $v ) ) )
{
return \IPS\DateTime::ts( $v, $timezone );
return \IPS\DateTime::ts( $v );
}
else if( \is_array( $v ) )
{
+101 -11
View File
@@ -66,7 +66,8 @@ class _Editor extends FormAbstract
'maxLength' => 16777215, // The default for MEDIUMTEXT */
'editorId' => NULL,
'allowAttachments' => TRUE,
'ipsPlugins' => "ipsautolink,ipsautosave,ipsctrlenter,ipscode,ipscontextmenu,ipsemoticon,ipsimage,ipslink,ipsmentions,ipspage,ipspaste,ipspreview,ipsquote,ipsspoiler,ipsautogrow,ipssource,removeformat"
'ipsPlugins' => "ipsautolink,ipsautosave,ipsctrlenter,ipscode,ipscontextmenu,ipsemoticon,ipsimage,ipslink,ipsmentions,ipspage,ipspaste,ipsquote,ipsspoiler,ipsautogrow,ipssource,removeformat",
'profanityBlock' => TRUE,
);
/**
@@ -103,7 +104,34 @@ class _Editor extends FormAbstract
if ( \IPS\Settings::i()->giphy_enabled )
{
$this->defaultOptions['ipsPlugins'] .= ',ipsgiphy';
$this->defaultOptions['ipsPlugins'] .= ',ipsgiphy';
}
if( \IPS\Dispatcher::hasInstance() and ( \IPS\Dispatcher::i()->controllerLocation == 'front' OR ( \IPS\Dispatcher::i()->controllerLocation == 'admin' AND \IPS\Dispatcher::i()->module->key === 'editor' AND \IPS\Dispatcher::i()->controller == 'toolbar' )))
{
$this->defaultOptions['ipsPlugins'] .= ',ipspreview';
}
if ( isset( $options['allButtons'] ) AND $options['allButtons'] )
{
$showStockReplies = TRUE;
}
else
{
$showStockReplies = FALSE;
foreach ( \IPS\core\StoredReplies::getStore() as $reply )
{
$reply = \IPS\core\StoredReplies::constructFromData( $reply );
if ( $reply->enabled and $reply->can( 'view' ) )
{
$showStockReplies = TRUE;
}
}
}
if ( $showStockReplies )
{
$this->defaultOptions['ipsPlugins'] .= ',ipsstockreplies';
}
if ( isset( \IPS\Request::i()->usingEditor ) and \IPS\Request::i()->isAjax() and \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'front' )
@@ -309,6 +337,23 @@ class _Editor extends FormAbstract
$attachmentArea = \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->editorAttachmentsPlaceholder( $this->name, $this->postKey, $this->noUploaderError, $this->canUseMediaExtension() );
}
if ( \IPS\Member::loggedIn()->group['g_bypass_badwords'] )
{
$this->options['profanityBlock'] = FALSE;
}
if ( $this->options['profanityBlock'] )
{
$this->options['profanityBlock'] = [];
foreach( \IPS\core\Profanity::getProfanity() AS $profanity )
{
if ( $profanity->action == 'block' )
{
$this->options['profanityBlock'][] = [ 'word' => $profanity->type, 'type' => $profanity->m_exact ? 'exact' : 'loose' ];
}
}
}
/* 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( array() ), $this->options['tags'] );
@@ -339,7 +384,12 @@ class _Editor extends FormAbstract
return $value;
}
/**
* @brief Save on queries and fetch the alt label would just the once
*/
protected $_altLabelWord = NULL;
/**
* Get Value
*
@@ -381,9 +431,22 @@ class _Editor extends FormAbstract
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 ) or \in_array( mb_strtolower( $ext ), \IPS\File::$audioExtensions ) )
{
$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'] );
$url = \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ) . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'];
if ( $attachment['attach_security_key'] )
{
$url .= "&key={$attachment['attach_security_key']}";
}
if ( \in_array( mb_strtolower( $ext ), \IPS\File::$videoExtensions ) )
{
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedVideo( $attachment['attach_location'], $url, $attachment['attach_file'], \IPS\File::getMimeType( $attachment['attach_file'] ), $attachment['attach_id'] );
}
elseif ( \in_array( mb_strtolower( $ext ), \IPS\File::$audioExtensions ) )
{
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedAudio( $attachment['attach_location'], $url, $attachment['attach_file'], \IPS\File::getMimeType( $attachment['attach_file'] ), $attachment['attach_id'] );
}
}
elseif ( $attachment['attach_is_image'] )
{
@@ -397,8 +460,21 @@ class _Editor extends FormAbstract
$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 );
$altText = NULL;
if ( ! empty( $attachment['attach_labels'] ) )
{
if ( $this->_altLabelWord === NULL )
{
/* This is stored with the post, so it cannot be the user's language */
$this->_altLabelWord = \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->get( 'alt_label_could_be' );
}
$altText = $this->_altLabelWord . ' ' . implode( ', ', \IPS\Text\Parser::getAttachmentLabels( $attachment ) );
}
$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, $altText );
}
else
{
@@ -409,7 +485,7 @@ class _Editor extends FormAbstract
}
$fileAttachments[] = \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedFile( $url, $attachment['attach_file'], FALSE, $attachment['attach_ext'], $attachment['attach_id'], $attachment['attach_security_key'] );
}
if ( !isset( $parser ) or !\in_array( $attachment['attach_id'], $parser->mappedAttachments ) )
{
$inserts[] = array(
@@ -435,7 +511,7 @@ class _Editor extends FormAbstract
{
\IPS\Db::i()->insert( 'core_attachments_map', $inserts, TRUE );
}
/* Clear out the post key for attachments that are claimed automatically */
if ( $this->options['attachIds'] )
{
@@ -457,7 +533,7 @@ class _Editor extends FormAbstract
}
catch ( \Exception $e ) { }
}
/* Throw any errors */
if ( $error )
{
@@ -638,10 +714,24 @@ class _Editor extends FormAbstract
'totalMaxSize' => $maxTotalSize,
'maxFileSize' => \IPS\Member::loggedIn()->group['g_attach_per_post'] ? ( \IPS\Member::loggedIn()->group['g_attach_per_post'] * 1024 ) / 1048576 : NULL,
'allowStockPhotos' => $allowStockPhotos,
'canBeModerated' => ($this->extension AND method_exists( $this->extension, 'canBeModerated' ) and $this->extension->canBeModerated( \IPS\Member::loggedIn(), $this ) ),
'callback' => function( $file ) use ( $postKey )
{
try
{
$fileInfo = \IPS\Db::i()->select( 'requires_moderation, labels', 'core_files_temp', array( 'contents=?', (string) $file ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
$requiresModeration = (bool) $fileInfo['requires_moderation'];
$labels = $fileInfo['labels'];
}
catch ( \UnderflowException $e )
{
$requiresModeration = FALSE;
$labels = NULL;
}
\IPS\Db::i()->delete( 'core_files_temp', array( 'contents=?', (string) $file ) );
$attachment = $file->makeAttachment( $postKey, \IPS\Member::loggedIn() );
$attachment = $file->makeAttachment( $postKey, \IPS\Member::loggedIn(), $requiresModeration, $labels );
return $attachment['attach_id'];
}
);
+9 -3
View File
@@ -130,6 +130,11 @@ class _Matrix extends \IPS\Helpers\Form
* @brief Form Id, Set it Matrix is part of a form
*/
public $formId = NULL;
/**
* @brief Determines whether the Row Titles are processed as raw html instead of a language string. Not needed if $langPrefix is set
*/
public $styledRowTitle = FALSE;
/**
* Get HTML
@@ -140,7 +145,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, $this->sortable );
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, $this->styledRowTitle );
}
catch ( \Exception $e )
{
@@ -159,7 +164,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, $this->sortable );
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, $this->styledRowTitle );
}
/**
@@ -402,7 +407,8 @@ class _Matrix extends \IPS\Helpers\Form
if ( isset( $element->options['unlimited'] ) )
{
$unlimitedKey = "{$rowId}[{$columnName}_unlimited]";
if ( $value = \IPS\Request::i()->valueFromArray( $unlimitedKey ) )
$value = \IPS\Request::i()->valueFromArray( $unlimitedKey );
if ( $value !== NULL )
{
$element->value = $value;
}
+2 -1
View File
@@ -297,7 +297,8 @@ 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->_title;
$title = isset( $node::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $node::$titleLangPrefix . $node->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : str_replace( "&#039;", "&apos;", htmlspecialchars( $node->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
$selected[ !( $node instanceof $nodeClass ) ? "{$node->_id}.s" : $node->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
if( isset( $val::$titleLangPrefix ) )
{
+13 -13
View File
@@ -184,15 +184,9 @@ class _Number extends FormAbstract
do the thousand seperator in case what we received was the former alredy, which would turn
for example "10,00" into "1000" */
$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 ) )
{
throw new \InvalidArgumentException( 'form_number_bad' );
}
/* Convert to int/float */
if ( $this->options['decimals'] )
if ( $this->options['decimals'] and $value != '' )
{
$value = \floatval( $value );
if ( \is_int( $this->options['decimals'] ) )
@@ -202,7 +196,7 @@ class _Number extends FormAbstract
}
else
{
$value = \intval( $value );
$value = ( $value != '' ) ? \intval( $value ) : '';
}
/* Return */
@@ -216,16 +210,22 @@ class _Number extends FormAbstract
* @return TRUE
*/
public function validate()
{
{
parent::validate();
if ( $this->options['unlimited'] === NULL or $this->value !== $this->options['unlimited'] )
{
if ( $this->options['min'] !== NULL and $this->value < $this->options['min'] )
{
/* If it's not numeric, throw an exception */
if ( ( !\is_numeric( $this->value ) and $this->value !== '' and $this->required === FALSE ) or ( !\is_numeric( $this->value ) and $this->required === TRUE ) )
{
throw new \InvalidArgumentException( 'form_number_bad' );
}
if ( $this->options['min'] !== NULL and (int) $this->value < $this->options['min'] )
{
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_number_min', FALSE, array( 'sprintf' => array( $this->options['min'] ) ) ) );
}
if ( $this->options['max'] !== NULL and $this->value > $this->options['max'] )
if ( $this->options['max'] !== NULL and (int) $this->value > $this->options['max'] )
{
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_number_max', FALSE, array( 'sprintf' => array( $this->options['max'] ) ) ) );
}
-1
View File
@@ -59,7 +59,6 @@ class _Poll extends FormAbstract
foreach ( $choice['choice'] as $i => $question )
{
$thisChoice['title'] = $question;
$thisChoice['count'] = isset( $choice['votes'][ $i ] ) ? $choice['votes'][ $i ] : 0;
$thisQuestion['choices'][ $i ] = $thisChoice;
}
-4
View File
@@ -333,10 +333,6 @@ class _Tel extends Text
array (
0 => '33',
),
'FX' =>
array (
0 => '33',
),
'GA' =>
array (
0 => '241',
+6 -1
View File
@@ -311,6 +311,11 @@ class _Text extends TextArea
/* Set up profanity filters */
foreach( \IPS\core\Profanity::getProfanity() AS $profanity )
{
if ( $profanity->action == 'block' )
{
continue;
}
if ( $profanity->m_exact )
{
$exactProfanity[ $profanity->type ] = $profanity;
@@ -441,7 +446,7 @@ class _Text extends TextArea
}
else
{
if ( mb_stristr( $value, $type ) )
if ( mb_stristr( $this->value[ $k ], $type ) )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( $v ) ) ) );
}
+9 -1
View File
@@ -78,7 +78,15 @@ class _Translatable extends FormAbstract
{
if ( \is_array( $this->options['sprintf'] ) )
{
$values[ $k ] = vsprintf( $v, $this->options['sprintf'] );
try
{
$values[ $k ] = vsprintf( $v, $this->options[ 'sprintf' ] );
}
catch ( \ValueError $e )
{
/* Note in pre-PHP8, vsprintf() returns FALSE */
$values[ $k ] = FALSE;
}
}
else
{
+61 -34
View File
@@ -45,6 +45,7 @@ class _Upload extends FormAbstract
'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
'canBeModerated' => TRUE // Set to TRUE if this uploader is capable of holding the submitted content for moderation
);
* @endcode
*/
@@ -67,7 +68,8 @@ class _Upload extends FormAbstract
'default' => NULL,
'obscure' => TRUE,
'supportsDelete' => TRUE,
'allowStockPhotos' => FALSE
'allowStockPhotos' => FALSE,
'canBeModerated' => FALSE
);
/**
@@ -167,7 +169,7 @@ class _Upload extends FormAbstract
/* If this has to be an image, set the allowed file types */
if ( isset( $options['image'] ) and !isset( $options['image']['optional'] ) and !isset( $options['allowedFileTypes'] ) )
{
$options['allowedFileTypes'] = \IPS\Image::supportedExtensions();
$options['allowedFileTypes'] = \IPS\Image::$imageExtensions;
}
/* Call parent constructor */
@@ -256,12 +258,16 @@ class _Upload extends FormAbstract
$image->setExifData( $fileObj->contents() );
}
$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 );
}
$fileObj->replace( (string) $image );
if ( $hasBeenResized or $image->hasBeenRotated )
{
$fileObj->replace( (string) $image );
}
}
catch ( \Exception $e ) { }
}
@@ -441,7 +447,6 @@ class _Upload extends FormAbstract
'size' => \IPS\Output\Plugin\Filesize::humanReadableFilesize( $file->filesize(), FALSE, TRUE ),
'sizeRaw' => $file->filesize(),
'default' => ( isset( $attachment ) AND $attachment['default'] ) ? $attachment['default'] : NULL,
'securityKey' => ( isset( $attachment ) ) ? $attachment['attach_security_key'] : $file->securityKey
);
}
catch( \UnderflowException $e ){}
@@ -482,7 +487,7 @@ class _Upload extends FormAbstract
{
/* Get the files we had already */
$return = array();
$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') );
$tempFiles = iterator_to_array( \IPS\Db::i()->select( 'id,contents,filename,exif,requires_moderation', 'core_files_temp', array( 'upload_key=?', md5( $this->name . session_id() ) ) )->setKeyField('id') );
if ( $this->options['storageExtension'] )
{
$existingName = $this->name . '_existing';
@@ -501,6 +506,7 @@ class _Upload extends FormAbstract
/* 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;
$file->requiresModeration = (bool) $tempFiles[ $tempId ]['requires_moderation'];
$return[ $id ] = $file;
}
else
@@ -701,7 +707,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::supportedExtensions() ) )
if( \in_array( $ext, \IPS\File::$safeFileExtensions ) AND \in_array( $ext, \IPS\Image::$imageExtensions ) )
{
if( \IPS\File::checkXssInFile( file_get_contents( $file['tmp_name'] ) ) )
{
@@ -718,36 +724,42 @@ class _Upload extends FormAbstract
else
{
$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 )
$canBeModerated = $options['canBeModerated'];
$fileObjects = \IPS\File::createFromUploads( $this->options['storageExtension'], $fieldName, $this->options['allowedFileTypes'], $this->options['maxFileSize'], $this->options['totalMaxSize'], 0, function( $contents, $filename, $i ) use ( $options, &$requiresModeration, $canBeModerated )
{
$ext = mb_strtolower( mb_substr( $filename, mb_strrpos( $filename, '.' ) + 1 ) );
/* Resize images */
if ( \in_array( $ext, \IPS\Image::supportedExtensions() ) AND ( \is_array( $options['image'] ) OR $options['checkImage'] ) )
/* Do image-specific stuff */
if ( \in_array( $ext, \IPS\Image::$imageExtensions ) )
{
try
/* Resize */
$image = NULL;
$hasBeenResized = FALSE;
if ( \is_array( $options['image'] ) OR $options['checkImage'] )
{
$image = \IPS\Image::create( $contents );
if( $image::exifSupported() )
try
{
$image->setExifData( $contents );
$image = \IPS\Image::create( $contents );
if( \is_array( $options['image'] ) )
{
$hasBeenResized = $image->resizeToMax( $options['image']['maxWidth'] ?: NULL, $options['image']['maxHeight'] ?: NULL );
}
if ( $hasBeenResized or $image->hasBeenRotated )
{
$contents = (string) $image;
}
}
if( \is_array( $options['image'] ) )
{
$image->resizeToMax( $options['image']['maxWidth'] ?: NULL, $options['image']['maxHeight'] ?: NULL );
}
return (string) $image;
catch ( \Exception $e ) {}
}
catch ( \Exception $e ) {}
}
return $contents;
}, $this->options['storageContainer'], $this->options['obscure'] );
foreach ( $fileObjects as $fileObj )
foreach ( $fileObjects as $i => $fileObj )
{
$exif = NULL;
@@ -756,15 +768,17 @@ class _Upload extends FormAbstract
$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'],
'exif' => $exif
) );
$data = $this->populateTempData( 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'],
'exif' => $exif
), $fileObj, $options );
$insertId = \IPS\Db::i()->insert( 'core_files_temp', $data );
if ( $this->options['callback'] )
{
@@ -799,6 +813,19 @@ class _Upload extends FormAbstract
throw new \InvalidArgumentException('form_required');
}
}
/**
* Finish populating the data before its inserted
*
* @param array $data Array of data to change/add to
* @param \IPS\File $fileObj File Object data
* @param array $options Field options
* @return array
*/
protected function populateTempData( array $data, \IPS\File $fileObj, array $options ): array
{
return $data;
}
/**
* String Value
+50 -9
View File
@@ -121,6 +121,11 @@ class _Content extends Table
* @brief Get reactions for content item if supported
*/
protected $getReactions = FALSE;
/**
* @brief If passing a container, limit content by that container
*/
protected $limitByContainer = TRUE;
/**
* Constructor
@@ -139,7 +144,7 @@ class _Content extends Table
* @param bool $getReactions Get reactions 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, $callback=NULL, $getFirstComment=FALSE, $getFollowerCount=FALSE, $getReactions=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, $limitByContainer=TRUE )
{
/* Init */
$this->include = array();
@@ -154,7 +159,8 @@ class _Content extends Table
$this->getFirstComment = $getFirstComment;
$this->getFollowerCount = $getFollowerCount;
$this->getReactions = $getReactions;
$this->limitByContainer = $limitByContainer;
/* Init */
parent::__construct( $baseUrl );
@@ -163,7 +169,11 @@ class _Content extends Table
/* Set container */
if ( $container !== NULL )
{
$this->where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['container'] . '=?', $container->_id );
if ( $this->limitByContainer )
{
$this->where[] = array($class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['container'] . '=?', $container->_id);
}
$this->sortDirection = ( $this->sortDirection !== NULL ) ? $this->sortDirection : $container->_sortOrder;
if ( !$this->sortBy and ! empty( $container->_sortBy ) )
@@ -210,6 +220,7 @@ class _Content extends Table
\IPS\Output::i()->metaTags['og:title'] = $container->metaTitle();
\IPS\Output::i()->metaTags['og:description'] = $container->metaDescription();
\IPS\Output::i()->linkTags['canonical'] = (string) $canonicalUrl;
\IPS\Output::i()->metaTags['og:url'] = (string) $canonicalUrl;
}
/* Set available sort options */
@@ -280,7 +291,7 @@ 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 )
if ( ( $this->container and $this->limitByContainer ) and $this->permCheck )
{
try
{
@@ -338,8 +349,8 @@ class _Content extends Table
}
/* Get results */
$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->count = $class::getItemsWithPermission( $where, $sortBy, NULL, $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, TRUE, $this->joins, ( $this->container and $this->limitByContainer ) ? $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 and $this->limitByContainer ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$this->pages = ceil( $this->count / $this->limit );
$rows = iterator_to_array( $it );
$idField = $class::$databaseColumnId;
@@ -347,19 +358,49 @@ class _Content extends Table
if ( isset( $this->getFirstComment ) and isset( $class::$databaseColumnMap['first_comment_id'] ) and $class::$firstCommentRequired )
{
$commentIds = array();
$archivedCommentIds = [];
$firstCommentField = $class::$databaseColumnMap['first_comment_id'];
foreach( $rows as $item )
{
$commentIds[ $item->$idField ] = $item->$firstCommentField;
/* Get us the archived comment ids*/
if ( isset( $class::$archiveClass ) and method_exists( $class, 'isArchived' ) AND $item->isArchived() )
{
$archivedCommentIds[ $item->$idField ] = $item->$firstCommentField;
}
else
{
$commentIds[ $item->$idField ] = $item->$firstCommentField;
}
}
$comments = array();
if ( \count( $archivedCommentIds ) )
{
$archivedCommentClass = $class::$archiveClass;
$commentField = $archivedCommentClass::$databaseColumnId;
foreach( $archivedCommentClass::getItemsWithPermission( array( \IPS\Db::i()->in( $archivedCommentClass::$databasePrefix . $commentField, array_values( $archivedCommentIds ) ) ), NULL, $this->limit ) as $cObj )
{
$comments[ $cObj->$commentField ] = $cObj;
}
foreach( $rows as $id => $item )
{
if ( isset( $archivedCommentIds[ $item->$idField ] ) and isset( $comments[ $archivedCommentIds[ $item->$idField ] ] ) )
{
$rows[ $id ]->firstComment = $comments[ $archivedCommentIds[ $item->$idField ] ];
}
}
}
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 )
{
+3 -3
View File
@@ -48,7 +48,7 @@ class _Custom extends Table
* @param array $dataSource Data source
* @param \IPS\Http\Url $baseUrl Base URL
*/
public function __construct( $dataSource, \IPS\Http\Url $baseUrl )
public function __construct( $dataSource, \IPS\Http\Url $baseUrl, $count = NULL )
{
$this->dataSource = $dataSource;
$this->count = \count( $this->dataSource );
@@ -107,7 +107,7 @@ class _Custom extends Table
}
/* Do we need to sort? */
if( !empty( $rows ) and $this->sortBy and isset( $rows[ key( $rows ) ][ $this->sortBy ] ) )
if( $this->parseSource and !empty( $rows ) and $this->sortBy and isset( $rows[ key( $rows ) ][ $this->sortBy ] ) )
{
$sortBy = $this->sortBy;
$sortDirection = $this->sortDirection;
@@ -125,7 +125,7 @@ class _Custom extends Table
}
/* Limit */
if ( $this->limit and $this->count > $this->limit )
if ( $this->limit and $this->count > $this->limit and $this->parseSource )
{
$rows = \array_slice( $rows, ( $this->limit * ( $this->page - 1 ) ), $this->limit, TRUE );
}
+10 -3
View File
@@ -62,6 +62,11 @@ class _Db extends Table
*/
public $keyField = NULL;
/**
* @brief Group by key
*/
public $groupBy = NULL;
/**
* @brief The database we will query against
*/
@@ -320,9 +325,10 @@ class _Db extends Table
}
}
$count = $count->first();
$selectPrefix = ( $this->groupBy ) ? '' : $this->table . '.*, ';
/* Now get column headers */
$query = $this->db->select( ( \count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ) );
$query = $this->db->select( ( \count( $selects ) ) ? $selectPrefix . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ), $this->groupBy );
if ( \count( $this->joins ) )
{
@@ -380,11 +386,12 @@ class _Db extends Table
/* Run query */
$rows = array();
$select = $this->db->select(
( \count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*',
( \count( $selects ) ) ? $selectPrefix . implode( ', ', $selects ) : '*',
$this->table,
$where,
$orderBy,
array( ( $this->limit * ( $this->page - 1 ) ), $limit )
array( ( $this->limit * ( $this->page - 1 ) ), $limit ),
$this->groupBy
);
if ( $this->index )
+7 -1
View File
@@ -138,6 +138,11 @@ abstract class _Table
* @brief Language key for table title
*/
public $title = '';
/**
* @brief Use realtime features within the table?
*/
public $enableRealtime = false;
/**
* @brief Parsers
@@ -609,7 +614,7 @@ abstract class _Table
$return[ $k ] = $v->_id;
}
}
else if( isset( $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ] ) and $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ] === \IPS\Helpers\Table\SEARCH_BOOL )
else if( isset( $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ][0] ) and $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ][0] === \IPS\Helpers\Table\SEARCH_BOOL )
{
$return[ $k . '_checkbox' ] = $v;
}
@@ -618,6 +623,7 @@ abstract class _Table
$return[ $k ] = $v;
}
}
return $return;
}