Files
IPB/applications/core/modules/admin/developer/acpmenu.php
T
2025-12-19 16:27:35 -08:00

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}&amp;module=" . ( $row['module_url'] ?? $k ) . "&amp;controller=" . $row['controller'] . ( $row['do'] ? "&amp;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 '';
}
}