Version 4.6.0

This commit is contained in:
Neo committed 2025-12-19 05:55:31 -08:00
1 parent f79dcf067a
commit 517a5e1f70
2036 files changed
+110041 -26162

No files matched your search

+1 -12
View File
@@ -95,18 +95,7 @@ class _Address extends FormAbstract
return NULL;
}
/**
* Calculate default country
*
* @return string|NULL
* @deprecated
*/
protected function _calculateDefaultCountry()
{
return static::calculateDefaultCountry();
}
/**
* Get Value
*
+16
View File
@@ -27,6 +27,22 @@ class _Captcha extends FormAbstract
*/
protected $captcha = NULL;
/**
* Does the configured CAPTCHA service support being added in a modal?
*
* @return bool
*/
public static function supportsModal()
{
if ( \IPS\Settings::i()->bot_antispam_type === 'none' )
{
return TRUE;
}
$class = '\IPS\Helpers\Form\Captcha\\' . mb_ucfirst( \IPS\Settings::i()->bot_antispam_type );
return ( !isset( $class::$supportsModal ) or $class::$supportsModal ); // isset() check is for backwards compatibility
}
/**
* Constructor
*
@@ -22,6 +22,11 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class _Invisible
{
/**
* Does this CAPTCHA service support being added in a modal?
*/
public static $supportsModal = TRUE;
/**
* @brief Error
*/
@@ -22,6 +22,11 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class _Keycaptcha
{
/**
* Does this CAPTCHA service support being added in a modal?
*/
public static $supportsModal = FALSE;
/**
* @brief Error
*/
@@ -22,6 +22,11 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class _Recaptcha2
{
/**
* Does this CAPTCHA service support being added in a modal?
*/
public static $supportsModal = TRUE;
/**
* @brief Error
*/
+7 -1
View File
@@ -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 );
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'] );
}
/**
@@ -113,6 +113,12 @@ class _CheckboxSet extends Select
/* We need the array keys. For custom fields we will have 0 => 1, 1 => 1 if checkboxes 0 and 1 are checked, while for Content we will have
customKey => 1, customOtherKey => 1 - we always need the array keys */
$value = \is_array( $value ) ? array_keys( $value ) : array();
if ( $this->options['unlimited'] !== NULL and $this->options['impliedUnlimited'] and \count( $value ) == \count( $this->options['options'] ) )
{
return $this->options['unlimited'];
}
if ( $this->options['returnLabels'] )
{
if ( \is_array( $value ) )
+55 -6
View File
@@ -34,6 +34,7 @@ class _Editor extends FormAbstract
2,
'foo'
),
'attachIdsLang' => NULL, // Language ID number if this Editor is part of a Translatable field.
'minimize' => 'clickme', // Language string to use for minimized view. NULL will mean editor is not minimized.
'minimizeIcon' => 'flag-us', // Icon to use for minimized view
'allButtons' => FALSE, // Only used for the customisation ACP page. Do not use.
@@ -53,6 +54,7 @@ class _Editor extends FormAbstract
'key' => NULL,
'autoSaveKey' => NULL,
'attachIds' => NULL,
'attachIdsLang' => NULL,
'minimize' => NULL,
'minimizeIcon' => 'fa fa-comment-o',
'allButtons' => FALSE,
@@ -400,7 +402,12 @@ class _Editor extends FormAbstract
}
else
{
$fileAttachments[] = \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedFile( \IPS\Http\Url::baseUrl() . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'], $attachment['attach_file'], FALSE, $attachment['attach_ext'], $attachment['attach_id'] );
$url = \IPS\Http\Url::baseUrl() . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'];
if ( $attachment['attach_security_key'] )
{
$url .= "&key={$attachment['attach_security_key']}";
}
$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 ) )
@@ -411,7 +418,8 @@ class _Editor extends FormAbstract
'id1' => ( \is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][0] ) ) ? $this->options['attachIds'][0] : NULL,
'id2' => ( \is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][1] ) ) ? $this->options['attachIds'][1] : NULL,
'id3' => ( \is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][2] ) ) ? $this->options['attachIds'][2] : NULL,
'temp' => \is_string( $this->options['attachIds'] ) ? $this->options['attachIds'] : ( $this->options['attachIds'] === NULL ? md5( $this->options['autoSaveKey'] ) : $this->options['attachIds'] )
'temp' => \is_string( $this->options['attachIds'] ) ? $this->options['attachIds'] : ( $this->options['attachIds'] === NULL ? md5( $this->options['autoSaveKey'] ) : $this->options['attachIds'] ),
'lang' => $this->options['attachIdsLang'],
);
}
}
@@ -488,7 +496,7 @@ class _Editor extends FormAbstract
*/
protected function _getParser()
{
return new \IPS\Text\Parser( TRUE, ( $this->options['attachIds'] === NULL ? md5( $this->options['autoSaveKey'] ) : $this->options['attachIds'] ), NULL, "{$this->options['app']}_{$this->options['key']}", !$this->bypassFilterProfanity(), !$this->canUseHtml(), method_exists( $this->extension, 'htmlPurifierConfig' ) ? array( $this->extension, 'htmlPurifierConfig' ) : NULL );
return new \IPS\Text\Parser( TRUE, ( $this->options['attachIds'] === NULL ? md5( $this->options['autoSaveKey'] ) : $this->options['attachIds'] ), NULL, "{$this->options['app']}_{$this->options['key']}", !$this->bypassFilterProfanity(), !$this->canUseHtml(), method_exists( $this->extension, 'htmlPurifierConfig' ) ? array( $this->extension, 'htmlPurifierConfig' ) : NULL, TRUE, $this->options['attachIdsLang'] );
}
/**
@@ -589,6 +597,7 @@ class _Editor extends FormAbstract
/* Reset the original filename based on the attachment record as filenames can be renamed if they contain special characters as AWS being the lowest common denominator cannot
handle special characters */
$file->originalFilename = $attachment['attach_file'];
$file->securityKey = $attachment['attach_security_key'];
$existing[ $attachment['attach_id'] ] = $file;
@@ -690,6 +699,10 @@ class _Editor extends FormAbstract
$where[] = array( "id{$i}=?", $id );
$i++;
}
if ( $this->options['attachIdsLang'] )
{
$where[] = array( "lang=?", $this->options['attachIdsLang'] );
}
\IPS\Db::i()->delete( 'core_attachments_map', $where );
}
@@ -759,8 +772,44 @@ class _Editor extends FormAbstract
{
$where[] = array( "id{$i}=?", $id );
$i++;
}
/* If we only want ones for particular languages, filter them out. We don't do this in the WHERE clause for backwards compatibility */
if ( $this->options['attachIdsLang'] )
{
$setToAllLangs = [];
$_existingAttachments = [];
foreach ( \IPS\Db::i()->select( '*', 'core_attachments_map', $where ) as $existingAttachment )
{
if ( $existingAttachment['lang'] === NULL )
{
$existingAttachment['lang'] = $this->options['attachIdsLang'];
if ( !\in_array( $existingAttachment['attachment_id'], $setToAllLangs ) )
{
\IPS\Db::i()->delete( 'core_attachments_map', [ 'attachment_id=? AND location_key=? AND id1=? AND id2=? AND id3=? and lang IS NULL', $existingAttachment['attachment_id'], $existingAttachment['location_key'], $existingAttachment['id1'], $existingAttachment['id2'], $existingAttachment['id3'] ] );
foreach ( \IPS\Lang::languages() as $lang )
{
$newRow = $existingAttachment;
$newRow['lang'] = $lang->id;
\IPS\Db::i()->insert( 'core_attachments_map', $newRow );
}
$setToAllLangs[] = $existingAttachment['attachment_id'];
}
}
if ( $existingAttachment['lang'] == $this->options['attachIdsLang'] )
{
$_existingAttachments[] = $existingAttachment['attachment_id'];
}
}
}
$_existingAttachments = iterator_to_array( \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', $where ) );
else
{
$_existingAttachments = iterator_to_array( \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', $where ) );
}
if ( !empty( $_existingAttachments ) )
{
$existingAttachments = \IPS\Db::i()->in( 'attach_id', $_existingAttachments ) . ' OR ';
@@ -836,11 +885,11 @@ class _Editor extends FormAbstract
}
elseif ( \IPS\Settings::i()->attach_allowed_types == 'media' )
{
return array_merge( \IPS\Image::$imageExtensions, \IPS\File::$videoExtensions );
return array_merge( \IPS\Image::supportedExtensions(), \IPS\File::$videoExtensions );
}
elseif ( \IPS\Settings::i()->attach_allowed_types == 'images' )
{
return \IPS\Image::$imageExtensions;
return \IPS\Image::supportedExtensions();
}
else
{
+1 -8
View File
@@ -506,19 +506,12 @@ class _Form
$errorTabs = array();
foreach ( $this->elements as $tab => $elements )
{
$html[ $tab ] = '';
foreach ( $elements as $k => $element )
{
if ( $element instanceof Form\Matrix )
{
$html[ $tab ] .= \IPS\Theme::i()->getTemplate( 'forms', 'core' )->emptyRow( $element->nested(), $k );
continue;
}
if ( !\is_string( $element ) and $element->error )
if ( !( $element instanceof Form\Matrix ) and !\is_string( $element ) and $element->error )
{
$errorTabs[] = $tab;
}
$html[ $tab ] .= ( $element instanceof \IPS\Helpers\Form\FormAbstract ) ? $element->rowHtml( $this ) : (string) $element;
}
}
+14 -1
View File
@@ -31,7 +31,7 @@ class _Item extends FormAbstract
'class' => '\IPS\core\Foo', // The \Content\Item class
'permissionCheck' => 'read', // If a permission key is provided, only content items that the member has that permission for will be available.
'maxItems' => NULL, // Maximum items allowed to select, or NULL for unlimited
'orderResults' => NULL|array // NULL or array( field, 'asc' ) where field is a mappable content item field (date, title, etc)
'orderResults' => NULL|FALSE|array // NULL for default date ordering, FALSE for no ordering, or array( field, 'asc' ) where field is a mappable content item field (date, title, etc)
'itemTemplate' => NULL // NULL or array of template group and template name array( \IPS\Theme::i()->getTemplate( 'group', 'app', 'location'), 'name' )
'containerIds' => array // Array of container IDs to limit the search
'minAjaxLength' => int // Number of characters to type before selector appears
@@ -163,6 +163,19 @@ class _Item extends FormAbstract
{
$items[ $item->$idField ] = $item;
}
/* If we didn't want to order results, reset the order of results as appropriate */
if( $this->options['orderResults'] === FALSE )
{
$_sortedItems = array();
foreach( $this->value as $id )
{
$_sortedItems[ $id ] = $items[ $id ];
}
$items = $_sortedItems;
}
}
$this->value = $items;
+1 -1
View File
@@ -393,7 +393,7 @@ class _Matrix extends \IPS\Helpers\Form
}
/* If the "check all" box was checked, set it to TRUE */
if ( $element instanceof Checkbox and isset( \IPS\Request::i()->__all[ $columnName ] ) )
if ( $element instanceof Checkbox and isset( \IPS\Request::i()->__all[ $columnName ] ) and ! $element->options['disabled'] )
{
$element->value = TRUE;
}
+1 -1
View File
@@ -403,7 +403,7 @@ class _Node extends FormAbstract
* @param bool $noJS No-javascript fallback
* @return void
*/
protected function _populate( &$nodes, &$disabled, $depth, $node, &$children, $depthLimit = NULL, $parent = NULL, $noJS = FALSE, $totalLimit = NULL, &$currentCount )
protected function _populate( &$nodes, &$disabled, $depth, $node, &$children, $depthLimit = NULL, $parent = NULL, $noJS = FALSE, $totalLimit = NULL, &$currentCount = 0 )
{
$showable = $this->_getShowable();
+3 -3
View File
@@ -47,7 +47,7 @@ class _Password extends Text
'strengthMember' => NULL,
'strengthRequest' => array(),
'htmlAutocomplete' => "current-password",
'enforceMaxLimit' => TRUE,
);
/**
@@ -87,7 +87,7 @@ class _Password extends Text
throw new \InvalidArgumentException( 'err_password_length' );
}
if ( mb_strlen( $this->value ) > 72 )
if ( $this->options['enforceMaxLimit'] and mb_strlen( $this->value ) > 72 )
{
throw new \InvalidArgumentException( 'err_password_toolong' );
}
@@ -149,7 +149,7 @@ class _Password extends Text
if ( !$valid )
{
throw new \InvalidArgumentException( 'login_err_bad_password' );
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'login_err_bad_password', FALSE, array( 'pluralize' => array( $method->authType() ) ) ) );
}
}
+5
View File
@@ -170,6 +170,11 @@ EOF;
/* Do all the questions have at least 2 options? */
foreach ( $this->value->choices as $question )
{
/* Do we have a question title? */
if ( empty( trim( $question['question'] ) ) )
{
throw new \InvalidArgumentException('form_poll_no_question_title');
}
if ( \count( $question['choice'] ) < 2 )
{
throw new \InvalidArgumentException('form_poll_too_few_answers');
+2
View File
@@ -41,6 +41,7 @@ class _Select extends FormAbstract
'noDefault' => TRUE // For radios, you can set this to TRUE if you do not want any radio option selected by default (useful for things like polls)
'returnLabels' => FALSE // If TRUE, will return the labels rather than the keys as the value
'sort' => FALSE, // If TRUE, options will be sorted by JavaScript. Useful where the options are language-dependant but you still want them to be in alphabetical order
'impliedUnlimited' => FALSE, // For checkbox sets, if 'unlimited' is set and this is FALSE a fancy toggler interface is presented. If TRUE, a checkbox set will be presented and the unlimited value will be implied if all options are checked.
);
* @endcode
*/
@@ -60,6 +61,7 @@ class _Select extends FormAbstract
'noDefault' => FALSE,
'returnLabels' => FALSE,
'sort' => FALSE,
'impliedUnlimited' => FALSE,
);
/**
+4
View File
@@ -504,6 +504,10 @@ class _Text extends TextArea
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( $section ) ) ) );
}
else
{
$newVal .= $section;
}
}
}
else
+4 -3
View File
@@ -73,7 +73,7 @@ class _Translatable extends FormAbstract
{
foreach( \IPS\Db::i()->select( '*', 'core_sys_lang_words', array( 'word_key=?', $this->options['key'] ) )->setKeyField('lang_id') as $k => $v )
{
$v = $v['word_custom'] ?: $v['word_default'];
$v = $v['word_custom'];
if ( $v or !isset( $values[ $k ] ) )
{
if ( \is_array( $this->options['sprintf'] ) )
@@ -118,7 +118,8 @@ class _Translatable extends FormAbstract
foreach ( \IPS\Lang::getEnabledLanguages() as $lang )
{
$options = $this->options['editor'];
$options['autoSaveKey'] .= $lang->id;
$options['autoSaveKey'] .= $lang->id;
$options['attachIdsLang'] = $lang->id;
$this->editors[ $lang->id ] = new Editor( "{$this->name}[{$lang->id}]", isset( $this->value[ $lang->id ] ) ? $this->value[ $lang->id ] : NULL, $this->required, $options );
}
}
@@ -153,7 +154,7 @@ class _Translatable extends FormAbstract
$return = array();
foreach ( $this->_getEditors() as $languageId => $editor )
{
$return[ $languageId ] = $editor->getValue();
$return[ $languageId ] = $editor->value;
}
return $return;
}
+17 -20
View File
@@ -167,7 +167,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::$imageExtensions;
$options['allowedFileTypes'] = \IPS\Image::supportedExtensions();
}
/* Call parent constructor */
@@ -213,9 +213,6 @@ class _Upload extends FormAbstract
/* Chunked */
if ( $storageClass::$supportsChunking and method_exists( $storageClass, 'chunkInit' ) and isset( \IPS\Request::i()->chunks ) and \IPS\Request::i()->chunks > 1 )
{
/* We need to be able to set data into sessions */
\IPS\Request::i()->setCookie( 'noCache', 1 );
/* If this is the FIRST chunk, start the process */
if ( \IPS\Request::i()->chunk == 0 )
{
@@ -259,16 +256,12 @@ class _Upload extends FormAbstract
$image->setExifData( $fileObj->contents() );
}
$hasBeenResized = FALSE;
if( \is_array( $this->options['image'] ) )
{
$hasBeenResized = $image->resizeToMax( $this->options['image']['maxWidth'] ?: NULL, $this->options['image']['maxHeight'] ?: NULL );
$image->resizeToMax( $this->options['image']['maxWidth'] ?: NULL, $this->options['image']['maxHeight'] ?: NULL );
}
if ( $hasBeenResized or $image->hasBeenRotated )
{
$fileObj->replace( (string) $image );
}
$fileObj->replace( (string) $image );
}
catch ( \Exception $e ) { }
}
@@ -298,6 +291,7 @@ class _Upload extends FormAbstract
'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,
'securityKey' => $fileObj->securityKey
) );
}
else
@@ -316,6 +310,7 @@ class _Upload extends FormAbstract
'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,
'securityKey' => $fileObj->securityKey
) );
}
}
@@ -446,6 +441,7 @@ 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 ){}
@@ -705,7 +701,7 @@ class _Upload extends FormAbstract
$ext = mb_strtolower( mb_substr( $file['name'], ( mb_strrpos( $file['name'], '.' ) + 1 ) ) );
/* Don't allow "XSS" in images */
if( \in_array( $ext, \IPS\File::$safeFileExtensions ) AND \in_array( $ext, \IPS\Image::$imageExtensions ) )
if( \in_array( $ext, \IPS\File::$safeFileExtensions ) AND \in_array( $ext, \IPS\Image::supportedExtensions() ) )
{
if( \IPS\File::checkXssInFile( file_get_contents( $file['tmp_name'] ) ) )
{
@@ -727,26 +723,27 @@ class _Upload extends FormAbstract
$ext = mb_strtolower( mb_substr( $filename, mb_strrpos( $filename, '.' ) + 1 ) );
/* Resize images */
if ( \in_array( $ext, \IPS\Image::$imageExtensions ) AND ( \is_array( $options['image'] ) OR $options['checkImage'] ) )
if ( \in_array( $ext, \IPS\Image::supportedExtensions() ) AND ( \is_array( $options['image'] ) OR $options['checkImage'] ) )
{
try
{
$image = \IPS\Image::create( $contents );
$hasBeenResized = FALSE;
if( $image::exifSupported() )
{
$image->setExifData( $contents );
}
if( \is_array( $options['image'] ) )
{
$image->resizeToMax( $options['image']['maxWidth'] ?: NULL, $options['image']['maxHeight'] ?: NULL );
}
if ( $hasBeenResized or $image->hasBeenRotated )
{
return (string) $image;
}
return (string) $image;
}
catch ( \Exception $e ) {}
}
return $contents;
}, $this->options['storageContainer'], $this->options['obscure'] );