<?php
/**
* App class.
*
* @package Imagely\NGG\Admin
*/
// TODO: Add comments at the end via ai.
// phpcs:disable Squiz.Commenting
namespace Imagely\NGG\Admin;
use Imagely\NGG\Admin\AMNotifications as Notifications;
use Imagely\NGG\DisplayType\LegacyTemplateLocator;
class App {
private $hook_suffixes = [];
public function __construct() {
// Constructor should be empty since we register hooks in hooks()
}
public function hooks() {
add_action( 'admin_menu', [ $this, 'admin_menu' ] );
add_action( 'admin_menu', [ $this, 'add_upgrade_menu_item' ], 1000 );
add_action( 'admin_menu', [ $this, 'add_cdn_menu_item' ], 11 );
add_action( 'admin_head', [ $this, 'admin_inline_styles' ] );
add_action( 'admin_footer', [ $this, 'admin_sidebar_target' ] );
}
public function admin_menu() {
$notifications = new Notifications();
// Notification count HTML to append to the menu.
$nav_append_count = '';
if ( absint( $notifications->get_count() ) > 0 ) {
$nav_append_count = "<span class='ngg-menu-notification-indicator update-plugins'>" . absint( $notifications->get_count() ) . '</span>';
}
$menu = 'imagely';
// Use NextGEN Gallery overview capability for main menu, matching legacy admin behavior
$this->hook_suffixes[] = add_menu_page(
__( 'Imagely', 'nggallery' ),
__( 'Imagely', 'nggallery' ) . $nav_append_count,
'NextGEN Gallery overview',
$menu,
[ $this, 'render_settings_page' ],
plugins_url( 'assets/images/logo-icon.png', NGG_PLUGIN_FILE ),
10
);
// Use proper NextGEN capabilities for each menu item, matching legacy admin behavior
$sub_menus = [
[
'name' => __( 'Galleries', 'nggallery' ),
'capability' => 'NextGEN Manage gallery',
'menu_slug' => $menu,
],
[
'name' => __( 'Add New Gallery', 'nggallery' ),
'capability' => 'NextGEN Upload images',
'menu_slug' => "$menu&create_gallery=1",
],
[
'name' => __( 'Edit Gallery', 'nggallery' ),
'capability' => 'NextGEN Manage gallery',
'menu_slug' => "$menu-add-new",
],
[
'name' => __( 'Albums', 'nggallery' ),
'capability' => 'NextGEN Edit album',
'menu_slug' => "$menu-albums",
],
[
'name' => __( 'Tags', 'nggallery' ),
'capability' => 'NextGEN Manage tags',
'menu_slug' => "$menu-tags",
],
];
// Only add eCommerce menu item if Pro level is installed
$pro_type = $this->get_pro_type_installed();
if ( 'pro' === $pro_type ) {
$sub_menus[] = [
'name' => __( 'eCommerce', 'nggallery' ),
'capability' => 'NextGEN Change options',
'menu_slug' => "$menu-ecommerce",
];
}
$sub_menus[] = [
'name' => __( 'Settings', 'nggallery' ),
'capability' => 'NextGEN Change options',
'menu_slug' => "$menu-settings",
];
$sub_menus[] = [
'name' => __( 'Layout Settings', 'nggallery' ),
'capability' => 'NextGEN Change style',
'menu_slug' => "$menu-layout-settings",
];
$sub_menus[] = [
'name' => __( 'About Us', 'nggallery' ),
'capability' => 'NextGEN Gallery overview',
'menu_slug' => "$menu-about-us",
];
foreach ( $sub_menus as $sub_menu ) {
$this->hook_suffixes[] = add_submenu_page(
$menu,
$sub_menu['name'],
$sub_menu['name'],
$sub_menu['capability'],
$sub_menu['menu_slug'],
[ $this, 'render_settings_page' ]
);
}
// Hide the "Edit Gallery" menu item from the sidebar (but keep it registered)
add_action( 'admin_head', [ $this, 'hide_edit_gallery_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
}
/**
* Hide the "Edit Gallery" submenu item from the sidebar.
* We still register it so the page exists (for direct URL access with ID),
* but we don't want it to appear in the menu.
*/
public function hide_edit_gallery_menu() {
global $submenu;
if ( ! isset( $submenu['imagely'] ) ) {
return;
}
foreach ( $submenu['imagely'] as $key => $item ) {
// Remove the "imagely-add-new" submenu item
if ( isset( $item[2] ) && 'imagely-add-new' === $item[2] ) {
unset( $submenu['imagely'][ $key ] );
}
// Remove the "imagely-layout-settings" submenu item (accessible via Settings tab)
if ( isset( $item[2] ) && 'imagely-layout-settings' === $item[2] ) {
unset( $submenu['imagely'][ $key ] );
}
}
}
public static function is_debug() {
// Check custom debug mode setting first
$settings = get_option( 'ngg_options', [] );
$debug_mode = isset( $settings['ngg_debug_mode'] ) && $settings['ngg_debug_mode'] ? filter_var( $settings['ngg_debug_mode'], FILTER_VALIDATE_BOOLEAN ) : false;
return $debug_mode;
}
public function admin_enqueue_scripts( $hook_suffix ) {
if ( ! in_array( $hook_suffix, $this->hook_suffixes, true ) ) {
return;
}
// Suppress WP admin notices on Imagely App pages
remove_all_actions( 'admin_notices' );
remove_all_actions( 'all_admin_notices' );
remove_all_actions( 'network_admin_notices' );
remove_all_actions( 'user_admin_notices' );
$script = require NGG_PLUGIN_DIR . 'adminApp/build/dependencies.php';
$script['version'] = self::is_debug() ? time() : $script['version'];
// Enqueue WordPress media scripts
wp_enqueue_media();
wp_enqueue_script(
'imagely-settings-app',
plugins_url( 'adminApp/build/index.min.js', NGG_PLUGIN_FILE ),
$script['dependencies'],
$script['version'],
true
);
// Load translations for the adminApp script
// WordPress will automatically load the JSON file based on the script path
wp_set_script_translations(
'imagely-settings-app',
'nggallery',
NGG_PLUGIN_DIR . 'static/I18N'
);
wp_localize_script(
'imagely-settings-app',
'imagelyApp',
self::get_imagely_app_data()
);
wp_enqueue_style(
'imagely-settings-app-styles',
plugins_url( 'adminApp/build/style.min.css', NGG_PLUGIN_FILE ),
[],
$script['version']
);
// Unregister wp forms.css
wp_deregister_style( 'forms-css' );
}
public function render_settings_page() {
$embed = isset( $_GET['embed'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['embed'] ) );
$embed_css = $embed ? '<style>
#adminmenumain, #wpadminbar, #screen-meta, #wpfooter, #wpbody-content > .notice, #wpbody-content > .updated, #wpbody-content > .error { display:none !important; }
html.wp-toolbar #wpcontent { margin: 0 !important; padding:0 !important; overflow: hidden !important; }
html.wp-toolbar { margin: 0 !important; padding:0 !important; overflow: hidden !important; }
#wpbody-content { padding-bottom: 0 !important; }
#wpbody, #wpbody-content { overflow: hidden !important; }
body { overflow: hidden; background: #fff; }
/* Ensure app fills iframe viewport */
html, body, #wpcontent, #wpbody, #wpbody-content, .imagely-wrap, #imagely-admin-app { height: 100vh !important; }
.imagely-wrap { margin: 0 !important; padding: 0 !important; overflow: hidden !important; }
#imagely-admin-app { overflow-y: auto !important; -webkit-overflow-scrolling: touch; }
</style>' : '';
$page = <<<HTML
{$embed_css}
<div class="imagely-wrap">
<div id="imagely-admin-app"></div>
</div>
HTML;
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentionally outputting HTML.
echo $page;
}
/**
* Get imagelyApp data for wp_localize_script.
* Centralized method to ensure consistency across admin pages and blocks.
*
* @return array ImagelyApp data array (basic fields only)
*/
public static function get_imagely_app_data() {
return [
'nonce' => wp_create_nonce( 'imagely-admin' ),
'nonce_preview' => wp_create_nonce( 'ngg_preview_shortcode' ),
'restURL' => esc_url_raw( rest_url() ),
'assetsURL' => plugins_url( 'assets', NGG_PLUGIN_FILE ),
'home_url' => esc_url_raw( get_home_url() ),
'adminUrl' => esc_url_raw( admin_url() ),
'pluginPath' => NGG_PLUGIN_DIR,
'plugin_url' => esc_url_raw( trailingslashit( plugins_url( '', NGG_PLUGIN_FILE ) ) ),
'debug' => self::is_debug(),
'proTypeInstalled' => self::get_pro_type_installed(),
'licenseData' => self::get_license_data(),
'enviraCdnConfig' => self::get_cdn_config(),
'canAccessRolesSettings' => self::can_access_roles_settings(),
'canAccessLicenseSettings' => self::can_access_license_settings(),
'legacyTemplates' => self::get_legacy_templates(),
];
}
/**
* Get available legacy templates organized by display type prefix.
*
* @return array Legacy templates grouped by prefix (gallery, imagebrowser, album, singlepic)
*/
public static function get_legacy_templates() {
$locator = LegacyTemplateLocator::get_instance();
$prefixes = [ 'gallery', 'imagebrowser', 'album', 'singlepic' ];
$templates = [];
foreach ( $prefixes as $prefix ) {
$templates[ $prefix ] = [];
// Add default option first.
$templates[ $prefix ]['default'] = __( 'Default', 'nggallery' );
// Get templates for this prefix.
$found = $locator->find_all( $prefix );
foreach ( $found as $label => $files ) {
foreach ( $files as $file ) {
$filename = basename( $file );
$templates[ $prefix ][ $file ] = "{$label}: {$filename}";
}
}
}
return $templates;
}
/**
* Returns the installed NextGen Pro type (pro, plus, starter, or lite), not the license status.
*/
public static function get_pro_type_installed() {
if ( class_exists( '\Imagely\NGGPro\Bootloader' ) ) {
return \Imagely\NGGPro\Bootloader::$plugin_id;
}
return 'lite';
}
/**
* Get current license data to expose to JavaScript.
* This mirrors the data structure from the license-actions/current endpoint.
*
* @return array License data array
*/
public static function get_license_data() {
$license_data = [
'license_key' => '',
'status' => 'free',
'level' => 'free',
'last_check' => 0,
'plugin_level' => 'free',
'is_valid' => false,
'expiration' => null,
'expires_soon' => false,
];
// Only get license data if we're not on the free version
$pro_type = self::get_pro_type_installed();
if ( 'lite' === $pro_type ) {
return $license_data;
}
// Get license using the same method as the REST endpoint
if ( class_exists( '\Imagely\NGGPro\License\Manager' ) ) {
$licensing = new \Imagely\NGGPro\License\Manager();
$current_product = $licensing::get_current_product();
$license_key = $licensing->get_license( $current_product );
if ( ! empty( $license_key ) ) {
$license_data['license_key'] = $license_key;
// Get plugin level
$license_data['plugin_level'] = $pro_type;
// Get license status
$status = get_option( 'ngg_license_status_' . $pro_type, '' );
if ( ! empty( $status ) ) {
$license_data['status'] = $status;
$license_data['is_valid'] = in_array( $status, [ 'active', 'valid' ], true );
}
// Get license level
$level = get_option( 'ngg_license_level_' . $pro_type, '' );
if ( ! empty( $level ) ) {
$license_data['level'] = $level;
}
// Get last check time
$last_check = get_option( 'ngg_last_license_check', 0 );
$license_data['last_check'] = (int) $last_check;
// Get license expiration
$expiration = get_option( 'ngg_license_expiration_' . $pro_type, '' );
if ( ! empty( $expiration ) ) {
if ( 'lifetime' === $expiration ) {
$license_data['expiration'] = 'lifetime';
$license_data['expires_soon'] = false;
} elseif ( is_numeric( $expiration ) ) {
$license_data['expiration'] = (int) $expiration;
// Check if license expires within 30 days
$thirty_days_from_now = time() + ( 30 * DAY_IN_SECONDS );
$license_data['expires_soon'] = ( $expiration < $thirty_days_from_now && $expiration > time() );
}
}
}
}
return $license_data;
}
/**
* Get system information for the admin app.
*
* @return array System information array
*/
public static function get_system_info() {
global $wp_version, $wpdb;
$settings = \Imagely\NGG\Settings\Settings::get_instance();
// Get database info
$db_server_info = method_exists( $wpdb, 'db_server_info' ) ? $wpdb->db_server_info() : '';
$mysql_type = stripos( $db_server_info, 'mariadb' ) !== false ? 'MariaDB' : 'MySQL';
$mysql_version = method_exists( $wpdb, 'db_version' ) ? $wpdb->db_version() : 'Unknown';
// Get upload directory info
$upload_dir = wp_upload_dir();
$system_info = [
// Existing fields
'php_version' => phpversion(),
'wordpress_version' => $wp_version,
'memory_limit' => ini_get( 'memory_limit' ),
'max_upload_size' => size_format( wp_max_upload_size() ),
'server_software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'Unknown',
'plugin_version' => defined( 'NGG_PLUGIN_VERSION' ) ? NGG_PLUGIN_VERSION : 'Unknown',
'gd_version' => self::get_gd_version(),
'imagick_version' => self::get_imagick_version(),
// Server Environment
'php_os' => PHP_OS,
'php_sapi' => php_sapi_name(),
'mysql_version' => $mysql_version,
'mysql_type' => $mysql_type,
'curl_version' => function_exists( 'curl_version' ) ? curl_version()['version'] : 'Not available',
'openssl_version' => defined( 'OPENSSL_VERSION_TEXT' ) ? OPENSSL_VERSION_TEXT : 'Not available',
// PHP Extensions
'exif_enabled' => function_exists( 'exif_read_data' ) ? 'Yes' : 'No',
'iptc_enabled' => function_exists( 'iptcparse' ) ? 'Yes' : 'No',
'mbstring_enabled' => extension_loaded( 'mbstring' ) ? 'Yes' : 'No',
'zip_enabled' => extension_loaded( 'zip' ) ? 'Yes' : 'No',
'fileinfo_enabled' => extension_loaded( 'fileinfo' ) ? 'Yes' : 'No',
// PHP Configuration
'post_max_size' => ini_get( 'post_max_size' ),
'max_execution_time' => ini_get( 'max_execution_time' ),
'max_input_vars' => ini_get( 'max_input_vars' ),
'upload_max_filesize' => ini_get( 'upload_max_filesize' ),
'allow_url_fopen' => ini_get( 'allow_url_fopen' ) ? 'Yes' : 'No',
// WordPress Configuration
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? 'Yes' : 'No',
'wp_debug_log' => defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ? 'Yes' : 'No',
'wp_memory_limit' => defined( 'WP_MEMORY_LIMIT' ) ? WP_MEMORY_LIMIT : 'Not set',
'site_url' => site_url(),
'home_url' => home_url(),
'is_multisite' => is_multisite() ? 'Yes' : 'No',
'active_theme' => wp_get_theme()->get( 'Name' ) . ' ' . wp_get_theme()->get( 'Version' ),
// NextGen Specific Settings
'ngg_options_version' => get_option( 'ngg_options_version', 'Not set' ),
'image_library_preference' => $settings->get( 'imgLibrary', 'gd' ),
'thumbnail_quality' => $settings->get( 'thumbquality', '100' ),
'backup_images' => $settings->get( 'imgBackup', false ) ? 'Yes' : 'No',
'galleries_count' => wp_count_posts( 'ngg_gallery' )->publish ?? 0,
'images_count' => $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}ngg_pictures" ) ?? 0,
// Server Paths
'wp_content_dir' => WP_CONTENT_DIR,
'upload_dir' => $upload_dir['basedir'],
'ngg_gallery_path' => $settings->get( 'gallerypath', 'Not set' ),
// Additional Settings
'timezone' => wp_timezone_string(),
'locale' => get_locale(),
'permalink_structure' => get_option( 'permalink_structure', 'Default' ),
// NextGen Legacy Settings
'show_legacy_admin_pages' => $settings->get( 'ngg_show_old_settings', false ) ? 'Yes' : 'No',
'activate_legacy_block' => $settings->get( 'ngg_installation_type', 'fresh' ) === 'existing' ? 'Yes' : 'No',
];
return $system_info;
}
/**
* Get GD version information.
*
* @return string GD version or 'Not available'
*/
private static function get_gd_version() {
if ( ! function_exists( 'gd_info' ) ) {
return 'Not available';
}
$gd_info = gd_info();
return isset( $gd_info['GD Version'] ) ? $gd_info['GD Version'] : 'Not available';
}
/**
* Get ImageMagick version information.
*
* @return string ImageMagick version or 'Not available'
*/
private static function get_imagick_version() {
if ( ! class_exists( 'Imagick' ) ) {
return 'Not available';
}
try {
// phpcs:ignore PHPCompatibility.Classes.NewClasses.imagickFound
$imagick = new \Imagick();
$version = $imagick->getVersion();
return isset( $version['versionString'] ) ? $version['versionString'] : 'Not available';
} catch ( \Exception $e ) {
return 'Not available';
}
}
/**
* Get CDN plugin configuration if active.
*
* @return array|null CDN configuration or null if not active
*/
private static function get_cdn_config() {
// Check if Envira CDN plugin is active.
if ( ! class_exists( 'Envira_CDN' ) ) {
return null;
}
// Check if CDN is enabled globally.
$cdn_config = get_option( 'envira_cdn_config', [] )['envira_cdn_config'] ?? [];
$enabled = isset( $cdn_config['enable'] ) ? $cdn_config['enable'] : false;
// Check if license is valid.
$license_valid = false;
if ( class_exists( 'Envira\CDN\Utils\Common' ) ) {
$license_key = \Envira\CDN\Utils\Common::get_license_key();
$license_valid = ! empty( $license_key );
}
return [
'enabled' => $enabled,
'license_valid' => $license_valid,
];
}
/**
* Check if current user can access roles and capabilities settings.
*
* @return bool
*/
public static function can_access_roles_settings() {
if ( ! is_super_admin() ) {
return false;
}
if ( ! is_multisite() ) {
return true;
}
$settings = \Imagely\NGG\Settings\Settings::get_instance();
return (bool) $settings->get( 'wpmuRoles' );
}
/**
* Check if current user can access license settings.
*
* @return bool
*/
public static function can_access_license_settings() {
if ( ! is_multisite() ) {
return current_user_can( 'manage_options' );
}
return is_super_admin();
}
/**
* Add lite-specific upgrade to pro menu item.
*
* @since 3.6.0
* @return void
*/
public function add_upgrade_menu_item() {
global $submenu;
// Only add upgrade menu item for lite version (hide for pro, plus, and starter)
$pro_type = $this->get_pro_type_installed();
if ( 'lite' !== $pro_type ) {
return;
}
// Use same capability as main menu
add_submenu_page(
'imagely',
esc_html__( 'Upgrade to Pro', 'nggallery' ),
esc_html__( 'Upgrade to Pro', 'nggallery' ),
'NextGEN Gallery overview',
esc_url( $this->get_utm_link( 'https://www.imagely.com/lite/', 'adminsidebar', 'unlockprosidebar' ) )
);
if ( ! current_user_can( 'NextGEN Gallery overview' ) ) {
return;
}
// Find the upgrade menu item and add CSS class for styling
if ( ! isset( $submenu['imagely'] ) ) {
return;
}
$upgrade_link_position = key(
array_filter(
$submenu['imagely'],
static function ( $item ) {
return str_contains( $item[2], 'https://www.imagely.com/lite/' );
}
)
);
if ( null === $upgrade_link_position ) {
return;
}
// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
if ( isset( $submenu['imagely'][ $upgrade_link_position ][4] ) ) {
$submenu['imagely'][ $upgrade_link_position ][4] .= ' imagely-sidebar-upgrade-pro';
} else {
$submenu['imagely'][ $upgrade_link_position ][] = 'imagely-sidebar-upgrade-pro';
}
// phpcs:enable WordPress.WP.GlobalVariablesOverride.Prohibited
}
/**
* Add CDN menu item.
*
* @return void
*/
public function add_cdn_menu_item() {
global $submenu;
$utm = '?utm_source=imagely&utm_medium=admin-menu&utm_campaign=imagely-cdn&utm_content=' . NGG_PLUGIN_VERSION;
// Only add if the submenu exists.
if ( isset( $submenu['imagely'] ) ) {
$submenu['imagely'][] = [
// Use HTML in the menu title, but be aware some WP versions may not render it.
'Imagely CDN <span class="imagely_cdn_new_badge">NEW</span>',
'manage_options',
'https://www.imagely.com/cdn/' . $utm,
];
}
}
/**
* Add inline styles for upgrade menu item.
*
* @since 3.6.0
* @return void
*/
public function admin_inline_styles() {
$pro_type = $this->get_pro_type_installed();
if ( 'lite' !== $pro_type ) {
return;
}
echo '<style>
.imagely-sidebar-upgrade-pro {
background-color: #37993B;
}
.imagely-sidebar-upgrade-pro a {
color: #fff !important;
}
</style>';
}
/**
* Make upgrade menu link open in new tab.
*
* @since 3.6.0
* @return void
*/
public function admin_sidebar_target() {
$pro_type = $this->get_pro_type_installed();
if ( 'lite' !== $pro_type ) {
return;
}
?>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('li.imagely-sidebar-upgrade-pro a').attr('target','_blank');
});
</script>
<?php
}
/**
* Get UTM link for marketing URLs.
*
* @since 3.6.0
* @param string $url Base URL.
* @param string $medium UTM medium parameter.
* @param string $campaign UTM campaign parameter.
* @param string $source UTM source parameter.
* @return string URL with UTM parameters.
*/
private function get_utm_link( $url, $medium = 'default', $campaign = 'default', $source = 'ngg' ) {
$params = apply_filters(
'ngg_marketing_parameters',
[
'url' => $url,
'medium' => $medium,
'campaign' => $campaign,
'source' => $source,
]
);
$url .= '?utm_source=' . $params['source'];
$url .= '&utm_medium=' . $params['medium'];
$url .= '&utm_campaign=' . $params['campaign'];
return $url;
}
}