Files
2025-12-19 16:27:35 -08:00

484 lines
13 KiB
PHP

<?php
/**
* @brief Theme Media Management
* @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
* @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 );
}
}