#62244 closed enhancement (fixed)
Just-in-time translation loading for plugins/themes not in the directory
Reported by: | swissspidy | Owned by: | swissspidy |
---|---|---|---|
Milestone: | 6.8 | Priority: | normal |
Severity: | normal | Version: | |
Component: | I18N | Keywords: | has-patch needs-dev-note |
Focuses: | Cc: |
Description
This is a follow-up to the just-in-time translation loading we added in WordPress 4.6, see #34114.
We could theoretically expand this to custom plugins as well.
If no translation files are found for a given domain, we could use get_plugins()
and check if there's a plugin with a matching slug, get its Domain Path
value (like _get_plugin_data_markup_translate
does) and then store that location in WP_Textdomain_Registry
.
This way, custom plugins won't need to call load_plugin_textdomain()
anymore as long as they define Domain Path
in the plugin headers.
Question is whether we can do this in a performant way, minimizing file reads, adding caching, etc.
Change History (33)
This ticket was mentioned in PR #7592 on WordPress/wordpress-develop by @swissspidy.
7 weeks ago
#1
- Keywords has-patch added
#3
@
13 days ago
- Owner set to swissspidy
- Resolution set to fixed
- Status changed from new to closed
In 59461:
@swissspidy commented on PR #7592:
13 days ago
#4
#6
@
13 days ago
- Resolution fixed deleted
- Status changed from closed to reopened
Changeset [59461] is causing the Airplane Mode plugin on my local development site to trigger a fatal error. It might be an indication that the plugin is doing it wrong, or it might be a bigger problem:
PHP Fatal error: Uncaught Error: Call to undefined function is_plugin_active() in wp-content/plugins/airplane-mode/airplane-mode.php:162 Stack trace: #0 wp-content/plugins/airplane-mode/airplane-mode.php(250): Airplane_Mode_Core->__construct() #1 wp-content/plugins/airplane-mode/airplane-mode.php(1317): Airplane_Mode_Core::getInstance() #2 wp-settings.php(526): include_once('wp...') #3 /var/www/wp-config.php(108): require_once('wp...') #4 wp-load.php(55): require_once('/var/www/wp-con...') #5 wp-blog-header.php(13): require_once('wp...') #6 _index.php(17): require('wp...') #7 index.php(19): require_once('_i...') #8 {main} thrown in wp-content/plugins/airplane-mode/airplane-mode.php on line 162
Reopening to investigate.
#7
@
13 days ago
I couldn't reproduce this at first, but then I realized you are using the current version from GitHub (0.2.7), which is newer than the one from the plugin directory (0.2.5).
The problem in that plugin is that it does the following:
if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( is_plugin_active( 'jetpack/jetpack.php' ) ) { $jetpack_plugin = get_plugin_data( trailingslashit( WP_PLUGIN_DIR ) . 'jetpack/jetpack.php' ); }
The problem is that get_plugin_data()
is now already defined, so the file isn't included. However, get_plugin_data
is only defined in that file, hence the fatal error about the function not existing.
The proper way would be:
if ( ! function_exists( 'get_plugin_data' ) || ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; }
I'll try find out if more plugins do it wrong like this (e.g. with GitHub Search), but that's gonna be difficult I think.
Patching Airplane Mode with the above change makes sense either way.
#8
@
12 days ago
This has also caused some fatals on wp-themes.com, although I was struggling to understand how, it's the same type of code as in comment 7, get_plugin_data()
is checked for and if not present plugin.php
is loaded, and elsewhere in the theme is_plugin_active()
is called.
Example: https://themes.trac.wordpress.org/ticket/182411#comment:2
Here's some affected themes:
- https://wordpress.org/themes/ajima/
- https://wordpress.org/themes/avenews/
- https://wordpress.org/themes/ecoscape/
- https://wordpress.org/themes/photographymax/
Sidenote: Making is_plugin_active()
always available would be a benefit to a bunch of various plugins/themes.
#9
@
12 days ago
Thanks!
Seems like it‘s common to use both functions together or only check for one function while needing both. Relocating is_plugin_active() would help—unless there are more functions people use without checking.
#10
@
12 days ago
Here's basic GitHub search that I manually skimmed through. I found a few more plugins potentially affected by this:
https://github.com/varunsridharan/wp-dependencies/blob/69cb89d618b0c30f4a1b7ee4abc2796eabe064a6/src/dependencies.php#L27-L63
https://github.com/pantheon-systems/WordPress/blob/e2366cd0ea334ac11db8ecbd87af64023ff80ba0/wp-content/mu-plugins/pantheon-mu-plugin/inc/compatibility/base.php#L76-L92
https://github.com/tomastm07/supermarket-sku110-dense-object-detection/blob/258d4fab8d99edae61eed823ee78484ab99b39ba/wp-content/plugins/sg-security/core/Plugins_Service/Plugins_Service.php#L61-L90
https://github.com/kanbanwp/kanban/blob/848722bccef253a13839a60a9f6122d6939eceb5/v3/kanban.php#L63-L74
They sometimes use is_plugin_active
or is_plugin_active_for_network
, potentially even others. So at least those two would need to be moved.
An alternative is to not move any functions but just load wp-admin/includes/plugin.php
early on, but that exposes too much IMO.
This ticket was mentioned in PR #7900 on WordPress/wordpress-develop by @swissspidy.
12 days ago
#11
Plugins: Make more plugin-related functions available early on. This is a follow-up to [59461], which moved `get_plugin_data()` from `wp-admin/includes/plugin.php` to `wp-includes/functions.php` so it's available during the plugin loading process. Related functions like `is_plugin_active()` are often used together and should therefore be moved as well, to improve backward compatibility for plugins which load `wp-admin/includes/plugin.php` only conditionally. Props johnbillion, dd32, swissspidy Fixes #62244.
Trac ticket: https://core.trac.wordpress.org/ticket/62244
#13
@
12 days ago
Another error reported in #62581 by @neo2k23:
Fatal error: Uncaught TypeError: basename(): Argument #1 ($path) must be of type string, Theme given in /home/public_html/wp-settings.php:686 Stack trace: #0 /home/public_html/wp-settings.php(686): basename(Object(Theme)) #1 /home/wp-config.php(96): require_once('/home/...') #2 /home/public_html/wp-load.php(55): require_once('/home/..') #3 /home/public_html/wp-admin/admin.php(34): require_once('/home/...') #4 /home/public_html/wp-admin/about.php(10): require_once('/home/...') #5 {main} thrown in /home/public_html/wp-settings.php on line 686
wp_get_active_and_valid_themes()
is supposed to return an array of strings. Theme
is not a class that exists in WordPress.
@neo2k23 are you using some special plugin/theme/drop-in that modifies the $wp_stylesheet_path
or $wp_template_path
globals or so? Where is the Theme
class coming from in your project?
#14
@
12 days ago
I am using a theme from themeforest. wp_stylesheet_path and wp_template_path are not modified by the theme or even called. No code to be found in the theme code.
Turned off all plugins. Same result.
#15
@
12 days ago
The theme has this code in the functions.php
<?php if(!class_exists('Theme')){ /* Load the Theme class. */ require_once (TEMPLATEPATH . '/framework/theme.php'); $theme = new Theme(); $options = include(TEMPLATEPATH . '/framework/info.php'); $theme->init($options); }
#16
@
12 days ago
Here is the complete code of the functions.php
<?php <?php /** * Theme framework initialization * * Sets up the theme and provides some helper functions. * * This file will not be overridden by theme updates. So you can add your custom functions down below where indicated and they are safe. * * When using a child theme (see http://codex.wordpress.org/Theme_Development and * http://codex.wordpress.org/Child_Themes), you can override certain functions * (those wrapped in a function_exists() call) by defining them first in your child theme's * functions.php file. The child theme's functions.php file is included before the parent * theme's file, so the child theme functions would be used. * This file is like a child functions file, and serves the same purpose, without having to go to the * trouble of implementing a child theme, if only having one or two simple custom functions. * * NOTE: IF UPDATING THE THEME BY FTP, RENAME THIS FILE SO THAT IT IS NOT OVERRIDDEN OR DO NOT ALLOW * IT TO BE REPLACED IN THE FTP UPDATE. IF SAVED BY A RENAME, OF COURSE REMEMBER TO COME BACK AND * COPY ITS CUSTOM FUNCTIONS TO YOUR NEW FUNCTIONS.PHP FILE. */ if(!class_exists('Theme')){ /* Load the Theme class. */ require_once (TEMPLATEPATH . '/framework/theme.php'); $theme = new Theme(); $options = include(TEMPLATEPATH . '/framework/info.php'); $theme->init($options); }
$theme is not called global but if i checked it before the class exists it already contains the path to the theme.
I can change it to any other name and that fixes the issue. I guess more custom themes could run into this.
This ticket was mentioned in PR #7901 on WordPress/wordpress-develop by @swissspidy.
12 days ago
#17
`
I18N: Do not reuse $theme
variable name after loading a theme's functions.php
file.
The file could declare its own $theme
variable, which would override the one used in the foreach
loop.
To prevent this, call wp_get_theme()
before loading the file and store the instance in a different variable.
Props neo2k23, swissspidy.
See #62244.
Trac ticket: https://core.trac.wordpress.org/ticket/62244
#18
follow-up:
↓ 20
@
12 days ago
Ah! That explains it! Thanks for sharing.
The theme defines $theme
in its functions.php
file, which overrides the $theme
variable from the foreach
loop.
Here's a PR that fixes this: https://github.com/WordPress/wordpress-develop/pull/7901
#20
in reply to:
↑ 18
@
12 days ago
Replying to swissspidy:
Ah! That explains it! Thanks for sharing.
The theme defines
$theme
in itsfunctions.php
file, which overrides the$theme
variable from theforeach
loop.
Here's a PR that fixes this: https://github.com/WordPress/wordpress-develop/pull/7901
Thank you for the fix. Myy issue is resolved. Although i had already altered $theme to $theme_init. :-)
@swissspidy commented on PR #7901:
12 days ago
#23
Committed in https://core.trac.wordpress.org/changeset/59466
#24
@
12 days ago
With that settled, I think we also need to add this if using a child theme:
if ( $wp_theme->parent() ) {
$wp_theme->parent()->load_textdomain();
}
Edit: actually, looks like that's done automatically.
@swissspidy commented on PR #7900:
7 days ago
#26
#27
follow-up:
↓ 28
@
5 days ago
@swissspidy [59479] is causing fatal errors for some plugins, I am seeing it with WooCommerce subscriptions. On the WordPress/WordPress repo, 8f08a8e2e1 (the prior commit) is running fine but once I checkout 789d1d9c2e9 (the commit above), I am getting fatal errors
Stack trace is as follows:
[04-Dec-2024 01:10:47 UTC] PHP Fatal error: Uncaught Error: Call to undefined function get_plugins() in /vagrant/content/plugins/woocommerce-subscriptions/includes/class-wc-subscriptions-dependency-manager.php:109 Stack trace: #0 /vagrant/content/plugins/woocommerce-subscriptions/includes/class-wc-subscriptions-dependency-manager.php(73): WC_Subscriptions_Dependency_Manager->get_woocommerce_active_version() #1 /vagrant/content/plugins/woocommerce-subscriptions/includes/class-wc-subscriptions-dependency-manager.php(45): WC_Subscriptions_Dependency_Manager->is_woocommerce_version_supported() #2 /vagrant/content/plugins/woocommerce-subscriptions/woocommerce-subscriptions.php(39): WC_Subscriptions_Dependency_Manager->has_valid_dependencies() #3 /vagrant/wp-build/wp-settings.php(526): include_once('/vagrant/conten...') #4 phar:///usr/local/src/wp-cli/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1374): require('/vagrant/wp-bui...') #5 phar:///usr/local/src/wp-cli/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1293): WP_CLI\Runner->load_wordpress() #6 phar:///usr/local/src/wp-cli/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LaunchRunner.php(28): WP_CLI\Runner->start() #7 phar:///usr/local/src/wp-cli/bin/wp/vendor/wp-cli/wp-cli/php/bootstrap.php(83): WP_CLI\Bootstrap\LaunchRunner->process(Object(WP_CLI\Bootstrap\BootstrapState)) #8 phar:///usr/local/src/wp-cli/bin/wp/vendor/wp-cli/wp-cli/php/wp-cli.php(32): WP_CLI\bootstrap() #9 phar:///usr/local/src/wp-cli/bin/wp/php/boot-phar.php(20): include('phar:///usr/loc...') #10 /usr/local/src/wp-cli/bin/wp(4): include('phar:///usr/loc...') #11 {main} thrown in /vagrant/content/plugins/woocommerce-subscriptions/includes/class-wc-subscriptions-dependency-manager.php on line 109
#28
in reply to:
↑ 27
@
5 days ago
Replying to peterwilsoncc:
@swissspidy [59479] is causing fatal errors for some plugins
Without looking at the code, I'm guessing this is the exact same issue as above, but for a different function. Probably conditionally looking for is_plugin_active()
and then assuming get_plugins()
is loaded too.
It looks like that's indeed the case:
// Load plugin.php if it's not already loaded.
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
// Loop through all active plugins and check if WooCommerce is active.
foreach ( get_plugins() as $plugin_slug => $plugin_data ) {
Here's a theme with the same problem:
https://themes.trac.wordpress.org/browser/gadgethub/1.0.6/includes/omnipress-theme-notice/class-theme-notice.php?marks=73,74,84#L65
(A whole bunch of similar themes too)
#29
follow-up:
↓ 31
@
5 days ago
Thanks!
While I don't like it, I feel like at this point it would be safer to just always include require_once ABSPATH . 'wp-admin/includes/plugin.php';
in wp-settings.php
. Otherwise we play whac-a-mole trying to move functions every time we encounter a plugin at this. Loading the whole file (with a few unrelated functions) early is not great, but better than causing fatal errors.
This ticket was mentioned in PR #7945 on WordPress/wordpress-develop by @swissspidy.
5 days ago
#30
Plugins: Load `wp-admin/includes/plugin.php` earlier. Partially reverts [59479] and [59461], which previously tried to move some functions from `wp-admin/includes/plugin.php` to `wp-includes/functions.php` so they are available early, so that `get_plugin_data()` can be used. However, other functions from that file are often used by plugins without necessarily checking whether they are available, easily causing fatal errors. Requiring this file directly is a safer approach to avoid such errors. Props peterwilsoncc, dd32, swissspidy. See #62244.
Trac ticket: https://core.trac.wordpress.org/ticket/62244
#31
in reply to:
↑ 29
@
4 days ago
Replying to swissspidy:
While I don't like it, I feel like at this point it would be safer to just always include
require_once ABSPATH . 'wp-admin/includes/plugin.php';
inwp-settings.php
.
Sadly, I do tend to agree.
Now works for custom themes and plugins with a
Domain Path
This way, themes and plugins will not have to use
load_theme_textdomain()
orload_plugin_textdomain()
anymore, as WordPress knows where to look for the files and will load the translations automatically the first time__()
is encountered.There's a minor hit for the file read caused by
get_plugin_data()
but that should be minor.Downside: loads
get_plugin_data()
/wp-admin/includes/plugin.php
inwp-settings.php
, making a lot of functions available earlier than usual.Trac ticket: https://core.trac.wordpress.org/ticket/62244