462 lines
14 KiB
PHP
462 lines
14 KiB
PHP
<?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 '';
|
|
}
|
|
} |