Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 31 Jan 2024 */ namespace IPS\core\modules\admin\developer; use DirectoryIterator; use IPS\Application; use IPS\Data\Store; use IPS\Developer; use IPS\Developer\Controller; use IPS\Helpers\Form; use IPS\Helpers\Form\Text; use IPS\Helpers\Tree\Tree; use IPS\Http\Url; use IPS\Member; use IPS\Output; use IPS\Output\UI\UiExtension; use IPS\Request; use IPS\Session; use IPS\Theme; use OutOfRangeException; use function defined; use function sprintf; use const IPS\IPS_FOLDER_PERMISSION; use const IPS\ROOT_PATH; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * extensions */ class extensions extends Controller { /** * @var bool */ public static bool $csrfProtected = true; /** * ... * * @return void */ protected function manage() : void { /* Check for unsupported extensions */ $message = ''; if( $badClasses = UiExtension::unsupportedExtensions( $this->application ) ) { $message = Theme::i()->getTemplate( 'global', 'core' )->message( Member::loggedIn()->language()->addToStack( 'dev_unsupported_uinode', true, array( 'sprintf' => implode( ", ", $badClasses ) ) ), 'warning' ); } $this->url = Url::internal( "app=core&module=developer&controller=extensions&appKey={$this->application->directory}" ); $table = new Tree( $this->url, 'dev_extensions', array( $this, 'extGetRoots' ), array( $this, 'extGetRow' ), array( $this, 'extGetRowParentId' ), array( $this, 'extGetChildren' ), NULL, FALSE, TRUE, TRUE ); Output::i()->output = $message . $table; } /** * Extensions: Get Root Rows * * @return array */ public function extGetRoots() : array { $rows = array(); foreach ( Application::applications() as $app ) { $haveExtensions = FALSE; if( is_dir( ROOT_PATH . "/applications/{$app->directory}/data/defaults/extensions" ) ) { foreach ( new DirectoryIterator( ROOT_PATH . "/applications/{$app->directory}/data/defaults/extensions" ) as $file ) { if ( mb_substr( $file, 0, 1 ) !== '.' AND $file != 'index.html' ) { $haveExtensions = TRUE; break; } } } if ( $haveExtensions === TRUE ) { $rows[ $app->directory ] = Theme::i()->getTemplate( 'trees', 'core' )->row( $this->url, $app->directory, $app->directory, TRUE ); } } return $rows; } /** * Extensions: Get row * * @param string $row Row ID * @param bool $root Is root? * @return string */ public function extGetRow( string $row, bool $root ) : string { return Theme::i()->getTemplate( 'trees', 'core' )->row( $this->url, $row, $row, TRUE, array(), '', NULL, NULL, $root ); } /** * Extensions: Get Children * * @param string $folder Folder * @return array */ public function extGetChildren( string $folder ) : array { $rows = array(); if ( is_dir( ROOT_PATH . "/applications/{$this->application->directory}/extensions/{$folder}" ) ) { foreach ( new DirectoryIterator( ROOT_PATH . "/applications/{$this->application->directory}/extensions/{$folder}" ) as $file ) { if ( mb_substr( $file, 0, 1 ) !== '.' AND $file != 'index.html' ) { $buttons = array(); if ( $file->isDir() ) { if ( file_exists( ROOT_PATH . "/applications/{$folder}/data/defaults/extensions/{$file}.txt" ) ) { $title = sprintf( Member::loggedIn()->language()->get( 'dev_extensions_create' ), $file ); $buttons['add'] = array( 'icon' => 'plus-circle', 'title' => $title, 'link' => Url::internal( "app=core&module=developer&controller=extensions&appKey={$this->application->directory}&do=addExtension&type={$file}&extapp={$folder}" ), 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => $title, 'ipsDialog-forceReload' => true ) ); } } else { $name = mb_substr( $file, 0, -4 ); if( $ideLink = Developer::getIdeHref( $file->getPathname() ) ) { $buttons['ide'] = [ 'icon' => 'fa-file-code', 'title' => 'open_in_ide', 'link' => $ideLink ]; } $buttons['delete'] = array( 'icon' => 'times-circle', 'title' => Member::loggedIn()->language()->addToStack('delete'), 'link' => Url::internal( "app=core&module=developer&controller=extensions&appKey={$this->application->directory}&do=removeExtension&file={$name}&ext={$folder}" )->csrf(), 'data' => array( 'delete' => '' ) ); } $name = str_replace( '\\', '/', mb_substr( $file->getPathName(), mb_strlen( ROOT_PATH . "/applications/{$this->application->directory}/extensions/" ) ) ); $rows[ $name ] = Theme::i()->getTemplate( 'trees', 'core' )->row( $this->url, $name, mb_substr( $name, mb_strlen( $folder ) + 1 ), $file->isDir(), $buttons, $file->isDir() ? ( Member::loggedIn()->language()->addToStack( 'ext__' . mb_substr( $name, mb_strlen( $folder ) + 1 ) ) ) : '', NULL, NULL, FALSE, NULL, NULL, NULL, FALSE, TRUE ); } } } $extensionApp = explode( '/', $folder ); if( count( $extensionApp ) == 1 ) { foreach ( new DirectoryIterator( ROOT_PATH . "/applications/{$extensionApp[0]}/data/defaults/extensions" ) as $file ) { if ( mb_substr( $file, 0, 1 ) !== '.' AND $file != 'index.html' ) { $name = $folder . '/' .mb_substr( $file->getPathName(), mb_strlen( ROOT_PATH . "/applications/{$extensionApp[0]}/data/defaults/extensions/" ), -4 ); if ( !isset( $rows[ $name ] ) ) { $rows[ $name ] = Theme::i()->getTemplate( 'trees', 'core' )->row( $this->url, $name, mb_substr( $name, mb_strlen( $folder ) + 1 ), $file->isDir(), array( 'add' => array( 'icon' => 'plus-circle', 'title' => Member::loggedIn()->language()->addToStack( 'dev_extensions_create', FALSE, array( 'sprintf' => array( mb_substr( $file, 0, -4 ) ) ) ), 'link' => Url::internal( "app=core&module=developer&controller=extensions&appKey={$this->application->directory}&do=addExtension&type=" . mb_substr( $file, 0, -4 ) . "&extapp={$folder}" ), 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('dev_extensions_create', FALSE, array( 'sprintf' => array( mb_substr( $file, 0, -4 ) ) ) ) ) ) ), Member::loggedIn()->language()->addToStack( 'ext__' . mb_substr( $name, mb_strlen( $folder ) + 1 ) ), null, // icon null, // draggablePosition false, // root null, // toggleStatus null, // locked null, // badge true, // titleHtml true // descriptionHtml ); } } } } ksort( $rows ); return $rows; } /** * Extensions: Get parent ID * * @param string $folder Folder name * @return string|null */ public function extGetRowParentId( string $folder ) : ?string { $bits = explode( "/", $folder ); if( count( $bits ) > 1 ) { return $bits[0]; } return NULL; } /** * Add Extension * * @return void */ protected function addExtension() : void { $form = new Form(); $form->hiddenFields['type'] = Request::i()->type; if( Member::loggedIn()->language()->checkKeyExists( 'ext__' . Request::i()->type ) ) { $form->addHeader( 'ext__' . Request::i()->type ); } $form->add( new Text( 'dev_extensions_classname', NULL, TRUE, array( 'regex' => '/^[A-Z0-9]+$/i' ) ) ); $additionalFields = Application::load( Request::i()->extapp )->extensionHelper( Request::i()->type, $this->application->directory ); foreach( $additionalFields as $field ) { $form->add( $field ); } if ( $values = $form->values() ) { /* Pre-process any custom fields */ $values = Application::load( Request::i()->extapp )->extensionGenerate( Request::i()->type, $this->application->directory, $values ); $search = array( "{subpackage}", '{date}', '{app}', '{class}', ); $replace = array( ( $this->application->directory != 'core' ) ? ( " * @subpackage\t" . Member::loggedIn()->language()->get( "__app_{$this->application->directory}" ) ) : '', date( 'd M Y' ), $this->application->directory, $values['dev_extensions_classname'] ); foreach( $additionalFields as $tag => $field ) { if( array_key_exists( $field->name, $values ) ) { if( is_array( $values[ $field->name ] ) and count( $values[ $field->name ] ) ) { $values[ $field->name ] = implode( ",\n", $values[ $field->name] ); } } $search[] = $tag; $replace[] = $values[ $field->name ]; } $contents = str_replace( $search, $replace, file_get_contents( ROOT_PATH . '/applications/' . Request::i()->extapp . '/data/defaults/extensions/' . Request::i()->type . '.txt' ) ); $dir = ROOT_PATH . "/applications/{$this->application->directory}/extensions/" . Request::i()->extapp . '/' . Request::i()->type; if ( !is_dir( $dir ) ) { mkdir( $dir, IPS_FOLDER_PERMISSION, TRUE ); } file_put_contents( ROOT_PATH . "/applications/{$this->application->directory}/extensions/" . Request::i()->extapp . '/' . Request::i()->type . "/{$values['dev_extensions_classname']}.php", $contents ); $this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/extensions.json", $this->application->buildExtensionsJson() ); /* Clear cache */ try { unset( Store::i()->extensions ); } catch( OutOfRangeException ){} Output::i()->redirect( Url::internal( "app=core&module=developer&controller=extensions&appKey={$this->application->directory}&root=" . Request::i()->extapp . '/' . Request::i()->type ), 'file_created' ); } Output::i()->output = Theme::i()->getTemplate('global')->block( Member::loggedIn()->language()->addToStack('dev_extensions_create', FALSE, array( 'sprintf' => array( Request::i()->type ) ) ), $form, FALSE ); } /** * Remove Extension * * @return void */ protected function removeExtension() : void { Session::i()->csrfCheck(); if( is_file( ROOT_PATH ."/applications/{$this->application->directory}/extensions/". Request::i()->ext.'/'. Request::i()->file.'.php')) { unlink( ROOT_PATH ."/applications/{$this->application->directory}/extensions/". Request::i()->ext.'/'. Request::i()->file.'.php' ); } $this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/extensions.json", $this->application->buildExtensionsJson() ); /* Clear cache */ try { unset( Store::i()->extensions ); } catch( OutOfRangeException ){} Output::i()->redirect( Url::internal( "app=core&module=developer&controller=extensions&appKey={$this->application->directory}" ), 'deleted' ); } }