Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @subpackage Content * @since 18 Jan 2024 */ namespace IPS\core\modules\admin\customization; /* To prevent PHP errors (extending class does not exist) revealing path */ use Exception; use IPS\Db; use IPS\Dispatcher; use IPS\File; use IPS\Helpers\Form; use IPS\Helpers\Form\Upload; use IPS\Http\Url; use IPS\Image; use IPS\Member; use IPS\Dispatcher\Controller; use IPS\Output; use IPS\Output\Plugin\Filesize; use IPS\Request; use IPS\Session; use IPS\Theme; use IPS\Widget; use OutOfRangeException; use UnderflowException; use function array_merge; use function count; use function defined; use function in_array; use function intval; use function is_array; use function json_encode; use function ltrim; use function mt_rand; use function time; if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * media */ class media extends Controller { /** * @brief Has been CSRF-protected */ public static bool $csrfProtected = TRUE; /** * Execute * * @return void */ public function execute() : void { Dispatcher::i()->checkAcpPermission( 'theme_sets_manage' ); parent::execute(); } /** * Delete many at once * * @return void */ public function deleteByFileIds() : void { Session::i()->csrfCheck(); $setId = intval( Request::i()->set_id ); try { $set = Theme::load( $setId ); } catch( OutOfRangeException $ex ) { Output::i()->error( 'node_error', '3T334/G', 403, '' ); } if ( isset( Request::i()->fileIds ) ) { $ids = Request::i()->fileIds; if ( ! is_array( $ids ) ) { $try = json_decode( $ids, TRUE ); if ( ! is_array( $try ) ) { $ids = array( $ids ); } else { $ids = $try; } } if ( count( $ids ) ) { foreach( $ids as $id ) { try { $current = Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=? and resource_id=?', $set->_id, $id ) )->first(); } catch( UnderflowException $ex ) { /* Just ignore it ssssh */ } /* Don't delete master resources */ if ( $current['resource_set_id'] > 0 ) { try { File::get( 'core_Theme', $current['resource_filename'] )->delete(); } catch( Exception $ex ) { } Db::i()->delete( 'core_theme_resources', array( 'resource_id=?', $id ) ); } Session::i()->log( 'acplog__theme_resource_deleted', array( $current['resource_filename'] => FALSE ) ); } /* Delete widget caches */ Widget::deleteCaches(); $set->buildResourceMap(); } } } /** * Show the pages tree * * @return void */ protected function manage() : void { $setId = intval( Request::i()->set_id ); try { $set = Theme::load( $setId ); } catch( OutOfRangeException $ex ) { Output::i()->error( 'node_error', '3T334/4', 403, '' ); } /* Display */ Output::i()->title = Member::loggedIn()->language()->addToStack('theme_custom_resources_page_title', FALSE, array( 'sprintf' => array( $set->_title ) ) ); Output::i()->breadcrumb[] = array( Url::internal( "app=core&module=customization&controller=themes" ), 'menu__core_customization_themes' ); Output::i()->breadcrumb[] = array( NULL, Output::i()->title ); /* Get the resources */ $media = []; foreach( Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=? and resource_user_edited=1', $setId ) ) as $row ) { $media[ $row['resource_id'] ] = media::getItemRow( array_merge( $row, [ 'url' => $set->resource( ltrim( $row['resource_path'], '/' ) . $row['resource_name'], $row['resource_app'], $row['resource_location'] ) ] ) ); } Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_customization.js', 'core' ) ); Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'customization/media.css', 'core', 'admin' ) ); if( Request::i()->isAjax() ) { Output::i()->sendOutput( json_encode( $media ), 200, 'application/json' ); } else { Output::i()->output = Theme::i()->getTemplate( 'customization', 'core', 'admin' )->media( $media, $set ); } } /** * Replace an existing file * * @return void */ public function replace() : void { if( !isset( Request::i()->id ) OR !Request::i()->id ) { Output::i()->error( 'missing_media_file', '3T334/2', 404, '' ); } $setId = intval( Request::i()->set_id ); try { $set = Theme::load( $setId ); } catch( OutOfRangeException $ex ) { Output::i()->error( 'node_error', '3T334/C', 403, '' ); } try { $row = Db::i()->select( '*', 'core_theme_resources', array( 'resource_id=? and resource_set_id=? and resource_user_edited=1', Request::i()->id, $setId ) )->first(); } catch( Exception $ex ) { Output::i()->error( 'missing_media_file', '3T334/3', 404, '' ); } $form = new Form( 'form', 'upload' ); $form->class = 'ipsForm--vertical ipsForm--replace-file ipsForm_noLabels'; $form->add( new Upload( 'media_filename', null, true, array( 'obscure' => false, 'multiple' => false, 'maxFileSize' => 1.2, 'storageExtension' => 'core_Theme', 'storageContainer' => 'set_resources_' . $set->_id ), NULL, NULL, NULL, 'core_theme_resource_filename' ) ); if ( $values = $form->values() ) { $save = $row; unset( $save['resource_id'] ); $save['resource_data'] = $values['media_filename']->contents(); $save['resource_added'] = time(); $save['resource_filename'] = (string) $values['media_filename']; $existingFileExtension = mb_substr( $row['resource_name'], mb_strrpos( $row['resource_name'], '.' ) + 1 ); $newFileExtension = mb_substr( $values['media_filename']->originalFilename, mb_strrpos( $values['media_filename']->originalFilename, '.' ) + 1 ); /* File extention changed? Change the name */ if( $existingFileExtension !== $newFileExtension ) { $save['resource_name'] = $values['media_filename']->originalFilename; } Session::i()->log( 'acplog__theme_resource_edited', array( $save['resource_filename'] => FALSE ) ); Db::i()->update( 'core_theme_resources', $save, array( 'resource_id=?', $row['resource_id'] ) ); /* Do any clean up */ $this->postUpload( $set ); if ( Request::i()->isAjax() ) { $media = []; foreach( Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=? and resource_user_edited=1', $set->id ) ) as $row ) { $media[ $row['resource_id'] ] = media::getItemRow( array_merge( $row, [ 'url' => $set->resource( ltrim( $row['resource_path'], '/' ) . $row['resource_name'], $row['resource_app'], $row['resource_location'] ) ] ) ); } Output::i()->sendOutput( json_encode( array( 'fileID' => $row['resource_id'], 'rows' => $media ) ), 200, 'application/json' ); } else { Output::i()->redirect( Url::internal( 'app=core&module=customization&controller=media&set_id=' . $set->id ) ); } } Output::i()->output = Theme::i()->getTemplate( 'global', 'core', 'admin' )->block( 'upload', $form, FALSE ); } /** * Upload items * * @return void */ public function upload() : void { $setId = intval( Request::i()->set_id ); try { $set = Theme::load( $setId ); } catch( OutOfRangeException $ex ) { Output::i()->error( 'node_error', '3T334/A', 403, '' ); } $form = new Form( 'form', 'upload' ); $form->class = 'ipsForm--vertical ipsForm--upload-items ipsForm_noLabels'; $form->add( new Upload( 'media_filename', null, true, array( 'obscure' => false, 'multiple' => true, 'maxFileSize' => 1.2, 'storageExtension' => 'core_Theme', 'storageContainer' => 'set_resources_' . $set->_id ), NULL, NULL, NULL, 'core_theme_resource_filename' ) ); if ( $values = $form->values() ) { $count = 0; foreach( $values['media_filename'] as $media ) { $save = array( 'resource_app' => 'core', 'resource_location' => 'front', 'resource_path' => '/custom/', 'resource_set_id' => $set->_id, 'resource_user_edited' => 1, 'resource_name' => $media->originalFilename, 'resource_added' => time(), 'resource_filename' => (string) $media, 'resource_data' => $media->contents() ); try { Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=? and resource_path=? and resource_location=? and resource_app=? and resource_name=?', $set->_id, $save['resource_path'], $save['resource_location'], $save['resource_app'], $save['resource_name'] ) )->first(); $ext = mb_substr( $save['resource_name'], ( mb_strrpos( $save['resource_name'], '.' ) + 1 ) ); $save['resource_name'] = mb_substr( $save['resource_name'], 0, ( mb_strrpos( $save['resource_name'], '.' ) ) ) . '_' . mt_rand() . '.' . $ext; } catch( UnderflowException $ex ) { } Session::i()->log( 'acplog__theme_resource_added', array( $save['resource_filename'] => FALSE ) ); Db::i()->insert( 'core_theme_resources', $save ); $count++; } /* Do any clean up */ $this->postUpload( $set ); if ( Request::i()->isAjax() ) { $media = []; foreach( Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=? and resource_user_edited=1', $set->id ) ) as $row ) { $media[ $row['resource_id'] ] = media::getItemRow( array_merge( $row, [ 'url' => $set->resource( ltrim( $row['resource_path'], '/' ) . $row['resource_name'], $row['resource_app'], $row['resource_location'] ) ] ) ); } Output::i()->sendOutput( json_encode( array( 'count' => $count, 'rows' => $media ) ), 200, 'application/json' ); } else { Output::i()->redirect( Url::internal( 'app=core&module=customization&controller=media&set_id=' . $set->id ) ); } } Output::i()->output = Theme::i()->getTemplate( 'global', 'core', 'admin' )->block( 'upload', $form, FALSE ); } /** * Do stuff after an upload / replace * * @param Theme $set * @return void */ protected function postUpload( Theme $set ) : void { $set->buildResourceMap(); /* Delete widget caches */ Widget::deleteCaches(); /* Resource may be used in CSS */ File::getClass('core_Theme')->deleteContainer( 'css_built_' . $set->id ); $set->css_map = array(); $set->css_updated = time(); $set->save(); foreach( $set->children() as $child ) { File::getClass('core_Theme')->deleteContainer( 'css_built_' . $child->id ); $child->css_map = array(); $child->css_updated = time(); $child->save(); } } /** * Tree Search * * @return void */ protected function search() : void { $setId = intval( Request::i()->set_id ); try { $set = Theme::load( $setId ); } catch( OutOfRangeException $ex ) { Output::i()->error( 'node_error', '3T334/E', 403, '' ); } $url = Url::internal( "app=core&module=customization&controller=media&set_id=" . $setId ); $media = array(); /* Get results */ foreach( Db::i()->select( '*', 'core_theme_resources', array( "resource_set_id=? and resource_user_edited=1 and resource_name LIKE CONCAT( '%', ?, '%' )", $setId, Request::i()->input ) ) as $row ) { $media[] = media::getItemRow( array_merge( $row, [ 'url' => $set->resource( ltrim( $row['resource_path'], '/' ) . $row['resource_name'], $row['resource_app'], $row['resource_location'] ) ] ) ); } if( Request::i()->isAjax() ) { Output::i()->sendOutput( json_encode( $media ), 200, 'application/json' ); } else { Output::i()->output = Theme::i()->getTemplate( 'trees', 'core' )->rows( $media, $url ); } } /** * Returns a JSON object with some file information * * @return void */ protected function getFileInfo() : void { $setId = intval( Request::i()->set_id ); try { $set = Theme::load( $setId ); } catch( OutOfRangeException $ex ) { Output::i()->error( 'node_error', '3T334/B', 403, '' ); } try { $row = Db::i()->select( '*', 'core_theme_resources', array( 'resource_id=? and resource_set_id=? and resource_user_edited=1', Request::i()->id, $setId ) )->first(); $fileObject = File::get( 'core_Theme', $row['resource_filename'] ); } catch( OutOfRangeException $ex ) { return; } /* Make a human-readable size */ $filesize = Filesize::humanReadableFilesize( $fileObject->filesize() ); Member::loggedIn()->language()->parseOutputForDisplay( $filesize ); $output = array( 'fileSize' => $filesize, 'dimensions' => NULL ); /* If this is an image we'll also show the dimensions */ if( $fileObject->isImage() ) { $dimensions = $fileObject->getImageDimensions(); $output['dimensions'] = $dimensions[0] . ' x ' . $dimensions[1]; } Output::i()->sendOutput( json_encode( $output ), 200, 'application/json' ); } /** * Return HTML for a page row * * @param array $item Row data * @return string HTML */ public static function getItemRow( array $item ): string { $item['file_type'] = 'other'; $ext = mb_strtolower( mb_substr( $item['resource_name'], mb_strrpos( $item['resource_name'], '.' ) + 1 ) ); if ( in_array( $ext, array_merge( [ 'svg' ], Image::supportedExtensions() ) ) ) { $item['file_type'] = 'image'; } else if ( in_array( $ext, [ 'txt', 'htm', 'html', 'css', 'js' ] ) ) { $item['file_type'] = 'text'; } else if ( in_array( $ext, [ 'woff', 'woff2', 'ttf', 'eot' ] ) ) { $item['file_type'] = 'font'; } return Theme::i()->getTemplate( 'customization', 'core', 'admin' )->mediaFileListing( $item ); } }