Version 4.1.19

This commit is contained in:
Neo committed 2025-12-19 05:38:56 -08:00
1 parent 28bd025b35
commit 2bd5025018
2674 files changed
+233518 -77766

No files matched your search

+171 -49
View File
@@ -2,9 +2,9 @@
/**
* @brief Editor class for Form Builder
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 5 Apr 2013
* @version SVN_VERSION_NUMBER
*/
@@ -30,29 +30,39 @@ class _Editor extends FormAbstract
'app' => 'core', // The application that owns this type of editor (as defined by an extension)
'key' => 'Example', // The key for this type of editor (as defined by an extension)
'autoSaveKey' => 'abc', // Pass a string which identifies this editor's purpose. For example, if the editor is for replying to a topic with ID 5, you could use "topic-reply-5". Make sure you pass the same key every time, but a different key for different editors.
'attachIds' => array( // ID numbers to idenfity content for attachments if the content has been saved - the first two must be int or null, the third must be string or null. If content has not been saved yet, you must claim attachments after saving.
'attachIds' => array( // ID numbers to identify content for attachments if the content has been saved - the first two must be int or null, the third must be string or null. If content has not been saved yet, you must claim attachments after saving.
1,
2,
'foo'
),
'minimize' => 'clickme', // Language string to use for mimimized view. NULL will mean editor is not minimized.
'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.
'tags' => array(), // An array of extra insertable tags in key => value pair with key being what is inserted and value serving as a description
'autoGrow' => FALSE, // Used to specify if editor should grow in size as content is added. Defaults to TRUE.
'controller' => NULL, // Used to specify the editor controller. Defaults to NULL, which will use app=core&module=system&controller=editor
'defaultIfNoAutoSave'=> FALSE, // If TRUE, the default value will not override any autosaved content
'minimizeWithContent'=> FALSE, // If TRUE, the editor will be minimized even if there is default content
'maxLength' => FALSE, // The maximum length. Note that the content is HTML, so this isn't the maximum number of visible characters, so should only be used for database limits. The database's max allowed packet will override if smaller
'editorId' => NULL, // Passed to editorAttachments. Only necessary if the name may be changed
);
* @endcode
*/
protected $defaultOptions = array(
'app' => NULL,
'key' => NULL,
'autoSaveKey' => NULL,
'attachIds' => NULL,
'minimize' => NULL,
'minimizeIcon' => 'fa fa-comment-o',
'allButtons' => FALSE,
'tags' => array(),
'autoGrow' => TRUE,
'app' => NULL,
'key' => NULL,
'autoSaveKey' => NULL,
'attachIds' => NULL,
'minimize' => NULL,
'minimizeIcon' => 'fa fa-comment-o',
'allButtons' => FALSE,
'tags' => array(),
'autoGrow' => TRUE,
'controller' => NULL,
'defaultIfNoAutoSave' => FALSE,
'minimizeWithContent' => FALSE,
'maxLength' => 16777215, // The default for MEDIUMTEXT */
'editorId' => NULL,
);
/**
@@ -75,6 +85,7 @@ class _Editor extends FormAbstract
* Sets that the field is required if there is a minimum length and vice-versa
*
* @see \IPS\Helpers\Form\Abstract::__construct
* @note $name and $htmlId must be different
* @return void
* @throws \OutOfBoundsException
*/
@@ -82,6 +93,8 @@ class _Editor extends FormAbstract
{
$args = func_get_args();
$this->postKey = md5( $args[3]['autoSaveKey'] . ':' . session_id() );
$this->defaultOptions['controller'] = 'app=core&module=system&controller=editor';
/* Get our extension */
if ( !isset( $args[3]['allButtons'] ) or !$args[3]['allButtons'] )
@@ -95,8 +108,9 @@ class _Editor extends FormAbstract
$this->extension = $extensions[ $args[3]['key'] ];
}
/* Don't mimize if we have a value */
if ( $args[1] or \IPS\Request::i()->$args[0] or ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] ) )
/* Don't minimize if we have a value */
$name = isset( $args[3]['editorId'] ) ? $args[3]['editorId'] : $args[0];
if ( ( !isset( $args[3]['minimizeWithContent'] ) or !$args[3]['minimizeWithContent'] ) and ( $args[1] or \IPS\Request::i()->$name or ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] ) ) )
{
$args[3]['minimize'] = NULL;
}
@@ -105,20 +119,20 @@ class _Editor extends FormAbstract
$this->options = array_merge( $this->defaultOptions, $args[3] );
if ( $this->canAttach() )
{
if ( isset( \IPS\Request::i()->getUploader ) and \IPS\Request::i()->getUploader === $args[0] or ( isset( \IPS\Request::i()->postKey ) and \IPS\Request::i()->postKey === $this->postKey and isset( \IPS\Request::i()->deleteFile ) ) )
if ( isset( \IPS\Request::i()->getUploader ) and \IPS\Request::i()->getUploader === $name or ( isset( \IPS\Request::i()->postKey ) and \IPS\Request::i()->postKey === $this->postKey and isset( \IPS\Request::i()->deleteFile ) ) )
{
if ( $uploader = $this->getUploader( $args[0]) )
if ( $uploader = $this->getUploader( $name ) )
{
\IPS\Output::i()->sendOutput( $uploader->html() );
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->editorAttachmentsPlaceholder( $args[0], $this->postKey ) );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->editorAttachmentsPlaceholder( $name, $this->postKey ) );
}
}
elseif( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'post' or !$this->options['minimize'] )
{
$this->uploader = $this->getUploader( $args[0] );
$this->uploader = $this->getUploader( $name );
}
else
{
@@ -126,8 +140,32 @@ class _Editor extends FormAbstract
}
}
/* Work out the biggest value MySQL will allow */
if ( !isset( \IPS\Data\Store::i()->maxAllowedPacket ) )
{
$maxAllowedPacket = 0;
foreach ( \IPS\Db::i()->query("SHOW VARIABLES LIKE 'max_allowed_packet'") as $row )
{
$maxAllowedPacket = $row['Value'];
}
\IPS\Data\Store::i()->maxAllowedPacket = $maxAllowedPacket;
}
if ( \IPS\Data\Store::i()->maxAllowedPacket )
{
if ( !isset( $args[3]['maxLength'] ) or $args[3]['maxLength'] > \IPS\Data\Store::i()->maxAllowedPacket )
{
$args[3]['maxLength'] = \IPS\Data\Store::i()->maxAllowedPacket;
}
}
/* Go */
call_user_func_array( 'parent::__construct', $args );
/* Preview? */
if ( \IPS\Request::i()->isAjax() and isset( \IPS\Request::i()->_previewField ) and \IPS\Request::i()->_previewField === $this->name )
{
\IPS\Output::i()->sendOutput( $this->getValue() );
}
/* Include editor JS - but not if loaded via ajax. JS loader will handle that on demand */
if( !\IPS\Request::i()->isAjax() )
@@ -179,24 +217,36 @@ class _Editor extends FormAbstract
}
}
}
/* Can we use HTML? */
if ( $this->canUseHtml() === TRUE )
{
array_unshift( $allowed['desktop'][0]['items'], 'Source' );
array_unshift( $allowed['tablet'][0]['items'], 'Source' );
array_unshift( $allowed['phone'][0]['items'], 'Source' );
if ( !empty( $allowed['desktop'][0]['items'] ) )
{
array_unshift( $allowed[ 'desktop' ][ 0 ][ 'items' ], 'Source' );
}
if ( !empty( $allowed['tablet'][0]['items'] ) )
{
array_unshift( $allowed[ 'tablet' ][ 0 ][ 'items' ], 'Source' );
}
if ( !empty( $allowed['phone'][0]['items'] ) )
{
array_unshift( $allowed[ 'phone' ][ 0 ][ 'items' ], 'Source' );
}
}
}
/* Clean resources in ACP */
$value = $this->value;
\IPS\Output::i()->parseFileObjectUrls( $value );
if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation == 'admin' )
{
$value = \IPS\Text\Parser::mungeResources( $value );
}
/* 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 );
/* Show full uploader */
if ( $this->uploader )
@@ -283,13 +333,7 @@ class _Editor extends FormAbstract
if ( $value )
{
$parser = $this->_getParser();
$parsedValue = $parser->parse( $value );
if( count( $parser->errors ) )
{
$error = new \DomainException( implode( '; ', array_unique( $parser->errors ) ) );
}
$value = $parsedValue;
$value = $parser->parse( $value );
}
/* Add any attachments that weren't inserted in the content */
@@ -306,7 +350,7 @@ class _Editor extends FormAbstract
}
else
{
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedFile( \IPS\Settings::i()->base_url . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'], $attachment['attach_file'] );
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedFile( \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ) . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'], $attachment['attach_file'] );
}
if ( !isset( $parser ) or !in_array( $attachment['attach_id'], $parser->mappedAttachments ) )
@@ -322,7 +366,7 @@ class _Editor extends FormAbstract
}
}
}
if( count( $inserts ) )
if( count( $inserts ) and !isset( \IPS\Request::i()->_previewField ) )
{
\IPS\Db::i()->insert( 'core_attachments_map', $inserts, TRUE );
}
@@ -367,7 +411,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']}", TRUE, !$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 );
}
/**
@@ -469,7 +513,7 @@ class _Editor extends FormAbstract
'callback' => function( $file ) use ( $postKey )
{
\IPS\Db::i()->delete( 'core_files_temp', array( 'contents=?', (string) $file ) );
$attachment = $file->makeAttachment( $postKey );
$attachment = $file->makeAttachment( $postKey, \IPS\Member::loggedIn() );
return $attachment['attach_id'];
}
);
@@ -506,22 +550,60 @@ class _Editor extends FormAbstract
/* Handle delete calls */
if ( isset( \IPS\Request::i()->postKey ) and \IPS\Request::i()->postKey == $this->postKey and isset( \IPS\Request::i()->deleteFile ) )
{
/* Get the attachment */
try
{
$attachment = \IPS\Db::i()->select( '*', 'core_attachments', array( 'attach_id=?', \IPS\Request::i()->deleteFile ) )->first();
\IPS\Db::i()->delete( 'core_attachments_map', array( array( 'location_key=?', "{$this->options['app']}_{$this->options['key']}" ), array( 'attachment_id=?', $attachment['attach_id'] ) ) );
$otherMaps = \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments_map', array( 'attachment_id=?', $attachment['attach_id'] ) )->first();
if ( !$otherMaps )
}
catch ( \UnderflowException $e )
{
\IPS\Output::i()->json( 'NO_ATTACHMENT' );
}
/* Delete the maps - Only do this for attachments that have actually been saved (if they haven't been saved, there's nothing in core_attachments_map for us to delete */
if ( isset( $this->options['attachIds'] ) and is_array( $this->options['attachIds'] ) and count( $this->options['attachIds'] ) )
{
$where = array( array( 'location_key=?', "{$this->options['app']}_{$this->options['key']}" ), array( 'attachment_id=?', $attachment['attach_id'] ) );
$i = 1;
foreach ( $this->options['attachIds'] as $id )
{
$where[] = array( "id{$i}=?", $id );
$i++;
}
\IPS\Db::i()->delete( 'core_attachments_map', $where );
}
else
{
/* If the attachment hasn't been claimed yet, it should only be deletable by the person who uploaded it */
if( $attachment['attach_member_id'] != \IPS\Member::loggedIn()->member_id )
{
\IPS\Output::i()->json( 'NO_ATTACHMENT' );
}
}
/* If there's no other maps, we can delete the attachment itself */
$otherMaps = \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments_map', array( 'attachment_id=?', $attachment['attach_id'] ) )->first();
if ( !$otherMaps )
{
\IPS\Db::i()->delete( 'core_attachments', array( 'attach_id=?', $attachment['attach_id'] ) );
try
{
\IPS\Db::i()->delete( 'core_attachments', array( 'attach_id=?', $attachment['attach_id'] ) );
\IPS\File::get( 'core_Attachment', $attachment['attach_location'] )->delete();
if ( $attachment['attach_thumb_location'] )
}
catch ( \Exception $e ) { }
if ( $attachment['attach_thumb_location'] )
{
try
{
\IPS\File::get( 'core_Attachment', $attachment['attach_thumb_location'] )->delete();
}
catch ( \Exception $e ) { }
}
}
catch ( \Exception $e ) { }
/* Output */
\IPS\Output::i()->json( 'OK' );
}
}
@@ -546,12 +628,19 @@ class _Editor extends FormAbstract
$emoticons = iterator_to_array( \IPS\Db::i()->select( '*', 'core_emoticons', array( 'typed<>?', '' ) )->setKeyField('typed') );
foreach( $emoticons as $key => $emoticon )
{
$emoticons[ $key ] = array(
'image' => (string) \IPS\File::get( "core_Emoticons", $emoticon['image'] )->url,
'image_2x' => $emoticon['image_2x'] ? (string) \IPS\File::get( "core_Emoticons", $emoticon['image_2x'] )->url . ' 2x' : FALSE,
'height' => $emoticon['height'] ?: FALSE,
'width' => $emoticon['width'] ?: FALSE,
);
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;
@@ -605,7 +694,40 @@ class _Editor extends FormAbstract
*/
public static function ckeditorVersion()
{
preg_match( '/CKEditor\s+Included version: (.+?)\s/', file_get_contents( \IPS\ROOT_PATH . '/Credits.txt' ), $matches );
return $matches[1];
if ( file_exists( \IPS\ROOT_PATH . '/Credits.txt' ) )
{
preg_match( '#\s+Included version: (.+?)\s+?Website: http://ckeditor.com#', file_get_contents( \IPS\ROOT_PATH . '/Credits.txt' ), $matches );
return $matches[1];
}
else
{
return "[Could not determine the CKEditor version. Please restore Credits.txt to your suite's root directory.]";
}
}
/**
* Bypass the Profanity Check
*/
protected function bypassFilterProfanity()
{
return (bool) \IPS\Member::loggedIn()->group['g_bypass_badwords'];
}
/**
* Validate
*
* @throws \InvalidArgumentException
* @return TRUE
*/
public function validate()
{
parent::validate();
if ( $this->options['maxLength'] and \strlen( $this->value ) > $this->options['maxLength'] )
{
throw new \DomainException('form_maxlength_unspecific');
}
return TRUE;
}
}