Version 5.0.0 beta 1
This commit is contained in:
1 parent
25ddeb65d6
commit
15c7beabc5
6736 files changed
+627902
-497943
No files matched your search
@@ -0,0 +1,462 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief acpmenu
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use DirectoryIterator;
|
||||
use http\Exception\InvalidArgumentException;
|
||||
use IPS\Application\Module;
|
||||
use IPS\Developer\Controller as DeveloperController;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Codemirror;
|
||||
use IPS\Helpers\Form\Select;
|
||||
use IPS\Helpers\Form\Stack;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Tree\Tree;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Theme;
|
||||
use function defined;
|
||||
use const IPS\REBUILD_QUICK;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* acpmenu
|
||||
*/
|
||||
class acpmenu extends DeveloperController
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$modules = $this->_getModules();
|
||||
$url = Url::internal( "app=core&module=developer&controller=acpmenu&appKey={$this->application->directory}" );
|
||||
$menu = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json" );
|
||||
$appKey = $this->application->directory;
|
||||
|
||||
/* Display the table */
|
||||
$tree = new Tree( $url, 'dev_acpmenu',
|
||||
/* Get Roots */
|
||||
function () use ( $url, $appKey, $menu )
|
||||
{
|
||||
$rows = array();
|
||||
$order = 1;
|
||||
foreach ( array_keys($menu) as $k )
|
||||
{
|
||||
$lang = "menu__{$appKey}_{$k}";
|
||||
$rows[ $k ] = Theme::i()->getTemplate( 'trees' )->row( $url, $k, Member::loggedIn()->language()->addToStack( $lang ), isset( $menu[ $k ] ), array(
|
||||
'add' => array(
|
||||
'icon' => 'plus-circle',
|
||||
'title' => 'acpmenu_add',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acpmenu&appKey={$appKey}&do=menuForm&module_key={$k}" )
|
||||
)
|
||||
), $lang, NULL, $order );
|
||||
$order++;
|
||||
}
|
||||
return $rows;
|
||||
},
|
||||
/* Get Row */
|
||||
function ( $k, $root ) use ( $url, $menu, $appKey )
|
||||
{
|
||||
$lang = "menu__{$appKey}_{$k}";
|
||||
return Theme::i()->getTemplate( 'trees' )->row( $url, $k, Member::loggedIn()->language()->addToStack( $lang ), isset( $menu[ $k ] ), array(
|
||||
'add' => array(
|
||||
'icon' => 'plus-circle',
|
||||
'title' => 'acpmenu_add',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acpmenu&appKey={$appKey}&do=menuForm&module_key={$k}" )
|
||||
)
|
||||
), $lang, NULL, NULL, $root );
|
||||
},
|
||||
/* Get Row Parent */
|
||||
function ()
|
||||
{
|
||||
return NULL;
|
||||
},
|
||||
/* Get Children */
|
||||
function ( $k ) use ( $url, $menu, $appKey )
|
||||
{
|
||||
$rows = array();
|
||||
$pos = 0;
|
||||
foreach ( $menu[ $k ] as $id => $row )
|
||||
{
|
||||
$description = "app={$appKey}&module=" . ( $row['module_url'] ?? $k ) . "&controller=" . $row['controller'] . ( $row['do'] ? "&do=" . $row['do'] : '' );
|
||||
|
||||
$lang = "menu__" . ( $row['activemenuitem'] ?? "{$appKey}_{$k}_{$id}" );
|
||||
$rows[ 's@' . $id ] = Theme::i()->getTemplate( 'trees' )->row( $url, 's@' . $id, Member::loggedIn()->language()->addToStack( $lang ), FALSE, array(
|
||||
'edit' => array(
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acpmenu&appKey={$appKey}&do=menuForm&module_key={$k}&id={$id}" ),
|
||||
'hotkey'=> 'e'
|
||||
),
|
||||
'delete' => array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acpmenu&appKey={$appKey}&do=menuDelete&module_key={$k}&id={$id}" ),
|
||||
'data' => array( 'delete' => '' )
|
||||
),
|
||||
), $description, NULL, ++$pos, FALSE, NULL, NULL, NULL, FALSE, FALSE, FALSE );
|
||||
}
|
||||
return $rows;
|
||||
},
|
||||
function() use ( $appKey ){
|
||||
return [
|
||||
'add' => [
|
||||
'icon' => 'plus',
|
||||
'title' => 'acpmenu_group_add',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acpmenu&appKey={$appKey}&do=menuGroupForm" ),
|
||||
'data' => [ 'ipsDialog' => '', 'ipsDialog-size' => 'narrow', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack( 'acpmenu_group_add' ) ]
|
||||
]
|
||||
];
|
||||
},
|
||||
FALSE,
|
||||
FALSE,
|
||||
TRUE
|
||||
);
|
||||
|
||||
Output::i()->output = (string) $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* New menu group
|
||||
* @return void
|
||||
*/
|
||||
protected function menuGroupForm() : void
|
||||
{
|
||||
$menu = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json" );
|
||||
|
||||
$form = new Form;
|
||||
$form->add( new Text( 'acpmenu_group_key', null, true, array(), function( $val ) use ( $menu ){
|
||||
if( isset( $menu[ $val ] ) )
|
||||
{
|
||||
throw new InvalidArgumentException( 'err_duplicate_acp_menu_group' );
|
||||
}
|
||||
} ) );
|
||||
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
$menu[ $values['acpmenu_group_key'] ] = [];
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json", $menu );
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu Form Item
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function menuForm() : void
|
||||
{
|
||||
/* Current Menu */
|
||||
$menu = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json" );
|
||||
|
||||
/* Get module and controllers */
|
||||
$menuGroup = Request::i()->module_key;
|
||||
|
||||
/* Load existing */
|
||||
$current = NULL;
|
||||
if ( Request::i()->id and isset( $menu[ $menuGroup ][ Request::i()->id ] ) )
|
||||
{
|
||||
$current = $menu[ $menuGroup ][ Request::i()->id ];
|
||||
if( isset( $current['module_url'] ) and $current['module_url'] != Request::i()->module_key )
|
||||
{
|
||||
Request::i()->module_key = $current['module_url'];
|
||||
}
|
||||
}
|
||||
|
||||
$module = $this->_loadModule( true );
|
||||
|
||||
$adminModules = [ '' => '' ];
|
||||
foreach( $this->_getModules()[ 'admin' ] as $k => $v )
|
||||
{
|
||||
$adminModules[ $k ] = $k;
|
||||
}
|
||||
|
||||
/* And controllers */
|
||||
$controllers = array();
|
||||
if( $module !== null )
|
||||
{
|
||||
foreach ( new DirectoryIterator( ROOT_PATH . "/applications/{$this->application->directory}/modules/admin/{$module->key}" ) as $file )
|
||||
{
|
||||
if ( !$file->isDot() and mb_substr( $file, -4 ) === '.php' )
|
||||
{
|
||||
$controllers[ mb_substr( $file, 0, -4 ) ] = (string) $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( new DirectoryIterator( ROOT_PATH . "/applications/{$this->application->directory}/modules/admin" ) as $moduleFolder )
|
||||
{
|
||||
if( $moduleFolder->isDir() and !$moduleFolder->isDot() )
|
||||
{
|
||||
foreach( new DirectoryIterator( $moduleFolder->getRealPath() ) as $file )
|
||||
{
|
||||
if ( !$file->isDot() and mb_substr( $file, -4 ) === '.php' )
|
||||
{
|
||||
$controllers[ $moduleFolder->getFilename() ][ mb_substr( $file, 0, -4 ) ] = (string) $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* And restrictions */
|
||||
$restrictions = $this->_getRestrictions( $module );
|
||||
|
||||
/* And tabs */
|
||||
$tabs = $this->_getAcpMenuTabs();
|
||||
|
||||
/* Show Form */
|
||||
$form = new Form();
|
||||
$form->addTab( 'acpmenu_tab_general' );
|
||||
$form->add( new Select( 'acpmenu_controller', ( $current ? $current['controller'] : NULL ), TRUE, array( 'options' => $controllers ) ) );
|
||||
$form->add( new Text( 'acpmenu_tab', ( $current ? $current['tab'] : $module?->application ), TRUE, array( 'autocomplete' => array(
|
||||
'source' => $tabs,
|
||||
'maxItems' => 1,
|
||||
) ) ) );
|
||||
$form->add( new Select( 'acpmenu_module_url', ( $current and isset( $current['module_url'] ) ) ? $current['module_url'] : null, false, [ 'options' => $adminModules ] ) );
|
||||
$form->add( new Text( 'acpmenu_doaction', ( $current ? $current['do'] : NULL ), FALSE, array(), NULL, 'do=' ) );
|
||||
$form->add( new Select( 'acpmenu_restriction', ( $current ? explode( ',', $current['restriction'] ) : '' ), FALSE, array( 'options' => $restrictions, 'multiple' => true, 'noDefault' => true ) ) );
|
||||
$form->add( new Select( 'acpmenu_subcontrollers', ( $current and isset( $current['subcontrollers'] ) ) ? explode( ',', $current['subcontrollers'] ) : null, false, array( 'options' => $controllers, 'multiple' => true, 'noDefault' => true ) ) );
|
||||
|
||||
$form->addTab( 'acpmenu_tab_advanced' );
|
||||
$form->add( new Text( 'acpmenu_activemenuitem', ( $current and isset( $current['activemenuitem'] ) ) ? $current['activemenuitem'] : null, false ) );
|
||||
$form->add( new Text( 'acpmenu_badge', ( $current and isset( $current['badge'] ) ) ? $current['badge'] : null, false ) );
|
||||
$form->add( new Codemirror( 'acpmenu_callback', ( $current and isset( $current['callback'] ) ) ? $current['callback'] : null, false, [ 'codeModeAllowedLanguages' => [ 'php' ] ] ) );
|
||||
|
||||
$menuChecks = [];
|
||||
if( $current and isset( $current['menu_checks'] ) )
|
||||
{
|
||||
foreach( $current['menu_checks'] as $param => $paramValues )
|
||||
{
|
||||
foreach( $paramValues as $pv )
|
||||
{
|
||||
$menuChecks[] = $param . '=' . $pv;
|
||||
}
|
||||
}
|
||||
}
|
||||
$form->add( new Stack( 'acpmenu_menu_checks', $menuChecks, false ) );
|
||||
|
||||
/* Handle submissions */
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$menuItemKey = $current ? Request::i()->id : $values['acpmenu_controller'];
|
||||
if( $current === null and isset( $menu[ $menuGroup ][ $menuItemKey ] ) and $values['acpmenu_doaction'] )
|
||||
{
|
||||
$menuItemKey .= $values['acpmenu_doaction'];
|
||||
}
|
||||
|
||||
$menuItem = array_merge( ( $current ?? [] ), [
|
||||
'tab' => $values['acpmenu_tab'],
|
||||
'controller' => $values['acpmenu_controller'],
|
||||
'do' => $values['acpmenu_doaction'],
|
||||
'restriction' => count( $values['acpmenu_restriction'] ) ? implode( ',', $values['acpmenu_restriction'] ) : ''
|
||||
] );
|
||||
|
||||
if( $menuItem['restriction'] and $restrictionModule = $this->_checkRestrictionModule( $menuItem['restriction'] ) )
|
||||
{
|
||||
if( $module === null or $restrictionModule != $module->key )
|
||||
{
|
||||
$menuItem['restriction_module'] = $restrictionModule;
|
||||
}
|
||||
}
|
||||
|
||||
if( $values['acpmenu_subcontrollers'] )
|
||||
{
|
||||
$menuItem['subcontrollers'] = implode( ",", $values['acpmenu_subcontrollers'] );
|
||||
}
|
||||
|
||||
foreach( [ 'module_url', 'callback', 'activemenuitem', 'badge' ] as $key )
|
||||
{
|
||||
if( $values[ 'acpmenu_' . $key ] )
|
||||
{
|
||||
$menuItem[ $key ] = $values['acpmenu_' . $key ];
|
||||
}
|
||||
}
|
||||
|
||||
if( !empty( $values['acpmenu_menu_checks'] ) )
|
||||
{
|
||||
$menuChecks = [];
|
||||
foreach( $values['acpmenu_menu_checks'] as $menuCheck )
|
||||
{
|
||||
$_menuCheck = explode( "=", $menuCheck );
|
||||
if( !isset( $menuChecks[ $_menuCheck[0] ] ) )
|
||||
{
|
||||
$menuChecks[ $_menuCheck[0] ] = [];
|
||||
}
|
||||
$menuChecks[ $_menuCheck[0] ][] = $_menuCheck[1];
|
||||
}
|
||||
$menuItem['menu_checks'] = $menuChecks;
|
||||
}
|
||||
|
||||
$menu[ $menuGroup ][ $menuItemKey ] = $menuItem;
|
||||
|
||||
/* If we edited the item key, remove the old one */
|
||||
if( $current !== null and Request::i()->id != $menuItemKey )
|
||||
{
|
||||
unset( $menu[ $menuGroup ][ Request::i()->id ] );
|
||||
}
|
||||
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json", $menu );
|
||||
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=acpmenu&appKey={$this->application->directory}&root={$menuGroup}" ) );
|
||||
}
|
||||
|
||||
/* Display */
|
||||
Output::i()->output = Theme::i()->getTemplate( 'global' )->block( 'acpmenu_add', $form, FALSE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Menu Item
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function menuDelete() : void
|
||||
{
|
||||
/* Make sure the user confirmed the deletion */
|
||||
Request::i()->confirmedDelete();
|
||||
|
||||
/* Get Menu */
|
||||
$menu = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json" );
|
||||
|
||||
/* Get module and controllers */
|
||||
$menuGroup = Request::i()->module_key;
|
||||
|
||||
/* Delete It */
|
||||
unset( $menu[ $menuGroup ][ Request::i()->id ] );
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json", $menu );
|
||||
|
||||
/* Redirect */
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=acpmenu&appKey={$this->application->directory}&root={$menuGroup}" ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function reorder() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
$menu = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json" );
|
||||
|
||||
/* Normalise AJAX vs non-AJAX */
|
||||
if( isset( Request::i()->ajax_order ) )
|
||||
{
|
||||
$order = array();
|
||||
$position = array();
|
||||
foreach( Request::i()->ajax_order as $id => $parent )
|
||||
{
|
||||
/* We have to fudge children "ids" to prevent conflicts in the array */
|
||||
$id = str_replace( 's@', '', $id );
|
||||
|
||||
if ( !isset( $order[ $parent ] ) )
|
||||
{
|
||||
$order[ $parent ] = array();
|
||||
$position[ $parent ] = 1;
|
||||
}
|
||||
$order[ $parent ][ $id ] = $position[ $parent ]++;
|
||||
}
|
||||
}
|
||||
/* Non-AJAX way */
|
||||
else
|
||||
{
|
||||
$order = array( Request::i()->root ?: 'null' => Request::i()->order );
|
||||
}
|
||||
|
||||
/* Sort */
|
||||
$_menu = $menu;
|
||||
$menu = array();
|
||||
|
||||
if( isset( $order['null'] ) )
|
||||
{
|
||||
foreach( $order['null'] as $key => $position )
|
||||
{
|
||||
foreach ( $_menu as $_parent => $_items )
|
||||
{
|
||||
if ( $key == $_parent )
|
||||
{
|
||||
$menu[ $_parent ] = $_menu[ $_parent ];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach( $_menu as $root => $items )
|
||||
{
|
||||
/* If we were sorting one level of the menu, and this is not it, then leave it as is */
|
||||
if( isset( Request::i()->root ) and Request::i()->root != $root )
|
||||
{
|
||||
$menu[ $root ] = $items;
|
||||
}
|
||||
elseif( isset( $order[ $root ] ) )
|
||||
{
|
||||
$menu[ $root ] = array();
|
||||
foreach( $order[ $root ] as $key => $position )
|
||||
{
|
||||
$menu[ $root ][ $key ] = $_menu[ $root ][ $key ];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Write */
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json", $menu );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the restriction module for the specified restriction key(s)
|
||||
*
|
||||
* @param string $restrictions
|
||||
* @return string
|
||||
*/
|
||||
protected function _checkRestrictionModule( string $restrictions ) : string
|
||||
{
|
||||
$allRestrictions = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json" );
|
||||
foreach( explode( ",", $restrictions ) as $_restriction )
|
||||
{
|
||||
foreach( $allRestrictions as $moduleKey => $groups )
|
||||
{
|
||||
foreach( $groups as $groupKey => $rows )
|
||||
{
|
||||
if( array_key_exists( $_restriction, $rows ) )
|
||||
{
|
||||
return $moduleKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief acprestrictions
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\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\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Theme;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* acprestrictions
|
||||
*/
|
||||
class acprestrictions extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected ?array $modules = null;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected ?array $restrictions = null;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$this->modules = $this->_getModules();
|
||||
$this->url = Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}" );
|
||||
$this->restrictions = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json" );
|
||||
$appKey = $this->application->directory;
|
||||
|
||||
/* Display the table */
|
||||
$tree = new Tree( $this->url, 'dev_acprestrictions', array( $this, 'restrictionsGetRoots' ), array( $this, 'restrictionsGetRow' ),
|
||||
function ( $k )
|
||||
{
|
||||
if ( mb_substr( $k, 0, 1 ) === 'M' )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'M' . mb_substr( $k, 1, mb_strpos( $k, '-' ) - 1 );
|
||||
}
|
||||
},
|
||||
array( $this, 'restrictionsGetchildren' ),
|
||||
NULL,
|
||||
FALSE,
|
||||
TRUE,
|
||||
TRUE
|
||||
);
|
||||
|
||||
Output::i()->output = (string) $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function reorder() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
/* Figure out the items we are reordering, we would only be reordering a single group */
|
||||
$newOrder = array();
|
||||
$position = 1;
|
||||
foreach( Request::i()->ajax_order as $i => $parent )
|
||||
{
|
||||
if( strpos( $i, '~' ) !== FALSE )
|
||||
{
|
||||
if( !isset( $newOrder[ $parent ] ) )
|
||||
{
|
||||
$newOrder[ $parent ] = array();
|
||||
}
|
||||
$newOrder[ $parent ][ $i ] = $position++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach( $newOrder as $groupKey => $order )
|
||||
{
|
||||
if( isset( Request::i()->ajax_order[ $groupKey ] ) )
|
||||
{
|
||||
$moduleKey = Request::i()->ajax_order[ $groupKey ];
|
||||
|
||||
/* Sort */
|
||||
uasort( $this->restrictions[ $moduleKey ][ $groupKey ], function( $a, $b ) use ( $order ) {
|
||||
return ( isset( $order[ str_replace( '_', '~', $a ) ] ) ? $order[ str_replace( '_', '~', $a ) ] : 0 ) - ( isset( $order[ str_replace( '_', '~', $b ) ] ) ? $order[ str_replace( '_', '~', $b ) ] : 0 );
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* Write */
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json", $this->restrictions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrictions: Get Root Rows
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function restrictionsGetRoots() : array
|
||||
{
|
||||
$rows = array();
|
||||
|
||||
if( !empty( $this->modules['admin'] ) )
|
||||
{
|
||||
foreach ( $this->modules['admin'] as $k => $module )
|
||||
{
|
||||
if ( empty( $module['protected'] ) )
|
||||
{
|
||||
$rows[ $k ] = $this->_restrictionsModuleRow( $this->url, $k, $this->restrictions, FALSE );
|
||||
}
|
||||
}
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrictions: Get Individual Row
|
||||
*
|
||||
* @param string $k ID
|
||||
* @param bool $root Is root row?
|
||||
* @return string
|
||||
*/
|
||||
public function restrictionsGetRow( string $k, bool $root ) : string
|
||||
{
|
||||
if ( mb_substr( $k, 0, 1 ) === 'M' )
|
||||
{
|
||||
return $this->_restrictionsModuleRow( $this->url, mb_substr( $k, 1 ), $this->restrictions, $root );
|
||||
}
|
||||
else
|
||||
{
|
||||
$moduleKey = mb_substr( $k, 1, mb_strpos( $k, '-' ) - 1 );
|
||||
$groupKey = mb_substr( $k, mb_strpos( $k, '-' ) + 1 );
|
||||
return $this->_restrictionsGroupRow( $this->url, $groupKey, $moduleKey, $this->restrictions[ $moduleKey ][ $groupKey ], $root );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrictions: Get Child Rows
|
||||
*
|
||||
* @param string $k ID
|
||||
* @return array
|
||||
*/
|
||||
public function restrictionsGetChildren( string $k ) : array
|
||||
{
|
||||
$rows = array();
|
||||
|
||||
if ( mb_substr( $k, 0, 1 ) === 'M' )
|
||||
{
|
||||
$k = mb_substr( $k, 1 );
|
||||
foreach ( $this->restrictions[ $k ] as $groupKey => $r )
|
||||
{
|
||||
$rows[ $groupKey ] = $this->_restrictionsGroupRow( $this->url, $groupKey, $k, $r, FALSE );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$moduleKey = mb_substr( $k, 1, mb_strpos( $k, '-' ) - 1 );
|
||||
$groupKey = mb_substr( $k, mb_strpos( $k, '-' ) + 1 );
|
||||
|
||||
$pos = 0;
|
||||
foreach ( $this->restrictions[ $moduleKey ][ $groupKey ] as $rKey )
|
||||
{
|
||||
$lang = "r__{$rKey}";
|
||||
$rows[ str_replace( '_', '~', $rKey ) ] = Theme::i()->getTemplate( 'trees' )->row( $this->url, $rKey, Member::loggedIn()->language()->addToStack( $lang ), FALSE, array(
|
||||
'delete' => array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}&do=restrictionRowDelete&module_key={$moduleKey}&group={$groupKey}&id={$rKey}" ),
|
||||
'data' => array( 'delete' => '' )
|
||||
)
|
||||
), $rKey, NULL, ++$pos );
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Module Row
|
||||
*
|
||||
* @param string|Url $url URL
|
||||
* @param string $k Key
|
||||
* @param array $restrictions Restrictions JSON
|
||||
* @param bool $root As root?
|
||||
* @return string
|
||||
*/
|
||||
protected function _restrictionsModuleRow( string|Url $url, string $k, array $restrictions, bool $root ) : string
|
||||
{
|
||||
return Theme::i()->getTemplate( 'trees' )->row( $url, 'M'.$k, $k, isset( $restrictions[ $k ] ), array(
|
||||
'add' => array(
|
||||
'icon' => 'plus-circle',
|
||||
'title' => 'acprestrictions_addgroup',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}&do=restrictionGroupForm&module_key={$k}&id=0" ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('acprestrictions_addgroup') )
|
||||
)
|
||||
), '', NULL, NULL, $root );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Group Row
|
||||
*
|
||||
* @param string|Url $url URL
|
||||
* @param string $groupKey Key
|
||||
* @param string $moduleKey Module Key
|
||||
* @param array $r Rows in this group
|
||||
* @param bool $root As root?
|
||||
* @return string
|
||||
*/
|
||||
protected function _restrictionsGroupRow( string|Url $url, string $groupKey, string $moduleKey, array $r, bool $root ) : string
|
||||
{
|
||||
$lang = "r__{$groupKey}";
|
||||
return Theme::i()->getTemplate( 'trees' )->row( $url, "G{$moduleKey}-{$groupKey}", Member::loggedIn()->language()->addToStack( $lang ), !empty( $r ), array(
|
||||
'add' => array(
|
||||
'icon' => 'plus-circle',
|
||||
'title' => 'acprestrictions_addrow',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}&do=restrictionRowForm&module_key={$moduleKey}&group={$groupKey}" ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('acprestrictions_addrow') )
|
||||
),
|
||||
'edit' => array(
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}&do=restrictionGroupForm&module_key={$moduleKey}&id={$groupKey}" ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('edit') ),
|
||||
'hotkey'=> 'e'
|
||||
),
|
||||
'delete'=> array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}&do=restrictionGroupDelete&module_key={$moduleKey}&id={$groupKey}" ),
|
||||
'data' => array( 'delete' => '' )
|
||||
)
|
||||
), '', NULL, NULL, $root );
|
||||
}
|
||||
|
||||
/**
|
||||
* Restriction Group Form
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function restrictionGroupForm() : void
|
||||
{
|
||||
/* Get restriction data */
|
||||
$restrictions = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json" );
|
||||
|
||||
/* Load module */
|
||||
$module = $this->_loadModule();
|
||||
|
||||
/* Do we have a default value? */
|
||||
$current = NULL;
|
||||
if ( Request::i()->id and isset( $restrictions[ $module->key ][ Request::i()->id ] ) )
|
||||
{
|
||||
$current = Request::i()->id;
|
||||
}
|
||||
|
||||
/* Build Form */
|
||||
$form = new Form();
|
||||
$form->add( new Text( 'acprestrictions_groupkey', $current ?: '', TRUE ) );
|
||||
|
||||
/* Handle submissions */
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$rows = array();
|
||||
if ( $current !== NULL )
|
||||
{
|
||||
$rows = $restrictions[ $module->key ][ $current ];
|
||||
unset( $restrictions[ $module->key ][ $current ] );
|
||||
}
|
||||
|
||||
$restrictions[ $module->key ][ $values['acprestrictions_groupkey'] ] = $rows;
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json", $restrictions );
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}&root=M{$module->key}" ) );
|
||||
}
|
||||
|
||||
/* Display */
|
||||
Output::i()->output = Theme::i()->getTemplate( 'global' )->block( 'acprestrictions_addgroup', $form, FALSE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Restriction Group
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function restrictionGroupDelete() : void
|
||||
{
|
||||
/* Make sure the user confirmed the deletion */
|
||||
Request::i()->confirmedDelete();
|
||||
|
||||
$restrictions = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json" );
|
||||
$module = $this->_loadModule();
|
||||
|
||||
if( isset( $restrictions[ $module->key ][ Request::i()->id ] ) )
|
||||
{
|
||||
unset( $restrictions[ $module->key ][ Request::i()->id ] );
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json", $restrictions );
|
||||
}
|
||||
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}&root=M{$module->key}" ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Restriction Row Form
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function restrictionRowForm() : void
|
||||
{
|
||||
$module = $this->_loadModule();
|
||||
$group = Request::i()->group;
|
||||
|
||||
$form = new Form();
|
||||
$form->add( new Text( 'acprestrictions_rowkey', NULL, TRUE ) );
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$restrictions = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json" );
|
||||
$restrictions[ $module->key ][ $group ][ $values['acprestrictions_rowkey'] ] = $values['acprestrictions_rowkey'];
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json", $restrictions );
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}&root=G{$module->key}-{$group}" ) );
|
||||
}
|
||||
Output::i()->output = Theme::i()->getTemplate( 'global' )->block( 'acprestrictions_addrow', $form, FALSE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Restriction Row
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function restrictionRowDelete() : void
|
||||
{
|
||||
/* Make sure the user confirmed the deletion */
|
||||
Request::i()->confirmedDelete();
|
||||
|
||||
$restrictions = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json" );
|
||||
$module = $this->_loadModule();
|
||||
$group = Request::i()->group;
|
||||
$id = Request::i()->id;
|
||||
|
||||
if( isset( $restrictions[ $module->key ][ $group ][ $id ] ) )
|
||||
{
|
||||
unset( $restrictions[ $module->key ][ $group ][ $id ] );
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json", $restrictions );
|
||||
}
|
||||
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=acprestrictions&appKey={$this->application->directory}&root=G{$module->key}-{$group}" ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief acpsearch
|
||||
* @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
|
||||
|
||||
* @since 05 Feb 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Select;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Table\Custom;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* acpsearch
|
||||
*/
|
||||
class acpsearch extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$json = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpsearch.json" );
|
||||
|
||||
$data = array();
|
||||
foreach( $json as $k => $v )
|
||||
{
|
||||
$data[ $k ] = array(
|
||||
'controller' => $k,
|
||||
'lang' => $v['lang_key'],
|
||||
'keywords' => implode( ", ", $v['keywords'] ),
|
||||
'restriction' => $v['restriction']
|
||||
);
|
||||
}
|
||||
|
||||
$table = new Custom( $data, $this->url );
|
||||
$table->quickSearch = 'keywords';
|
||||
$table->include = [ 'controller', 'keywords' ];
|
||||
$table->langPrefix = 'dev_acpsearch_';
|
||||
$table->parsers = [
|
||||
'controller' => function( $val, $row )
|
||||
{
|
||||
parse_str( $row['controller'], $params );
|
||||
if( ( isset( $params['app'] ) and isset( $params['module'] ) ) and ( !isset( $row['restriction'] ) or Member::loggedIn()->hasAcpRestriction( $params['app'], $params['module'], $row['restriction'] ) ) )
|
||||
{
|
||||
$title = "<a href='" . Url::internal( $row['controller'] ) . "' target='_blank'>" . Member::loggedIn()->language()->addToStack( $row['lang'] ) . "</a>";
|
||||
}
|
||||
|
||||
$title = Member::loggedIn()->language()->addToStack( $row['lang'] );
|
||||
return $title . "<div class='i-font-size_-2 i-color_soft'>{$row['controller']}</div>";
|
||||
}
|
||||
];
|
||||
|
||||
$table->rootButtons = array(
|
||||
'add' => array(
|
||||
'icon' => 'plus',
|
||||
'title' => 'add',
|
||||
'link' => $this->url->setQueryString( 'do', 'form' ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('add') )
|
||||
)
|
||||
);
|
||||
|
||||
$url = $this->url;
|
||||
$table->rowButtons = function( $row ) use ( $url )
|
||||
{
|
||||
return array(
|
||||
'view' => array(
|
||||
'icon' => 'search',
|
||||
'title' => 'view',
|
||||
'link' => Url::internal( $row['controller'] ),
|
||||
'target' => '_blank'
|
||||
),
|
||||
'edit' => array(
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => $url->setQueryString( array( 'do' => 'form', 'key' => urlencode( $row['controller'] ) ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('edit') )
|
||||
),
|
||||
'delete' => array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $url->setQueryString( array( 'do' => 'delete', 'key' => urlencode( $row['controller'] ) ) )->csrf(),
|
||||
'data' => array( 'confirm' => '' )
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
Output::i()->output = (string) $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function form() : void
|
||||
{
|
||||
$jsonFile = ROOT_PATH . "/applications/{$this->application->directory}/data/acpsearch.json";
|
||||
$json = $this->_getJson( $jsonFile );
|
||||
|
||||
$controller = isset( Request::i()->key ) ? urldecode( Request::i()->key ) : null;
|
||||
$row = ( $controller !== null and isset( $json[ $controller ] ) ) ? $json[ $controller ] : array();
|
||||
|
||||
$restrictions = array( '' => 'no_restriction' );
|
||||
foreach( $this->application->modules( 'admin' ) as $module )
|
||||
{
|
||||
$moduleKey = 'menu__' . $this->application->directory . '_' . $module->key;
|
||||
$moduleRestrictions = array();
|
||||
foreach( $this->_getRestrictions( $module ) as $group => $r )
|
||||
{
|
||||
foreach( $r as $k => $v )
|
||||
{
|
||||
$moduleRestrictions[ $k ] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
if( count( $moduleRestrictions ) )
|
||||
{
|
||||
$restrictions[ $moduleKey ] = $moduleRestrictions;
|
||||
}
|
||||
}
|
||||
|
||||
$form = new Form;
|
||||
$form->add( new Text( 'dev_acpsearch_controller', $controller, true ) );
|
||||
$form->add( new Text( 'dev_acpsearch_lang', $row['lang_key'] ?? null, true ) );
|
||||
$form->add( new Select( 'dev_acpsearch_restriction', $row['restriction'] ?? null, false, array(
|
||||
'options' => $restrictions
|
||||
) ) );
|
||||
$form->add( new Form\Stack( 'dev_acpsearch_keywords', $row['keywords'] ?? null, true ) );
|
||||
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
$keywordData = [
|
||||
'lang_key' => $values['dev_acpsearch_lang'],
|
||||
'restriction' => $values['dev_acpsearch_restriction'] ?? null,
|
||||
'keywords' => $values['dev_acpsearch_keywords']
|
||||
];
|
||||
|
||||
if( $controller == $values['dev_acpsearch_controller'] )
|
||||
{
|
||||
$json[ $controller ] = $keywordData;
|
||||
}
|
||||
else
|
||||
{
|
||||
$json[ $values['dev_acpsearch_controller'] ] = $keywordData;
|
||||
if( isset( $json[ $controller ] ) )
|
||||
{
|
||||
unset( $json[ $controller ] );
|
||||
}
|
||||
}
|
||||
|
||||
$this->_writeJson( $jsonFile, $json );
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function delete() : void
|
||||
{
|
||||
Request::i()->confirmedDelete();
|
||||
|
||||
$jsonFile = ROOT_PATH . "/applications/{$this->application->directory}/data/acpsearch.json";
|
||||
$json = $this->_getJson( $jsonFile );
|
||||
|
||||
$controller = isset( Request::i()->key ) ? urldecode( Request::i()->key ) : null;
|
||||
|
||||
if( isset( $json[ $controller ] ) )
|
||||
{
|
||||
unset( $json[ $controller ] );
|
||||
}
|
||||
|
||||
$this->_writeJson( $jsonFile, $json );
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief cmstemplates
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use IPS\cms\Templates;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Output;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* cmstemplates
|
||||
*/
|
||||
class cmstemplates extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$templateConfigFile = ROOT_PATH . "/applications/{$this->application->directory}/dev/cmsTemplates.json";
|
||||
$preSelectedTemplates = array( 'templates_database' => [], 'templates_block' => [], 'templates_page' => [] );
|
||||
|
||||
if( file_exists( $templateConfigFile ) )
|
||||
{
|
||||
$preSelectedTemplates = json_decode( file_get_contents( $templateConfigFile ), TRUE );
|
||||
}
|
||||
|
||||
$form = Templates::exportForm( TRUE, $preSelectedTemplates );
|
||||
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/dev/cmsTemplates.json", $values );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief customtemplates
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use Exception;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Tree\Tree;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Theme;
|
||||
use IPS\Theme\CustomTemplate;
|
||||
use OutOfRangeException;
|
||||
use function defined;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* customtemplates
|
||||
*/
|
||||
class customtemplates extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$url = Url::internal( "app=core&module=developer&controller=customtemplates&appKey={$this->application->directory}" );
|
||||
$appKey = $this->application->directory;
|
||||
$templates = [];
|
||||
|
||||
foreach( CustomTemplate::all() as $template )
|
||||
{
|
||||
if ( $template->app == $appKey )
|
||||
{
|
||||
$templates[] = $template;
|
||||
}
|
||||
}
|
||||
|
||||
/* Display the table */
|
||||
$tree = new Tree( $url, 'dev_custom_templates',
|
||||
/* Get Roots */
|
||||
function () use ( $url, $appKey, $templates )
|
||||
{
|
||||
$rows = array();
|
||||
$order = 1;
|
||||
foreach ( $templates as $template )
|
||||
{
|
||||
$rows[ $template->id ] = Theme::i()->getTemplate( 'trees' )->row( $url, $template->id, $template->name, false, array(
|
||||
'add' => array(
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=customtemplates&appKey={$appKey}&do=customTemplateForm&id=" . $template->id ),
|
||||
),
|
||||
$buttons['delete'] = array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=customtemplates&appKey={$appKey}&do=customTemplateDelete&id=" . $template->id ),
|
||||
'data' => ['delete' => '']
|
||||
)
|
||||
), NULL, NULL, $order, FALSE, NULL, NULL, $template->_badge );
|
||||
$order++;
|
||||
}
|
||||
return $rows;
|
||||
},
|
||||
/* Get Row */
|
||||
NULL,
|
||||
/* Get Row Parent */
|
||||
function ()
|
||||
{
|
||||
return NULL;
|
||||
},
|
||||
/* Get Children */
|
||||
NULL,
|
||||
/* Get Root Buttons */
|
||||
function() use ( $appKey )
|
||||
{
|
||||
return array(
|
||||
'add' => array(
|
||||
'icon' => 'plus',
|
||||
'title' => 'custom_template_create_new',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=customtemplates&appKey={$appKey}&do=customTemplateForm" ),
|
||||
),
|
||||
);
|
||||
},
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE
|
||||
);
|
||||
|
||||
Output::i()->output = (string) $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a template
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function customTemplateDelete(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$current = CustomTemplate::load( Request::i()->id );
|
||||
}
|
||||
catch( Exception )
|
||||
{
|
||||
Output::i()->error( 'node_error', '2C124/1', 404 );
|
||||
}
|
||||
|
||||
/* We just made you, now we must destroy you */
|
||||
$current->delete();
|
||||
|
||||
$this->application->buildCustomTemplates();
|
||||
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=customtemplates&appKey={$this->application->directory}" ), 'deleted' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom template add/edit form
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function customTemplateForm(): void
|
||||
{
|
||||
if ( isset( Request::i()->id ) )
|
||||
{
|
||||
$current = CustomTemplate::load( Request::i()->id );
|
||||
}
|
||||
else
|
||||
{
|
||||
$current = new CustomTemplate;
|
||||
}
|
||||
|
||||
$current::$scopeKey = 'appKey';
|
||||
$current::$scopeValue = Request::i()->appKey;
|
||||
|
||||
/* Show Form */
|
||||
$form = new Form();
|
||||
$current->form( $form );
|
||||
|
||||
/* Handle submissions */
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$current->saveForm( $current->formatFormValues( $values ) );
|
||||
$this->application->buildCustomTemplates();
|
||||
|
||||
if( Request::i()->isAjax() )
|
||||
{
|
||||
Output::i()->json( array() );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( isset( Request::i()->save_and_reload ) )
|
||||
{
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=customtemplates&appKey={$this->application->directory}&do=customTemplateForm&id=" . $current->id ), 'saved' );
|
||||
}
|
||||
else
|
||||
{
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=customtemplates&appKey={$this->application->directory}" ), 'saved' );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Output::i()->breadcrumb[] = array(
|
||||
Url::internal( "app=core&module=developer&controller=customtemplates&appKey={$this->application->directory}" ),
|
||||
'custom_templates'
|
||||
);
|
||||
|
||||
/* Display */
|
||||
Output::i()->output = Theme::i()->getTemplate( 'global' )->block( 'custom_templates', $form, FALSE );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function reorder() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
/* Normalise AJAX vs non-AJAX */
|
||||
if( isset( Request::i()->ajax_order ) )
|
||||
{
|
||||
$order = array();
|
||||
$position = array();
|
||||
foreach( Request::i()->ajax_order as $id => $parent )
|
||||
{
|
||||
if ( !isset( $order[ $parent ] ) )
|
||||
{
|
||||
$order[ $parent ] = array();
|
||||
$position[ $parent ] = 1;
|
||||
}
|
||||
$order[ $parent ][ $id ] = $position[ $parent ]++;
|
||||
}
|
||||
}
|
||||
/* Non-AJAX way */
|
||||
else
|
||||
{
|
||||
$order = array( Request::i()->root ?: 'null' => Request::i()->order );
|
||||
}
|
||||
|
||||
/* Okay, now order */
|
||||
if ( isset( $order['null'] ) )
|
||||
{
|
||||
foreach ( $order['null'] as $id => $position )
|
||||
{
|
||||
try
|
||||
{
|
||||
$node = CustomTemplate::load( $id );
|
||||
$node->order = (int) $position;
|
||||
$node->save();
|
||||
}
|
||||
catch ( OutOfRangeException $e ){}
|
||||
}
|
||||
}
|
||||
|
||||
$this->application->buildCustomTemplates();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,949 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief details
|
||||
* @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
|
||||
|
||||
* @since 06 Feb 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use DirectoryIterator;
|
||||
use IPS\Content\Item;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Extensions\NotificationsAbstract;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\IPS;
|
||||
use IPS\Lang;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Permissions;
|
||||
use IPS\Output;
|
||||
use IPS\Theme;
|
||||
use function count;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* details
|
||||
*/
|
||||
class details extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
Output::i()->sidebar['actions']['edit'] = [
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => $this->url->setQueryString( 'do', 'edit' )
|
||||
];
|
||||
|
||||
Output::i()->globalControllers[] = 'core.admin.developer.details';
|
||||
Output::i()->title = Member::loggedIn()->language()->addToStack( 'dev_details' );
|
||||
Output::i()->output = Theme::i()->getTemplate( 'developer' )->dashboard( $this->application );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function edit() : void
|
||||
{
|
||||
$form = new Form;
|
||||
$this->application->form( $form );
|
||||
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
$this->application->saveForm( $this->application->formatFormValues( $values ) );
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function scan() : void
|
||||
{
|
||||
/* If this is a 1st party app, and we don't have our super special constant, don't scan it */
|
||||
if( in_array( $this->application->directory, IPS::$ipsApps ) and ( !defined( '\IPS\USE_DEVELOPMENT_BUILDS' ) or \IPS\USE_DEVELOPMENT_BUILDS === false ) )
|
||||
{
|
||||
Output::i()->json( array(
|
||||
'html' => Theme::i()->getTemplate( 'support' )->supportBlockList( array() ),
|
||||
'criticalIssues' => 0,
|
||||
'recommendedIssues' => 0
|
||||
) );
|
||||
}
|
||||
|
||||
$scanResults = array( 'advice' => 0, 'failures' => 0, 'list' => array() );
|
||||
|
||||
$languageStrings = Lang::readLangFiles( $this->application->directory );
|
||||
|
||||
$missingAdminStrings = $this->_scanAdminStrings( $languageStrings );
|
||||
if( count( $missingAdminStrings ) )
|
||||
{
|
||||
$total = 0;
|
||||
foreach( $missingAdminStrings as $k => $v )
|
||||
{
|
||||
$total += count( $v );
|
||||
}
|
||||
|
||||
$scanResults['failures']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => TRUE,
|
||||
'advice' => FALSE,
|
||||
'success' => FALSE,
|
||||
'element' => 'acpDevLang',
|
||||
'dialogTitle' => Member::loggedIn()->language()->addToStack( 'devscan__missing_admin_strings_title' ),
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_admin_strings', false, array( 'pluralize' => array( $total, $total ) ) ),
|
||||
'body' => Theme::i()->getTemplate( 'developer' )->missingStrings( $missingAdminStrings, $this->application->directory ),
|
||||
'learnmore' => true
|
||||
);
|
||||
}
|
||||
|
||||
$missingFrontStrings = $this->_scanFrontStrings( $languageStrings );
|
||||
if( count( $missingFrontStrings ) )
|
||||
{
|
||||
$total = 0;
|
||||
foreach( $missingFrontStrings as $k => $v )
|
||||
{
|
||||
$total += count( $v );
|
||||
}
|
||||
|
||||
$scanResults['failures']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => true,
|
||||
'advice' => false,
|
||||
'success' => false,
|
||||
'element' => 'frontDevLang',
|
||||
'dialogTitle' => Member::loggedIn()->language()->addToStack( 'devscan__missing_front_strings_title' ),
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_front_strings', false, array( 'pluralize' => array( $total, $total ) ) ),
|
||||
'body' => Theme::i()->getTemplate( 'developer' )->missingStrings( $missingFrontStrings, $this->application->directory ),
|
||||
'learnmore' => true
|
||||
);
|
||||
}
|
||||
|
||||
$missingSearchKeywords = $this->_scanSearchKeywords( $languageStrings );
|
||||
if( count( $missingSearchKeywords ) )
|
||||
{
|
||||
$total = 0;
|
||||
foreach( $missingSearchKeywords as $k => $v )
|
||||
{
|
||||
$total += count( $v );
|
||||
}
|
||||
|
||||
$scanResults['failures']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => true,
|
||||
'advice' => false,
|
||||
'success' => false,
|
||||
'element' => 'acpDevSearchKeywords',
|
||||
'dialogTitle' => Member::loggedIn()->language()->addToStack( 'devscan__missing_search_keywords_title' ),
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_search_keywords', false, array( 'pluralize' => array( $total, $total ) ) ),
|
||||
'body' => Theme::i()->getTemplate( 'developer' )->missingSearchKeywords( $missingSearchKeywords, $this->application->directory ),
|
||||
'learnmore' => true
|
||||
);
|
||||
}
|
||||
|
||||
$duplicateSearchKeywords = $this->_scanSearchKeywordsForDupes();
|
||||
if( count( $duplicateSearchKeywords ) )
|
||||
{
|
||||
$total = count( $duplicateSearchKeywords );
|
||||
|
||||
$scanResults['advice']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => false,
|
||||
'advice' => true,
|
||||
'success' => false,
|
||||
'element' => 'acpDevSearchKeywordsDupes',
|
||||
'dialogTitle' => Member::loggedIn()->language()->addToStack( 'devscan__dupe_search_keywords_title' ),
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__dupe_search_keywords', false, array( 'pluralize' => array( $total, $total ) ) ),
|
||||
'body' => Theme::i()->getTemplate( 'developer' )->duplicateSearchKeywords( $duplicateSearchKeywords, $this->application->directory ),
|
||||
'learnmore' => true
|
||||
);
|
||||
}
|
||||
|
||||
$missingEmailTemplates = $this->_checkEmailTemplates();
|
||||
if( count( $missingEmailTemplates ) )
|
||||
{
|
||||
$scanResults['failures']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => true,
|
||||
'advice' => false,
|
||||
'success' => false,
|
||||
'element' => 'frontDevEmail',
|
||||
'dialogTitle' => Member::loggedIn()->language()->addToStack( 'devscan__missing_emailtpl_title' ),
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_emailtpl', false, array( 'pluralize' => array( count( $missingEmailTemplates ), count( $missingEmailTemplates ) ) ) ),
|
||||
'body' => Theme::i()->getTemplate( 'developer' )->missingTemplates( $missingEmailTemplates, $this->application->directory ),
|
||||
'learnmore' => true
|
||||
);
|
||||
}
|
||||
|
||||
/* Check for content-related extensions */
|
||||
foreach( $this->application->extensions( 'core', 'ContentRouter' ) as $ext )
|
||||
{
|
||||
/* @var Item $itemClass */
|
||||
foreach( $ext->classes as $itemClass )
|
||||
{
|
||||
/* Bypass for Pages */
|
||||
if( strpos( $itemClass, 'IPS\cms\Records' ) !== false )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$class = $itemClass::$containerNodeClass;
|
||||
if( in_array( Permissions::class, class_implements( $class ) ) )
|
||||
{
|
||||
$permissionsExtension = false;
|
||||
foreach( $this->application->extensions( 'core', 'Permissions' ) as $ext )
|
||||
{
|
||||
if( array_key_exists( $class, $ext->getNodeClasses() ) )
|
||||
{
|
||||
$permissionsExtension = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( !$permissionsExtension )
|
||||
{
|
||||
$scanResults['advice']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => false,
|
||||
'advice' => true,
|
||||
'success' => false,
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_permissions', false, array( 'sprintf' => $class ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check EditorLocation for comments/reviews */
|
||||
$missingExtensions = [];
|
||||
$extensions = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/extensions.json" );
|
||||
if( isset( $itemClass::$commentClass ) or isset( $itemClass::$reviewClass ) )
|
||||
{
|
||||
$key = IPS::mb_ucfirst( $itemClass::$module );
|
||||
if( !isset( $extensions['core']['EditorLocations'] ) or !array_key_exists( $key, $extensions['core']['EditorLocations'] ) )
|
||||
{
|
||||
$missingExtensions[] = $this->application->directory . '_' . $key;
|
||||
$scanResults['failures']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => true,
|
||||
'advice' => false,
|
||||
'success' => false,
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_editor', false, array( 'sprintf' => $this->application->directory . '_' . $key ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Check form elements */
|
||||
foreach( $itemClass::formElements( null, null ) as $element )
|
||||
{
|
||||
/* Check editor fields for missing EditorLocation extensions */
|
||||
if( $element instanceof Form\Editor )
|
||||
{
|
||||
/* Did we already check this? */
|
||||
if( in_array( $element->options['app'] . '_' . $element->options['key'], $missingExtensions ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$json = ( $element->options['app'] == $this->application->directory ) ? $extensions : $this->_getJson( ROOT_PATH . "/applications/" . $element->options['app'] . "/data/extensions.json" );
|
||||
|
||||
if( !isset( $json['core']['EditorLocations'] ) or !array_key_exists( $element->options['key'], $json['core']['EditorLocations'] ) )
|
||||
{
|
||||
$missingExtensions[] = $element->options['app'] . '_' . $element->options['key'];
|
||||
$scanResults['failures']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => true,
|
||||
'advice' => false,
|
||||
'success' => false,
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_editor', false, array( 'sprintf' => $element->options['app'] . '_' . $element->options['key'] ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
elseif( $element instanceof Form\Upload and isset( $element->options['storageExtension'] ) )
|
||||
{
|
||||
/* Did we already check this? */
|
||||
if( in_array( $element->options['storageExtension'], $missingExtensions ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list( $app, $key ) = explode( "_", $element->options['storageExtension'] );
|
||||
$json = ( $app == $this->application->directory ) ? $extensions : $this->_getJson( ROOT_PATH . "/applications/" . $app . "/data/extensions.json" );
|
||||
|
||||
if( !isset( $json['core']['FileStorage'] ) or !array_key_exists( $key, $json['core']['FileStorage'] ) )
|
||||
{
|
||||
$missingExtensions[] = $element->options['storageExtension'];
|
||||
$scanResults['failures']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => true,
|
||||
'advice' => false,
|
||||
'success' => false,
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_filestorage', false, array( 'sprintf' => $element->options['storageExtension'] ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( count( $this->application->modules( 'front' ) ) )
|
||||
{
|
||||
/* Check for Front Navigation items */
|
||||
$frontNavigation = $this->application->extensions( 'core', 'FrontNavigation', false );
|
||||
if( !count( $frontNavigation ) )
|
||||
{
|
||||
$scanResults['advice']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => false,
|
||||
'advice' => true,
|
||||
'success' => false,
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_frontnav' )
|
||||
);
|
||||
}
|
||||
|
||||
/* Check FURLs */
|
||||
$furls = $this->parseFurlsFile( ROOT_PATH . "/applications/" . $this->application->directory . "/data/furl.json" );
|
||||
if( !isset( $furls['pages'] ) or empty( $furls['pages'] ) )
|
||||
{
|
||||
$scanResults['advice']++;
|
||||
$scanResults['list'][] = array(
|
||||
'critical' => false,
|
||||
'advice' => true,
|
||||
'success' => false,
|
||||
'detail' => Member::loggedIn()->language()->addToStack( 'devscan__missing_furls' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sort so that critical errors are first */
|
||||
$resultList = $scanResults['list'];
|
||||
uasort( $resultList, function( $a, $b )
|
||||
{
|
||||
if( $a['critical'] and $b['critical'] )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
elseif( $a['critical'] and !$b['critical'] )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
Output::i()->json( array(
|
||||
'html' => Theme::i()->getTemplate( 'support' )->supportBlockList( $resultList ),
|
||||
'criticalIssues' => $scanResults['failures'],
|
||||
'recommendedIssues' => $scanResults['advice']
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for missing admin language strings
|
||||
*
|
||||
* @param array $languageStrings
|
||||
* @return array
|
||||
*/
|
||||
protected function _scanAdminStrings( array $languageStrings ) : array
|
||||
{
|
||||
$missingAdminStrings = [ 'menu' => [], 'restrictions' => [], 'keywords' => [], 'settings' => [], 'notifications' => [] ];
|
||||
|
||||
/* Start with the acp menu */
|
||||
$menu = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/acpmenu.json" );
|
||||
foreach( $menu as $module => $controllers )
|
||||
{
|
||||
$string = 'menu__' . $this->application->directory . '_' . $module;
|
||||
$hasSubMenuItems = false;
|
||||
|
||||
foreach( $controllers as $controller => $menuData )
|
||||
{
|
||||
if( !array_key_exists( 'menutab__' . $menuData['tab'], $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['menu'][] = 'menutab__' . $menuData['tab'];
|
||||
}
|
||||
if( !array_key_exists( 'menutab__' . $menuData['tab'] . '_icon', $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['menu'][] = 'menutab__' . $menuData['tab'] . '_icon';
|
||||
}
|
||||
|
||||
if( isset( $menuData['activemenuitem'] ) and $menuData['activemenuitem'] )
|
||||
{
|
||||
if( !array_key_exists( 'menu__' . $menuData['activemenuitem'], $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['menu'][] = 'menu__' . $menuData['activemenuitem'];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$hasSubMenuItems = true;
|
||||
if( !array_key_exists( $string . '_' . $controller, $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['menu'][] = $string . '_' . $controller;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( $hasSubMenuItems and !array_key_exists( $string, $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['menu'][] = $string;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check ACP restrictions */
|
||||
$restrictions = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/acprestrictions.json" );
|
||||
foreach( $restrictions as $module => $groups )
|
||||
{
|
||||
foreach( $groups as $group => $keys )
|
||||
{
|
||||
if( !array_key_exists( 'r__' . $group, $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['restrictions'][] = 'r__' . $group;
|
||||
}
|
||||
|
||||
foreach( $keys as $k => $v )
|
||||
{
|
||||
if( !array_key_exists( 'r__' . $k, $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['restrictions'][] = 'r__' . $k;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check search keywords */
|
||||
$keywords = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/acpsearch.json" );
|
||||
foreach( $keywords as $keyword )
|
||||
{
|
||||
if( !array_key_exists( $keyword['lang_key'], $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['keywords'][] = $keyword['lang_key'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Check Settings */
|
||||
$settings = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/settings.json" );
|
||||
foreach( $settings as $setting )
|
||||
{
|
||||
if( !array_key_exists( $setting['key'], $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['settings'][] = $setting['key'];
|
||||
}
|
||||
}
|
||||
|
||||
/* I really hate notifications */
|
||||
foreach( $this->application->extensions( 'core', 'Notifications', false ) as $extensionKey => $ext )
|
||||
{
|
||||
/* @var NotificationsAbstract $ext */
|
||||
$stringsToCheck = [];
|
||||
foreach( $ext::configurationOptions() as $key => $config )
|
||||
{
|
||||
if( isset( $config['adminDescription'] ) )
|
||||
{
|
||||
$stringsToCheck[] = $config['adminDescription'];
|
||||
}
|
||||
if( isset( $config['admin_lang'] ) )
|
||||
{
|
||||
$stringsToCheck = array_merge( $stringsToCheck, array_values( $config['admin_lang'] ) );
|
||||
}
|
||||
|
||||
foreach( $stringsToCheck as $string )
|
||||
{
|
||||
if( !array_key_exists( $string, $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['notifications'][] = $string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check Email Templates */
|
||||
if( file_exists( ROOT_PATH . "/applications/" . $this->application->directory . "/dev/email" ) )
|
||||
{
|
||||
foreach( new DirectoryIterator( ROOT_PATH . "/applications/" . $this->application->directory . "/dev/email" ) as $file )
|
||||
{
|
||||
if ( $file->isDir() or $file->isDot() )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$dot = strrpos( $file->getFilename(), '.' );
|
||||
$extension = strtolower( substr( $file->getFilename(), $dot + 1 ) );
|
||||
if ( $extension == 'txt' )
|
||||
{
|
||||
$emailKey = substr( $file->getFilename(), 0, $dot );
|
||||
if( !array_key_exists( 'emailtpl_' . $emailKey, $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['emails'][] = 'emailtpl_' . $emailKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* FileStorage extensions */
|
||||
foreach( $this->application->extensions( 'core', 'FileStorage', false ) as $extensionKey => $ext )
|
||||
{
|
||||
$stringToCheck = 'filestorage__' . $this->application->directory . '_' . $extensionKey;
|
||||
if( !array_key_exists( $stringToCheck, $languageStrings ) )
|
||||
{
|
||||
$missingAdminStrings['filestorage'][] = $stringToCheck;
|
||||
}
|
||||
}
|
||||
|
||||
/* re-scan in case another app has the string */
|
||||
return $this->_rescanStrings( $missingAdminStrings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan for missing front-end strings
|
||||
*
|
||||
* @param array $languageStrings
|
||||
* @return array
|
||||
*/
|
||||
protected function _scanFrontStrings( array $languageStrings ) : array
|
||||
{
|
||||
$missingFrontStrings = [ 'modules' => [], 'content' => [], 'contentform' => [],'widgets' => [], 'notifications' => [] ];
|
||||
|
||||
/* Check modules */
|
||||
foreach( $this->application->modules( 'front' ) as $module )
|
||||
{
|
||||
$string = 'module__' . $this->application->directory . '_' . $module->key;
|
||||
if( $module->key != 'ajax' and !array_key_exists( $string, $languageStrings ) )
|
||||
{
|
||||
$missingFrontStrings['modules'][] = $string;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check items/comments/reviews */
|
||||
foreach( $this->application->extensions( 'core', 'ContentRouter' ) as $ext )
|
||||
{
|
||||
/* @var Item $itemClass */
|
||||
foreach( $ext->classes as $itemClass )
|
||||
{
|
||||
$classes = [ $itemClass ];
|
||||
if( isset( $itemClass::$commentClass ) )
|
||||
{
|
||||
$classes[] = $itemClass::$commentClass;
|
||||
}
|
||||
if( isset( $itemClass::$reviewClass ) )
|
||||
{
|
||||
$classes[] = $itemClass::$reviewClass;
|
||||
}
|
||||
|
||||
foreach( $classes as $class )
|
||||
{
|
||||
$stringsToCheck = array( $class::$title, '__indefart_' . $class::$title, '__defart_' . $class::$title, $class::$title . '_pl', $class::$title . '_pl_lc' );
|
||||
|
||||
foreach( $stringsToCheck as $string )
|
||||
{
|
||||
if( !array_key_exists( $string, $languageStrings ) )
|
||||
{
|
||||
/* We might have a _plural instead of a _pl */
|
||||
if( $string == '__defart_' . $class::$title . '_pl' and array_key_exists( $string . 'ural', $languageStrings ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$missingFrontStrings['content'][] = $string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$class = $itemClass::$containerNodeClass;
|
||||
$stringsToCheck = array( $class::$nodeTitle, $class::$nodeTitle . '_sg', $class::$nodeTitle . '_sg_lc' );
|
||||
foreach( $stringsToCheck as $string )
|
||||
{
|
||||
if( !array_key_exists( $string, $languageStrings ) )
|
||||
{
|
||||
$missingFrontStrings['content'][] = $string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check form elements */
|
||||
foreach( $itemClass::formElements( null, null ) as $element )
|
||||
{
|
||||
/* Hard-coding a list of strings to skip, mainly because we
|
||||
have some nodes that are also items and this causes a whole bunch of issues here */
|
||||
if( in_array( $element->name, [ 'container'] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* @var Form\FormAbstract $element */
|
||||
if( !array_key_exists( $element->name, $languageStrings ) )
|
||||
{
|
||||
$missingFrontStrings['contentform'][] = $element->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check Widgets */
|
||||
$widgets = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/widgets.json" );
|
||||
foreach( $widgets as $k => $v )
|
||||
{
|
||||
if( !array_key_exists( 'block_' . $k, $languageStrings ) )
|
||||
{
|
||||
$missingFrontStrings['widgets'][] = 'block_' . $k;
|
||||
}
|
||||
}
|
||||
|
||||
/* Friggin notifications */
|
||||
foreach( $this->application->extensions( 'core', 'Notifications', false ) as $extensionKey => $ext )
|
||||
{
|
||||
/* @var NotificationsAbstract $ext */
|
||||
$stringsToCheck = [ 'notifications__' . $this->application->directory . '_' . $extensionKey ];
|
||||
foreach( $ext::configurationOptions() as $key => $config )
|
||||
{
|
||||
if( isset( $config['title'] ) )
|
||||
{
|
||||
$stringsToCheck[] = $config['title'];
|
||||
}
|
||||
if( isset( $config['description'] ) )
|
||||
{
|
||||
$stringsToCheck[] = $config['description'];
|
||||
}
|
||||
if( isset( $config['notificationTypes'] ) and count( $config['notificationTypes'] ) )
|
||||
{
|
||||
foreach( $config['notificationTypes'] as $type )
|
||||
{
|
||||
if( file_exists( ROOT_PATH . "/applications/" . $this->application->directory . "dev/email/notification_" . $type . ".phtml" ) )
|
||||
{
|
||||
$stringsToCheck[] = 'mailsub__' . $this->application->directory . '_notification_' . $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach( $stringsToCheck as $string )
|
||||
{
|
||||
if( !array_key_exists( $string, $languageStrings ) )
|
||||
{
|
||||
$missingFrontStrings['notifications'][] = $string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Admin notification emails */
|
||||
foreach( $this->application->extensions( 'core', 'AdminNotifications', false ) as $extensionKey => $ext )
|
||||
{
|
||||
$string = 'mailsub__' . $this->application->directory . '_acp_notification_' . $extensionKey;
|
||||
if( !array_key_exists( $string, $languageStrings ) )
|
||||
{
|
||||
$missingFrontStrings['notifications'][] = $string;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check Email Templates */
|
||||
if( file_exists( ROOT_PATH . "/applications/" . $this->application->directory . "/dev/email" ) )
|
||||
{
|
||||
foreach( new DirectoryIterator( ROOT_PATH . "/applications/" . $this->application->directory . "/dev/email" ) as $file )
|
||||
{
|
||||
if ( $file->isDir() or $file->isDot() )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$dot = strrpos( $file->getFilename(), '.' );
|
||||
$extension = strtolower( substr( $file->getFilename(), $dot + 1 ) );
|
||||
if ( $extension == 'txt' )
|
||||
{
|
||||
$emailKey = substr( $file->getFilename(), 0, $dot );
|
||||
if( !array_key_exists( 'mailsub__' . $this->application->directory . '_' . $emailKey, $languageStrings ) )
|
||||
{
|
||||
$missingFrontStrings['notifications'][] = 'mailsub__' . $this->application->directory . '_' . $emailKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* re-scan in case another app has the string */
|
||||
return $this->_rescanStrings( $missingFrontStrings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check missing strings against other apps
|
||||
*
|
||||
* @param array $missingStrings
|
||||
* @return array
|
||||
*/
|
||||
protected function _rescanStrings( array $missingStrings ) : array
|
||||
{
|
||||
foreach( $missingStrings as $group => $missing )
|
||||
{
|
||||
$missing = array_unique( $missing );
|
||||
if( count( $missing ) )
|
||||
{
|
||||
foreach( $missing as $index => $string )
|
||||
{
|
||||
if( Member::loggedIn()->language()->checkKeyExists( $string ) )
|
||||
{
|
||||
unset( $missing[ $index ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Do we still have anything missing? */
|
||||
if( count( $missing ) )
|
||||
{
|
||||
$missingStrings[ $group ] = $missing;
|
||||
}
|
||||
else
|
||||
{
|
||||
unset( $missingStrings[ $group ] );
|
||||
}
|
||||
}
|
||||
|
||||
return $missingStrings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the menu file to check for the presence of search keywords
|
||||
*
|
||||
* @param array $languageStrings
|
||||
* @return array
|
||||
*/
|
||||
protected function _scanSearchKeywords( array $languageStrings=array() ) : array
|
||||
{
|
||||
$missingKeywords = [ 'menu' => [], 'settings' => [] ];
|
||||
|
||||
/* Load the current search keyword strings */
|
||||
$keywords = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/acpsearch.json" );
|
||||
$existingKeywords = [];
|
||||
foreach( $keywords as $k => $v )
|
||||
{
|
||||
$existingKeywords[] = $v['lang_key'];
|
||||
}
|
||||
|
||||
/* First check the menu items */
|
||||
$menu = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/acpmenu.json" );
|
||||
foreach( $menu as $module => $controllers )
|
||||
{
|
||||
$hasSubMenuItems = false;
|
||||
foreach( $controllers as $controller => $menuData )
|
||||
{
|
||||
/* Skip the dev center */
|
||||
if( $menuData['tab'] == 'developer' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$string = 'menu__' . ( $menuData['activemenuitem'] ?? $this->application->directory . '_' . $module . '_' . $controller );
|
||||
if( !in_array( $string, $existingKeywords ) )
|
||||
{
|
||||
/* Maybe we are referencing the URI somewhere with a different key */
|
||||
$uri = "app=" . $this->application->directory . "&module=" . ( $menuData['module_url'] ?? $module ) . "&controller=" . $controller . ( ( isset( $menuData['do'] ) and $menuData['do'] ) ? "&do=" . $menuData['do'] : '' );
|
||||
if( !array_key_exists( $uri, $keywords ) )
|
||||
{
|
||||
$missingKeywords['menu'][] = $string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Now check settings */
|
||||
$settings = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/settings.json" );
|
||||
foreach( $settings as $setting )
|
||||
{
|
||||
/* Skip settings that don't have a language string, because those are likely hidden */
|
||||
if( array_key_exists( $setting['key'], $languageStrings ) and !in_array( $setting['key'], $existingKeywords ) )
|
||||
{
|
||||
/* Skip sitemap settings */
|
||||
if( str_starts_with( $setting['key'], 'sitemap' ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if the setting is somewhere in the list */
|
||||
$found = false;
|
||||
foreach( array_keys( $keywords ) as $uri )
|
||||
{
|
||||
if( strpos( $uri, "searchResult={$setting['key']}" ) !== false )
|
||||
{
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( !$found )
|
||||
{
|
||||
//$missingKeywords['settings'][] = $setting['key'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Clear empty sections */
|
||||
foreach( $missingKeywords as $group => $missing )
|
||||
{
|
||||
if( !count( $missing ) )
|
||||
{
|
||||
unset( $missingKeywords[ $group ] );
|
||||
}
|
||||
}
|
||||
|
||||
return $missingKeywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the ACP Search keywords and check for duplicates
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function _scanSearchKeywordsForDupes() : array
|
||||
{
|
||||
$usedLangs = [];
|
||||
$dupes = [];
|
||||
$usedParsedLangs = [];
|
||||
$dupeParsedLangs = [];
|
||||
$acpSearch = $this->_getJson( ROOT_PATH . '/applications/' . $this->application->directory . '/data/acpsearch.json' );
|
||||
foreach( $acpSearch as $url => $data )
|
||||
{
|
||||
if( array_key_exists( $data['lang_key'], $usedLangs ) )
|
||||
{
|
||||
if( !array_key_exists( $data['lang_key'], $dupes ) )
|
||||
{
|
||||
$dupes[ $data['lang_key'] ] = [ $usedLangs[ $data['lang_key'] ] ];
|
||||
}
|
||||
$dupes[ $data['lang_key'] ][] = $url;
|
||||
}
|
||||
else
|
||||
{
|
||||
$usedLangs[ $data['lang_key'] ] = $url;
|
||||
}
|
||||
|
||||
if( Member::loggedIn()->language()->checkKeyExists( $data['lang_key'] ) )
|
||||
{
|
||||
$parsedLangKey = Member::loggedIn()->language()->get( $data['lang_key'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$parsedLangKey = $data['lang_key'];
|
||||
}
|
||||
|
||||
if( array_key_exists( $parsedLangKey, $usedParsedLangs ) )
|
||||
{
|
||||
if( !array_key_exists( $parsedLangKey, $dupeParsedLangs ) )
|
||||
{
|
||||
$dupeParsedLangs[ $parsedLangKey ] = [
|
||||
$usedParsedLangs[ $parsedLangKey ]['key'] => [ $usedParsedLangs[ $parsedLangKey ]['url'] ]
|
||||
];
|
||||
}
|
||||
|
||||
if( !array_key_exists( $data['lang_key'], $dupeParsedLangs[ $parsedLangKey ] ) )
|
||||
{
|
||||
$dupeParsedLangs[ $parsedLangKey ][ $data['lang_key'] ] = [];
|
||||
}
|
||||
|
||||
$dupeParsedLangs[ $parsedLangKey ][ $data['lang_key'] ][] = $url;
|
||||
}
|
||||
else
|
||||
{
|
||||
$usedParsedLangs[ $parsedLangKey ] = [
|
||||
'key' => $data['lang_key'],
|
||||
'url' => $url
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$return = array_keys( $dupes );
|
||||
foreach( $dupeParsedLangs as $parsedLangKey => $data )
|
||||
{
|
||||
foreach( $data as $langKey => $urls )
|
||||
{
|
||||
if( !in_array( $langKey, $return ) )
|
||||
{
|
||||
$return[] = $langKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the email templates against the templates
|
||||
* in the dev/email folder
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function _checkEmailTemplates() : array
|
||||
{
|
||||
/* First build a list of email templates that we have */
|
||||
$templates = [];
|
||||
$emailFolder = ROOT_PATH . "/applications/" . $this->application->directory . "/dev/email";
|
||||
|
||||
/* If we don't have an email folder, then we are missing all of them */
|
||||
if( file_exists( $emailFolder ) )
|
||||
{
|
||||
foreach( new DirectoryIterator( $emailFolder ) as $file )
|
||||
{
|
||||
if( $file->isDir() or $file->isDot() )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$dot = strrpos( $file->getFilename(), '.' );
|
||||
$extension = strtolower( substr( $file->getFilename(), $dot + 1 ) );
|
||||
if ( $extension == 'txt' )
|
||||
{
|
||||
$templates[] = substr( $file->getFilename(), 0, $dot );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check the notifications */
|
||||
$missingTemplates = [];
|
||||
foreach( $this->application->extensions( 'core', 'Notifications', false ) as $extensionKey => $ext )
|
||||
{
|
||||
/* @var NotificationsAbstract $ext */
|
||||
foreach( $ext::configurationOptions() as $key => $config )
|
||||
{
|
||||
/* Don't bother with notifications that don't support email */
|
||||
if( isset( $config['disabled'] ) and in_array( 'email', $config['disabled'] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( isset( $config['notificationTypes'] ) and count( $config['notificationTypes'] ) )
|
||||
{
|
||||
foreach( $config['notificationTypes'] as $type )
|
||||
{
|
||||
if( !in_array( 'notification_' . $type, $templates ) )
|
||||
{
|
||||
$missingTemplates[] = 'notification_' . $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $missingTemplates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief extensions
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use DirectoryIterator;
|
||||
use IPS\Application;
|
||||
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 function defined;
|
||||
use function sprintf;
|
||||
use const IPS\IPS_FOLDER_PERMISSION;
|
||||
use const IPS\REBUILD_QUICK;
|
||||
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() );
|
||||
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() );
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=extensions&appKey={$this->application->directory}" ), 'deleted' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief furls
|
||||
* @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
|
||||
|
||||
* @since 05 Feb 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use IPS\Application;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Form\YesNo;
|
||||
use IPS\Helpers\Tree\Tree;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Theme;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* furls
|
||||
*/
|
||||
class furls extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $json = [];
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$this->json = $this->_getFurls();
|
||||
|
||||
$tree = new Tree(
|
||||
$this->url,
|
||||
'dev_furls',
|
||||
array( $this, '_getFurlKeys' ),
|
||||
array( $this, '_getFurlRow' ),
|
||||
function( $folder ){
|
||||
return null;
|
||||
},
|
||||
null,
|
||||
function(){
|
||||
return array(
|
||||
'add' => array(
|
||||
'icon' => 'plus',
|
||||
'title' => 'add',
|
||||
'link' => $this->url->setQueryString( 'do', 'form' )
|
||||
),
|
||||
'top' => array(
|
||||
'icon' => 'edit',
|
||||
'title' => 'dev_furl_toplevel_edit',
|
||||
'link' => $this->url->setQueryString( 'do', 'topLevel' ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack( 'dev_furl_toplevel_edit' ) )
|
||||
)
|
||||
);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
Output::i()->headerMessage = Theme::i()->getTemplate( 'global' )->message( 'dev_furls_info', 'info i-margin-bottom_1' );
|
||||
|
||||
Output::i()->output = (string) $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* FURL Roots
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function _getFurlKeys() : array
|
||||
{
|
||||
$roots = [];
|
||||
foreach( $this->json['pages'] as $key => $data )
|
||||
{
|
||||
/* Replace all underscores because otherwise the sortable JS does not pick up the correct key */
|
||||
$roots[ str_replace( '_', '|', $key ) ] = $this->_getFurlRow( $key, $data );
|
||||
}
|
||||
return $roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* FURL Rows
|
||||
*
|
||||
* @param string $key
|
||||
* @param array|null $data
|
||||
* @return string
|
||||
*/
|
||||
public function _getFurlRow( string $key, ?array $data ) : string
|
||||
{
|
||||
$key = str_replace( '|', '_', $key );
|
||||
if( empty( $data ) )
|
||||
{
|
||||
$data = $this->json[ 'pages' ][ $key ] ?? array();
|
||||
}
|
||||
|
||||
$allKeys = array_keys( $this->json['pages'] );
|
||||
$position = array_search( $key, $allKeys ) + 1;
|
||||
|
||||
return Theme::i()->getTemplate( 'trees' )->row(
|
||||
$this->url,
|
||||
str_replace( '_', '|', $key ),
|
||||
$key,
|
||||
false,
|
||||
array(
|
||||
'edit' => array(
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'form', 'key' => $key ) )
|
||||
),
|
||||
'delete' => array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'delete', 'key' => $key ) )->csrf(),
|
||||
'data' => array( 'confirm' => '' )
|
||||
)
|
||||
),
|
||||
Theme::i()->getTemplate( 'developer', 'core' )->furlRowDescription( $data, $this->json['topLevel'] ),
|
||||
null,
|
||||
$position,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function search() : void
|
||||
{
|
||||
$rows = [];
|
||||
$this->json = $this->_getFurls();
|
||||
foreach( $this->json['pages'] as $key => $data )
|
||||
{
|
||||
if( stripos( $data['friendly'], Request::i()->input ) !== false or ( isset( $data['alias'] ) and stripos( $data['alias'], Request::i()->input ) !== false ) or stripos( $data['real'], Request::i()->input ) !== false )
|
||||
{
|
||||
$rows[] = $this->_getFurlRow( $key, $data );
|
||||
}
|
||||
}
|
||||
|
||||
Output::i()->output = Theme::i()->getTemplate( 'trees', 'core' )->rows( $rows, '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function reorder() : void
|
||||
{
|
||||
$json = $this->_getFurls();
|
||||
$originalList = $json['pages'];
|
||||
$json['pages'] = [];
|
||||
foreach( Request::i()->ajax_order as $key => $empty )
|
||||
{
|
||||
$key = str_replace( '|', '_', $key );
|
||||
$json['pages'][ $key ] = $originalList[ $key ];
|
||||
}
|
||||
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/furl.json", $json );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function form() : void
|
||||
{
|
||||
$jsonFile = ROOT_PATH . "/applications/{$this->application->directory}/data/furl.json";
|
||||
$json = $this->_getFurls();
|
||||
|
||||
$furlKey = Request::i()->key ?? null;
|
||||
$row = ( $furlKey !== null and isset( $json['pages'][ $furlKey ] ) ) ? $json['pages'][ $furlKey ] : array();
|
||||
|
||||
$self = $this;
|
||||
$form = new Form;
|
||||
$form->add( new Text( 'dev_furl_key', $furlKey, true, array(), function( $val ) use ( $self, $furlKey, $json ){
|
||||
/* Check the current app first, but only if we're adding a new key */
|
||||
if( $furlKey === null and isset( $json['pages'][ $val ] ) )
|
||||
{
|
||||
throw new InvalidArgumentException( "err__dev_duplicate_furl_key" );
|
||||
}
|
||||
|
||||
/* Now loop through all other apps and check for duplicate keys */
|
||||
foreach( Application::applications() as $application )
|
||||
{
|
||||
if( $application->directory != $self->application->directory )
|
||||
{
|
||||
$furls = $self->parseFurlsFile( ROOT_PATH . "/applications/{$application->directory}/data/furl.json" );
|
||||
if( isset( $furls['pages'][ $val ] ) )
|
||||
{
|
||||
throw new InvalidArgumentException( "err__dev_duplicate_furl_key" );
|
||||
}
|
||||
}
|
||||
}
|
||||
} ) );
|
||||
|
||||
|
||||
$form->add( new Text( 'dev_furl_friendly', $row['friendly'] ?? null, true, array(), function( $val ) use ( $self, $json ){
|
||||
/* Do we have a duplicate in this app? */
|
||||
foreach( $json['pages'] as $key => $data )
|
||||
{
|
||||
if( $data['friendly'] == $val and $key != Request::i()->dev_furl_key )
|
||||
{
|
||||
throw new InvalidArgumentException( "err__dev_duplicate_furl_friendly" );
|
||||
}
|
||||
}
|
||||
|
||||
/* Check other apps for duplicates, this time including the topLevel in the test */
|
||||
$val = ( $json['topLevel'] ? $json['topLevel'] . '/' : '' ) . $val;
|
||||
foreach( Application::applications() as $application )
|
||||
{
|
||||
if( $application->directory != $self->application->directory )
|
||||
{
|
||||
$furls = $self->parseFurlsFile( ROOT_PATH . "/applications/{$application->directory}/data/furl.json" );
|
||||
if( isset( $furls['pages'] ) )
|
||||
{
|
||||
foreach( $furls['pages'] as $key => $data )
|
||||
{
|
||||
$test = ( $furls['topLevel'] ? $furls['topLevel'] . '/' : '' ) . $data['friendly'];
|
||||
if( $test == $val )
|
||||
{
|
||||
throw new InvalidArgumentException( "err__dev_duplicate_furl_friendly" );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, ( $json['topLevel'] ? $json['topLevel'] . '/' : '' ) ) );
|
||||
$form->add( new Text( 'dev_furl_real', $row['real'] ?? null, true ) );
|
||||
$form->add( new Text( 'dev_furl_verify', $row['verify'] ?? null, false ) );
|
||||
$form->add( new Text( 'dev_furl_alias', $row['alias'] ?? null, false, array(), function( $val ) use ( $self, $furlKey, $json ){
|
||||
if( $val )
|
||||
{
|
||||
/* Does the alias match the FURL? */
|
||||
if( $val == Request::i()->dev_furl_friendly )
|
||||
{
|
||||
throw new InvalidArgumentException( 'err__dev_equal_alias' );
|
||||
}
|
||||
|
||||
/* Check the current app */
|
||||
foreach( $json['pages'] as $k => $v )
|
||||
{
|
||||
if( $k != $furlKey and $v['alias'] == $val )
|
||||
{
|
||||
throw new InvalidArgumentException( "err__dev_duplicate_alias" );
|
||||
}
|
||||
}
|
||||
|
||||
/* Now loop through all other apps and check for duplicate aliases */
|
||||
foreach( Application::applications() as $application )
|
||||
{
|
||||
if( $application->directory != $self->application->directory )
|
||||
{
|
||||
$furls = $self->parseFurlsFile( ROOT_PATH . "/applications/{$application->directory}/data/furl.json" );
|
||||
foreach( $furls['pages'] as $k => $v )
|
||||
{
|
||||
if( $v['alias'] == $val )
|
||||
{
|
||||
throw new InvalidArgumentException( "err__dev_duplicate_alias" );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ) );
|
||||
$form->add( new YesNo( 'dev_furl_seopagination', $row['seoPagination'] ?? false, false ) );
|
||||
|
||||
$form->addHtml( Theme::i()->getTemplate( 'forms' )->blurb( 'dev_furl_seotitle_class_info', true, true ) );
|
||||
$form->add( new Text( 'dev_furl_seotitle_class', isset( $row['seoTitles'] ) ? mb_substr( $row['seoTitles'][0]['class'], 5 ) : null, false, array(), function( $val ){
|
||||
if( empty( $val ) and ( Request::i()->dev_furl_seotitle_id or Request::i()->dev_furl_seotitle_property ) )
|
||||
{
|
||||
throw new InvalidArgumentException( 'form_required' );
|
||||
}
|
||||
}, 'IPS\\' ) );
|
||||
$form->add( new Text( 'dev_furl_seotitle_id', isset( $row['seoTitles'] ) ? $row['seoTitles'][0]['queryParam'] : null, false, array(), function( $val ){
|
||||
if( empty( $val ) and ( Request::i()->dev_furl_seotitle_class or Request::i()->dev_furl_seotitle_property ) )
|
||||
{
|
||||
throw new InvalidArgumentException( 'form_required' );
|
||||
}
|
||||
} ) );
|
||||
$form->add( new Text( 'dev_furl_seotitle_property', isset( $row['seoTitles'] ) ? $row['seoTitles'][0]['property'] : null, false, array(), function( $val ){
|
||||
if( empty( $val ) and ( Request::i()->dev_furl_seotitle_class or Request::i()->dev_furl_seotitle_id ) )
|
||||
{
|
||||
throw new InvalidArgumentException( 'form_required' );
|
||||
}
|
||||
} ) );
|
||||
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
/* If we changed the key, remove the old one */
|
||||
if( $furlKey !== null and $values['dev_furl_key'] != $furlKey )
|
||||
{
|
||||
unset( $json['pages'][ $furlKey ] );
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'friendly' => $values['dev_furl_friendly'],
|
||||
'real' => $values['dev_furl_real']
|
||||
);
|
||||
|
||||
if( $values['dev_furl_verify'] )
|
||||
{
|
||||
$data['verify'] = $values['dev_furl_verify'];
|
||||
}
|
||||
|
||||
if( $values['dev_furl_alias'] )
|
||||
{
|
||||
$data['alias'] = $values['dev_furl_alias'];
|
||||
}
|
||||
|
||||
if( $values['dev_furl_seopagination'] )
|
||||
{
|
||||
$data['seoPagination'] = true;
|
||||
}
|
||||
|
||||
/* Check for SEO Title */
|
||||
if( $values['dev_furl_seotitle_class'] and $values['dev_furl_seotitle_id'] and $values['dev_furl_seotitle_property'] )
|
||||
{
|
||||
$data['seoTitles'] = [
|
||||
[
|
||||
'class' => '\\IPS\\' . $values['dev_furl_seotitle_class'],
|
||||
'queryParam' => $values['dev_furl_seotitle_id'],
|
||||
'property' => $values['dev_furl_seotitle_property']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$json['pages'][ $values['dev_furl_key'] ] = $data;
|
||||
|
||||
$this->_writeJson( $jsonFile, $json );
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function delete() : void
|
||||
{
|
||||
Request::i()->confirmedDelete();
|
||||
|
||||
$json = $this->_getFurls();
|
||||
if( isset( $json['pages'][ Request::i()->key ] ) )
|
||||
{
|
||||
unset( $json['pages'][ Request::i()->key ] );
|
||||
}
|
||||
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/furl.json", $json );
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the top-level for the FURLs of this app
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function topLevel() : void
|
||||
{
|
||||
$jsonFile = ROOT_PATH . "/applications/{$this->application->directory}/data/furl.json";
|
||||
$json = $this->_getFurls();
|
||||
|
||||
$form = new Form;
|
||||
$form->add( new Text( 'dev_furl_toplevel', $json['topLevel'], false ) );
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
$json['topLevel'] = $values['dev_furl_toplevel'];
|
||||
$this->_writeJson( $jsonFile, $json );
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and parse the FURL json file.
|
||||
* We need to account for the very long illegal comments in there
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function _getFurls() : array
|
||||
{
|
||||
$file = ROOT_PATH . "/applications/{$this->application->directory}/data/furl.json";
|
||||
if( !file_exists( $file ) )
|
||||
{
|
||||
return array(
|
||||
'topLevel' => '',
|
||||
'pages' => array()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->parseFurlsFile( $file );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief listeners
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use DomainException;
|
||||
use IPS\Application;
|
||||
use IPS\Data\Store;
|
||||
use IPS\Developer;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Events\ListenerType;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Table\Custom;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use OutOfRangeException;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* listeners
|
||||
*/
|
||||
class listeners extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$json = ROOT_PATH . "/applications/" . $this->application->directory . "/data/listeners.json";
|
||||
if ( !file_exists( $json ) )
|
||||
{
|
||||
file_put_contents( $json, json_encode( array() ) );
|
||||
}
|
||||
|
||||
$data = array();
|
||||
foreach ( json_decode( file_get_contents( $json ), TRUE ) as $k => $f )
|
||||
{
|
||||
$data[ $k ] = array(
|
||||
'dev_listeners_filename' => $k,
|
||||
'dev_listeners_fires' => $f['classname']::getExtendedClass()
|
||||
);
|
||||
}
|
||||
|
||||
$table = new Custom( $data, Url::internal( "app=core&module=developer&controller=listeners&appKey=" . $this->application->directory ) );
|
||||
$table->include = array( 'dev_listeners_filename', 'dev_listeners_fires' );
|
||||
$table->sortBy = $table->sortBy ?: 'dev_listeners_filename';
|
||||
$table->sortDirection = $table->sortDirection ?: 'asc';
|
||||
$table->rootButtons = array(
|
||||
'add' => array(
|
||||
'icon' => 'plus',
|
||||
'title' => 'add',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=listeners&do=listenerAdd&appKey=" . $this->application->directory ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('add') )
|
||||
)
|
||||
);
|
||||
|
||||
$table->rowButtons = function( $row )
|
||||
{
|
||||
$buttons = [];
|
||||
$path = ROOT_PATH . "/applications/" . $this->application->directory . '/listeners/' . $row['dev_listeners_filename'] . '.php';
|
||||
|
||||
if( $ideLink = Developer::getIdeHref( $path ) )
|
||||
{
|
||||
$buttons['ide'] = [
|
||||
'icon' => 'fa-file-code',
|
||||
'title' => 'open_in_ide',
|
||||
'link' => $ideLink
|
||||
];
|
||||
};
|
||||
$buttons['delete'] = array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => Url::internal( "app=core&module=developer&controller=listeners&do=listenerDelete&appKey=" . $this->application->directory . "&key=" . $row['dev_listeners_filename'] )->csrf(),
|
||||
'data' => array( 'delete' => '' )
|
||||
);
|
||||
return $buttons;
|
||||
};
|
||||
|
||||
Output::i()->output = (string) $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function listenerAdd() : void
|
||||
{
|
||||
$form = new Form;
|
||||
|
||||
$types = array();
|
||||
$toggles = array();
|
||||
foreach( ListenerType::listenerTypes() as $k => $v )
|
||||
{
|
||||
/** @var $v ListenerType */
|
||||
$types[$k] = 'dev_listeners_' . $k;
|
||||
if( $v::$requiresClassDeclaration )
|
||||
{
|
||||
$toggles[$k] = array( 'dev_listeners_classname' );
|
||||
}
|
||||
}
|
||||
|
||||
$form->add( new Form\Select( 'dev_listeners_type', NULL, TRUE, array(
|
||||
'options' => $types,
|
||||
'toggles' => $toggles
|
||||
) ) );
|
||||
$form->add( new Text( 'dev_listeners_classname', NULL, NULL, array(), function( $val )
|
||||
{
|
||||
$class = 'IPS\\' . $val;
|
||||
if( !class_exists( $class ) )
|
||||
{
|
||||
throw new DomainException( 'dev_listeners_not_supported' );
|
||||
}
|
||||
|
||||
/* @var ListenerType $listenerType */
|
||||
$listenerType = 'IPS\\Events\\ListenerType\\' . Request::i()->dev_listeners_type;
|
||||
if( !$listenerType::supportsObject( $class ) )
|
||||
{
|
||||
throw new DomainException( 'dev_listeners_not_supported' );
|
||||
}
|
||||
|
||||
}, 'IPS\\', NULL, 'dev_listeners_classname' ) );
|
||||
$form->add( new Text( 'dev_listeners_filename', NULL, TRUE, array( 'regex' => '/^[a-z0-9_]*$/i' ) ) );
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$json = ROOT_PATH . "/applications/" . $this->application->directory . "/data/listeners.json";
|
||||
$listenerDirectory = ROOT_PATH . "/applications/" . $this->application->directory . "/listeners";
|
||||
|
||||
/* @var ListenerType $baseListener */
|
||||
$baseListener = 'IPS\\Events\\ListenerType\\' . $values['dev_listeners_type'];
|
||||
$extendedClass = ( isset( $values['dev_listeners_classname'] ) AND $values['dev_listeners_classname'] ) ? 'IPS\\' . $values['dev_listeners_classname'] : NULL;
|
||||
|
||||
/* Write PHP file */
|
||||
$listenerFile = $listenerDirectory . "/{$values['dev_listeners_filename']}.php";
|
||||
if( !file_exists( $listenerFile ) )
|
||||
{
|
||||
if( !is_dir( $listenerDirectory ) )
|
||||
{
|
||||
mkdir( $listenerDirectory );
|
||||
chmod( $listenerDirectory, IPS_FOLDER_PERMISSION );
|
||||
}
|
||||
|
||||
file_put_contents( $listenerFile, str_replace(
|
||||
array(
|
||||
'{app}',
|
||||
'{filename}',
|
||||
'{date}',
|
||||
'{class}'
|
||||
),
|
||||
array(
|
||||
$this->application->directory,
|
||||
$values['dev_listeners_filename'],
|
||||
date( 'd M Y' ),
|
||||
'\\' . ( $extendedClass ? $extendedClass . '::class' : '' )
|
||||
),
|
||||
file_get_contents( ROOT_PATH . "/applications/core/data/defaults/listeners/{$values['dev_listeners_type']}.txt" )
|
||||
) );
|
||||
}
|
||||
|
||||
/* Update JSON file */
|
||||
$listeners = json_decode( file_get_contents( $json ), TRUE );
|
||||
$listeners[ $values['dev_listeners_filename'] ] = array(
|
||||
'type' => $values['dev_listeners_type'],
|
||||
'classname' => 'IPS\\' . $this->application->directory . '\\listeners\\' . $values['dev_listeners_filename'],
|
||||
'extends' => ( $extendedClass ?: $baseListener::getExtendedClass() )
|
||||
);
|
||||
Application::writeJson( $json, $listeners );
|
||||
|
||||
/* Clear listener cache */
|
||||
try
|
||||
{
|
||||
unset( Store::i()->listeners );
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
|
||||
/* Redirect */
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=listeners&appKey=" . $this->application->directory ), 'saved' );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a listener
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function listenerDelete() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
$json = ROOT_PATH . "/applications/" . $this->application->directory . "/data/listeners.json";
|
||||
$listenerDirectory = ROOT_PATH . "/applications/" . $this->application->directory . "/listeners";
|
||||
|
||||
$listeners = json_decode( file_get_contents( $json ), TRUE );
|
||||
if ( array_key_exists( Request::i()->key, $listeners ) )
|
||||
{
|
||||
unset( $listeners[ Request::i()->key ] );
|
||||
Application::writeJson( $json, $listeners );
|
||||
|
||||
if ( file_exists( $listenerDirectory . "/" . Request::i()->key . ".php" ) )
|
||||
{
|
||||
unlink( $listenerDirectory . "/" . Request::i()->key . ".php" );
|
||||
}
|
||||
|
||||
/* Clear listener cache */
|
||||
try
|
||||
{
|
||||
unset( Store::i()->listeners );
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=listeners&do=listenerDelete&appKey=" . $this->application->directory ), 'saved' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief modulesadmin
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use DirectoryIterator;
|
||||
use DomainException;
|
||||
use IPS\Application\Module;
|
||||
use IPS\Developer;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Select;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Form\YesNo;
|
||||
use IPS\Helpers\Tree\Tree;
|
||||
use IPS\Http\Url;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Theme;
|
||||
use OutOfRangeException;
|
||||
use function defined;
|
||||
use const IPS\FILE_PERMISSION_NO_WRITE;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* modulesadmin
|
||||
*/
|
||||
class modules extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
/* Get modules */
|
||||
$appKey = $this->application->directory;
|
||||
$modules = $this->_getModules();
|
||||
$location = Request::i()->location ?? 'front';
|
||||
|
||||
$this->header = 'dev_modules' . $location;
|
||||
$this->url = $this->url->setQueryString( 'location', $location );
|
||||
|
||||
/* Show tree */
|
||||
$url = $this->url;
|
||||
$tree = new Tree(
|
||||
$url,
|
||||
Member::loggedIn()->language()->addToStack('dev_modules', FALSE, array( 'sprintf' => array( ucwords( $location ) ) ) ),
|
||||
/* Get Roots */
|
||||
function() use ( $appKey, $location, $modules, $url )
|
||||
{
|
||||
$rows = array();
|
||||
|
||||
if( !empty($modules[ $location ]) AND is_array($modules[ $location ]) )
|
||||
{
|
||||
foreach ( $modules[ $location ] as $k => $module )
|
||||
{
|
||||
$rows[ $k ] = Theme::i()->getTemplate( 'trees' )->row( $url, $k, $k, TRUE, array(
|
||||
'default'=> array(
|
||||
'icon' => ( array_key_exists( 'default', $module ) ) ? ( $module['default'] ? 'star' : 'regular fa-star' ) : 'regular fa-star',
|
||||
'title' => 'make_default_module',
|
||||
'link' => $url->setQueryString( array( 'do' => 'setDefaultModule', 'id' => $module['id'] ) )->csrf(),
|
||||
),
|
||||
'add' => array(
|
||||
'icon' => 'plus-circle',
|
||||
'title' => 'modules_add_controller',
|
||||
'link' => $url->setQueryString( array( 'do' => 'addController', 'module_key' => $k ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('modules_add_controller') )
|
||||
),
|
||||
'edit' => array(
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => $url->setQueryString( array( 'do' => 'moduleForm', 'key' => $k ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('edit') ),
|
||||
'hotkey' => 'e'
|
||||
),
|
||||
'delete' => array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $url->setQueryString( array( 'do' => 'deleteModule', 'key' => $k ) ),
|
||||
'data' => array( 'delete' => '' )
|
||||
)
|
||||
), "", NULL, NULL );
|
||||
}
|
||||
}
|
||||
return $rows;
|
||||
},
|
||||
/* Get Row */
|
||||
function( $key, $root=FALSE ) use ( $url, $appKey, $location )
|
||||
{
|
||||
return Theme::i()->getTemplate( 'trees' )->row( $url, $key, $key, TRUE, array(
|
||||
'add' => array(
|
||||
'icon' => 'plus-circle',
|
||||
'title' => 'modules_add_controller',
|
||||
'link' => $url->setQueryString( array( 'do' => 'addController', 'module_key' => $key ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('modules_add_controller') )
|
||||
),
|
||||
'edit' => array(
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => $url->setQueryString( array( 'do' => 'moduleForm', 'key' => $key ) ),
|
||||
'data' => array( 'ipDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('edit') ),
|
||||
'hotkey' => 'e'
|
||||
),
|
||||
'delete' => array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $url->setQueryString( array( 'do' => 'deleteModule', 'key' => $key ) ),
|
||||
'data' => array( 'delete' => '' )
|
||||
)
|
||||
), '', NULL, NULL, $root );
|
||||
},
|
||||
/* Get Row's Parent ID */
|
||||
function( $id )
|
||||
{
|
||||
return NULL;
|
||||
},
|
||||
/* Get Children */
|
||||
function( $key ) use ( $appKey, $location, $modules, $url )
|
||||
{
|
||||
$rows = array();
|
||||
foreach ( new DirectoryIterator( ROOT_PATH . "/applications/{$appKey}/modules/{$location}/{$key}" ) as $controller )
|
||||
{
|
||||
if ( $controller->isFile() and substr( $controller, 0, 1 ) !== '.' and $controller->getFilename() !== 'index.html' )
|
||||
{
|
||||
$buttons = [];
|
||||
if( $ideLink = Developer::getIdeHref( $controller->getPathname() ) )
|
||||
{
|
||||
$buttons['ide'] = [
|
||||
'icon' => 'fa-file-code',
|
||||
'title' => 'open_in_ide',
|
||||
'link' => $ideLink
|
||||
];
|
||||
};
|
||||
$buttons['default'] = array(
|
||||
'icon' => ( str_replace( '.php', '', $controller ) == $modules[ $location ][ $key ]['default_controller'] ) ? 'fa-star' : 'regular fa-star',
|
||||
'title' => 'modules_make_default',
|
||||
'link' => $url->setQueryString( array( 'do' => 'setDefaultController', 'root' => $key, 'default' => (string)$controller ) )->csrf(),
|
||||
);
|
||||
|
||||
$buttons[ 'delete'] = array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $url->setQueryString( array( 'do' => 'deleteController', 'root' => $key, 'delete' => (string) $controller ) )->csrf(),
|
||||
'data' => array( 'delete' => '' )
|
||||
);
|
||||
|
||||
$rows[] = Theme::i()->getTemplate( 'trees' )->row( $url, $controller, $controller, FALSE, $buttons, '', NULL, NULL, FALSE, NULL, NULL, ( $modules[ $location ][ $key ]['default_controller'] == $controller ? array( 'green', 'modules_default' ) : NULL ) );
|
||||
}
|
||||
}
|
||||
return $rows;
|
||||
},
|
||||
/* Get Root Buttons */
|
||||
function() use ( $appKey, $location, $url )
|
||||
{
|
||||
return array(
|
||||
'add' => array(
|
||||
'icon' => 'plus',
|
||||
'title' => 'modules_add',
|
||||
'link' => $url->setQueryString( 'do', 'moduleForm' ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('modules_add') )
|
||||
),
|
||||
);
|
||||
},
|
||||
FALSE,
|
||||
TRUE,
|
||||
TRUE
|
||||
);
|
||||
|
||||
Output::i()->output = (string) $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make this the default module
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setDefaultModule() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
try
|
||||
{
|
||||
$module = Module::load( Request::i()->id );
|
||||
$module->setAsDefault();
|
||||
|
||||
$this->_writeModules( $this->_getModules() );
|
||||
}
|
||||
catch ( OutOfRangeException $e )
|
||||
{
|
||||
Output::i()->error( 'no_module_for_default', '2C133/A', 403, '' );
|
||||
}
|
||||
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=modules&appKey={$module->application}&location={$module->area}" ), 'saved' );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add/Edit Module
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function moduleForm() : void
|
||||
{
|
||||
/* Get JSON */
|
||||
$modules = $this->_getModules();
|
||||
$application = $this->application;
|
||||
$location = Request::i()->location;
|
||||
|
||||
/* Load existing module if we're editing */
|
||||
if ( Request::i()->key )
|
||||
{
|
||||
if ( !isset( $modules[ $location ][ Request::i()->key ] ) )
|
||||
{
|
||||
Output::i()->error( 'node_error', '2C103/J', 404, '' );
|
||||
}
|
||||
$current = array( 'module_key' => Request::i()->key, 'protected' => $modules[ $location ][ Request::i()->key ]['protected'], 'default_controller' => $modules[ $location ][ Request::i()->key ]['default_controller'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$current = array( 'module_key' => NULL, 'protected' => FALSE, 'default_controller' => NULL );
|
||||
}
|
||||
|
||||
/* Build the form */
|
||||
$form = new Form();
|
||||
$form->add( new Text( 'module_key', $current['module_key'], TRUE, array( 'maxLength' => 32 ), function( $val ) use ( $application, $modules, $location, $current )
|
||||
{
|
||||
if ( !preg_match( '/^[a-z]*$/', $val ) )
|
||||
{
|
||||
throw new DomainException( 'module_key_bad' );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$module = ( !$current['module_key'] OR $current['module_key'] != $val ) ? Module::load( $val, 'sys_module_key', array( 'sys_module_application=? and sys_module_area=?', $application->directory, $location ) ) : NULL;
|
||||
if( $module === NULL )
|
||||
{
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
|
||||
throw new DomainException( 'module_key_exists' );
|
||||
}
|
||||
catch ( OutOfRangeException $e ) {}
|
||||
} ) );
|
||||
$form->add( new YesNo( 'module_protected', $current['protected'], TRUE ) );
|
||||
if ( Request::i()->key )
|
||||
{
|
||||
$form->add( new Text( 'module_default_controller', $current['default_controller'] ) );
|
||||
}
|
||||
Output::i()->output = Theme::i()->getTemplate( 'global' )->block( 'modules_add', $form, FALSE );
|
||||
if ( Request::i()->isAjax() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Handle submissions */
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
if( $current['module_key'] )
|
||||
{
|
||||
$module = Module::get( $this->application->directory, $current['module_key'], $location );
|
||||
}
|
||||
else
|
||||
{
|
||||
$module = new Module;
|
||||
$module->application = $this->application->directory;
|
||||
$module->area = $location;
|
||||
}
|
||||
|
||||
$module->key = $values['module_key'];
|
||||
$module->protected = $values['module_protected'];
|
||||
$module->default_controller = $values['module_default_controller'] ?? '';
|
||||
$module->save();
|
||||
|
||||
$modules[ $location ][ $module->key ] = array(
|
||||
'default_controller' => $module->default_controller,
|
||||
'protected' => $module->protected,
|
||||
'default' => $module->default
|
||||
);
|
||||
|
||||
if( $current['module_key'] AND $current['module_key'] != $module->key )
|
||||
{
|
||||
unset( $modules[ $location ][ $current['module_key'] ] );
|
||||
}
|
||||
|
||||
$this->_writeModules( $modules );
|
||||
|
||||
if( $current['module_key'] )
|
||||
{
|
||||
$oldDir = ROOT_PATH . "/applications/{$this->application->directory}/modules/{$location}/{$current['module_key']}";
|
||||
$newDir = ROOT_PATH . "/applications/{$this->application->directory}/modules/{$location}/{$module->key}";
|
||||
@rename( $oldDir, $newDir );
|
||||
chmod( $newDir, IPS_FOLDER_PERMISSION );
|
||||
}
|
||||
else
|
||||
{
|
||||
$dir = ROOT_PATH . "/applications/{$this->application->directory}/modules/{$location}/{$module->key}";
|
||||
@mkdir( $dir );
|
||||
chmod( $dir, IPS_FOLDER_PERMISSION );
|
||||
}
|
||||
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=modules&appKey={$this->application->directory}&location={$location}" ), 'saved' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Module
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function deleteModule() : void
|
||||
{
|
||||
/* Make sure the user confirmed the deletion */
|
||||
Request::i()->confirmedDelete();
|
||||
|
||||
/* Load the module */
|
||||
try
|
||||
{
|
||||
$module = Module::get( $this->application->directory, Request::i()->key, Request::i()->location );
|
||||
}
|
||||
catch ( OutOfRangeException $e )
|
||||
{
|
||||
Output::i()->error( 'node_error', '2C103/K', 404, '' );
|
||||
}
|
||||
|
||||
/* Remove it from the JSON */
|
||||
$modules = $this->_getModules();
|
||||
unset( $modules[ $module->area ][ $module->key ] );
|
||||
$this->_writeModules( $modules );
|
||||
|
||||
/* Delete it */
|
||||
$location = $module->area;
|
||||
$module->delete();
|
||||
|
||||
/* Redirect */
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=modules&appKey={$this->application->directory}&location={$location}" ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Controller
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addController() : void
|
||||
{
|
||||
$module = $this->_loadModule();
|
||||
$targetDir = ROOT_PATH . "/applications/{$this->application->directory}/modules/" . Request::i()->location . '/' . $module->key . '/';
|
||||
|
||||
$form = new Form();
|
||||
$form->add( new Text( 'filename', NULL, TRUE, array(), function( $val ) use ( $targetDir )
|
||||
{
|
||||
if ( file_exists( $targetDir . $val . '.php' ) )
|
||||
{
|
||||
throw new DomainException( 'modules_controller_exists' );
|
||||
}
|
||||
}, NULL, '.php' ) );
|
||||
|
||||
if ( Request::i()->location === 'admin' )
|
||||
{
|
||||
$form->add( new Select( 'type', NULL, TRUE, array(
|
||||
'options' => array(
|
||||
'blank' => 'controllertype_blank',
|
||||
'node' => 'controllertype_node',
|
||||
'list' => 'controllertype_list',
|
||||
),
|
||||
'toggles' => array(
|
||||
'node' => array( 'model_name' ),
|
||||
'list' => array( 'database_table_name' ),
|
||||
)
|
||||
) ) );
|
||||
|
||||
$form->add( new Text( 'model_name', NULL, FALSE, array(),NULL, NULL, NULL, 'model_name' ) );
|
||||
|
||||
$form->add( new Text( 'database_table_name', NULL, FALSE, array(),NULL, NULL, NULL, 'database_table_name' ) );
|
||||
|
||||
$form->add( new Text( 'acpmenu_tab', $this->application->directory, FALSE, array( 'autocomplete' => array(
|
||||
'source' => $this->_getAcpMenuTabs(),
|
||||
'maxItems' => 1
|
||||
) ) ) );
|
||||
$form->add( new Select( 'acpmenu_restriction', '__create', FALSE, array( 'options' => array_merge( array(
|
||||
'' => 'acpmenu_norestriction',
|
||||
'__create' => 'controller_rcreate',
|
||||
), $this->_getRestrictions( $module ) ) ) ) );
|
||||
}
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
if( !isset($values['type']) )
|
||||
{
|
||||
$values['type'] = 'blank';
|
||||
}
|
||||
|
||||
/* Create a restriction? */
|
||||
$restriction = NULL;
|
||||
if ( isset( $values['acpmenu_restriction'] ) and $values['acpmenu_restriction'] )
|
||||
{
|
||||
if ( $values['acpmenu_restriction'] === '__create' )
|
||||
{
|
||||
$restrictions = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json" );
|
||||
$restrictions[ $module->key ][ $values['filename'] ]["{$values['filename']}_manage"] = "{$values['filename']}_manage";
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json", $restrictions );
|
||||
$restriction = "{$values['filename']}_manage";
|
||||
}
|
||||
else
|
||||
{
|
||||
$restriction = $values['acpmenu_restriction'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Work out the contents */
|
||||
$contents = str_replace(
|
||||
array(
|
||||
'{controller}',
|
||||
"{subpackage}",
|
||||
'{date}',
|
||||
'{app}',
|
||||
'{module}',
|
||||
'{location}',
|
||||
'{restriction}',
|
||||
'{node_model}',
|
||||
'{table_name}'
|
||||
),
|
||||
array(
|
||||
$values['filename'],
|
||||
( $this->application->directory != 'core' ) ? ( " * @subpackage\t" . Member::loggedIn()->language()->get( "__app_{$this->application->directory}" ) ) : '',
|
||||
date( 'd M Y' ),
|
||||
$this->application->directory,
|
||||
Request::i()->module_key,
|
||||
Request::i()->location,
|
||||
$restriction ? '\IPS\Dispatcher::i()->checkAcpPermission( \''.$restriction.'\' );' : '',
|
||||
$values['model_name'] ?? NULL,
|
||||
$values['database_table_name'] ?? NULL,
|
||||
),
|
||||
file_get_contents( ROOT_PATH . "/applications/core/data/defaults/Controller" . IPS::mb_ucfirst( $values['type'] ) . '.txt' )
|
||||
);
|
||||
|
||||
/* If this isn't an IPS app, strip out our header */
|
||||
if ( !in_array( $this->application->directory, IPS::$ipsApps ) )
|
||||
{
|
||||
$contents = preg_replace( '/(<\?php\s)\/*.+?\*\//s', '$1', $contents );
|
||||
}
|
||||
|
||||
/* Write */
|
||||
if( @file_put_contents( $targetDir . $values['filename'] . '.php', $contents ) === FALSE )
|
||||
{
|
||||
Output::i()->error( 'dev_could_not_write_controller', '1C103/H', 403, '' );
|
||||
}
|
||||
|
||||
@chmod( $targetDir . $values['filename'] . '.php', FILE_PERMISSION_NO_WRITE );
|
||||
|
||||
/* Add to the menu? */
|
||||
if ( isset( $values['acpmenu_tab'] ) and $values['acpmenu_tab'] )
|
||||
{
|
||||
$menu = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json" );
|
||||
$menu[ $module->key ][ $values['filename'] ] = array(
|
||||
'tab' => $values['acpmenu_tab'],
|
||||
'controller' => $values['filename'],
|
||||
'do' => '',
|
||||
'restriction' => $restriction,
|
||||
'subcontrollers' => ''
|
||||
);
|
||||
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json", $menu );
|
||||
}
|
||||
|
||||
/* Boink */
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=modules&appKey={$this->application->directory}&location=" . Request::i()->location . "&root=" . Request::i()->module_key ) );
|
||||
}
|
||||
|
||||
Output::i()->output = Theme::i()->getTemplate( 'global' )->block( 'modules_add_controller', $form, FALSE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a default controller for a module
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setDefaultController() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
$location = Request::i()->location;
|
||||
$modules = $this->_getModules();
|
||||
$modules[ $location ][ Request::i()->root ]['default_controller'] = mb_substr( Request::i()->default, 0, -4 );
|
||||
$this->_writeModules( $modules );
|
||||
|
||||
$module = Module::get( $this->application->directory, Request::i()->root, $location );
|
||||
$module->default_controller = mb_substr( Request::i()->default, 0, -4 );
|
||||
$module->save();
|
||||
|
||||
if( !Request::i()->isAjax() )
|
||||
{
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=modules&location={$location}&appKey=" . $this->application->directory ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a controller
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function deleteController() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
$appKey = $this->application->directory;
|
||||
$location = Request::i()->location;
|
||||
|
||||
if ( @unlink( ROOT_PATH . "/applications/{$appKey}/modules/{$location}/" . Request::i()->root . '/' . Request::i()->delete ) === FALSE )
|
||||
{
|
||||
Output::i()->error( 'dev_could_not_write_controller', '1C103/I', 403, '' );
|
||||
}
|
||||
|
||||
/* delete all the other stuff */
|
||||
|
||||
$restrictions = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json" );
|
||||
if (isset ( $restrictions[ Request::i()->root ][ mb_substr(Request::i()->delete,0 ,-4 )] ) )
|
||||
{
|
||||
unset($restrictions[ Request::i()->root ][ mb_substr(Request::i()->delete,0 ,-4 )] );
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json", $restrictions );
|
||||
}
|
||||
|
||||
$menu = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json" );
|
||||
if (isset ( $menu[ Request::i()->root ][ mb_substr(Request::i()->delete,0 ,-4 )] ) )
|
||||
{
|
||||
unset($menu[ Request::i()->root ][ mb_substr(Request::i()->delete,0 ,-4 )] );
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/acpmenu.json", $menu );
|
||||
}
|
||||
|
||||
if( !Request::i()->isAjax() )
|
||||
{
|
||||
Output::i()->redirect( Url::internal( "app=core&module=developer&controller=modules&appKey={$appKey}&location={$location}" ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write modules.json file
|
||||
*
|
||||
* @param array $json Data
|
||||
* @return void
|
||||
*/
|
||||
protected function _writeModules( array $json ) : void
|
||||
{
|
||||
foreach( $json as $location => $module )
|
||||
{
|
||||
foreach( $module as $name => $data )
|
||||
{
|
||||
foreach( $data as $k => $v )
|
||||
{
|
||||
if ( ! in_array( $k, array( 'protected', 'default_controller', 'default' ) ) )
|
||||
{
|
||||
unset( $json[ $location ][ $name ][ $k ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/modules.json", $json );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief parser
|
||||
* @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
|
||||
|
||||
* @since 27 Mar 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Stack;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* parser
|
||||
*/
|
||||
class parser extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$jsonFile = ROOT_PATH . "/applications/{$this->application->directory}/data/parser.json";
|
||||
$json = $this->_getJson( $jsonFile );
|
||||
|
||||
$form = new Form( 'form' );
|
||||
$form->addMessage( Member::loggedIn()->language()->addToStack('editor_allowed_formmsg') );
|
||||
|
||||
$form->add( new Stack( 'editor_allowed_classes', $json['css'] ?? [], false ) );
|
||||
$form->add( new Stack( 'editor_allowed_datacontrollers', $json['controllers'] ?? [], false ) );
|
||||
$form->add( new Stack( 'editor_allowed_iframe_bases', $json['iframe'] ?? [], false ) );
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
$json = [
|
||||
'css' => $values['editor_allowed_classes'],
|
||||
'controllers' => $values['editor_allowed_datacontrollers'],
|
||||
'iframe' => $values['editor_allowed_iframe_bases']
|
||||
];
|
||||
|
||||
$this->_writeJson( $jsonFile, $json );
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief settings
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use IPS\Db;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Helpers\Form\Matrix;
|
||||
use IPS\IPS;
|
||||
use IPS\Output;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* settings
|
||||
*/
|
||||
class settings extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$settings = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/settings.json" );
|
||||
|
||||
$form = new Matrix();
|
||||
$form->langPrefix = 'dev_settings_';
|
||||
$form->columns = array(
|
||||
'key' => array( 'Text', NULL, TRUE ),
|
||||
'default' => array( 'Text' ),
|
||||
);
|
||||
if ( in_array( $this->application->directory, IPS::$ipsApps ) )
|
||||
{
|
||||
$form->columns['report'] = array( 'Select', 'none', FALSE, array( 'options' => array(
|
||||
'none' => 'dev_settings_none',
|
||||
'full' => 'dev_settings_full',
|
||||
'bool' => 'dev_settings_bool',
|
||||
) ) );
|
||||
}
|
||||
$form->rows = $settings;
|
||||
|
||||
if ( $form->values() !== FALSE )
|
||||
{
|
||||
$values = $form->values();
|
||||
|
||||
if ( !empty( $form->changedRows ) )
|
||||
{
|
||||
foreach ( $form->changedRows as $key )
|
||||
{
|
||||
Db::i()->update( 'core_sys_conf_settings', array( 'conf_key' => $values[ $key ]['key'], 'conf_default' => $values[ $key ]['default'] ), array( 'conf_key=?', $form->rows[ $key ]['key'] ) );
|
||||
}
|
||||
}
|
||||
if ( !empty( $form->removedRows ) )
|
||||
{
|
||||
$delete = array();
|
||||
foreach ( $form->removedRows as $key )
|
||||
{
|
||||
$delete[] = $form->rows[ $key ]['key'];
|
||||
}
|
||||
|
||||
Db::i()->delete( 'core_sys_conf_settings', Db::i()->in( 'conf_key', $delete ) );
|
||||
}
|
||||
|
||||
/* Load existing keys before we add new ones, to prevent duplicates */
|
||||
$existingKeys = iterator_to_array(
|
||||
Db::i()->select( 'conf_key', 'core_sys_conf_settings', [ 'conf_app=?', $this->application->directory ] )
|
||||
);
|
||||
|
||||
if ( !empty( $form->addedRows ) )
|
||||
{
|
||||
$insert = array();
|
||||
foreach ( $form->addedRows as $key )
|
||||
{
|
||||
if( !in_array( $values[ $key ]['key'], $existingKeys ) )
|
||||
{
|
||||
$insert[] = array( 'conf_key' => $values[ $key ]['key'], 'conf_value' => $values[ $key ]['default'], 'conf_default' => $values[ $key ]['default'], 'conf_app' => $this->application->directory );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If this is a duplicate, remove it from the values,
|
||||
otherwise it will add it to the JSON file */
|
||||
unset( $values[ $key ] );
|
||||
}
|
||||
}
|
||||
|
||||
if( count( $insert ) )
|
||||
{
|
||||
Db::i()->insert( 'core_sys_conf_settings', $insert );
|
||||
}
|
||||
}
|
||||
|
||||
$save = array();
|
||||
foreach ( $values as $data )
|
||||
{
|
||||
if ( $data['key'] )
|
||||
{
|
||||
$save[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
usort( $save, function( $a, $b )
|
||||
{
|
||||
return strnatcmp( $a['key'], $b['key'] );
|
||||
} );
|
||||
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/settings.json", $save );
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief tasks
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use DateInterval;
|
||||
use DomainException;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\Developer;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Table\Custom;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Task;
|
||||
use IPS\Theme;
|
||||
use OutOfRangeException;
|
||||
use UnderflowException;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* tasks
|
||||
*/
|
||||
class tasks extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$json = ROOT_PATH . "/applications/{$this->application->directory}/data/tasks.json";
|
||||
|
||||
$data = array();
|
||||
foreach ( json_decode( file_get_contents( $json ), TRUE ) as $k => $f )
|
||||
{
|
||||
$data[ $k ] = array(
|
||||
'dev_task_key' => $k,
|
||||
'dev_task_frequency' => $f
|
||||
);
|
||||
}
|
||||
|
||||
$table = new Custom( $data, $this->url );
|
||||
$table->quickSearch = 'dev_task_key';
|
||||
$table->rootButtons = array(
|
||||
'add' => array(
|
||||
'icon' => 'plus',
|
||||
'title' => 'add',
|
||||
'link' => $this->url->setQueryString( 'do', 'taskForm' ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('add') )
|
||||
)
|
||||
);
|
||||
|
||||
$url = $this->url;
|
||||
$table->rowButtons = function( $row ) use ( $url )
|
||||
{
|
||||
$task = null;
|
||||
try
|
||||
{
|
||||
$task = Task::load( $row['dev_task_key'], 'key' );
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
|
||||
$buttons = array();
|
||||
|
||||
$fileLink = ROOT_PATH . "/applications/{$this->application->directory}/tasks/{$row['dev_task_key']}.php";
|
||||
if( $ideLink = Developer::getIdeHref($fileLink ) )
|
||||
{
|
||||
$buttons['ide'] = [
|
||||
'icon' => 'fa-file-code',
|
||||
'title' => 'open_in_ide',
|
||||
'link' => $ideLink
|
||||
];
|
||||
};
|
||||
|
||||
$buttons['edit'] = array(
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => $url->setQueryString( array( 'do' => 'taskForm', 'key' => $row['dev_task_key'] ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('edit') )
|
||||
);
|
||||
$buttons['delete'] = array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $url->setQueryString( array( 'do' => 'taskDelete', 'key' => $row['dev_task_key'] ) )->csrf(),
|
||||
'data' => array( 'delete' => '' )
|
||||
);
|
||||
|
||||
if( $task !== null )
|
||||
{
|
||||
if( $task->running )
|
||||
{
|
||||
$buttons['unlock'] = [
|
||||
'icon' => 'unlock',
|
||||
'title' => 'task_manager_unlock',
|
||||
'link' => Url::internal( "app=core&module=settings&controller=advanced&do=unlockTask&id={$task->id}" )->csrf()
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
$buttons['run'] = [
|
||||
'icon' => 'play-circle',
|
||||
'title' => 'task_manager_run',
|
||||
'link' => Url::internal( "app=core&module=settings&controller=advanced&do=runTask&id={$task->id}" )->csrf()
|
||||
];
|
||||
}
|
||||
|
||||
$buttons['logs'] = array(
|
||||
'icon' => 'search',
|
||||
'title' => 'task_manager_logs',
|
||||
'link' => Url::internal( "app=core&module=settings&controller=advanced&do=taskLogs&id={$task->id}" )
|
||||
);
|
||||
}
|
||||
|
||||
return $buttons;
|
||||
};
|
||||
|
||||
$table->parsers = array(
|
||||
'dev_task_frequency' => function( $v )
|
||||
{
|
||||
$interval = new DateInterval( $v );
|
||||
$return = array();
|
||||
foreach ( array( 'y' => 'years', 'm' => 'months', 'd' => 'days', 'h' => 'hours', 'i' => 'minutes', 's' => 'seconds' ) as $k => $v )
|
||||
{
|
||||
if ( $interval->$k )
|
||||
{
|
||||
$return[] = Member::loggedIn()->language()->addToStack( 'every_x_' . $v, FALSE, array( 'pluralize' => array( $interval->format( '%' . $k ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
return Member::loggedIn()->language()->formatList( $return );
|
||||
}
|
||||
);
|
||||
|
||||
Output::i()->output = (string) $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function taskForm() : void
|
||||
{
|
||||
$json = ROOT_PATH . "/applications/{$this->application->directory}/data/tasks.json";
|
||||
$form = new Form;
|
||||
|
||||
$current = NULL;
|
||||
if ( isset( Request::i()->key ) )
|
||||
{
|
||||
$tasks = json_decode( file_get_contents( $json ), TRUE );
|
||||
if ( array_key_exists( Request::i()->key, $tasks ) )
|
||||
{
|
||||
$current = array(
|
||||
'dev_task_key' => Request::i()->key,
|
||||
'dev_task_frequency' => new DateInterval( $tasks[ Request::i()->key ] )
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
$current['id'] = Db::i()->select( 'id', 'core_tasks', array( '`key`=?', Request::i()->key ) )->first();
|
||||
}
|
||||
catch( UnderflowException $e ){}
|
||||
|
||||
$form->hiddenValues['old'] = $current['dev_task_key'];
|
||||
}
|
||||
unset( $tasks );
|
||||
}
|
||||
|
||||
$form->add( new Text( 'dev_task_key', $current ? $current['dev_task_key'] : NULL, TRUE, array( 'maxLength' => 255, 'regex' => '/^[a-z0-9_]*$/i' ), function( $val ) use ( $current )
|
||||
{
|
||||
$where = array( array( '`key`=?', $val ) );
|
||||
|
||||
if ( isset( $current['id'] ) )
|
||||
{
|
||||
$where[] = array( 'id<>?', $current['id'] );
|
||||
}
|
||||
|
||||
if ( Db::i()->select( 'count(*)', 'core_tasks', $where )->first() )
|
||||
{
|
||||
throw new DomainException( 'dev_task_key_err' );
|
||||
}
|
||||
} ) );
|
||||
$form->add( new Form\Custom( 'dev_task_frequency', $current ? $current['dev_task_frequency'] : NULL, TRUE, array(
|
||||
'getHtml' => function( $element )
|
||||
{
|
||||
return Theme::i()->getTemplate( 'forms', 'core' )->dateinterval( $element->name, $element->value ?: new DateInterval( 'P0D' ) );
|
||||
},
|
||||
'formatValue' => function ( $element )
|
||||
{
|
||||
if ( !( $element->value instanceof DateInterval ) )
|
||||
{
|
||||
if( !empty($element->value) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$interval = new DateInterval( "P{$element->value['y']}Y{$element->value['m']}M{$element->value['d']}DT{$element->value['h']}H{$element->value['i']}M{$element->value['s']}S" );
|
||||
}
|
||||
catch( Exception $e )
|
||||
{
|
||||
$interval = DateInterval::createFromDateString('1 day');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$interval = DateInterval::createFromDateString('1 day');
|
||||
}
|
||||
|
||||
return $interval;
|
||||
}
|
||||
return $element->value;
|
||||
}
|
||||
), function ( $val )
|
||||
{
|
||||
foreach ( $val as $k => $v )
|
||||
{
|
||||
if ( $v )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new InvalidArgumentException( 'form_required' );
|
||||
} ) );
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
/* Write PHP file */
|
||||
$taskDirectory = ROOT_PATH . "/applications/{$this->application->directory}/tasks";
|
||||
$taskFile = $taskDirectory . "/{$values['dev_task_key']}.php";
|
||||
if ( isset( $values['old'] ) and $values['old'] !== $values['dev_task_key'] and file_exists( $taskDirectory . "/{$values['old']}.php" ) )
|
||||
{
|
||||
@rename( $taskDirectory . "/{$values['old']}.php", $taskFile );
|
||||
Db::i()->delete( 'core_tasks', array( '`key`=?', $values['old'] ) );
|
||||
}
|
||||
if ( !file_exists( $taskFile ) )
|
||||
{
|
||||
if ( !is_dir( $taskDirectory ) )
|
||||
{
|
||||
mkdir( $taskDirectory );
|
||||
chmod( $taskDirectory, IPS_FOLDER_PERMISSION);
|
||||
}
|
||||
|
||||
file_put_contents( $taskFile, str_replace(
|
||||
array(
|
||||
'{key}',
|
||||
"{subpackage}\n",
|
||||
'{date}',
|
||||
'{namespace}',
|
||||
),
|
||||
array(
|
||||
$values['dev_task_key'],
|
||||
( $this->application->directory != 'core' ) ? ( " * @subpackage\t" . $this->application->directory . "\n" ) : '',
|
||||
date( 'd M Y' ),
|
||||
$this->application->directory . '\tasks',
|
||||
),
|
||||
file_get_contents( ROOT_PATH . "/applications/core/data/defaults/Task.txt" )
|
||||
) );
|
||||
}
|
||||
|
||||
/* Add to DB */
|
||||
$frequency = "P{$values['dev_task_frequency']->y}Y{$values['dev_task_frequency']->m}M{$values['dev_task_frequency']->d}DT{$values['dev_task_frequency']->h}H{$values['dev_task_frequency']->i}M{$values['dev_task_frequency']->s}S";
|
||||
Db::i()->replace( 'core_tasks', array(
|
||||
'app' => $this->application->directory,
|
||||
'key' => $values['dev_task_key'],
|
||||
'frequency' => $frequency,
|
||||
'next_run' => DateTime::create()->add( new DateInterval( $frequency ) )->getTimestamp(),
|
||||
'running' => 0,
|
||||
) );
|
||||
|
||||
/* Add to JSON file */
|
||||
$tasks = json_decode( file_get_contents( $json ), TRUE );
|
||||
$tasks[ $values['dev_task_key'] ] = $frequency;
|
||||
if ( isset( $values['old'] ) and $values['old'] !== $values['dev_task_key'] and isset( $tasks[ $values['old'] ] ) )
|
||||
{
|
||||
unset( $tasks[ $values['old'] ] );
|
||||
}
|
||||
file_put_contents( $json, json_encode( $tasks ) );
|
||||
|
||||
/* Redirect */
|
||||
Output::i()->redirect( $this->url, 'saved' );
|
||||
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function taskDelete() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
$json = ROOT_PATH . "/applications/{$this->application->directory}/data/tasks.json";
|
||||
|
||||
$tasks = json_decode( file_get_contents( $json ), TRUE );
|
||||
if ( array_key_exists( Request::i()->key, $tasks ) )
|
||||
{
|
||||
unset( $tasks[ Request::i()->key ] );
|
||||
file_put_contents( $json, json_encode( $tasks ) );
|
||||
|
||||
$taskDirectory = ROOT_PATH . "/applications/{$this->application->directory}/tasks";
|
||||
if ( file_exists( $taskDirectory . "/" . Request::i()->key . ".php" ) )
|
||||
{
|
||||
unlink( $taskDirectory . "/" . Request::i()->key . ".php" );
|
||||
}
|
||||
|
||||
Db::i()->delete( 'core_tasks', array( 'app=? AND `key`=?', $this->application->directory, Request::i()->key ) );
|
||||
}
|
||||
Output::i()->redirect( $this->url, 'saved' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief themeeditor
|
||||
* @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
|
||||
|
||||
* @since 16 Feb 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use IPS\Application;
|
||||
use IPS\Db;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Helpers\Badge;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Tree\Tree;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Theme;
|
||||
use IPS\Theme\Editor\Category;
|
||||
use IPS\Theme\Editor\Setting;
|
||||
use OutOfRangeException;
|
||||
use function defined;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* themeeditor
|
||||
*/
|
||||
class themeeditor extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
$tree = new Tree(
|
||||
$this->url,
|
||||
'dev_themeeditor',
|
||||
array( $this, '_getCategories' ),
|
||||
array( $this, '_getCategoryRow' ),
|
||||
function( $id ){
|
||||
return null;
|
||||
},
|
||||
array( $this, '_getCategoryChildren' ),
|
||||
function(){
|
||||
return [
|
||||
'add' => [
|
||||
'icon' => 'plus',
|
||||
'title' => 'add',
|
||||
'link' => $this->url->setQueryString( 'do', 'categoryForm' ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('add') )
|
||||
]
|
||||
];
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
Output::i()->output = (string) $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function _getCategories() : array
|
||||
{
|
||||
$return = [];
|
||||
foreach( Category::roots( null ) as $category )
|
||||
{
|
||||
/* @var Category $category */
|
||||
if( $category->app and !$category->set_id )
|
||||
{
|
||||
$return[ $category->id ] = $this->_getCategoryRow( $category );
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|Category $category
|
||||
* @return string
|
||||
*/
|
||||
public function _getCategoryRow( int|Category $category ) : string
|
||||
{
|
||||
if( is_numeric( $category ) )
|
||||
{
|
||||
$category = Category::load( $category );
|
||||
}
|
||||
|
||||
$buttons = [];
|
||||
|
||||
if( $category->app == $this->application->directory )
|
||||
{
|
||||
$buttons['edit'] = [
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'categoryForm', 'id' => $category->id ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('edit') )
|
||||
];
|
||||
$buttons['delete'] = [
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'categoryDelete', 'id' => $category->id ) )->csrf(),
|
||||
'data' => ( $category->hasChildren( NULL ) or $category->showDeleteOrMoveForm() ) ? array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('delete') ) : array( 'delete' => '' ),
|
||||
'hotkey'=> 'd'
|
||||
];
|
||||
}
|
||||
|
||||
if( $category->hasChildren( null, null, false ) or !$category->hasContents() )
|
||||
{
|
||||
$buttons['child'] = [
|
||||
'icon' => 'plus',
|
||||
'title' => 'themeeditor_cat_add',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'categoryForm', 'parent' => $category->id ) )
|
||||
];
|
||||
}
|
||||
elseif( $category->hasSettings() or $category->hasColors() or !$category->hasContents() )
|
||||
{
|
||||
$buttons['add'] = array(
|
||||
'icon' => 'plus-circle',
|
||||
'title' => Category::$nodeTitle . '_add_child',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'settingForm', 'category' => $category->id ) )
|
||||
);
|
||||
}
|
||||
|
||||
$badge = null;
|
||||
if( $category->app and $category->app != $this->application->directory )
|
||||
{
|
||||
$badge = [
|
||||
0 => 'ipsBadge ipsBadge--intermediary',
|
||||
1 => Application::$titleLangPrefix . $category->app,
|
||||
2 => new Badge( Badge::BADGE_INTERMEDIARY, Member::loggedIn()->language()->addToStack( Application::$titleLangPrefix . $category->app ), icon: 'fa-lock' )
|
||||
];
|
||||
}
|
||||
|
||||
return Theme::i()->getTemplate( 'trees', 'core' )->row(
|
||||
$this->url,
|
||||
$category->id,
|
||||
$category->_title,
|
||||
$category->hasContents(),
|
||||
$buttons,
|
||||
'', // description
|
||||
$category->icon(),
|
||||
$category->position,
|
||||
false,
|
||||
null, // toggleStatus
|
||||
( $category->app != $this->application->directory ), // locked
|
||||
$badge, // badge
|
||||
false, // titleHtml
|
||||
false // descriptionHtml
|
||||
);
|
||||
}
|
||||
|
||||
public function _getCategoryChildren( int|Category $category ) : array
|
||||
{
|
||||
if( is_numeric( $category ) )
|
||||
{
|
||||
$category = Category::load( $category );
|
||||
}
|
||||
|
||||
$children = [];
|
||||
foreach( $category->children() as $child )
|
||||
{
|
||||
if( $child->set_id )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( $child instanceof Setting )
|
||||
{
|
||||
$children[ 's.' . $child->id ] = $this->_getSettingRow( $child );
|
||||
}
|
||||
else
|
||||
{
|
||||
$children[ $child->id ] = $this->_getCategoryRow( $child );
|
||||
}
|
||||
}
|
||||
return $children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|Setting $setting
|
||||
* @return string
|
||||
*/
|
||||
public function _getSettingRow( int|Setting $setting ) : string
|
||||
{
|
||||
if( is_numeric( $setting ) )
|
||||
{
|
||||
$setting = Setting::load( $setting );
|
||||
}
|
||||
|
||||
$buttons = [];
|
||||
if( $setting->app == $this->application->directory )
|
||||
{
|
||||
$buttons['edit'] = [
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'settingForm', 'id' => $setting->id ) )
|
||||
];
|
||||
$buttons['delete'] = [
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'settingDelete', 'id' => $setting->id ) )->csrf(),
|
||||
'data' => array( 'delete' => '' ),
|
||||
'hotkey'=> 'd'
|
||||
];
|
||||
}
|
||||
|
||||
$badge = null;
|
||||
if( $setting->app != $this->application->directory )
|
||||
{
|
||||
$badge = [
|
||||
0 => 'ipsBadge ipsBadge--intermediary',
|
||||
1 => Application::$titleLangPrefix . $setting->app,
|
||||
2 => new Badge( Badge::BADGE_INTERMEDIARY, Member::loggedIn()->language()->addToStack( Application::$titleLangPrefix . $setting->app ), icon: 'fa-lock' )
|
||||
];
|
||||
}
|
||||
|
||||
return Theme::i()->getTemplate( 'trees', 'core' )->row(
|
||||
$this->url,
|
||||
's.' . $setting->id,
|
||||
$setting->_title,
|
||||
false,
|
||||
$buttons,
|
||||
$setting->description, // description
|
||||
null, // icon
|
||||
$setting->position,
|
||||
false,
|
||||
null, // toggleStatus
|
||||
null, // locked
|
||||
$badge, // badge
|
||||
false, // titleHtml
|
||||
true, // descriptionHtml
|
||||
false, // acceptsChildren
|
||||
false // canBeRoot
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function categoryForm() : void
|
||||
{
|
||||
$form = new Form;
|
||||
if( isset( Request::i()->id ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$category = Category::load( Request::i()->id );
|
||||
if( !$category->canEdit() or $category->app != $this->application->directory )
|
||||
{
|
||||
Output::i()->error( 'node_noperm_edit', '2D101/A', 403 );
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException )
|
||||
{
|
||||
Output::i()->error( 'node_error', '2D101/B', 404 );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$category = new Category;
|
||||
$category->app = $this->application->directory;
|
||||
$category->parent = Request::i()->parent ?? 0;
|
||||
}
|
||||
|
||||
$category->form( $form );
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
$category->saveForm( $category->formatFormValues( $values ) );
|
||||
|
||||
$this->application->buildThemeEditorSettings();
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function categoryDelete() : void
|
||||
{
|
||||
try
|
||||
{
|
||||
$category = Category::load( Request::i()->id );
|
||||
if( !$category->canEdit() or $category->app != $this->application->directory )
|
||||
{
|
||||
Output::i()->error( 'node_noperm_edit', '2D101/C', 403 );
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException )
|
||||
{
|
||||
Output::i()->error( 'node_error', '2D101/D', 404 );
|
||||
}
|
||||
|
||||
/* Do we have any children or content? */
|
||||
if ( $category->hasChildren( NULL ) or $category->showDeleteOrMoveForm() or isset( Request::i()->ajaxValidate ) )
|
||||
{
|
||||
$form = $category->deleteOrMoveForm();
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$category->deleteOrMoveFormSubmit( $values );
|
||||
|
||||
if ( isset( $values['node_move_children'] ) AND $values['node_move_children'] )
|
||||
{
|
||||
$moveToId = ( isset( $values['node_destination'] ) ) ? $values['node_destination'] : Request::i()->node_move_children;
|
||||
|
||||
Session::i()->log( 'acplog__node_deleted_m', array( Category::$nodeTitle => TRUE, $category->titleForLog() => FALSE, $category::load( $moveToId )->titleForLog() => FALSE ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
Session::i()->log( 'acplog__node_deleted_c', array( Category::$nodeTitle => TRUE, $category->titleForLog() => FALSE ) );
|
||||
}
|
||||
|
||||
$this->application->buildThemeEditorSettings();
|
||||
|
||||
Output::i()->redirect( $this->url, 'deleted' );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Show form */
|
||||
Output::i()->output = $form;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Make sure the user confirmed the deletion */
|
||||
Request::i()->confirmedDelete();
|
||||
}
|
||||
|
||||
/* Delete it */
|
||||
Session::i()->log( 'acplog__node_deleted', array( Category::$nodeTitle => TRUE, $category->titleForLog() => FALSE ) );
|
||||
$category->delete();
|
||||
|
||||
$this->application->buildThemeEditorSettings();
|
||||
|
||||
/* Boink */
|
||||
if( Request::i()->isAjax() )
|
||||
{
|
||||
Output::i()->json( "OK" );
|
||||
}
|
||||
else
|
||||
{
|
||||
Output::i()->redirect( $this->url->setQueryString( array( 'root' => ( $category->parent() ? $category->parent()->_id : '' ) ) ), 'deleted' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function settingForm() : void
|
||||
{
|
||||
$form = new Form;
|
||||
if( isset( Request::i()->id ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$setting = Setting::load( Request::i()->id );
|
||||
if( !$setting->canEdit() or $setting->app != $this->application->directory )
|
||||
{
|
||||
Output::i()->error( 'node_noperm_edit', '2D101/E', 403 );
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException )
|
||||
{
|
||||
Output::i()->error( 'node_error', '2D101/F', 404 );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$setting = new Setting;
|
||||
$setting->app = $this->application->directory;
|
||||
$setting->category_id = Request::i()->category ?? 0;
|
||||
}
|
||||
|
||||
$setting->form( $form );
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
$setting->saveForm( $setting->formatFormValues( $values ) );
|
||||
|
||||
$this->application->buildThemeEditorSettings();
|
||||
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
Output::i()->output = (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function settingDelete() : void
|
||||
{
|
||||
Request::i()->confirmedDelete();
|
||||
|
||||
try
|
||||
{
|
||||
$setting = Setting::load( Request::i()->id );
|
||||
Session::i()->log( 'acplog__node_deleted', array( Setting::$nodeTitle => TRUE, $setting->titleForLog() => FALSE ) );
|
||||
$setting->delete();
|
||||
$this->application->buildThemeEditorSettings();
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
|
||||
Output::i()->redirect( $this->url, 'deleted' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function reorder() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
/* Normalise AJAX vs non-AJAX */
|
||||
if( isset( Request::i()->ajax_order ) )
|
||||
{
|
||||
$order = array();
|
||||
$position = array();
|
||||
foreach( Request::i()->ajax_order as $id => $parent )
|
||||
{
|
||||
if ( !isset( $order[ $parent ] ) )
|
||||
{
|
||||
$order[ $parent ] = array();
|
||||
$position[ $parent ] = 1;
|
||||
}
|
||||
$order[ $parent ][ $id ] = $position[ $parent ]++;
|
||||
}
|
||||
}
|
||||
/* Non-AJAX way */
|
||||
else
|
||||
{
|
||||
$order = array( Request::i()->root ?: 'null' => Request::i()->order );
|
||||
}
|
||||
|
||||
/* Okay, now order */
|
||||
foreach( $order as $parent => $settings )
|
||||
{
|
||||
foreach( $settings as $id => $position )
|
||||
{
|
||||
if( mb_substr( $id, 0, 2 ) == 's.' )
|
||||
{
|
||||
try
|
||||
{
|
||||
$setting = Setting::load( mb_substr( $id, 2 ) );
|
||||
$setting->category_id = $parent;
|
||||
$setting->position = $position;
|
||||
$setting->save();
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$category = Category::load( $id );
|
||||
$category->position = $position;
|
||||
|
||||
if( is_numeric( $parent ) )
|
||||
{
|
||||
$category->parent = $parent;
|
||||
}
|
||||
|
||||
$category->save();
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->application->buildThemeEditorSettings();
|
||||
|
||||
/* If this is an AJAX request, just respond */
|
||||
if( Request::i()->isAjax() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
/* Otherwise, redirect */
|
||||
else
|
||||
{
|
||||
Output::i()->redirect( $this->url->setQueryString( array( 'root' => Request::i()->root ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function search() : void
|
||||
{
|
||||
$rows = array();
|
||||
|
||||
/* Get results */
|
||||
$nodeClass = Category::class;
|
||||
$results = $nodeClass::search( '_title', Request::i()->input, '_title' );
|
||||
|
||||
/* Get results of subnodes */
|
||||
if ( isset( $nodeClass::$subnodeClass ) )
|
||||
{
|
||||
$subnodeClass = $nodeClass::$subnodeClass;
|
||||
$results = array_merge( $results, array_values( $subnodeClass::search( '_title', Request::i()->input, '_title' ) ) );
|
||||
|
||||
usort( $results, function( $a, $b ) {
|
||||
return strnatcasecmp( $a->_title, $b->_title );
|
||||
} );
|
||||
}
|
||||
|
||||
/* Convert to HTML */
|
||||
foreach ( $results as $result )
|
||||
{
|
||||
if( $result instanceof Category )
|
||||
{
|
||||
$rows[ $result->_id ] = $this->_getCategoryRow( $result );
|
||||
}
|
||||
else
|
||||
{
|
||||
$rows[ 's.' . $result->_id ] = $this->_getSettingRow( $result );
|
||||
}
|
||||
}
|
||||
|
||||
Output::i()->output = Theme::i()->getTemplate( 'trees', 'core' )->rows( $rows, '' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief versions
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use DirectoryIterator;
|
||||
use DomainException;
|
||||
use InvalidArgumentException;
|
||||
use IPS\Db\Exception as DbException;
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Form\TextArea;
|
||||
use IPS\Helpers\Form\Upload;
|
||||
use IPS\Helpers\Tree\Tree;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Theme;
|
||||
use IPS\Xml\SimpleXML;
|
||||
use ParseError;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* versions
|
||||
*/
|
||||
class versions extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $json = [];
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
/* Create the file if it doesn't exist */
|
||||
$this->json = $this->_getVersions();
|
||||
|
||||
/* Build node tree */
|
||||
$appKey = $this->application->directory;
|
||||
$url = $this->url;
|
||||
$tree = new Tree(
|
||||
$this->url,
|
||||
'dev_versions',
|
||||
/* Get Roots */
|
||||
array( $this, '_getVersionRows' ),
|
||||
/* Get Row */
|
||||
array( $this, '_getVersionRow' ),
|
||||
/* Get Row's Parent ID */
|
||||
function( $id )
|
||||
{
|
||||
return NULL;
|
||||
},
|
||||
/* Get Children */
|
||||
array( $this, '_getQueriesRows' ),
|
||||
/* Get Root Buttons */
|
||||
function() use ( $appKey, $url )
|
||||
{
|
||||
return array(
|
||||
'add' => array(
|
||||
'icon' => 'plus',
|
||||
'title' => 'versions_add',
|
||||
'link' => $url->setQueryString( array( 'do' => 'addVersion' ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('versions_add') )
|
||||
)
|
||||
);
|
||||
},
|
||||
FALSE,
|
||||
FALSE,
|
||||
TRUE
|
||||
);
|
||||
|
||||
Output::i()->output = (string) $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function reorder() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
/* Normalise AJAX vs non-AJAX */
|
||||
if( isset( Request::i()->ajax_order ) )
|
||||
{
|
||||
$order = array();
|
||||
$position = array();
|
||||
foreach( Request::i()->ajax_order as $id => $parent )
|
||||
{
|
||||
if ( !isset( $order[ $parent ] ) )
|
||||
{
|
||||
$order[ $parent ] = array();
|
||||
$position[ $parent ] = 1;
|
||||
}
|
||||
$order[ $parent ][ $id ] = $position[ $parent ]++;
|
||||
}
|
||||
}
|
||||
/* Non-AJAX way */
|
||||
else
|
||||
{
|
||||
$order = array( Request::i()->root ?: 'null' => Request::i()->order );
|
||||
}
|
||||
|
||||
/* Work out */
|
||||
$queries = array();
|
||||
$write = array();
|
||||
foreach ( $order as $versionKey => $keys )
|
||||
{
|
||||
if ( $versionKey != 'null' )
|
||||
{
|
||||
asort( $keys );
|
||||
foreach ( $keys as $key => $position )
|
||||
{
|
||||
$versionNumber = mb_substr( $key, 0, mb_strpos( $key, '.' ) );
|
||||
if ( !isset( $queries[ $versionNumber ] ) )
|
||||
{
|
||||
$queries[ $versionNumber ] = $this->_getQueries( $versionNumber );
|
||||
}
|
||||
$write[ $versionNumber ][] = $queries[ $versionNumber ][ mb_substr( $key, mb_strpos( $key, '.' ) + 1 ) ];
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ( $write as $versionNumber => $queries )
|
||||
{
|
||||
$this->_writeQueries( $versionNumber, $queries );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all version rows
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function _getVersionRows() : array
|
||||
{
|
||||
$rows = array( 'install' => $this->_getVersionRow( 'install' ) );
|
||||
foreach ( $this->json as $long => $human )
|
||||
{
|
||||
array_unshift( $rows, $this->_getVersionRow( $long ) );
|
||||
}
|
||||
array_unshift( $rows, $this->_getVersionRow( 'working' ) );
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get individual version row
|
||||
*
|
||||
* @param int|string $long Version ID
|
||||
* @param bool $root Format this as the root node?
|
||||
* @return string
|
||||
*/
|
||||
public function _getVersionRow( int|string $long, bool $root=FALSE ) : string
|
||||
{
|
||||
$dir = ROOT_PATH . "/applications/{$this->application->directory}/setup/" . ( $long === 'install' ? $long : "upg_{$long}" );
|
||||
|
||||
$hasChildren = FALSE;
|
||||
if ( is_dir( $dir ) )
|
||||
{
|
||||
foreach ( new DirectoryIterator( $dir ) as $file )
|
||||
{
|
||||
if ( !$file->isDot() and mb_substr( $file, 0, 1 ) !== '.' and $file != 'index.html' )
|
||||
{
|
||||
$hasChildren = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$buttons = array();
|
||||
|
||||
if( $long != 'install' )
|
||||
{
|
||||
$buttons['phpcode'] = array(
|
||||
'icon' => 'code',
|
||||
'title' => 'versions_code',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'versionCode', 'id' => $long ) )->csrf()
|
||||
);
|
||||
}
|
||||
|
||||
$buttons['add'] = array(
|
||||
'icon' => 'plus-circle',
|
||||
'title' => 'versions_query',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'addVersionQuery', 'id' => $long ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('versions_query'), 'ipsDialog-remoteVerify' => "false" )
|
||||
);
|
||||
|
||||
if( $long != 'install' and $long !== 'working' )
|
||||
{
|
||||
$buttons['delete'] = array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'deleteVersion', 'id' => $long ) ),
|
||||
'data' => array( 'delete' => '' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $long === 'install' )
|
||||
{
|
||||
$nameToDisplay = Member::loggedIn()->language()->addToStack('versions_install');
|
||||
}
|
||||
elseif ( $long === 'working' )
|
||||
{
|
||||
$nameToDisplay = Member::loggedIn()->language()->addToStack('versions_working');
|
||||
}
|
||||
else
|
||||
{
|
||||
$nameToDisplay = $this->json[$long] ?? $long;
|
||||
}
|
||||
|
||||
return Theme::i()->getTemplate( 'trees', 'core' )->row( $this->url, $long, $nameToDisplay, $hasChildren, $buttons, $long, NULL, NULL, $root );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get versions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function _getVersions() : array
|
||||
{
|
||||
$result = $this->_getJson( ROOT_PATH . "/applications/{$this->application->directory}/data/versions.json" );
|
||||
ksort( $result );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write versions.json file
|
||||
*
|
||||
* @param array $json Data
|
||||
* @return void
|
||||
*/
|
||||
protected function _writeVersions( array $json ) : void
|
||||
{
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$this->application->directory}/data/versions.json", $json );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PHP class for a version
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function versionCode() : void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
/* Get version */
|
||||
if ( Request::i()->id !== 'working' )
|
||||
{
|
||||
$long = intval( Request::i()->id );
|
||||
$json = $this->_getVersions();
|
||||
if ( !isset( $json[ $long ] ) )
|
||||
{
|
||||
Output::i()->error( 'node_error', '2C103/8', 404, '' );
|
||||
}
|
||||
$human = $json[ $long ];
|
||||
}
|
||||
else
|
||||
{
|
||||
$long = 'working';
|
||||
$human = '{version_human}';
|
||||
}
|
||||
|
||||
/* Write the file if we don't already have one */
|
||||
$phpFilePath = ROOT_PATH . "/applications/{$this->application->directory}/setup/upg_{$long}";
|
||||
$phpFile = $phpFilePath . '/upgrade.php';
|
||||
if ( !file_exists( $phpFile ) )
|
||||
{
|
||||
/* Work out the contents */
|
||||
$contents = str_replace(
|
||||
array(
|
||||
'{version_human}',
|
||||
"{subpackage}",
|
||||
'{date}',
|
||||
'{app}',
|
||||
'{version_long}',
|
||||
),
|
||||
array(
|
||||
$human,
|
||||
( $this->application->directory != 'core' ) ? ( " * @subpackage\t" . Member::loggedIn()->language()->get( "__app_{$this->application->directory}" ) ) : '',
|
||||
date( 'd M Y' ),
|
||||
$this->application->directory,
|
||||
$long,
|
||||
),
|
||||
file_get_contents( ROOT_PATH . "/applications/core/data/defaults/Upgrade.txt" )
|
||||
);
|
||||
|
||||
/* If this isn't an IPS app, strip out our header */
|
||||
if ( !in_array( $this->application->directory, IPS::$ipsApps ) )
|
||||
{
|
||||
$contents = preg_replace( '/(<\?php\s)\/*.+?\*\//s', '$1', $contents );
|
||||
}
|
||||
|
||||
/* Write */
|
||||
if ( !is_dir( $phpFilePath ) )
|
||||
{
|
||||
mkdir( $phpFilePath );
|
||||
chmod( $phpFilePath, IPS_FOLDER_PERMISSION );
|
||||
}
|
||||
if( @file_put_contents( $phpFile, $contents ) === FALSE )
|
||||
{
|
||||
Output::i()->error( 'dev_could_not_write_setup', '1C103/9', 403, '' );
|
||||
}
|
||||
}
|
||||
|
||||
/* And redirect */
|
||||
Output::i()->redirect( $this->url, 'file_created' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Versions: Add Version
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addVersion() : void
|
||||
{
|
||||
/* Load existing versions.json file */
|
||||
$json = $this->_getVersions();
|
||||
|
||||
/* Get form */
|
||||
$activeTab = Request::i()->tab ?: 'new';
|
||||
$form = new Form( 'versions_add' );
|
||||
switch ( $activeTab )
|
||||
{
|
||||
/* Create New */
|
||||
case 'new':
|
||||
$form->addMessage( 'versions_add_information' );
|
||||
$form->add( new Text( 'versions_human', NULL, TRUE, array( 'placeholder' => '1.0.0' ), function( $val )
|
||||
{
|
||||
if ( !preg_match( '/^([0-9]+\.[0-9]+\.[0-9]+)/', $val ) )
|
||||
{
|
||||
throw new DomainException( 'versions_human_bad' );
|
||||
}
|
||||
} ) );
|
||||
$form->add( new Text( 'versions_long', NULL, TRUE, array( 'placeholder' => '10000' ), function( $val ) use ( $json )
|
||||
{
|
||||
if ( !preg_match( '/^\d*$/', $val ) )
|
||||
{
|
||||
throw new DomainException( 'form_number_bad' );
|
||||
}
|
||||
if( isset( $json[ $val ] ) )
|
||||
{
|
||||
throw new DomainException( 'versions_long_exists' );
|
||||
}
|
||||
} ) );
|
||||
break;
|
||||
|
||||
/* Upload */
|
||||
case 'upload':
|
||||
$appKey = $this->application->directory;
|
||||
$form->add( new Upload(
|
||||
'upload',
|
||||
NULL,
|
||||
TRUE,
|
||||
array( 'allowedFileTypes' => array( 'xml' ), 'temporary' => TRUE )
|
||||
) );
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/* Has the form been submitted? */
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
/* Add values */
|
||||
$toAdd = array();
|
||||
switch ( $activeTab )
|
||||
{
|
||||
/* New Version */
|
||||
case 'new':
|
||||
$json[ $values['versions_long'] ] = $values['versions_human'];
|
||||
|
||||
/* If this was for the core, add it to all IPS apps */
|
||||
if ( $this->application->directory === 'core' )
|
||||
{
|
||||
foreach ( IPS::$ipsApps as $_appKey )
|
||||
{
|
||||
$appJson = $this->_getJson( ROOT_PATH . "/applications/{$_appKey}/data/versions.json" );
|
||||
$appJson[ $values['versions_long'] ] = $values['versions_human'];
|
||||
$this->_writeJson( ROOT_PATH . "/applications/{$_appKey}/data/versions.json", $appJson );
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
/* Uploaded versions.xml file */
|
||||
case 'upload':
|
||||
$xml = NULL;
|
||||
try
|
||||
{
|
||||
$xml = SimpleXML::loadFile( $values['upload'] );
|
||||
}
|
||||
catch ( InvalidArgumentException $e ) {}
|
||||
|
||||
if ( !$xml or $xml->getName() !== 'versions' )
|
||||
{
|
||||
Output::i()->redirect( $this->url, 'versions_upload_badxml' );
|
||||
}
|
||||
|
||||
foreach ( $xml as $version )
|
||||
{
|
||||
$json[ (int) $version->long ] = (string) $version->human;
|
||||
}
|
||||
unlink( $values['upload'] );
|
||||
break;
|
||||
}
|
||||
|
||||
/* Save a snapshot of the default theme for diffs */
|
||||
if ( $this->application->directory === 'core' )
|
||||
{
|
||||
Theme::master()->saveHistorySnapshot();
|
||||
}
|
||||
|
||||
/* Save it */
|
||||
$this->_writeVersions( $json );
|
||||
|
||||
/* Redirect */
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
if( Request::i()->isAjax() and $activeTab == 'upload' )
|
||||
{
|
||||
Output::i()->output = $form;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* If not, show it */
|
||||
Output::i()->output = Theme::i()->getTemplate( 'global' )->tabs(
|
||||
array(
|
||||
'new' => 'versions_add_new',
|
||||
'upload' => 'versions_add_upload',
|
||||
),
|
||||
$activeTab,
|
||||
$form,
|
||||
$this->url->setQueryString( array( 'do' => 'addVersion' ) )
|
||||
);
|
||||
|
||||
if( Request::i()->isAjax() )
|
||||
{
|
||||
if( Request::i()->existing )
|
||||
{
|
||||
Output::i()->output = $form;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Version
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function deleteVersion() : void
|
||||
{
|
||||
/* Make sure the user confirmed the deletion */
|
||||
Request::i()->confirmedDelete();
|
||||
|
||||
$versionsFile = ROOT_PATH . "/applications/{$this->application->directory}/data/versions.json";
|
||||
$json = $this->_getVersions();
|
||||
if ( isset( $json[ intval( Request::i()->id ) ] ) )
|
||||
{
|
||||
unset( $json[ intval( Request::i()->id ) ] );
|
||||
}
|
||||
$this->_writeVersions( $json );
|
||||
Output::i()->redirect( $this->url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add version query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addVersionQuery() : void
|
||||
{
|
||||
/* Build Form */
|
||||
$form = new Form( 'add_version_query' );
|
||||
$form->add( new TextArea( 'versions_query_code', '\IPS\Db::i()->', TRUE, array( 'size' => 45 ), function( $val )
|
||||
{
|
||||
/* Check it starts with \IPS\Db::i()-> */
|
||||
$val = trim( $val );
|
||||
if ( mb_substr( $val, 0, 14 ) !== '\IPS\Db::i()->' )
|
||||
{
|
||||
throw new DomainException( 'versions_query_start' );
|
||||
}
|
||||
|
||||
/* Check there's only one query */
|
||||
if ( mb_substr( $val, -1 ) !== ';' )
|
||||
{
|
||||
$val .= ';';
|
||||
}
|
||||
if ( mb_substr_count( $val, ';' ) > 1 )
|
||||
{
|
||||
throw new DomainException( 'versions_query_one' );
|
||||
}
|
||||
|
||||
/* Check our Regex will be okay with it */
|
||||
preg_match( '/^\\\IPS\\\Db::i\(\)->(.+?)\(\s*[\'"](.+?)[\'"]\s*(,\s*(.+?))?\)\s*;$/', $val, $matches );
|
||||
if ( empty( $matches ) )
|
||||
{
|
||||
throw new DomainException( 'versions_query_format' );
|
||||
}
|
||||
|
||||
/* Run it if we're adding it to the current working version */
|
||||
if( Request::i()->id == 'working' )
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( @eval( $val ) === FALSE )
|
||||
{
|
||||
throw new DomainException( 'versions_query_phperror' );
|
||||
}
|
||||
}
|
||||
catch ( ParseError $e )
|
||||
{
|
||||
throw new DomainException( 'versions_query_phperror' );
|
||||
}
|
||||
}
|
||||
catch ( DbException $e )
|
||||
{
|
||||
throw new DomainException( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
} ) );
|
||||
|
||||
/* If submitted, add to json file */
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
/* Get our file */
|
||||
$version = Request::i()->id;
|
||||
$json = $this->_getQueries( $version );
|
||||
|
||||
/* Work out the different parts of the query */
|
||||
$val = trim( $values['versions_query_code'] );
|
||||
if ( mb_substr( $val, -1 ) !== ';' )
|
||||
{
|
||||
$val .= ';';
|
||||
}
|
||||
preg_match( '/^\\\IPS\\\Db::i\(\)->(.+?)\(\s*(.+?)\s*\)\s*;$/', $val, $matches );
|
||||
|
||||
/* Add it on */
|
||||
$json[] = array( 'method' => $matches[1], 'params' => eval( 'return array( ' . $matches[2] . ' );' ) );
|
||||
|
||||
/* Write it */
|
||||
$this->_writeQueries( $version, $json );
|
||||
|
||||
/* Redirect us */
|
||||
Output::i()->redirect( $this->url->setQueryString( 'root', $version ) );
|
||||
}
|
||||
|
||||
/* Or display it */
|
||||
else
|
||||
{
|
||||
Output::i()->output .= Theme::i()->getTemplate( 'global' )->block( 'versions_query', $form, FALSE );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Version Query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function deleteVersionQuery() : void
|
||||
{
|
||||
/* Make sure the user confirmed the deletion */
|
||||
Request::i()->confirmedDelete();
|
||||
|
||||
$version = ( Request::i()->version == 'install' ? 'install' : Request::i()->version );
|
||||
|
||||
$json = $this->_getQueries( $version );
|
||||
unset( $json[ intval( Request::i()->query ) ] );
|
||||
$this->_writeQueries( $version, $json );
|
||||
Output::i()->redirect( $this->url->setQueryString( 'root', $version ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the queries rows for a version
|
||||
*
|
||||
* @param int|string $long Version ID
|
||||
* @return array
|
||||
*/
|
||||
public function _getQueriesRows( int|string $long ) : array
|
||||
{
|
||||
$queries = $this->_getQueries( $long );
|
||||
$order = 1;
|
||||
$rows = array();
|
||||
foreach ( $queries as $qid => $data )
|
||||
{
|
||||
$params = array();
|
||||
if ( isset( $data['params'] ) and is_array( $data['params'] ) )
|
||||
{
|
||||
foreach ( $data['params'] as $v )
|
||||
{
|
||||
$params[] = var_export( $v, TRUE );
|
||||
}
|
||||
}
|
||||
|
||||
$rows["{$long}.{$qid}"] = Theme::i()->getTemplate( 'trees', 'core' )->row( $this->url, "{$long}.{$qid}", str_replace( '<?php', '', highlight_string( "<?php \\IPS\\Db::i()->{$data['method']}( ". implode( ', ', $params ) ." )", TRUE ) ), FALSE, array(
|
||||
'delete' => array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $this->url->setQueryString( array( 'do' => 'deleteVersionQuery', 'version' => $long, 'query' => $qid ) ),
|
||||
'data' => array( 'delete' => '' )
|
||||
)
|
||||
), NULL, NULL, NULL, FALSE, NULL, NULL, NULL, TRUE, FALSE, FALSE, FALSE );
|
||||
|
||||
$order++;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief widgets
|
||||
* @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
|
||||
|
||||
* @since 31 Jan 2024
|
||||
*/
|
||||
|
||||
namespace IPS\core\modules\admin\developer;
|
||||
|
||||
use IPS\Developer\Controller;
|
||||
use IPS\Output;
|
||||
use IPS\Widget;
|
||||
use function defined;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* widgets
|
||||
*/
|
||||
class widgets extends Controller
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $csrfProtected = true;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function manage() : void
|
||||
{
|
||||
Output::i()->output = Widget::devTable(
|
||||
ROOT_PATH . "/applications/{$this->application->directory}/data/widgets.json",
|
||||
$this->url,
|
||||
ROOT_PATH . "/applications/{$this->application->directory}/widgets",
|
||||
$this->application->directory,
|
||||
$this->application->directory,
|
||||
$this->application->directory
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user