979 lines
29 KiB
PHP
979 lines
29 KiB
PHP
<?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 function str_replace;
|
|
use function strtolower;
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Extensions */
|
|
$extensionStrings = [
|
|
'ModeratorPermission' => 'modperms__{app}_{key}',
|
|
'AchievementAction' => 'AchievementAction__{key}',
|
|
'AdminNotifications' => 'acp_notification_{key}',
|
|
'CommunityEnhancements' => 'enhancements__{app}_{key}',
|
|
'Dashboard' => 'block_{app}_{key}',
|
|
'EditorLocations' => 'editor__{app}_{key}',
|
|
'EditorMedia' => 'editorMedia_{app}_{key}',
|
|
'FileStorage' => 'filestorage__{app}_{key}',
|
|
'GroupForm' => 'group__{app}_{key}',
|
|
'IpAddresses' => 'ipAddresses__{app}_{key}',
|
|
'LiveSearch' => 'acp_search_title_{app}_{key}',
|
|
'MemberACPProfileTabs' => 'memberACPProfileTitle_{app}_{key}',
|
|
'MFAArea' => 'MFA_{app}_{key}',
|
|
];
|
|
|
|
$appExtensions = $this->_getJson( ROOT_PATH . "/applications/" . $this->application->directory . "/data/extensions.json" );
|
|
foreach( $appExtensions as $_app => $_extensions )
|
|
{
|
|
foreach( $_extensions as $extensionType => $extensions )
|
|
{
|
|
if( !isset( $extensionStrings[ $extensionType ] ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach( $extensions as $extensionName => $extensionClass )
|
|
{
|
|
$stringToCheck = str_replace( [ '{app}', '{key}' ], [ $this->application->directory, $extensionName ], $extensionStrings[ $extensionType ] );
|
|
if( !array_key_exists( $stringToCheck, $languageStrings ) )
|
|
{
|
|
$missingAdminStrings[ strtolower( $extensionType ) ][] = $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;
|
|
}
|
|
} |