<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[57545] trunk: Upgrade/Install: Introduce Plugin Dependencies.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/57545">57545</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/57545","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>costdev</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-02-06 23:44:09 +0000 (Tue, 06 Feb 2024)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Upgrade/Install: Introduce Plugin Dependencies.
Introduces a new "Requires Plugins" plugin header so that plugin developers can list the slugs of the plugins theirs depends on.
This will inform users of the requirements, and provide links to the WordPress.org Plugins Repository that they can click to install and activate the dependencies first.
Plugins whose requirements are not met cannot be installed or activated, and they will be deactivated automatically if their requirements become unmet.
Plugins that others rely on cannot be deactivated or deleted until their dependent plugins are deactivated or deleted.
In memory of Alex Mills and Alex King.
WordPress Remembers.
Props ahoereth, afragen, alanfuller, alexkingorg, amykamala, anonymized_10690803, apeatling, ashfame, atimmer, audrasjb, aristath, azaozz, batmoo, beaulebens, blobaugh, bobbingwide, boonebgorges, brianhenryie, chanthaboune, chrisdavidmiles, coolmann, costdev, courane01, danielbachhuber, davidperez, dd32, Denis-de-Bernardy, dingo_d, DJPaul, dougal, DrewAPicture, ethitter, filosofo, georgestephanis, giuseppemazzapica-1, goldenapples, griffinjt, hellofromTonya, husobj, ideag, jarednova, jbobich, jbrinley, jltallon, joedolson, johnciacia, johnjamesjacoby, joppuyo, jsmoriss, karmatosed, kebbet, knutsp, kraftbj, kraftner, kurtpayne, lkraav, logikal16, luisherranz, man4toman, markjaquith, matt, mbijon, megphillips91, mikeschinkel, mordauk, morehawes, mrwweb, mte90, mukesh27, mzaweb, nacin, norcross, nvwd, nwjames, obliviousharmony, ocean90, oglekler, paaljoachim, pauldewouters, pbaylies, pbiron, peterwilsoncc, Philipp15b, poena, pogidude, retlehs, rmccue, ryan, sabreuse, sc0ttkclark, scribu
, sereedmedia, SergeyBiryukov, ShaneF, shidouhikari, soean, spacedmonkey, stephenh1988, swissspidy, taylorde, tazotodua, threadi, TimothyBlynJacobs, TJNowell, tollmanz, toscho, tropicalista, Viper007Bond, westi, whiteshadow, williamsba1, wpsmith, ZaneMatthew.
Fixes <a href="https://core.trac.wordpress.org/ticket/22316">#22316</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcjs_enqueueswpupdatesjs">trunk/src/js/_enqueues/wp/updates.js</a></li>
<li><a href="#trunksrcwpadminadminajaxphp">trunk/src/wp-admin/admin-ajax.php</a></li>
<li><a href="#trunksrcwpadmincsscommoncss">trunk/src/wp-admin/css/common.css</a></li>
<li><a href="#trunksrcwpadmincsslisttablescss">trunk/src/wp-admin/css/list-tables.css</a></li>
<li><a href="#trunksrcwpadminincludesajaxactionsphp">trunk/src/wp-admin/includes/ajax-actions.php</a></li>
<li><a href="#trunksrcwpadminincludesclasspluginupgraderphp">trunk/src/wp-admin/includes/class-plugin-upgrader.php</a></li>
<li><a href="#trunksrcwpadminincludesclasswpplugininstalllisttablephp">trunk/src/wp-admin/includes/class-wp-plugin-install-list-table.php</a></li>
<li><a href="#trunksrcwpadminincludesclasswppluginslisttablephp">trunk/src/wp-admin/includes/class-wp-plugins-list-table.php</a></li>
<li><a href="#trunksrcwpadminincludesplugininstallphp">trunk/src/wp-admin/includes/plugin-install.php</a></li>
<li><a href="#trunksrcwpadminincludespluginphp">trunk/src/wp-admin/includes/plugin.php</a></li>
<li><a href="#trunksrcwpadminplugininstallphp">trunk/src/wp-admin/plugin-install.php</a></li>
<li><a href="#trunksrcwpadminpluginsphp">trunk/src/wp-admin/plugins.php</a></li>
<li><a href="#trunksrcwpincludesfunctionsphp">trunk/src/wp-includes/functions.php</a></li>
<li><a href="#trunksrcwpincludesloadphp">trunk/src/wp-includes/load.php</a></li>
<li><a href="#trunksrcwpsettingsphp">trunk/src/wp-settings.php</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesclasswpplugindependenciesphp">trunk/src/wp-includes/class-wp-plugin-dependencies.php</a></li>
<li>trunk/tests/phpunit/tests/admin/plugin-dependencies/</li>
<li><a href="#trunktestsphpunittestsadminplugindependenciesbasephp">trunk/tests/phpunit/tests/admin/plugin-dependencies/base.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependenciesgetDependenciesphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencies.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependenciesgetDependencyDataphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyData.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependenciesgetDependencyFilepathphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyFilepath.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependenciesgetDependencyNamesphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyNames.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependenciesgetDependentFilepathphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependentFilepath.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependenciesgetDependentNamesphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependentNames.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependenciesgetDependentsphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependents.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependencieshasActiveDependentsphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/hasActiveDependents.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependencieshasCircularDependencyphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/hasCircularDependency.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependencieshasDependenciesphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/hasDependencies.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependencieshasDependentsphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/hasDependents.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependencieshasUnmetDependenciesphp">trunk/tests/phpunit/tests/admin/plugin-dependencies/hasUnmetDependencies.php</a></li>
<li><a href="#trunktestsphpunittestsadminplugindependenciesinitializephp">trunk/tests/phpunit/tests/admin/plugin-dependencies/initialize.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcjs_enqueueswpupdatesjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/js/_enqueues/wp/updates.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/js/_enqueues/wp/updates.js 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/js/_enqueues/wp/updates.js 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -414,6 +414,31 @@
</span><span class="cx" style="display: block; padding: 0 10px"> };
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Sends a message from a modal to the main screen to update buttons in plugin cards.
+ *
+ * @since 6.5.0
+ *
+ * @param {Object} data An object of data to use for the button.
+ * @param {string} data.slug The plugin's slug.
+ * @param {string} data.text The text to use for the button.
+ * @param {string} data.ariaLabel The value for the button's aria-label attribute. An empty string removes the attribute.
+ * @param {string=} data.status Optional. An identifier for the status.
+ * @param {string=} data.removeClasses Optional. A space-separated list of classes to remove from the button.
+ * @param {string=} data.addClasses Optional. A space-separated list of classes to add to the button.
+ * @param {string=} data.href Optional. The button's URL.
+ * @param {string=} data.pluginName Optional. The plugin's name.
+ * @param {string=} data.plugin Optional. The plugin file, relative to the plugins directory.
+ */
+ wp.updates.setCardButtonStatus = function( data ) {
+ var target = window.parent === window ? null : window.parent;
+
+ $.support.postMessage = !! window.postMessage;
+ if ( false !== $.support.postMessage && null !== target && -1 === window.parent.location.pathname.indexOf( 'index.php' ) ) {
+ target.postMessage( JSON.stringify( data ), window.location.origin );
+ }
+ };
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Decrements the update counts throughout the various menus.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * This includes the toolbar, the "Updates" menu item and the menu items
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -452,7 +477,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> wp.updates.updatePlugin = function( args ) {
</span><span class="cx" style="display: block; padding: 0 10px"> var $updateRow, $card, $message, message,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $adminBarUpdates = $( '#wp-admin-bar-updates' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $adminBarUpdates = $( '#wp-admin-bar-updates' ),
+ buttonText = __( 'Updating...' );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> args = _.extend( {
</span><span class="cx" style="display: block; padding: 0 10px"> success: wp.updates.updatePluginSuccess,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -468,7 +494,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $updateRow.find( '.plugin-title strong' ).text()
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $card = $( '.plugin-card-' + args.slug );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $card = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' );
</ins><span class="cx" style="display: block; padding: 0 10px"> $message = $card.find( '.update-now' ).addClass( 'updating-message' );
</span><span class="cx" style="display: block; padding: 0 10px"> message = sprintf(
</span><span class="cx" style="display: block; padding: 0 10px"> /* translators: %s: Plugin name and version. */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -488,10 +514,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $message
</span><span class="cx" style="display: block; padding: 0 10px"> .attr( 'aria-label', message )
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- .text( __( 'Updating...' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .text( buttonText );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $document.trigger( 'wp-plugin-updating', args );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'plugin-information-footer' === $card.attr('id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'updating-plugin',
+ slug: args.slug,
+ addClasses: 'updating-message',
+ text: buttonText,
+ ariaLabel: message
+ }
+ );
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> return wp.updates.ajax( 'update-plugin', args );
</span><span class="cx" style="display: block; padding: 0 10px"> };
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -511,7 +549,13 @@
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> wp.updates.updatePluginSuccess = function( response ) {
</span><span class="cx" style="display: block; padding: 0 10px"> var $pluginRow, $updateMessage, newText,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $adminBarUpdates = $( '#wp-admin-bar-updates' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $adminBarUpdates = $( '#wp-admin-bar-updates' ),
+ buttonText = _x( 'Updated!', 'plugin' ),
+ ariaLabel = sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( '%s updated!', 'plugin' ),
+ response.pluginName
+ );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' )
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -528,7 +572,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> // Clear the "time to next auto-update" text.
</span><span class="cx" style="display: block; padding: 0 10px"> $pluginRow.find( '.auto-update-time' ).empty();
</span><span class="cx" style="display: block; padding: 0 10px"> } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $updateMessage = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.update-now' )
</ins><span class="cx" style="display: block; padding: 0 10px"> .removeClass( 'updating-message' )
</span><span class="cx" style="display: block; padding: 0 10px"> .addClass( 'button-disabled updated-message' );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -536,19 +580,25 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $adminBarUpdates.removeClass( 'spin' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $updateMessage
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- .attr(
- 'aria-label',
- sprintf(
- /* translators: %s: Plugin name and version. */
- _x( '%s updated!', 'plugin' ),
- response.pluginName
- )
- )
- .text( _x( 'Updated!', 'plugin' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .attr( 'aria-label', ariaLabel )
+ .text( buttonText );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> wp.a11y.speak( __( 'Update completed successfully.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- wp.updates.decrementCount( 'plugin' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'plugin_install_from_iframe' !== $updateMessage.attr( 'id' ) ) {
+ wp.updates.decrementCount( 'plugin' );
+ } else {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'updated-plugin',
+ slug: response.slug,
+ removeClasses: 'updating-message',
+ addClasses: 'button-disabled updated-message',
+ text: buttonText,
+ ariaLabel: ariaLabel
+ }
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $document.trigger( 'wp-plugin-update-success', response );
</span><span class="cx" style="display: block; padding: 0 10px"> };
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -567,7 +617,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @param {string} response.errorMessage The error that occurred.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> wp.updates.updatePluginError = function( response ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- var $pluginRow, $card, $message, errorMessage,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ var $pluginRow, $card, $message, errorMessage, buttonText, ariaLabel,
</ins><span class="cx" style="display: block; padding: 0 10px"> $adminBarUpdates = $( '#wp-admin-bar-updates' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -608,28 +658,32 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $message.find( 'p' ).removeAttr( 'aria-label' );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $card = $( '.plugin-card-' + response.slug )
- .addClass( 'plugin-card-update-failed' )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ buttonText = __( 'Update failed.' );
+
+ $card = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' )
</ins><span class="cx" style="display: block; padding: 0 10px"> .append( wp.updates.adminNotice( {
</span><span class="cx" style="display: block; padding: 0 10px"> className: 'update-message notice-error notice-alt is-dismissible',
</span><span class="cx" style="display: block; padding: 0 10px"> message: errorMessage
</span><span class="cx" style="display: block; padding: 0 10px"> } ) );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $card.hasClass( 'plugin-card-' + response.slug ) ) {
+ $card.addClass( 'plugin-card-update-failed' );
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $card.find( '.update-now' )
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- .text( __( 'Update failed.' ) )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .text( buttonText )
</ins><span class="cx" style="display: block; padding: 0 10px"> .removeClass( 'updating-message' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( response.pluginName ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $card.find( '.update-now' )
- .attr(
- 'aria-label',
- sprintf(
- /* translators: %s: Plugin name and version. */
- _x( '%s update failed.', 'plugin' ),
- response.pluginName
- )
- );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ ariaLabel = sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( '%s update failed.', 'plugin' ),
+ response.pluginName
+ );
+
+ $card.find( '.update-now' ).attr( 'aria-label', ariaLabel );
</ins><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ ariaLabel = '';
</ins><span class="cx" style="display: block; padding: 0 10px"> $card.find( '.update-now' ).removeAttr( 'aria-label' );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -652,6 +706,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> wp.a11y.speak( errorMessage, 'assertive' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'plugin-information-footer' === $card.attr('id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'plugin-update-failed',
+ slug: response.slug,
+ removeClasses: 'updating-message',
+ text: buttonText,
+ ariaLabel: ariaLabel
+ }
+ );
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $document.trigger( 'wp-plugin-update-error', response );
</span><span class="cx" style="display: block; padding: 0 10px"> };
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -668,8 +734,10 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * decorated with an abort() method.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> wp.updates.installPlugin = function( args ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- var $card = $( '.plugin-card-' + args.slug ),
- $message = $card.find( '.install-now' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ var $card = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ),
+ $message = $card.find( '.install-now' ),
+ buttonText = __( 'Installing...' ),
+ ariaLabel;
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> args = _.extend( {
</span><span class="cx" style="display: block; padding: 0 10px"> success: wp.updates.installPluginSuccess,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -684,17 +752,16 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $message.data( 'originaltext', $message.html() );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ ariaLabel = sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( 'Installing %s...', 'plugin' ),
+ $message.data( 'name' )
+ );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $message
</span><span class="cx" style="display: block; padding: 0 10px"> .addClass( 'updating-message' )
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- .attr(
- 'aria-label',
- sprintf(
- /* translators: %s: Plugin name and version. */
- _x( 'Installing %s...', 'plugin' ),
- $message.data( 'name' )
- )
- )
- .text( __( 'Installing...' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .attr( 'aria-label', ariaLabel )
+ .text( buttonText );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> wp.a11y.speak( __( 'Installing... please wait.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -703,6 +770,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $document.trigger( 'wp-plugin-installing', args );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'installing-plugin',
+ slug: args.slug,
+ addClasses: 'updating-message',
+ text: buttonText,
+ ariaLabel: ariaLabel
+ }
+ );
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> return wp.updates.ajax( 'install-plugin', args );
</span><span class="cx" style="display: block; padding: 0 10px"> };
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -717,20 +796,19 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @param {string} response.activateUrl URL to activate the just installed plugin.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> wp.updates.installPluginSuccess = function( response ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ),
+ buttonText = _x( 'Installed!', 'plugin' ),
+ ariaLabel = sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( '%s installed!', 'plugin' ),
+ response.pluginName
+ );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $message
</span><span class="cx" style="display: block; padding: 0 10px"> .removeClass( 'updating-message' )
</span><span class="cx" style="display: block; padding: 0 10px"> .addClass( 'updated-message installed button-disabled' )
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- .attr(
- 'aria-label',
- sprintf(
- /* translators: %s: Plugin name and version. */
- _x( '%s installed!', 'plugin' ),
- response.pluginName
- )
- )
- .text( _x( 'Installed!', 'plugin' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .attr( 'aria-label', ariaLabel )
+ .text( buttonText );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> wp.a11y.speak( __( 'Installation completed successfully.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -738,36 +816,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( response.activateUrl ) {
</span><span class="cx" style="display: block; padding: 0 10px"> setTimeout( function() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ wp.updates.checkPluginDependencies( {
+ slug: response.slug
+ } );
+ }, 1000 );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- // Transform the 'Install' button into an 'Activate' button.
- $message.removeClass( 'install-now installed button-disabled updated-message' )
- .addClass( 'activate-now button-primary' )
- .attr( 'href', response.activateUrl );
-
- if ( 'plugins-network' === pagenow ) {
- $message
- .attr(
- 'aria-label',
- sprintf(
- /* translators: %s: Plugin name. */
- _x( 'Network Activate %s', 'plugin' ),
- response.pluginName
- )
- )
- .text( __( 'Network Activate' ) );
- } else {
- $message
- .attr(
- 'aria-label',
- sprintf(
- /* translators: %s: Plugin name. */
- _x( 'Activate %s', 'plugin' ),
- response.pluginName
- )
- )
- .text( __( 'Activate' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'installed-plugin',
+ slug: response.slug,
+ removeClasses: 'updating-message',
+ addClasses: 'updated-message installed button-disabled',
+ text: buttonText,
+ ariaLabel: ariaLabel
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- }, 1000 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> };
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -783,8 +848,14 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @param {string} response.errorMessage The error that occurred.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> wp.updates.installPluginError = function( response ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- var $card = $( '.plugin-card-' + response.slug ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ var $card = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ),
</ins><span class="cx" style="display: block; padding: 0 10px"> $button = $card.find( '.install-now' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ buttonText = __( 'Installation failed.' ),
+ ariaLabel = sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( '%s installation failed', 'plugin' ),
+ $button.data( 'name' )
+ ),
</ins><span class="cx" style="display: block; padding: 0 10px"> errorMessage;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -817,22 +888,335 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $button
</span><span class="cx" style="display: block; padding: 0 10px"> .removeClass( 'updating-message' ).addClass( 'button-disabled' )
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- .attr(
- 'aria-label',
- sprintf(
- /* translators: %s: Plugin name and version. */
- _x( '%s installation failed', 'plugin' ),
- $button.data( 'name' )
- )
- )
- .text( __( 'Installation failed.' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .attr( 'aria-label', ariaLabel )
+ .text( buttonText );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> wp.a11y.speak( errorMessage, 'assertive' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ wp.updates.setCardButtonStatus(
+ {
+ status: 'plugin-install-failed',
+ slug: response.slug,
+ removeClasses: 'updating-message',
+ addClasses: 'button-disabled',
+ text: buttonText,
+ ariaLabel: ariaLabel
+ }
+ );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $document.trigger( 'wp-plugin-install-error', response );
</span><span class="cx" style="display: block; padding: 0 10px"> };
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Sends an Ajax request to the server to check a plugin's dependencies.
+ *
+ * @since 6.5.0
+ *
+ * @param {Object} args Arguments.
+ * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository.
+ * @param {checkPluginDependenciesSuccess=} args.success Optional. Success callback. Default: wp.updates.checkPluginDependenciesSuccess
+ * @param {checkPluginDependenciesError=} args.error Optional. Error callback. Default: wp.updates.checkPluginDependenciesError
+ * @return {$.promise} A jQuery promise that represents the request,
+ * decorated with an abort() method.
+ */
+ wp.updates.checkPluginDependencies = function( args ) {
+ args = _.extend( {
+ success: wp.updates.checkPluginDependenciesSuccess,
+ error: wp.updates.checkPluginDependenciesError
+ }, args );
+
+ wp.a11y.speak( __( 'Checking plugin dependencies... please wait.' ) );
+ $document.trigger( 'wp-checking-plugin-dependencies', args );
+
+ return wp.updates.ajax( 'check_plugin_dependencies', args );
+ };
+
+ /**
+ * Updates the UI appropriately after a successful plugin dependencies check.
+ *
+ * @since 6.5.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the checked plugin.
+ * @param {string} response.pluginName Name of the checked plugin.
+ * @param {string} response.plugin The plugin file, relative to the plugins directory.
+ * @param {string} response.activateUrl URL to activate the just checked plugin.
+ */
+ wp.updates.checkPluginDependenciesSuccess = function( response ) {
+ var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ),
+ buttonText, ariaLabel;
+
+ // Transform the 'Install' button into an 'Activate' button.
+ $message
+ .removeClass( 'install-now installed button-disabled updated-message' )
+ .addClass( 'activate-now button-primary' )
+ .attr( 'href', response.activateUrl );
+
+ wp.a11y.speak( __( 'Plugin dependencies check completed successfully.' ) );
+ $document.trigger( 'wp-check-plugin-dependencies-success', response );
+
+ if ( 'plugins-network' === pagenow ) {
+ buttonText = _x( 'Network Activate' );
+ ariaLabel = sprintf(
+ /* translators: %s: Plugin name. */
+ _x( 'Network Activate %s', 'plugin' ),
+ response.pluginName
+ );
+
+ $message
+ .attr( 'aria-label', ariaLabel )
+ .text( buttonText );
+ } else {
+ buttonText = _x( 'Activate', 'plugin' );
+ ariaLabel = sprintf(
+ /* translators: %s: Plugin name. */
+ _x( 'Activate %s', 'plugin' ),
+ response.pluginName
+ );
+
+ $message
+ .attr( 'aria-label', ariaLabel )
+ .attr( 'data-name', response.pluginName )
+ .attr( 'data-slug', response.slug )
+ .attr( 'data-plugin', response.plugin )
+ .text( buttonText );
+ }
+
+ if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'dependencies-check-success',
+ slug: response.slug,
+ removeClasses: 'install-now installed button-disabled updated-message',
+ addClasses: 'activate-now button-primary',
+ text: buttonText,
+ ariaLabel: ariaLabel,
+ pluginName: response.pluginName,
+ plugin: response.plugin,
+ href: response.activateUrl
+ }
+ );
+ }
+ };
+
+ /**
+ * Updates the UI appropriately after a failed plugin dependencies check.
+ *
+ * @since 6.5.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the plugin to be checked.
+ * @param {string=} response.pluginName Optional. Name of the plugin to be checked.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ */
+ wp.updates.checkPluginDependenciesError = function( response ) {
+ var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ),
+ buttonText = __( 'Activate' ),
+ ariaLabel = sprintf(
+ /* translators: 1: Plugin name, 2. The reason the plugin cannot be activated. */
+ _x( 'Cannot activate %1$s. %2$s', 'plugin' ),
+ response.pluginName,
+ response.errorMessage
+ ),
+ errorMessage;
+
+ if ( ! wp.updates.isValidResponse( response, 'check-dependencies' ) ) {
+ return;
+ }
+
+ errorMessage = sprintf(
+ /* translators: %s: Error string for a failed activation. */
+ __( 'Activation failed: %s' ),
+ response.errorMessage
+ );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+ $document.trigger( 'wp-check-plugin-dependencies-error', response );
+
+ $message
+ .removeClass( 'install-now installed updated-message' )
+ .addClass( 'activate-now button-primary' )
+ .attr( 'aria-label', ariaLabel )
+ .text( buttonText );
+
+ if ( 'plugin-information-footer' === $message.parent().attr('id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'dependencies-check-failed',
+ slug: response.slug,
+ removeClasses: 'install-now installed updated-message',
+ addClasses: 'activate-now button-primary',
+ text: buttonText,
+ ariaLabel: ariaLabel
+ }
+ );
+ }
+ };
+
+ /**
+ * Sends an Ajax request to the server to activate a plugin.
+ *
+ * @since 6.5.0
+ *
+ * @param {Object} args Arguments.
+ * @param {string} args.name The name of the plugin.
+ * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository.
+ * @param {string} args.plugin The plugin file, relative to the plugins directory.
+ * @param {activatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.activatePluginSuccess
+ * @param {activatePluginError=} args.error Optional. Error callback. Default: wp.updates.activatePluginError
+ * @return {$.promise} A jQuery promise that represents the request,
+ * decorated with an abort() method.
+ */
+ wp.updates.activatePlugin = function( args ) {
+ var $message = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ).find( '.activate-now, .activating-message' );
+
+ args = _.extend( {
+ success: wp.updates.activatePluginSuccess,
+ error: wp.updates.activatePluginError
+ }, args );
+
+ wp.a11y.speak( __( 'Activating... please wait.' ) );
+ $document.trigger( 'wp-activating-plugin', args );
+
+ if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'activating-plugin',
+ slug: args.slug,
+ removeClasses: 'installed updated-message button-primary',
+ addClasses: 'activating-message',
+ text: _x( 'Activating...', 'plugin' ),
+ ariaLabel: sprintf(
+ /* translators: %s: Plugin name. */
+ _x( 'Activating %s', 'plugin' ),
+ args.name
+ )
+ }
+ );
+ }
+
+ return wp.updates.ajax( 'activate-plugin', args );
+ };
+
+ /**
+ * Updates the UI appropriately after a successful plugin activation.
+ *
+ * @since 6.5.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the activated plugin.
+ * @param {string} response.pluginName Name of the activated plugin.
+ * @param {string} response.plugin The plugin file, relative to the plugins directory.
+ */
+ wp.updates.activatePluginSuccess = function( response ) {
+ var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ),
+ buttonText = _x( 'Activated!', 'plugin' ),
+ ariaLabel = sprintf(
+ /* translators: %s: The plugin name. */
+ '%s activated successfully.',
+ response.pluginName
+ );
+
+ wp.a11y.speak( __( 'Activation completed successfully.' ) );
+ $document.trigger( 'wp-plugin-activate-success', response );
+
+ $message
+ .removeClass( 'activating-message' )
+ .addClass( 'activated-message button-disabled' )
+ .attr( 'aria-label', ariaLabel )
+ .text( buttonText );
+
+ if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'activated-plugin',
+ slug: response.slug,
+ removeClasses: 'activating-message',
+ addClasses: 'activated-message button-disabled',
+ text: buttonText,
+ ariaLabel: ariaLabel
+ }
+ );
+ }
+
+ setTimeout( function() {
+ $message.removeClass( 'activated-message' )
+ .text( _x( 'Active', 'plugin' ) );
+
+ if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'plugin-active',
+ slug: response.slug,
+ removeClasses: 'activated-message',
+ text: _x( 'Active', 'plugin' ),
+ ariaLabel: sprintf(
+ /* translators: %s: The plugin name. */
+ '%s is active.',
+ response.pluginName
+ )
+ }
+ );
+ }
+ }, 1000 );
+ };
+
+ /**
+ * Updates the UI appropriately after a failed plugin activation.
+ *
+ * @since 6.5.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the plugin to be activated.
+ * @param {string=} response.pluginName Optional. Name of the plugin to be activated.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ */
+ wp.updates.activatePluginError = function( response ) {
+ var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ),
+ buttonText = __( 'Activation failed.' ),
+ ariaLabel = sprintf(
+ /* translators: %s: Plugin name. */
+ _x( '%s activation failed', 'plugin' ),
+ response.pluginName
+ ),
+ errorMessage;
+
+ if ( ! wp.updates.isValidResponse( response, 'activate' ) ) {
+ return;
+ }
+
+ errorMessage = sprintf(
+ /* translators: %s: Error string for a failed activation. */
+ __( 'Activation failed: %s' ),
+ response.errorMessage
+ );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+ $document.trigger( 'wp-plugin-activate-error', response );
+
+ $message
+ .removeClass( 'install-now installed activating-message' )
+ .addClass( 'button-disabled' )
+ .attr( 'aria-label', ariaLabel )
+ .text( buttonText );
+
+ if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
+ wp.updates.setCardButtonStatus(
+ {
+ status: 'plugin-activation-failed',
+ slug: response.slug,
+ removeClasses: 'install-now installed activating-message',
+ addClasses: 'button-disabled',
+ text: buttonText,
+ ariaLabel: ariaLabel
+ }
+ );
+ }
+ };
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Updates the UI appropriately after a successful importer install.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 4.6.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1970,6 +2354,16 @@
</span><span class="cx" style="display: block; padding: 0 10px"> errorMessage = __( 'Installation failed: %s' );
</span><span class="cx" style="display: block; padding: 0 10px"> break;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ case 'check-dependencies':
+ /* translators: %s: Error string for a failed dependencies check. */
+ errorMessage = __( 'Dependencies check failed: %s' );
+ break;
+
+ case 'activate':
+ /* translators: %s: Error string for a failed activation. */
+ errorMessage = __( 'Activation failed: %s' );
+ break;
+
</ins><span class="cx" style="display: block; padding: 0 10px"> case 'delete':
</span><span class="cx" style="display: block; padding: 0 10px"> /* translators: %s: Error string for a failed deletion. */
</span><span class="cx" style="display: block; padding: 0 10px"> errorMessage = __( 'Deletion failed: %s' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2025,7 +2419,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> };
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $( function() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- var $pluginFilter = $( '#plugin-filter' ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ var $pluginFilter = $( '#plugin-filter, #plugin-information-footer' ),
</ins><span class="cx" style="display: block; padding: 0 10px"> $bulkActionForm = $( '#bulk-action-form' ),
</span><span class="cx" style="display: block; padding: 0 10px"> $filesystemForm = $( '#request-filesystem-credentials-form' ),
</span><span class="cx" style="display: block; padding: 0 10px"> $filesystemModal = $( '#request-filesystem-credentials-dialog' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2242,6 +2636,44 @@
</span><span class="cx" style="display: block; padding: 0 10px"> } );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Click handler for plugin activations in plugin activation view.
+ *
+ * @since 6.5.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $pluginFilter.on( 'click', '.activate-now', function( event ) {
+ var $activateButton = $( event.target );
+
+ event.preventDefault();
+
+ if ( $activateButton.hasClass( 'activating-message' ) || $activateButton.hasClass( 'button-disabled' ) ) {
+ return;
+ }
+
+ $activateButton
+ .removeClass( 'activate-now button-primary' )
+ .addClass( 'activating-message' )
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Plugin name. */
+ _x( 'Activating %s', 'plugin' ),
+ $activateButton.data( 'name' )
+ )
+ )
+ .text( _x( 'Activating...', 'plugin' ) );
+
+ wp.updates.activatePlugin(
+ {
+ name: $activateButton.data( 'name' ),
+ slug: $activateButton.data( 'slug' ),
+ plugin: $activateButton.data( 'plugin' )
+ }
+ );
+ });
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Click handler for importer plugins installs in the Import screen.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 4.6.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2767,35 +3199,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> } );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Click handler for installing a plugin from the details modal on `plugin-install.php`.
- *
- * @since 4.6.0
- *
- * @param {Event} event Event interface.
- */
- $( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
- var target = window.parent === window ? null : window.parent,
- install;
-
- $.support.postMessage = !! window.postMessage;
-
- if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
- return;
- }
-
- event.preventDefault();
-
- install = {
- action: 'install-plugin',
- data: {
- slug: $( this ).data( 'slug' )
- }
- };
-
- target.postMessage( JSON.stringify( install ), window.location.origin );
- } );
-
- /**
</del><span class="cx" style="display: block; padding: 0 10px"> * Handles postMessage events.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 4.2.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2818,10 +3221,48 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( ! message || 'undefined' === typeof message.action ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( ! message ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> return;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if (
+ 'undefined' !== typeof message.status &&
+ 'undefined' !== typeof message.slug &&
+ 'undefined' !== typeof message.text &&
+ 'undefined' !== typeof message.ariaLabel
+ ) {
+ var $card = $( '.plugin-card-' + message.slug ),
+ $message = $card.find( '[data-slug="' + message.slug + '"]' );
+
+ if ( 'undefined' !== typeof message.removeClasses ) {
+ $message.removeClass( message.removeClasses );
+ }
+
+ if ( 'undefined' !== typeof message.addClasses ) {
+ $message.addClass( message.addClasses );
+ }
+
+ if ( '' === message.ariaLabel ) {
+ $message.removeAttr( 'aria-label' );
+ } else {
+ $message.attr( 'aria-label', message.ariaLabel );
+ }
+
+ if ( 'dependencies-check-success' === message.status ) {
+ $message
+ .attr( 'data-name', message.pluginName )
+ .attr( 'data-slug', message.slug )
+ .attr( 'data-plugin', message.plugin )
+ .attr( 'href', message.href );
+ }
+
+ $message.text( message.text );
+ }
+
+ if ( 'undefined' === typeof message.action || 'undefined' === typeof message.data.slug ) {
+ return;
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> switch ( message.action ) {
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2832,10 +3273,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> case 'install-plugin':
</span><span class="cx" style="display: block; padding: 0 10px"> case 'update-plugin':
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
- window.tb_remove();
- /* jscs:enable */
-
</del><span class="cx" style="display: block; padding: 0 10px"> message.data = wp.updates._addCallbacks( message.data, message.action );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> wp.updates.queue.push( message );
</span></span></pre></div>
<a id="trunksrcwpadminadminajaxphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/admin-ajax.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/admin-ajax.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/admin-ajax.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -117,6 +117,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'parse-media-shortcode',
</span><span class="cx" style="display: block; padding: 0 10px"> 'destroy-sessions',
</span><span class="cx" style="display: block; padding: 0 10px"> 'install-plugin',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'activate-plugin',
</ins><span class="cx" style="display: block; padding: 0 10px"> 'update-plugin',
</span><span class="cx" style="display: block; padding: 0 10px"> 'crop-image',
</span><span class="cx" style="display: block; padding: 0 10px"> 'generate-password',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -169,6 +170,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'wp_ajax_nopriv_heartbeat', 'wp_ajax_nopriv_heartbeat', 1 );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+// Register Plugin Dependencies Ajax calls.
+add_action( 'wp_ajax_check_plugin_dependencies', array( 'WP_Plugin_Dependencies', 'check_plugin_dependencies_during_ajax' ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $action = $_REQUEST['action'];
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( is_user_logged_in() ) {
</span></span></pre></div>
<a id="trunksrcwpadmincsscommoncss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/css/common.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/common.css 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/css/common.css 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1500,6 +1500,22 @@
</span><span class="cx" style="display: block; padding: 0 10px"> background-color: #f0f6fc;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#plugin-information-footer .update-now:not(.button-disabled):before {
+ color: #d63638;
+ content: "\f463";
+ display: inline-block;
+ font: normal 20px/1 dashicons;
+ margin: -3px 5px 0 -2px;
+ speak: never;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ vertical-align: middle;
+}
+
+#plugin-information-footer .notice {
+ margin-top: -5px;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> .update-message p:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .updating-message p:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .updated-message p:before,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1507,7 +1523,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> .button.updating-message:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .button.updated-message:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .button.installed:before,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.button.installing:before {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.button.installing:before,
+.button.activating-message:before,
+.button.activated-message:before {
</ins><span class="cx" style="display: block; padding: 0 10px"> display: inline-block;
</span><span class="cx" style="display: block; padding: 0 10px"> font: normal 20px/1 'dashicons';
</span><span class="cx" style="display: block; padding: 0 10px"> -webkit-font-smoothing: antialiased;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1544,7 +1562,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> .updating-message p:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .import-php .updating-message:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .button.updating-message:before,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.button.installing:before {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.button.installing:before,
+.button.activating-message:before {
</ins><span class="cx" style="display: block; padding: 0 10px"> color: #d63638;
</span><span class="cx" style="display: block; padding: 0 10px"> content: "\f463";
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1554,6 +1573,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> .import-php .updating-message:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .button.updating-message:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .button.installing:before,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.button.activating-message:before,
</ins><span class="cx" style="display: block; padding: 0 10px"> .plugins .column-auto-updates .dashicons-update.spin,
</span><span class="cx" style="display: block; padding: 0 10px"> .theme-overlay .theme-autoupdate .dashicons-update.spin {
</span><span class="cx" style="display: block; padding: 0 10px"> animation: rotation 2s infinite linear;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1564,6 +1584,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> .import-php .updating-message:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .button.updating-message:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .button.installing:before,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .button.activating-message:before,
</ins><span class="cx" style="display: block; padding: 0 10px"> .plugins .column-auto-updates .dashicons-update.spin,
</span><span class="cx" style="display: block; padding: 0 10px"> .theme-overlay .theme-autoupdate .dashicons-update.spin {
</span><span class="cx" style="display: block; padding: 0 10px"> animation: none;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1577,7 +1598,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /* Updated icon (check mark). */
</span><span class="cx" style="display: block; padding: 0 10px"> .updated-message p:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .installed p:before,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.button.updated-message:before {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.button.updated-message:before,
+.button.activated-message:before {
</ins><span class="cx" style="display: block; padding: 0 10px"> color: #68de7c;
</span><span class="cx" style="display: block; padding: 0 10px"> content: "\f147";
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1662,19 +1684,37 @@
</span><span class="cx" style="display: block; padding: 0 10px"> .button.updating-message:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .button.updated-message:before,
</span><span class="cx" style="display: block; padding: 0 10px"> .button.installed:before,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.button.installing:before {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.button.installing:before,
+.button.activated-message:before,
+.button.activating-message:before {
</ins><span class="cx" style="display: block; padding: 0 10px"> margin: 3px 5px 0 -2px;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.button-primary.updating-message:before {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#plugin-information-footer .button.installed:before,
+#plugin-information-footer .button.installing:before,
+#plugin-information-footer .button.updating-message:before,
+#plugin-information-footer .button.updated-message:before,
+#plugin-information-footer .button.activated-message:before,
+#plugin-information-footer .button.activating-message:before {
+ margin: 9px 5px 0 -2px;
+}
+
+#plugin-information-footer .button.update-now.updating-message:before {
+ margin: -3px 5px 0 -2px;
+}
+
+.button-primary.updating-message:before,
+.button-primary.activating-message:before {
</ins><span class="cx" style="display: block; padding: 0 10px"> color: #fff;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.button-primary.updated-message:before {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.button-primary.updated-message:before,
+.button-primary.activated-message:before {
</ins><span class="cx" style="display: block; padding: 0 10px"> color: #9ec2e6;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.button.updated-message {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.button.updated-message,
+.button.activated-message {
</ins><span class="cx" style="display: block; padding: 0 10px"> transition-property: border, background, color;
</span><span class="cx" style="display: block; padding: 0 10px"> transition-duration: .05s;
</span><span class="cx" style="display: block; padding: 0 10px"> transition-timing-function: ease-in-out;
</span></span></pre></div>
<a id="trunksrcwpadmincsslisttablescss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/css/list-tables.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/list-tables.css 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/css/list-tables.css 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -585,8 +585,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> z-index: 1;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.check-column input:where(:not(:disabled)):hover,
-.check-column:hover input:where(:not(:disabled)) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.check-column .label-covers-full-cell:hover + input:not(:disabled) {
</ins><span class="cx" style="display: block; padding: 0 10px"> box-shadow: 0 0 0 1px #2271b1;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1548,12 +1547,98 @@
</span><span class="cx" style="display: block; padding: 0 10px"> line-height: 1.3;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.plugin-card .name,
</del><span class="cx" style="display: block; padding: 0 10px"> .plugin-card .desc {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- margin-left: 148px; /* icon + margin */
- margin-right: 128px; /* action links + margin */
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ margin-inline: 0;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.plugin-card .name, .plugin-card .desc > p {
+ margin-left: 148px;
+}
+
+@media (min-width: 1101px) {
+ .plugin-card .name, .plugin-card .desc > p {
+ margin-right: 128px;
+ }
+}
+
+@media (min-width: 481px) and (max-width: 781px) {
+ .plugin-card .name, .plugin-card .desc > p {
+ margin-right: 128px;
+ }
+}
+
+.plugin-card .column-description {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+}
+
+.plugin-card .column-description > p {
+ margin-top: 0;
+}
+
+.plugin-card .column-description .authors {
+ order: 1;
+}
+
+.plugin-card .column-description .plugin-dependencies {
+ order: 2;
+}
+
+.plugin-card .column-description p:empty {
+ display: none;
+}
+
+.plugin-card .plugin-dependencies {
+ background-color: #e5f5fa;
+ border-left: 3px solid #72aee6;
+ margin-bottom: .5em;
+ padding: 15px;
+}
+
+.plugin-card .plugin-dependencies-explainer-text {
+ margin-block: 0;
+}
+
+.plugin-card .plugin-dependency {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: .5em;
+ column-gap: 1%;
+ row-gap: .5em;
+}
+
+.plugin-card .plugin-dependency:nth-child(2),
+.plugin-card .plugin-dependency:last-child {
+ margin-top: 1em;
+}
+
+.plugin-card .plugin-dependency-name {
+ flex-basis: 74%;
+}
+
+.plugin-card .plugin-dependency .more-details-link {
+ margin-left: auto;
+}
+
+.rtl .plugin-card .plugin-dependency .more-details-link {
+ margin-right: auto;
+}
+
+@media (max-width: 939px) {
+ .plugin-card .plugin-dependency-name {
+ flex-basis: 69%;
+ }
+ .plugin-card .plugin-dependency .more-details-link {
+ }
+}
+
+.plugins #the-list .required-by,
+.plugins #the-list .requires {
+ margin-top: 1em;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> .plugin-card .action-links {
</span><span class="cx" style="display: block; padding: 0 10px"> position: absolute;
</span><span class="cx" style="display: block; padding: 0 10px"> top: 20px;
</span></span></pre></div>
<a id="trunksrcwpadminincludesajaxactionsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/ajax-actions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/ajax-actions.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/includes/ajax-actions.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4579,6 +4579,56 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Handles activating a plugin via AJAX.
+ *
+ * @since 6.5.0
+ */
+function wp_ajax_activate_plugin() {
+ check_ajax_referer( 'updates' );
+
+ if ( empty( $_POST['name'] ) || empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) {
+ wp_send_json_error(
+ array(
+ 'slug' => '',
+ 'pluginName' => '',
+ 'plugin' => '',
+ 'errorCode' => 'no_plugin_specified',
+ 'errorMessage' => __( 'No plugin specified.' ),
+ )
+ );
+ }
+
+ $status = array(
+ 'activate' => 'plugin',
+ 'slug' => wp_unslash( $_POST['slug'] ),
+ 'pluginName' => wp_unslash( $_POST['name'] ),
+ 'plugin' => wp_unslash( $_POST['plugin'] ),
+ );
+
+ if ( ! current_user_can( 'activate_plugin', $status['plugin'] ) ) {
+ $status['errorMessage'] = __( 'Sorry, you are not allowed to activate plugins on this site.' );
+ wp_send_json_error( $status );
+ }
+
+ if ( is_plugin_active( $status['plugin'] ) ) {
+ $status['errorMessage'] = sprintf(
+ /* translators: %s: Plugin name. */
+ __( '%s is already active.' ),
+ $status['pluginName']
+ );
+ }
+
+ $activated = activate_plugin( $status['plugin'] );
+
+ if ( is_wp_error( $activated ) ) {
+ $status['errorMessage'] = $activated->get_error_message();
+ wp_send_json_error( $status );
+ }
+
+ wp_send_json_success( $status );
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Handles updating a plugin via AJAX.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 4.2.0
</span></span></pre></div>
<a id="trunksrcwpadminincludesclasspluginupgraderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/class-plugin-upgrader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/class-plugin-upgrader.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/includes/class-plugin-upgrader.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -155,6 +155,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> // Force refresh of plugin update information.
</span><span class="cx" style="display: block; padding: 0 10px"> wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $all_plugin_data = get_option( 'plugin_data', array() );
+ $plugin_file = $this->new_plugin_data['file'];
+ unset( $this->new_plugin_data['file'] );
+ $all_plugin_data[ $plugin_file ] = $this->new_plugin_data;
+ update_option( 'plugin_data', $all_plugin_data );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( $parsed_args['overwrite_package'] ) {
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px"> * Fires when the upgrader has successfully overwritten a currently installed
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -482,7 +488,16 @@
</span><span class="cx" style="display: block; padding: 0 10px"> foreach ( $files as $file ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $info = get_plugin_data( $file, false, false );
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! empty( $info['Name'] ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->new_plugin_data = $info;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $basename = basename( $file );
+ $dirname = basename( dirname( $file ) );
+
+ if ( '.' === $dirname ) {
+ $plugin_file = $basename;
+ } else {
+ $plugin_file = "$dirname/$basename";
+ }
+ $this->new_plugin_data = ( $info );
+ $this->new_plugin_data['file'] = $plugin_file;
</ins><span class="cx" style="display: block; padding: 0 10px"> break;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpadminincludesclasswpplugininstalllisttablephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/class-wp-plugin-install-list-table.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/class-wp-plugin-install-list-table.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/includes/class-wp-plugin-install-list-table.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -525,6 +525,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> // Remove any HTML from the description.
</span><span class="cx" style="display: block; padding: 0 10px"> $description = strip_tags( $plugin['short_description'] );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $description .= $this->get_dependencies_notice( $plugin );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px"> * Filters the plugin card description on the Add Plugins screen.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -555,103 +557,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $action_links = array();
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) {
- $status = install_plugin_install_status( $plugin );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $action_links[] = wp_get_plugin_action_button( $name, $plugin, $compatible_php, $compatible_wp );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- switch ( $status['status'] ) {
- case 'install':
- if ( $status['url'] ) {
- if ( $compatible_php && $compatible_wp ) {
- $action_links[] = sprintf(
- '<a class="install-now button" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
- esc_attr( $plugin['slug'] ),
- esc_url( $status['url'] ),
- /* translators: %s: Plugin name and version. */
- esc_attr( sprintf( _x( 'Install %s now', 'plugin' ), $name ) ),
- esc_attr( $name ),
- __( 'Install Now' )
- );
- } else {
- $action_links[] = sprintf(
- '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
- _x( 'Cannot Install', 'plugin' )
- );
- }
- }
- break;
-
- case 'update_available':
- if ( $status['url'] ) {
- if ( $compatible_php && $compatible_wp ) {
- $action_links[] = sprintf(
- '<a class="update-now button aria-button-if-js" data-plugin="%s" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
- esc_attr( $status['file'] ),
- esc_attr( $plugin['slug'] ),
- esc_url( $status['url'] ),
- /* translators: %s: Plugin name and version. */
- esc_attr( sprintf( _x( 'Update %s now', 'plugin' ), $name ) ),
- esc_attr( $name ),
- __( 'Update Now' )
- );
- } else {
- $action_links[] = sprintf(
- '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
- _x( 'Cannot Update', 'plugin' )
- );
- }
- }
- break;
-
- case 'latest_installed':
- case 'newer_installed':
- if ( is_plugin_active( $status['file'] ) ) {
- $action_links[] = sprintf(
- '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
- _x( 'Active', 'plugin' )
- );
- } elseif ( current_user_can( 'activate_plugin', $status['file'] ) ) {
- if ( $compatible_php && $compatible_wp ) {
- $button_text = __( 'Activate' );
- /* translators: %s: Plugin name. */
- $button_label = _x( 'Activate %s', 'plugin' );
- $activate_url = add_query_arg(
- array(
- '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $status['file'] ),
- 'action' => 'activate',
- 'plugin' => $status['file'],
- ),
- network_admin_url( 'plugins.php' )
- );
-
- if ( is_network_admin() ) {
- $button_text = __( 'Network Activate' );
- /* translators: %s: Plugin name. */
- $button_label = _x( 'Network Activate %s', 'plugin' );
- $activate_url = add_query_arg( array( 'networkwide' => 1 ), $activate_url );
- }
-
- $action_links[] = sprintf(
- '<a href="%1$s" class="button activate-now" aria-label="%2$s">%3$s</a>',
- esc_url( $activate_url ),
- esc_attr( sprintf( $button_label, $plugin['name'] ) ),
- $button_text
- );
- } else {
- $action_links[] = sprintf(
- '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
- _x( 'Cannot Activate', 'plugin' )
- );
- }
- } else {
- $action_links[] = sprintf(
- '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
- _x( 'Installed', 'plugin' )
- );
- }
- break;
- }
- }
-
</del><span class="cx" style="display: block; padding: 0 10px"> $details_link = self_admin_url(
</span><span class="cx" style="display: block; padding: 0 10px"> 'plugin-install.php?tab=plugin-information&plugin=' . $plugin['slug'] .
</span><span class="cx" style="display: block; padding: 0 10px"> '&TB_iframe=true&width=600&height=550'
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -828,4 +735,90 @@
</span><span class="cx" style="display: block; padding: 0 10px"> echo '</div></div>';
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ /**
+ * Returns a notice containing a list of dependencies required by the plugin.
+ *
+ * @since 6.5.0
+ *
+ * @param array $plugin_data An array of plugin data. See {@see plugins_api()}
+ * for the list of possible values.
+ * @return string A notice containing a list of dependencies required by the plugin,
+ * or an empty string if none is required.
+ */
+ protected function get_dependencies_notice( $plugin_data ) {
+ if ( empty( $plugin_data['requires_plugins'] ) ) {
+ return '';
+ }
+
+ $no_name_markup = '<div class="plugin-dependency"><span class="plugin-dependency-name">%s</span></div>';
+ $has_name_markup = '<div class="plugin-dependency"><span class="plugin-dependency-name">%s</span> %s</div>';
+
+ $dependencies_list = '';
+ foreach ( $plugin_data['requires_plugins'] as $dependency ) {
+ $dependency_data = WP_Plugin_Dependencies::get_dependency_data( $dependency );
+
+ if (
+ false !== $dependency_data &&
+ ! empty( $dependency_data['name'] ) &&
+ ! empty( $dependency_data['slug'] ) &&
+ ! empty( $dependency_data['version'] )
+ ) {
+ $more_details_link = $this->get_more_details_link( $dependency_data['name'], $dependency_data['slug'] );
+ $dependencies_list .= sprintf( $has_name_markup, esc_html( $dependency_data['name'] ), $more_details_link );
+ continue;
+ }
+
+ $result = plugins_api( 'plugin_information', array( 'slug' => $dependency ) );
+
+ if ( ! empty( $result->name ) ) {
+ $more_details_link = $this->get_more_details_link( $result->name, $result->slug );
+ $dependencies_list .= sprintf( $has_name_markup, esc_html( $result->name ), $more_details_link );
+ continue;
+ }
+
+ $dependencies_list .= sprintf( $no_name_markup, esc_html( $dependency ) );
+ }
+
+ $dependencies_notice = sprintf(
+ '<div class="plugin-dependencies"><p class="plugin-dependencies-explainer-text">%s</p> %s</div>',
+ '<strong>' . __( 'Additional plugins are required' ) . '</strong>',
+ $dependencies_list
+ );
+
+ return $dependencies_notice;
+ }
+
+ /**
+ * Creates a 'More details' link for the plugin.
+ *
+ * @since 6.5.0
+ *
+ * @param string $name The plugin's name.
+ * @param string $slug The plugin's slug.
+ * @return string The 'More details' link for the plugin.
+ */
+ protected function get_more_details_link( $name, $slug ) {
+ $url = add_query_arg(
+ array(
+ 'tab' => 'plugin-information',
+ 'plugin' => $slug,
+ 'TB_iframe' => 'true',
+ 'width' => '600',
+ 'height' => '550',
+ ),
+ network_admin_url( 'plugin-install.php' )
+ );
+
+ $more_details_link = sprintf(
+ '<a href="%1$s" class="more-details-link thickbox open-plugin-details-modal" aria-label="%2$s" data-title="%3$s">%4$s</a>',
+ esc_url( $url ),
+ /* translators: %s: Plugin name. */
+ sprintf( __( 'More information about %s' ), esc_html( $name ) ),
+ esc_attr( $name ),
+ __( 'More Details' )
+ );
+
+ return $more_details_link;
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpadminincludesclasswppluginslisttablephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/class-wp-plugins-list-table.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/class-wp-plugins-list-table.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/includes/class-wp-plugins-list-table.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -754,6 +754,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $compatible_php = is_php_version_compatible( $requires_php );
</span><span class="cx" style="display: block; padding: 0 10px"> $compatible_wp = is_wp_version_compatible( $requires_wp );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $has_dependents = WP_Plugin_Dependencies::has_dependents( $plugin_file );
+ $has_active_dependents = WP_Plugin_Dependencies::has_active_dependents( $plugin_file );
+ $has_unmet_dependencies = WP_Plugin_Dependencies::has_unmet_dependencies( $plugin_file );
+ $has_circular_dependency = WP_Plugin_Dependencies::has_circular_dependency( $plugin_file );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( 'mustuse' === $context ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $is_active = true;
</span><span class="cx" style="display: block; padding: 0 10px"> } elseif ( 'dropins' === $context ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -796,26 +801,53 @@
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $screen->in_admin( 'network' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $is_active ) {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( current_user_can( 'manage_network_plugins' ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $actions['deactivate'] = sprintf(
- '<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
- wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'deactivate-plugin_' . $plugin_file ),
- esc_attr( $plugin_id_attr ),
- /* translators: %s: Plugin name. */
- esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
- __( 'Network Deactivate' )
- );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $has_active_dependents ) {
+ $actions['deactivate'] = __( 'Deactivate' ) .
+ '<span class="screen-reader-text">' .
+ __( 'You cannot deactivate this plugin as other plugins require it.' ) .
+ '</span>';
+
+ } else {
+ $deactivate_url = 'plugins.php?action=deactivate' .
+ '&plugin=' . urlencode( $plugin_file ) .
+ '&plugin_status=' . $context .
+ '&paged=' . $page .
+ '&s=' . $s;
+
+ $actions['deactivate'] = sprintf(
+ '<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
+ wp_nonce_url( $deactivate_url, 'deactivate-plugin_' . $plugin_file ),
+ esc_attr( $plugin_id_attr ),
+ /* translators: %s: Plugin name. */
+ esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
+ __( 'Network Deactivate' )
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( current_user_can( 'manage_network_plugins' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $compatible_php && $compatible_wp ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $actions['activate'] = sprintf(
- '<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
- wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'activate-plugin_' . $plugin_file ),
- esc_attr( $plugin_id_attr ),
- /* translators: %s: Plugin name. */
- esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
- __( 'Network Activate' )
- );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $has_unmet_dependencies ) {
+ $actions['activate'] = __( 'Network Activate' ) .
+ '<span class="screen-reader-text">' .
+ __( 'You cannot activate this plugin as it has unmet requirements.' ) .
+ '</span>';
+ } else {
+ $activate_url = 'plugins.php?action=activate' .
+ '&plugin=' . urlencode( $plugin_file ) .
+ '&plugin_status=' . $context .
+ '&paged=' . $page .
+ '&s=' . $s;
+
+ $actions['activate'] = sprintf(
+ '<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
+ wp_nonce_url( $activate_url, 'activate-plugin_' . $plugin_file ),
+ esc_attr( $plugin_id_attr ),
+ /* translators: %s: Plugin name. */
+ esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
+ __( 'Network Activate' )
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><span class="cx" style="display: block; padding: 0 10px"> $actions['activate'] = sprintf(
</span><span class="cx" style="display: block; padding: 0 10px"> '<span>%s</span>',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -825,14 +857,27 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $actions['delete'] = sprintf(
- '<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
- wp_nonce_url( 'plugins.php?action=delete-selected&checked[]=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'bulk-plugins' ),
- esc_attr( $plugin_id_attr ),
- /* translators: %s: Plugin name. */
- esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
- __( 'Delete' )
- );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $has_dependents && ! $has_circular_dependency ) {
+ $actions['delete'] = __( 'Delete' ) .
+ '<span class="screen-reader-text">' .
+ __( 'You cannot delete this plugin as other plugins require it.' ) .
+ '</span>';
+ } else {
+ $delete_url = 'plugins.php?action=delete-selected' .
+ '&checked[]=' . urlencode( $plugin_file ) .
+ '&plugin_status=' . $context .
+ '&paged=' . $page .
+ '&s=' . $s;
+
+ $actions['delete'] = sprintf(
+ '<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
+ wp_nonce_url( $delete_url, 'bulk-plugins' ),
+ esc_attr( $plugin_id_attr ),
+ /* translators: %s: Plugin name. */
+ esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
+ __( 'Delete' )
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -846,20 +891,39 @@
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> } elseif ( $is_active ) {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( current_user_can( 'deactivate_plugin', $plugin_file ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $actions['deactivate'] = sprintf(
- '<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
- wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'deactivate-plugin_' . $plugin_file ),
- esc_attr( $plugin_id_attr ),
- /* translators: %s: Plugin name. */
- esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
- __( 'Deactivate' )
- );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $has_active_dependents ) {
+ $actions['deactivate'] = __( 'Deactivate' ) .
+ '<span class="screen-reader-text">' .
+ __( 'You cannot deactivate this plugin as other plugins depend on it.' ) .
+ '</span>';
+ } else {
+ $deactivate_url = 'plugins.php?action=deactivate' .
+ '&plugin=' . urlencode( $plugin_file ) .
+ '&plugin_status=' . $context .
+ '&paged=' . $page .
+ '&s=' . $s;
+
+ $actions['deactivate'] = sprintf(
+ '<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
+ wp_nonce_url( $deactivate_url, 'deactivate-plugin_' . $plugin_file ),
+ esc_attr( $plugin_id_attr ),
+ /* translators: %s: Plugin name. */
+ esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
+ __( 'Deactivate' )
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( current_user_can( 'resume_plugin', $plugin_file ) && is_plugin_paused( $plugin_file ) ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $resume_url = 'plugins.php?action=resume' .
+ '&plugin=' . urlencode( $plugin_file ) .
+ '&plugin_status=' . $context .
+ '&paged=' . $page .
+ '&s=' . $s;
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $actions['resume'] = sprintf(
</span><span class="cx" style="display: block; padding: 0 10px"> '<a href="%s" id="resume-%s" class="resume-link" aria-label="%s">%s</a>',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ wp_nonce_url( $resume_url, 'resume-plugin_' . $plugin_file ),
</ins><span class="cx" style="display: block; padding: 0 10px"> esc_attr( $plugin_id_attr ),
</span><span class="cx" style="display: block; padding: 0 10px"> /* translators: %s: Plugin name. */
</span><span class="cx" style="display: block; padding: 0 10px"> esc_attr( sprintf( _x( 'Resume %s', 'plugin' ), $plugin_data['Name'] ) ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -869,14 +933,27 @@
</span><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( current_user_can( 'activate_plugin', $plugin_file ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $compatible_php && $compatible_wp ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $actions['activate'] = sprintf(
- '<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
- wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'activate-plugin_' . $plugin_file ),
- esc_attr( $plugin_id_attr ),
- /* translators: %s: Plugin name. */
- esc_attr( sprintf( _x( 'Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
- __( 'Activate' )
- );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $has_unmet_dependencies ) {
+ $actions['activate'] = __( 'Activate' ) .
+ '<span class="screen-reader-text">' .
+ __( 'You cannot activate this plugin as it has unmet requirements.' ) .
+ '</span>';
+ } else {
+ $activate_url = 'plugins.php?action=activate' .
+ '&plugin=' . urlencode( $plugin_file ) .
+ '&plugin_status=' . $context .
+ '&paged=' . $page .
+ '&s=' . $s;
+
+ $actions['activate'] = sprintf(
+ '<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
+ wp_nonce_url( $activate_url, 'activate-plugin_' . $plugin_file ),
+ esc_attr( $plugin_id_attr ),
+ /* translators: %s: Plugin name. */
+ esc_attr( sprintf( _x( 'Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
+ __( 'Activate' )
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><span class="cx" style="display: block; padding: 0 10px"> $actions['activate'] = sprintf(
</span><span class="cx" style="display: block; padding: 0 10px"> '<span>%s</span>',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -886,14 +963,27 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! is_multisite() && current_user_can( 'delete_plugins' ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $actions['delete'] = sprintf(
- '<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
- wp_nonce_url( 'plugins.php?action=delete-selected&checked[]=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'bulk-plugins' ),
- esc_attr( $plugin_id_attr ),
- /* translators: %s: Plugin name. */
- esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
- __( 'Delete' )
- );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $has_dependents && ! $has_circular_dependency ) {
+ $actions['delete'] = __( 'Delete' ) .
+ '<span class="screen-reader-text">' .
+ __( 'You cannot delete this plugin as other plugins require it.' ) .
+ '</span>';
+ } else {
+ $delete_url = 'plugins.php?action=delete-selected' .
+ '&checked[]=' . urlencode( $plugin_file ) .
+ '&plugin_status=' . $context .
+ '&paged=' . $page .
+ '&s=' . $s;
+
+ $actions['delete'] = sprintf(
+ '<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
+ wp_nonce_url( $delete_url, 'bulk-plugins' ),
+ esc_attr( $plugin_id_attr ),
+ /* translators: %s: Plugin name. */
+ esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
+ __( 'Delete' )
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> } // End if $is_active.
</span><span class="cx" style="display: block; padding: 0 10px"> } // End if $screen->in_admin( 'network' ).
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -988,17 +1078,28 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $class = $is_active ? 'active' : 'inactive';
</span><span class="cx" style="display: block; padding: 0 10px"> $checkbox_id = 'checkbox_' . md5( $plugin_file );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $disabled = '';
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( $restrict_network_active || $restrict_network_only || in_array( $status, array( 'mustuse', 'dropins' ), true ) || ! $compatible_php ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $has_active_dependents || $has_unmet_dependencies ) {
+ $disabled = 'disabled';
+ }
+
+ if (
+ $restrict_network_active ||
+ $restrict_network_only ||
+ in_array( $status, array( 'mustuse', 'dropins' ), true ) ||
+ ! $compatible_php
+ ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> $checkbox = '';
</span><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><span class="cx" style="display: block; padding: 0 10px"> $checkbox = sprintf(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- '<input type="checkbox" name="checked[]" value="%1$s" id="%2$s" />' .
- '<label for="%2$s"><span class="screen-reader-text">%3$s</span></label>',
- esc_attr( $plugin_file ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ '<label class="label-covers-full-cell" for="%1$s">' .
+ '<span class="screen-reader-text">%2$s</span></label>' .
+ '<input type="checkbox" name="checked[]" value="%3$s" id="%1$s" ' . $disabled . '/>',
</ins><span class="cx" style="display: block; padding: 0 10px"> $checkbox_id,
</span><span class="cx" style="display: block; padding: 0 10px"> /* translators: Hidden accessibility text. %s: Plugin name. */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- sprintf( __( 'Select %s' ), $plugin_data['Name'] )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ sprintf( __( 'Select %s' ), $plugin_data['Name'] ),
+ esc_attr( $plugin_file )
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1007,8 +1108,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $plugin_name = $plugin_data['Name'];
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( ! empty( $totals['upgrade'] ) && ! empty( $plugin_data['update'] )
- || ! $compatible_php || ! $compatible_wp
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if (
+ ! empty( $totals['upgrade'] ) &&
+ ! empty( $plugin_data['update'] ) ||
+ ! $compatible_php ||
+ ! $compatible_wp
</ins><span class="cx" style="display: block; padding: 0 10px"> ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $class .= ' update';
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1057,15 +1161,19 @@
</span><span class="cx" style="display: block; padding: 0 10px"> <div class='$class second plugin-version-author-uri'>";
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $plugin_meta = array();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( ! empty( $plugin_data['Version'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> /* translators: %s: Plugin version number. */
</span><span class="cx" style="display: block; padding: 0 10px"> $plugin_meta[] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( ! empty( $plugin_data['Author'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $author = $plugin_data['Author'];
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( ! empty( $plugin_data['AuthorURI'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $author = '<a href="' . $plugin_data['AuthorURI'] . '">' . $plugin_data['Author'] . '</a>';
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> /* translators: %s: Plugin author name. */
</span><span class="cx" style="display: block; padding: 0 10px"> $plugin_meta[] = sprintf( __( 'By %s' ), $author );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1149,6 +1257,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> echo '</div>';
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $has_dependents ) {
+ $this->add_dependents_to_dependency_plugin_row( $plugin_file );
+ }
+
+ if ( WP_Plugin_Dependencies::has_dependencies( $plugin_file ) ) {
+ $this->add_dependencies_to_dependent_plugin_row( $plugin_file );
+ }
+
+ /**
+ * Fires after plugin row meta.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file Refer to {@see 'plugin_row_meta'} filter.
+ * @param array $plugin_data Refer to {@see 'plugin_row_meta'} filter.
+ */
+ do_action( 'after_plugin_row_meta', $plugin_file, $plugin_data );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( $paused ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $notice_text = __( 'This plugin failed to load properly and is paused during recovery mode.' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1391,4 +1517,111 @@
</span><span class="cx" style="display: block; padding: 0 10px"> protected function get_primary_column_name() {
</span><span class="cx" style="display: block; padding: 0 10px"> return 'name';
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ /**
+ * Prints a list of other plugins that depend on the plugin.
+ *
+ * @since 6.5.0
+ *
+ * @param string $dependency The dependency's filepath, relative to the plugins directory.
+ */
+ protected function add_dependents_to_dependency_plugin_row( $dependency ) {
+ $dependent_names = WP_Plugin_Dependencies::get_dependent_names( $dependency );
+
+ if ( empty( $dependent_names ) ) {
+ return;
+ }
+
+ $dependency_note = __( 'Note: this plugin cannot be deactivated or deleted until the plugins that require it are deactivated or deleted.' );
+ printf(
+ '<div class="required-by"><p><strong>%1$s</strong> %2$s</p><p>%3$s</p></div>',
+ __( 'Required by:' ),
+ esc_html( implode( ' | ', $dependent_names ) ),
+ $dependency_note
+ );
+ }
+
+ /**
+ * Prints a list of other plugins that the plugin depends on.
+ *
+ * @since 6.5.0
+ *
+ * @param string $dependent The dependent plugin's filepath, relative to the plugins directory.
+ */
+ protected function add_dependencies_to_dependent_plugin_row( $dependent ) {
+ $dependency_names = WP_Plugin_Dependencies::get_dependency_names( $dependent );
+
+ if ( array() === $dependency_names ) {
+ return;
+ }
+
+ $links = array();
+ foreach ( $dependency_names as $slug => $name ) {
+ $links[] = $this->get_dependency_view_details_link( $name, $slug );
+ }
+
+ $dependency_note = __( 'Note: this plugin cannot be activated until the plugins that are required by it are activated.' );
+
+ printf(
+ '<div class="requires"><p><strong>%1$s</strong> %2$s</p><p>%3$s</p></div>',
+ __( 'Requires:' ),
+ implode( ' | ', $links ),
+ $dependency_note
+ );
+ }
+
+ /**
+ * Returns a 'View details' like link for a dependency.
+ *
+ * @since 6.5.0
+ *
+ * @param string $name The dependency's name.
+ * @param string $slug The dependency's slug.
+ * @return string A 'View details' link for the dependency.
+ */
+ protected function get_dependency_view_details_link( $name, $slug ) {
+ $dependency_data = WP_Plugin_Dependencies::get_dependency_data( $slug );
+
+ if ( false === $dependency_data
+ || $name === $slug
+ || $name !== $dependency_data['name']
+ || empty( $dependency_data['version'] )
+ ) {
+ return $name;
+ }
+
+ return $this->get_view_details_link( $name, $slug );
+ }
+
+ /**
+ * Returns a 'View details' link for the plugin.
+ *
+ * @since 6.5.0
+ *
+ * @param string $name The plugin's name.
+ * @param string $slug The plugin's slug.
+ * @return string A 'View details' link for the plugin.
+ */
+ protected function get_view_details_link( $name, $slug ) {
+ $url = add_query_arg(
+ array(
+ 'tab' => 'plugin-information',
+ 'plugin' => $slug,
+ 'TB_iframe' => 'true',
+ 'width' => '600',
+ 'height' => '550',
+ ),
+ network_admin_url( 'plugin-install.php' )
+ );
+
+ $name_attr = esc_attr( $name );
+ return sprintf(
+ "<a href='%s' class='thickbox open-plugin-details-modal' aria-label='%s' data-title='%s'>%s</a>",
+ esc_url( $url ),
+ /* translators: %s: Plugin name. */
+ sprintf( __( 'More information about %s' ), $name_attr ),
+ $name_attr,
+ esc_html( $name )
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpadminincludesplugininstallphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/plugin-install.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/plugin-install.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/includes/plugin-install.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -884,43 +884,176 @@
</span><span class="cx" style="display: block; padding: 0 10px"> echo "</div>\n"; // #plugin-information-scrollable
</span><span class="cx" style="display: block; padding: 0 10px"> echo "<div id='$tab-footer'>\n";
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! empty( $api->download_link ) && ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $status = install_plugin_install_status( $api );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $button = wp_get_plugin_action_button( $api->name, $api, $compatible_php, $compatible_wp );
+ $button = str_replace( 'class="', 'class="right ', $button );
+
+ if ( ! str_contains( $button, __( 'Activate' ) ) ) {
+ $button = str_replace( 'class="', 'id="plugin_install_from_iframe" class="', $button );
+ }
+
+ echo wp_kses_post( $button );
+ }
+ echo "</div>\n";
+
+ wp_print_request_filesystem_credentials_modal();
+ wp_print_admin_notice_templates();
+
+ iframe_footer();
+ exit;
+}
+
+/**
+ * Gets the markup for the plugin install action button.
+ *
+ * @since 6.5.0
+ *
+ * @param string $name Plugin name.
+ * @param array|object $data {
+ * An array or object of plugin data. Can be retrieved from the API.
+ *
+ * @type string $slug The plugin slug.
+ * @type string[] $requires_plugins An array of plugin dependency slugs.
+ * @type string $version The plugin's version string. Used when getting the install status.
+ * }
+ * @param bool $compatible_php The result of a PHP compatibility check.
+ * @param bool $compatible_wp The result of a WP compatibility check.
+ * @return string $button The markup for the dependency row button.
+ */
+function wp_get_plugin_action_button( $name, $data, $compatible_php, $compatible_wp ) {
+ $button = '';
+ $data = (object) $data;
+ $status = install_plugin_install_status( $data );
+ $requires_plugins = $data->requires_plugins ?? array();
+
+ // Determine the status of plugin dependencies.
+ $installed_plugins = get_plugins();
+ $active_plugins = get_option( 'active_plugins' );
+ $plugin_dependencies_count = count( $requires_plugins );
+ $installed_plugin_dependencies_count = 0;
+ $active_plugin_dependencies_count = 0;
+ foreach ( $requires_plugins as $dependency ) {
+ foreach ( array_keys( $installed_plugins ) as $installed_plugin_file ) {
+ if ( str_contains( $installed_plugin_file, '/' ) && explode( '/', $installed_plugin_file )[0] === $dependency ) {
+ ++$installed_plugin_dependencies_count;
+ }
+ }
+
+ foreach ( $active_plugins as $active_plugin_file ) {
+ if ( str_contains( $active_plugin_file, '/' ) && explode( '/', $active_plugin_file )[0] === $dependency ) {
+ ++$active_plugin_dependencies_count;
+ }
+ }
+ }
+ $all_plugin_dependencies_installed = $installed_plugin_dependencies_count === $plugin_dependencies_count;
+ $all_plugin_dependencies_active = $active_plugin_dependencies_count === $plugin_dependencies_count;
+
+ sprintf(
+ '<a class="install-now button" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
+ esc_attr( $data->slug ),
+ esc_url( $status['url'] ),
+ /* translators: %s: Plugin name and version. */
+ esc_attr( sprintf( _x( 'Install %s now', 'plugin' ), $name ) ),
+ esc_attr( $name ),
+ __( 'Install Now' )
+ );
+
+ if ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> switch ( $status['status'] ) {
</span><span class="cx" style="display: block; padding: 0 10px"> case 'install':
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $status['url'] ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( $compatible_php && $compatible_wp ) {
- echo '<a data-slug="' . esc_attr( $api->slug ) . '" id="plugin_install_from_iframe" class="button button-primary right" href="' . $status['url'] . '" target="_parent">' . __( 'Install Now' ) . '</a>';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $compatible_php && $compatible_wp && $all_plugin_dependencies_installed && ! empty( $data->download_link ) ) {
+ $button = sprintf(
+ '<a class="install-now button" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
+ esc_attr( $data->slug ),
+ esc_url( $status['url'] ),
+ /* translators: %s: Plugin name and version. */
+ esc_attr( sprintf( _x( 'Install %s now', 'plugin' ), $name ) ),
+ esc_attr( $name ),
+ __( 'Install Now' )
+ );
</ins><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- printf(
- '<button type="button" class="button button-primary button-disabled right" disabled="disabled">%s</button>',
- _x( 'Cannot Install', 'plugin' )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $button = sprintf(
+ '<button type="button" class="install-now button button-disabled" disabled="disabled">%s</button>',
+ _x( 'Install Now', 'plugin' )
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> break;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> case 'update_available':
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $status['url'] ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( $compatible_php ) {
- echo '<a data-slug="' . esc_attr( $api->slug ) . '" data-plugin="' . esc_attr( $status['file'] ) . '" id="plugin_update_from_iframe" class="button button-primary right" href="' . $status['url'] . '" target="_parent">' . __( 'Install Update Now' ) . '</a>';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $compatible_php && $compatible_wp ) {
+ $button = sprintf(
+ '<a class="update-now button aria-button-if-js" data-plugin="%s" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
+ esc_attr( $status['file'] ),
+ esc_attr( $data->slug ),
+ esc_url( $status['url'] ),
+ /* translators: %s: Plugin name and version. */
+ esc_attr( sprintf( _x( 'Update %s now', 'plugin' ), $name ) ),
+ esc_attr( $name ),
+ __( 'Update Now' )
+ );
</ins><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- printf(
- '<button type="button" class="button button-primary button-disabled right" disabled="disabled">%s</button>',
- _x( 'Cannot Update', 'plugin' )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $button = sprintf(
+ '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
+ _x( 'Update Now', 'plugin' )
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> break;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ case 'latest_installed':
</ins><span class="cx" style="display: block; padding: 0 10px"> case 'newer_installed':
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- /* translators: %s: Plugin version. */
- echo '<a class="button button-primary right disabled">' . sprintf( __( 'Newer Version (%s) Installed' ), esc_html( $status['version'] ) ) . '</a>';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( is_plugin_active( $status['file'] ) ) {
+ $button = sprintf(
+ '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
+ _x( 'Active', 'plugin' )
+ );
+ } elseif ( current_user_can( 'activate_plugin', $status['file'] ) ) {
+ if ( $compatible_php && $compatible_wp && $all_plugin_dependencies_active ) {
+ $button_text = __( 'Activate' );
+ /* translators: %s: Plugin name. */
+ $button_label = _x( 'Activate %s', 'plugin' );
+ $activate_url = add_query_arg(
+ array(
+ '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $status['file'] ),
+ 'action' => 'activate',
+ 'plugin' => $status['file'],
+ ),
+ network_admin_url( 'plugins.php' )
+ );
+
+ if ( is_network_admin() ) {
+ $button_text = __( 'Network Activate' );
+ /* translators: %s: Plugin name. */
+ $button_label = _x( 'Network Activate %s', 'plugin' );
+ $activate_url = add_query_arg( array( 'networkwide' => 1 ), $activate_url );
+ }
+
+ $button = sprintf(
+ '<a href="%1$s" data-name="%2$s" data-slug="%3$s" data-plugin="%4$s" class="button button-primary activate-now" aria-label="%5$s">%6$s</a>',
+ esc_url( $activate_url ),
+ esc_attr( $name ),
+ esc_attr( $data->slug ),
+ esc_attr( $status['file'] ),
+ esc_attr( sprintf( $button_label, $name ) ),
+ $button_text
+ );
+ } else {
+ $button = sprintf(
+ '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
+ is_network_admin() ? _x( 'Network Activate %s', 'plugin' ) : _x( 'Activate', 'plugin' )
+ );
+ }
+ } else {
+ $button = sprintf(
+ '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
+ _x( 'Installed', 'plugin' )
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> break;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- case 'latest_installed':
- echo '<a class="button button-primary right disabled">' . __( 'Latest Version Installed' ) . '</a>';
- break;
</del><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ return $button;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- echo "</div>\n";
-
- iframe_footer();
- exit;
</del><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpadminincludespluginphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/plugin.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/plugin.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/includes/plugin.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -45,6 +45,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 1.5.0
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 5.3.0 Added support for `Requires at least` and `Requires PHP` headers.
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 5.8.0 Added support for `Update URI` header.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.5.0 Added support for `Requires Plugins` header.
</ins><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @param string $plugin_file Absolute path to the main plugin file.
</span><span class="cx" style="display: block; padding: 0 10px"> * @param bool $markup Optional. If the returned data should have HTML markup applied.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -53,39 +54,41 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @return array {
</span><span class="cx" style="display: block; padding: 0 10px"> * Plugin data. Values will be empty if not supplied by the plugin.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @type string $Name Name of the plugin. Should be unique.
- * @type string $PluginURI Plugin URI.
- * @type string $Version Plugin version.
- * @type string $Description Plugin description.
- * @type string $Author Plugin author's name.
- * @type string $AuthorURI Plugin author's website address (if set).
- * @type string $TextDomain Plugin textdomain.
- * @type string $DomainPath Plugin's relative directory path to .mo files.
- * @type bool $Network Whether the plugin can only be activated network-wide.
- * @type string $RequiresWP Minimum required version of WordPress.
- * @type string $RequiresPHP Minimum required version of PHP.
- * @type string $UpdateURI ID of the plugin for update purposes, should be a URI.
- * @type string $Title Title of the plugin and link to the plugin's site (if set).
- * @type string $AuthorName Plugin author's name.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @type string $Name Name of the plugin. Should be unique.
+ * @type string $PluginURI Plugin URI.
+ * @type string $Version Plugin version.
+ * @type string $Description Plugin description.
+ * @type string $Author Plugin author's name.
+ * @type string $AuthorURI Plugin author's website address (if set).
+ * @type string $TextDomain Plugin textdomain.
+ * @type string $DomainPath Plugin's relative directory path to .mo files.
+ * @type bool $Network Whether the plugin can only be activated network-wide.
+ * @type string $RequiresWP Minimum required version of WordPress.
+ * @type string $RequiresPHP Minimum required version of PHP.
+ * @type string $UpdateURI ID of the plugin for update purposes, should be a URI.
+ * @type string $RequiresPlugins Comma separated list of dot org plugin slugs.
+ * @type string $Title Title of the plugin and link to the plugin's site (if set).
+ * @type string $AuthorName Plugin author's name.
</ins><span class="cx" style="display: block; padding: 0 10px"> * }
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $default_headers = array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'Name' => 'Plugin Name',
- 'PluginURI' => 'Plugin URI',
- 'Version' => 'Version',
- 'Description' => 'Description',
- 'Author' => 'Author',
- 'AuthorURI' => 'Author URI',
- 'TextDomain' => 'Text Domain',
- 'DomainPath' => 'Domain Path',
- 'Network' => 'Network',
- 'RequiresWP' => 'Requires at least',
- 'RequiresPHP' => 'Requires PHP',
- 'UpdateURI' => 'Update URI',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'Name' => 'Plugin Name',
+ 'PluginURI' => 'Plugin URI',
+ 'Version' => 'Version',
+ 'Description' => 'Description',
+ 'Author' => 'Author',
+ 'AuthorURI' => 'Author URI',
+ 'TextDomain' => 'Text Domain',
+ 'DomainPath' => 'Domain Path',
+ 'Network' => 'Network',
+ 'RequiresWP' => 'Requires at least',
+ 'RequiresPHP' => 'Requires PHP',
+ 'UpdateURI' => 'Update URI',
+ 'RequiresPlugins' => 'Requires Plugins',
</ins><span class="cx" style="display: block; padding: 0 10px"> // Site Wide Only is deprecated in favor of Network.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- '_sitewide' => 'Site Wide Only',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ '_sitewide' => 'Site Wide Only',
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -330,6 +333,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return $wp_plugins;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $new_plugin_data = array();
</ins><span class="cx" style="display: block; padding: 0 10px"> foreach ( $plugin_files as $plugin_file ) {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! is_readable( "$plugin_root/$plugin_file" ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> continue;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -342,6 +346,13 @@
</span><span class="cx" style="display: block; padding: 0 10px"> continue;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $new_plugin_file = str_replace(
+ trailingslashit( WP_PLUGIN_DIR ),
+ '',
+ "$plugin_root/$plugin_file"
+ );
+
+ $new_plugin_data[ $new_plugin_file ] = $plugin_data;
</ins><span class="cx" style="display: block; padding: 0 10px"> $wp_plugins[ plugin_basename( $plugin_file ) ] = $plugin_data;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -349,6 +360,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $cache_plugins[ $plugin_folder ] = $wp_plugins;
</span><span class="cx" style="display: block; padding: 0 10px"> wp_cache_set( 'plugins', $cache_plugins, 'plugins' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ update_option( 'plugin_data', $new_plugin_data );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> return $wp_plugins;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -951,6 +963,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $plugins_dir = trailingslashit( $plugins_dir );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $plugin_translations = wp_get_installed_translations( 'plugins' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $all_plugin_data = get_option( 'plugin_data', array() );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $errors = array();
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -995,6 +1008,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $errors[] = $plugin_file;
</span><span class="cx" style="display: block; padding: 0 10px"> continue;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ unset( $all_plugin_data[ $plugin_file ] );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $plugin_slug = dirname( $plugin_file );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1043,6 +1057,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> return new WP_Error( 'could_not_remove_plugin', sprintf( $message, implode( ', ', $errors ) ) );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ update_option( 'plugin_data', $all_plugin_data );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> return true;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1114,7 +1129,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px"> * Validates the plugin requirements for WordPress version and PHP version.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Uses the information from `Requires at least` and `Requires PHP` headers
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Uses the information from `Requires at least`, `Requires PHP` and `Requires Plugins` headers
</ins><span class="cx" style="display: block; padding: 0 10px"> * defined in the plugin's main PHP file.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 5.2.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1121,6 +1136,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 5.3.0 Added support for reading the headers from the plugin's
</span><span class="cx" style="display: block; padding: 0 10px"> * main PHP file, with `readme.txt` as a fallback.
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 5.8.0 Removed support for using `readme.txt` as a fallback.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.5.0 Added support for the 'Requires Plugins' header.
</ins><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @param string $plugin Path to the plugin file relative to the plugins directory.
</span><span class="cx" style="display: block; padding: 0 10px"> * @return true|WP_Error True if requirements are met, WP_Error on failure.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1129,8 +1145,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $plugin_headers = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $requirements = array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'requires' => ! empty( $plugin_headers['RequiresWP'] ) ? $plugin_headers['RequiresWP'] : '',
- 'requires_php' => ! empty( $plugin_headers['RequiresPHP'] ) ? $plugin_headers['RequiresPHP'] : '',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'requires' => ! empty( $plugin_headers['RequiresWP'] ) ? $plugin_headers['RequiresWP'] : '',
+ 'requires_php' => ! empty( $plugin_headers['RequiresPHP'] ) ? $plugin_headers['RequiresPHP'] : '',
+ 'requires_plugins' => ! empty( $plugin_headers['RequiresPlugins'] ) ? $plugin_headers['RequiresPlugins'] : '',
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $compatible_wp = is_wp_version_compatible( $requirements['requires'] );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1185,6 +1202,31 @@
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( WP_Plugin_Dependencies::has_unmet_dependencies( $plugin ) ) {
+ $dependencies = WP_Plugin_Dependencies::get_dependencies( $plugin );
+ $unmet_dependencies = array();
+
+ foreach ( $dependencies as $dependency ) {
+ $dependency_file = WP_Plugin_Dependencies::get_dependency_filepath( $dependency );
+
+ if ( false === $dependency_file ) {
+ $unmet_dependencies['not_installed'][] = $dependency;
+ } elseif ( is_plugin_inactive( $dependency_file ) ) {
+ $unmet_dependencies['inactive'][] = $dependency;
+ }
+ }
+
+ return new WP_Error(
+ 'plugin_missing_dependencies',
+ '<p>' . sprintf(
+ /* translators: %s: Plugin name. */
+ _x( '<strong>Error:</strong> %s requires plugins that are not installed or activated.', 'plugin' ),
+ $plugin_headers['Name']
+ ) . '</p>',
+ $unmet_dependencies
+ );
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> return true;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunksrcwpadminplugininstallphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/plugin-install.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/plugin-install.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/plugin-install.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -134,6 +134,10 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * WordPress Administration Template Header.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> require_once ABSPATH . 'wp-admin/admin-header.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+WP_Plugin_Dependencies::display_admin_notice_for_unmet_dependencies();
+WP_Plugin_Dependencies::display_admin_notice_for_deactivated_dependents();
+WP_Plugin_Dependencies::display_admin_notice_for_circular_dependencies();
</ins><span class="cx" style="display: block; padding: 0 10px"> ?>
</span><span class="cx" style="display: block; padding: 0 10px"> <div class="wrap <?php echo esc_attr( "plugin-install-tab-$tab" ); ?>">
</span><span class="cx" style="display: block; padding: 0 10px"> <h1 class="wp-heading-inline">
</span></span></pre></div>
<a id="trunksrcwpadminpluginsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/plugins.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/plugins.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-admin/plugins.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -739,6 +739,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> ?>
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php WP_Plugin_Dependencies::display_admin_notice_for_unmet_dependencies(); ?>
+<?php WP_Plugin_Dependencies::display_admin_notice_for_deactivated_dependents(); ?>
+<?php WP_Plugin_Dependencies::display_admin_notice_for_circular_dependencies(); ?>
</ins><span class="cx" style="display: block; padding: 0 10px"> <div class="wrap">
</span><span class="cx" style="display: block; padding: 0 10px"> <h1 class="wp-heading-inline">
</span><span class="cx" style="display: block; padding: 0 10px"> <?php
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpplugindependenciesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/class-wp-plugin-dependencies.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-plugin-dependencies.php (rev 0)
+++ trunk/src/wp-includes/class-wp-plugin-dependencies.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,949 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * WordPress Plugin Administration API: WP_Plugin_Dependencies class
+ *
+ * @package WordPress
+ * @subpackage Administration
+ * @since 6.5.0
+ */
+
+/**
+ * Core class for installing plugin dependencies.
+ *
+ * It is designed to add plugin dependencies as designated in the
+ * `Requires Plugins` header to a new view in the plugins install page.
+ */
+class WP_Plugin_Dependencies {
+
+ /**
+ * Holds 'get_plugins()'.
+ *
+ * @since 6.5.0
+ *
+ * @var array
+ */
+ protected static $plugins;
+
+ /**
+ * Holds plugin directory names to compare with cache.
+ *
+ * @since 6.5.0
+ *
+ * @var array
+ */
+ protected static $plugin_dirnames;
+
+ /**
+ * Holds sanitized plugin dependency slugs.
+ *
+ * Keyed on the dependent plugin's filepath,
+ * relative to the plugins directory.
+ *
+ * @since 6.5.0
+ *
+ * @var array
+ */
+ protected static $dependencies;
+
+ /**
+ * Holds an array of sanitized plugin dependency slugs.
+ *
+ * @since 6.5.0
+ *
+ * @var array
+ */
+ protected static $dependency_slugs;
+
+ /**
+ * Holds an array of dependent plugin slugs.
+ *
+ * Keyed on the dependent plugin's filepath,
+ * relative to the plugins directory.
+ *
+ * @since 6.5.0
+ *
+ * @var array
+ */
+ protected static $dependent_slugs;
+
+ /**
+ * Holds 'plugins_api()' data for plugin dependencies.
+ *
+ * @since 6.5.0
+ *
+ * @var array
+ */
+ protected static $dependency_api_data;
+
+ /**
+ * Holds plugin dependency filepaths, relative to the plugins directory.
+ *
+ * Keyed on the dependency's slug.
+ *
+ * @since 6.5.0
+ *
+ * @var string[]
+ */
+ protected static $dependency_filepaths;
+
+ /**
+ * An array of circular dependency pairings.
+ *
+ * @since 6.5.0
+ *
+ * @var array[]
+ */
+ protected static $circular_dependencies_pairs;
+
+ /**
+ * An array of circular dependency slugs.
+ *
+ * @since 6.5.0
+ *
+ * @var string[]
+ */
+ protected static $circular_dependencies_slugs;
+
+ /**
+ * Initializes by fetching plugin header and plugin API data,
+ * and deactivating dependents with unmet dependencies.
+ *
+ * @since 6.5.0
+ */
+ public static function initialize() {
+ self::read_dependencies_from_plugin_headers();
+ self::get_dependency_api_data();
+ self::deactivate_dependents_with_unmet_dependencies();
+ }
+
+ /**
+ * Determines whether the plugin has plugins that depend on it.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file The plugin's filepath, relative to the plugins directory.
+ * @return bool Whether the plugin has plugins that depend on it.
+ */
+ public static function has_dependents( $plugin_file ) {
+ return in_array( self::convert_to_slug( $plugin_file ), self::$dependency_slugs, true );
+ }
+
+ /**
+ * Determines whether the plugin has plugin dependencies.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file The plugin's filepath, relative to the plugins directory.
+ * @return bool Whether a plugin has plugin dependencies.
+ */
+ public static function has_dependencies( $plugin_file ) {
+ return isset( self::$dependencies[ $plugin_file ] );
+ }
+
+ /**
+ * Determines whether the plugin has active dependents.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file The plugin's filepath, relative to the plugins directory.
+ * @return bool Whether the plugin has active dependents.
+ */
+ public static function has_active_dependents( $plugin_file ) {
+ require_once ABSPATH . '/wp-admin/includes/plugin.php';
+
+ $dependents = self::get_dependents( self::convert_to_slug( $plugin_file ) );
+ foreach ( $dependents as $dependent ) {
+ if ( is_plugin_active( $dependent ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Gets filepaths of plugins that require the dependency.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug The dependency's slug.
+ * @return array An array of dependent plugin filepaths, relative to the plugins directory.
+ */
+ public static function get_dependents( $slug ) {
+ $dependents = array();
+
+ foreach ( self::$dependencies as $dependent => $dependencies ) {
+ if ( in_array( $slug, $dependencies, true ) ) {
+ $dependents[] = $dependent;
+ }
+ }
+
+ return $dependents;
+ }
+
+ /**
+ * Gets the slugs of plugins that the dependent requires.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file The dependent plugin's filepath, relative to the plugins directory.
+ * @return array An array of dependency plugin slugs.
+ */
+ public static function get_dependencies( $plugin_file ) {
+ if ( isset( self::$dependencies[ $plugin_file ] ) ) {
+ return self::$dependencies[ $plugin_file ];
+ }
+
+ return array();
+ }
+
+ /**
+ * Gets a dependent plugin's filepath.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug The dependent plugin's slug.
+ * @return string|false The dependent plugin's filepath, relative to the plugins directory,
+ * or false if the plugin has no dependencies.
+ */
+ public static function get_dependent_filepath( $slug ) {
+ $filepath = array_search( $slug, self::$dependent_slugs, true );
+
+ return $filepath ? $filepath : false;
+ }
+
+ /**
+ * Determines whether the plugin has unmet dependencies.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file The plugin's filepath, relative to the plugins directory.
+ * @return bool Whether the plugin has unmet dependencies.
+ */
+ public static function has_unmet_dependencies( $plugin_file ) {
+ if ( ! isset( self::$dependencies[ $plugin_file ] ) ) {
+ return false;
+ }
+
+ require_once ABSPATH . '/wp-admin/includes/plugin.php';
+
+ foreach ( self::$dependencies[ $plugin_file ] as $dependency ) {
+ $dependency_filepath = self::get_dependency_filepath( $dependency );
+
+ if ( false === $dependency_filepath || is_plugin_inactive( $dependency_filepath ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Determines whether the plugin has a circular dependency.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file The plugin's filepath, relative to the plugins directory.
+ * @return bool Whether the plugin has a circular dependency.
+ */
+ public static function has_circular_dependency( $plugin_file ) {
+ if ( ! is_array( self::$circular_dependencies_slugs ) ) {
+ self::get_circular_dependencies();
+ }
+
+ if ( ! empty( self::$circular_dependencies_slugs ) ) {
+ $slug = self::convert_to_slug( $plugin_file );
+
+ if ( in_array( $slug, self::$circular_dependencies_slugs, true ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Gets the names of plugins that require the plugin.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file The plugin's filepath, relative to the plugins directory.
+ * @return array An array of dependent names.
+ */
+ public static function get_dependent_names( $plugin_file ) {
+ $dependent_names = array();
+ $plugins = self::get_plugins();
+ $slug = self::convert_to_slug( $plugin_file );
+
+ foreach ( self::get_dependents( $slug ) as $dependent ) {
+ $dependent_names[ $dependent ] = $plugins[ $dependent ]['Name'];
+ }
+ sort( $dependent_names );
+
+ return $dependent_names;
+ }
+
+ /**
+ * Gets the names of plugins required by the plugin.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file The dependent plugin's filepath, relative to the plugins directory.
+ * @return array An array of dependency names.
+ */
+ public static function get_dependency_names( $plugin_file ) {
+ $dependency_api_data = self::get_dependency_api_data();
+ $dependencies = self::get_dependencies( $plugin_file );
+ $plugins = self::get_plugins();
+
+ $dependency_names = array();
+ foreach ( $dependencies as $dependency ) {
+ // Use the name if it's available, otherwise fall back to the slug.
+ if ( isset( $dependency_api_data[ $dependency ]['name'] ) ) {
+ $name = $dependency_api_data[ $dependency ]['name'];
+ } else {
+ $dependency_filepath = self::get_dependency_filepath( $dependency );
+ if ( false !== $dependency_filepath ) {
+ $name = $plugins[ $dependency_filepath ]['Name'];
+ } else {
+ $name = $dependency;
+ }
+ }
+
+ $dependency_names[ $dependency ] = $name;
+ }
+
+ return $dependency_names;
+ }
+
+ /**
+ * Gets the filepath for a dependency, relative to the plugin's directory.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug The dependency's slug.
+ * @return string|false If installed, the dependency's filepath relative to the plugins directory, otherwise false.
+ */
+ public static function get_dependency_filepath( $slug ) {
+ $dependency_filepaths = self::get_dependency_filepaths();
+
+ if ( ! isset( $dependency_filepaths[ $slug ] ) ) {
+ return false;
+ }
+
+ return $dependency_filepaths[ $slug ];
+ }
+
+ /**
+ * Returns API data for the dependency.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug The dependency's slug.
+ * @return array|false The dependency's API data on success, otherwise false.
+ */
+ public static function get_dependency_data( $slug ) {
+ $dependency_api_data = self::get_dependency_api_data();
+
+ if ( isset( $dependency_api_data[ $slug ] ) ) {
+ return $dependency_api_data[ $slug ];
+ }
+
+ return false;
+ }
+
+ /**
+ * Displays an admin notice if dependencies are not installed.
+ *
+ * @since 6.5.0
+ */
+ public static function display_admin_notice_for_unmet_dependencies() {
+ if ( in_array( false, self::get_dependency_filepaths(), true ) ) {
+ wp_admin_notice(
+ __( 'There are additional plugin dependencies that must be installed.' ),
+ array(
+ 'type' => 'info',
+ )
+ );
+ }
+ }
+
+ /**
+ * Displays an admin notice if dependencies have been deactivated.
+ *
+ * @since 6.5.0
+ */
+ public static function display_admin_notice_for_deactivated_dependents() {
+ /*
+ * Plugin deactivated if dependencies not met.
+ * Transient on a 10 second timeout.
+ */
+ $deactivate_requires = get_site_transient( 'wp_plugin_dependencies_deactivated_plugins' );
+ if ( ! empty( $deactivate_requires ) ) {
+ $deactivated_plugins = '';
+ foreach ( $deactivate_requires as $deactivated ) {
+ $deactivated_plugins .= '<li>' . esc_html( self::$plugins[ $deactivated ]['Name'] ) . '</li>';
+ }
+ wp_admin_notice(
+ sprintf(
+ /* translators: 1: plugin names */
+ __( 'The following plugin(s) have been deactivated due to uninstalled or inactive dependencies: %s' ),
+ "<ul>$deactivated_plugins</ul>"
+ ),
+ array(
+ 'type' => 'error',
+ 'dismissible' => true,
+ )
+ );
+ }
+ }
+
+ /**
+ * Displays an admin notice if circular dependencies are installed.
+ *
+ * @since 6.5.0
+ */
+ public static function display_admin_notice_for_circular_dependencies() {
+ $circular_dependencies = self::get_circular_dependencies();
+ if ( ! empty( $circular_dependencies ) && count( $circular_dependencies ) > 1 ) {
+ $circular_dependencies = array_unique( $circular_dependencies, SORT_REGULAR );
+ $plugins = self::get_plugins();
+ $plugin_dirnames = self::get_plugin_dirnames();
+
+ // Build output lines.
+ $circular_dependency_lines = '';
+ foreach ( $circular_dependencies as $circular_dependency ) {
+ $first_filepath = $plugin_dirnames[ $circular_dependency[0] ];
+ $second_filepath = $plugin_dirnames[ $circular_dependency[1] ];
+ $circular_dependency_lines .= sprintf(
+ /* translators: 1: First plugin name, 2: Second plugin name. */
+ '<li>' . _x( '%1$s requires %2$s', 'The first plugin requires the second plugin.' ) . '</li>',
+ '<strong>' . esc_html( $plugins[ $first_filepath ]['Name'] ) . '</strong>',
+ '<strong>' . esc_html( $plugins[ $second_filepath ]['Name'] ) . '</strong>'
+ );
+ }
+
+ wp_admin_notice(
+ sprintf(
+ '<p>%1$s</p><ul>%2$s</ul><p>%3$s</p>',
+ __( 'These plugins cannot be activated because their requirements are invalid. ' ),
+ $circular_dependency_lines,
+ __( 'Please contact the plugin authors for more information.' )
+ ),
+ array(
+ 'type' => 'warning',
+ 'paragraph_wrap' => false,
+ )
+ );
+ }
+ }
+
+ /**
+ * Checks plugin dependencies after a plugin is installed via AJAX.
+ *
+ * @since 6.5.0
+ */
+ public static function check_plugin_dependencies_during_ajax() {
+ check_ajax_referer( 'updates' );
+
+ if ( empty( $_POST['slug'] ) ) {
+ wp_send_json_error(
+ array(
+ 'slug' => '',
+ 'pluginName' => '',
+ 'errorCode' => 'no_plugin_specified',
+ 'errorMessage' => __( 'No plugin specified.' ),
+ )
+ );
+ }
+
+ $slug = sanitize_key( wp_unslash( $_POST['slug'] ) );
+ $status = array( 'slug' => $slug );
+
+ self::get_plugins();
+ self::get_plugin_dirnames();
+
+ if ( ! isset( self::$plugin_dirnames[ $slug ] ) ) {
+ $status['errorCode'] = 'plugin_not_installed';
+ $status['errorMessage'] = __( 'The plugin is not installed.' );
+ wp_send_json_error( $status );
+ }
+
+ $plugin_file = self::$plugin_dirnames[ $slug ];
+ $status['pluginName'] = self::$plugins[ $plugin_file ]['Name'];
+ $status['plugin'] = $plugin_file;
+
+ if ( current_user_can( 'activate_plugin', $plugin_file ) && is_plugin_inactive( $plugin_file ) ) {
+ $status['activateUrl'] = add_query_arg(
+ array(
+ '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $plugin_file ),
+ 'action' => 'activate',
+ 'plugin' => $plugin_file,
+ ),
+ is_multisite() ? network_admin_url( 'plugins.php' ) : admin_url( 'plugins.php' )
+ );
+ }
+
+ if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) {
+ $status['activateUrl'] = add_query_arg( array( 'networkwide' => 1 ), $status['activateUrl'] );
+ }
+
+ $dependencies = self::get_dependencies( $plugin_file );
+ if ( empty( $dependencies ) ) {
+ $status['message'] = __( 'The plugin has no required plugins.' );
+ wp_send_json_success( $status );
+ }
+
+ require_once ABSPATH . '/wp-admin/includes/plugin.php';
+
+ $inactive_dependencies = array();
+ foreach ( $dependencies as $dependency ) {
+ if ( false === self::$plugin_dirnames[ $dependency ] || is_plugin_inactive( self::$plugin_dirnames[ $dependency ] ) ) {
+ $inactive_dependencies[] = $dependency;
+ }
+ }
+
+ if ( ! empty( $inactive_dependencies ) ) {
+ $inactive_dependency_names = array_map(
+ function ( $dependency ) {
+ if ( isset( self::$dependency_api_data[ $dependency ]['Name'] ) ) {
+ $inactive_dependency_name = self::$dependency_api_data[ $dependency ]['Name'];
+ } else {
+ $inactive_dependency_name = $dependency;
+ }
+ return $inactive_dependency_name;
+ },
+ $inactive_dependencies
+ );
+
+ $status['errorCode'] = 'inactive_dependencies';
+ $status['errorMessage'] = sprintf(
+ /* translators: %s: A list of inactive dependency plugin names. */
+ __( 'The following plugins must be activated first: %s.' ),
+ implode( ', ', $inactive_dependency_names )
+ );
+ $status['errorData'] = array_combine( $inactive_dependencies, $inactive_dependency_names );
+
+ wp_send_json_error( $status );
+ }
+
+ $status['message'] = __( 'All required plugins are installed and activated.' );
+ wp_send_json_success( $status );
+ }
+
+ /**
+ * Gets data for installed plugins.
+ *
+ * @since 6.5.0
+ *
+ * @return array An array of plugin data.
+ */
+ protected static function get_plugins() {
+ if ( is_array( self::$plugins ) ) {
+ return self::$plugins;
+ }
+
+ $all_plugin_data = get_option( 'plugin_data', array() );
+
+ if ( empty( $all_plugin_data ) ) {
+ require_once ABSPATH . '/wp-admin/includes/plugin.php';
+ $all_plugin_data = get_plugins();
+ }
+
+ self::$plugins = $all_plugin_data;
+
+ return self::$plugins;
+ }
+
+ /**
+ * Reads and stores dependency slugs from a plugin's 'Requires Plugins' header.
+ *
+ * @since 6.5.0
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ */
+ protected static function read_dependencies_from_plugin_headers() {
+ self::$dependencies = array();
+ self::$dependency_slugs = array();
+ self::$dependent_slugs = array();
+ $plugins = self::get_plugins();
+ foreach ( $plugins as $plugin => $header ) {
+ if ( '' === $header['RequiresPlugins'] ) {
+ continue;
+ }
+
+ $dependency_slugs = self::sanitize_dependency_slugs( $header['RequiresPlugins'] );
+ self::$dependencies[ $plugin ] = $dependency_slugs;
+ self::$dependency_slugs = array_merge( self::$dependency_slugs, $dependency_slugs );
+
+ $dependent_slug = self::convert_to_slug( $plugin );
+ self::$dependent_slugs[ $plugin ] = $dependent_slug;
+ }
+ self::$dependency_slugs = array_unique( self::$dependency_slugs );
+ }
+
+ /**
+ * Sanitizes slugs.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slugs A comma-separated string of plugin dependency slugs.
+ * @return array An array of sanitized plugin dependency slugs.
+ */
+ protected static function sanitize_dependency_slugs( $slugs ) {
+ $sanitized_slugs = array();
+ $slugs = explode( ',', $slugs );
+
+ foreach ( $slugs as $slug ) {
+ $slug = trim( $slug );
+
+ /**
+ * Filters a plugin dependency's slug before matching to
+ * the WordPress.org slug format.
+ *
+ * Can be used to switch between free and premium plugin slugs, for example.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug The slug.
+ */
+ $slug = apply_filters( 'wp_plugin_dependencies_slug', $slug );
+
+ // Match to WordPress.org slug format.
+ if ( preg_match( '/^[a-z0-9]+(-[a-z0-9]+)*$/mu', $slug ) ) {
+ $sanitized_slugs[] = $slug;
+ }
+ }
+ $sanitized_slugs = array_unique( $sanitized_slugs );
+ sort( $sanitized_slugs );
+
+ return $sanitized_slugs;
+ }
+
+ /**
+ * Gets plugin filepaths for active plugins that depend on the dependency.
+ *
+ * Recurses for each dependent that is also a dependency.
+ *
+ * @param string $plugin_file The dependency's filepath, relative to the plugin directory.
+ * @return string[] An array of active dependent plugin filepaths, relative to the plugin directory.
+ */
+ protected static function get_active_dependents_in_dependency_tree( $plugin_file ) {
+ $all_dependents = array();
+ $dependents = self::get_dependents( self::convert_to_slug( $plugin_file ) );
+
+ if ( empty( $dependents ) ) {
+ return $all_dependents;
+ }
+
+ require_once ABSPATH . '/wp-admin/includes/plugin.php';
+ foreach ( $dependents as $dependent ) {
+ if ( is_plugin_active( $dependent ) ) {
+ $all_dependents[] = $dependent;
+ $all_dependents = array_merge(
+ $all_dependents,
+ self::get_active_dependents_in_dependency_tree( $dependent )
+ );
+ }
+ }
+
+ return $all_dependents;
+ }
+
+ /**
+ * Deactivates dependent plugins with unmet dependencies.
+ *
+ * @since 6.5.0
+ */
+ protected static function deactivate_dependents_with_unmet_dependencies() {
+ $dependents_to_deactivate = array();
+ $circular_dependencies = array_reduce(
+ self::get_circular_dependencies(),
+ function ( $all_circular, $circular_pair ) {
+ return array_merge( $all_circular, $circular_pair );
+ },
+ array()
+ );
+
+ require_once ABSPATH . '/wp-admin/includes/plugin.php';
+ foreach ( self::$dependencies as $dependent => $dependencies ) {
+ // Skip dependents that are no longer installed or aren't active.
+ if ( ! array_key_exists( $dependent, self::$plugins ) || is_plugin_inactive( $dependent ) ) {
+ continue;
+ }
+
+ // Skip plugins within a circular dependency tree or plugins that have no unmet dependencies.
+ if ( in_array( $dependent, $circular_dependencies, true ) || ! self::has_unmet_dependencies( $dependent ) ) {
+ continue;
+ }
+
+ $dependents_to_deactivate[] = $dependent;
+
+ // Also add any plugins that rely on any of this plugin's dependents.
+ $dependents_to_deactivate = array_merge(
+ $dependents_to_deactivate,
+ self::get_active_dependents_in_dependency_tree( $dependent )
+ );
+ }
+
+ $dependents_to_deactivate = array_unique( $dependents_to_deactivate );
+
+ deactivate_plugins( $dependents_to_deactivate );
+ set_site_transient( 'wp_plugin_dependencies_deactivated_plugins', $dependents_to_deactivate, 10 );
+ }
+
+ /**
+ * Gets the filepath of installed dependencies.
+ * If a dependency is not installed, the filepath defaults to false.
+ *
+ * @since 6.5.0
+ *
+ * @return array An array of install dependencies filepaths, relative to the plugins directory.
+ */
+ protected static function get_dependency_filepaths() {
+ if ( is_array( self::$dependency_filepaths ) ) {
+ return self::$dependency_filepaths;
+ }
+
+ self::$dependency_filepaths = array();
+
+ $plugin_dirnames = self::get_plugin_dirnames();
+ foreach ( self::$dependency_slugs as $slug ) {
+ if ( isset( $plugin_dirnames[ $slug ] ) ) {
+ self::$dependency_filepaths[ $slug ] = $plugin_dirnames[ $slug ];
+ continue;
+ }
+
+ self::$dependency_filepaths[ $slug ] = false;
+ }
+
+ return self::$dependency_filepaths;
+ }
+
+ /**
+ * Retrieves and stores dependency plugin data from the WordPress.org Plugin API.
+ *
+ * @since 6.5.0
+ *
+ * @global string $pagenow The filename of the current screen.
+ *
+ * @return array|void An array of dependency API data, or void on early exit.
+ */
+ protected static function get_dependency_api_data() {
+ global $pagenow;
+
+ if ( ! is_admin() || ( 'plugins.php' !== $pagenow && 'plugin-install.php' !== $pagenow ) ) {
+ return;
+ }
+
+ if ( is_array( self::$dependency_api_data ) ) {
+ return self::$dependency_api_data;
+ }
+
+ $plugins = self::get_plugins();
+ self::$dependency_api_data = (array) get_site_transient( 'wp_plugin_dependencies_plugin_data' );
+ foreach ( self::$dependency_slugs as $slug ) {
+ // Set transient for individual data, remove from self::$dependency_api_data if transient expired.
+ if ( ! get_site_transient( "wp_plugin_dependencies_plugin_timeout_{$slug}" ) ) {
+ unset( self::$dependency_api_data[ $slug ] );
+ set_site_transient( "wp_plugin_dependencies_plugin_timeout_{$slug}", true, 12 * HOUR_IN_SECONDS );
+ }
+
+ if ( isset( self::$dependency_api_data[ $slug ] ) ) {
+ if ( false === self::$dependency_api_data[ $slug ] ) {
+ $dependency_file = self::get_dependency_filepath( $slug );
+
+ if ( false === $dependency_file ) {
+ self::$dependency_api_data[ $slug ] = array( 'Name' => $slug );
+ } else {
+ self::$dependency_api_data[ $slug ] = array( 'Name' => $plugins[ $dependency_file ]['Name'] );
+ }
+ continue;
+ }
+
+ // Don't hit the Plugin API if data exists.
+ if ( ! empty( self::$dependency_api_data[ $slug ]['last_updated'] ) ) {
+ continue;
+ }
+ }
+
+ if ( ! function_exists( 'plugins_api' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
+ }
+
+ $information = plugins_api(
+ 'plugin_information',
+ array(
+ 'slug' => $slug,
+ 'fields' => array(
+ 'short_description' => true,
+ 'icons' => true,
+ ),
+ )
+ );
+
+ if ( is_wp_error( $information ) ) {
+ continue;
+ }
+
+ self::$dependency_api_data[ $slug ] = (array) $information;
+ // plugins_api() returns 'name' not 'Name'.
+ self::$dependency_api_data[ $information->slug ]['Name'] = self::$dependency_api_data[ $information->slug ]['name'];
+ set_site_transient( 'wp_plugin_dependencies_plugin_data', self::$dependency_api_data, 0 );
+ }
+
+ // Remove from self::$dependency_api_data if slug no longer a dependency.
+ $differences = array_diff( array_keys( self::$dependency_api_data ), self::$dependency_slugs );
+ foreach ( $differences as $difference ) {
+ unset( self::$dependency_api_data[ $difference ] );
+ }
+
+ ksort( self::$dependency_api_data );
+ // Remove empty elements.
+ self::$dependency_api_data = array_filter( self::$dependency_api_data );
+ set_site_transient( 'wp_plugin_dependencies_plugin_data', self::$dependency_api_data, 0 );
+
+ return self::$dependency_api_data;
+ }
+
+ /**
+ * Gets plugin directory names.
+ *
+ * @since 6.5.0
+ *
+ * @return array An array of plugin directory names.
+ */
+ protected static function get_plugin_dirnames() {
+ if ( is_array( self::$plugin_dirnames ) ) {
+ return self::$plugin_dirnames;
+ }
+
+ self::$plugin_dirnames = array();
+
+ $plugin_files = array_keys( self::get_plugins() );
+ foreach ( $plugin_files as $plugin_file ) {
+ $slug = self::convert_to_slug( $plugin_file );
+ self::$plugin_dirnames[ $slug ] = $plugin_file;
+ }
+
+ return self::$plugin_dirnames;
+ }
+
+ /**
+ * Gets circular dependency data.
+ *
+ * @since 6.5.0
+ *
+ * @return array[] An array of circular dependency pairings.
+ */
+ protected static function get_circular_dependencies() {
+ if ( is_array( self::$circular_dependencies_pairs ) ) {
+ return self::$circular_dependencies_pairs;
+ }
+
+ self::$circular_dependencies_slugs = array();
+
+ self::$circular_dependencies_pairs = array();
+ foreach ( self::$dependencies as $dependent => $dependencies ) {
+ /*
+ * $dependent is in 'a/a.php' format. Dependencies are stored as slugs, i.e. 'a'.
+ *
+ * Convert $dependent to slug format for checking.
+ */
+ $dependent_slug = self::convert_to_slug( $dependent );
+
+ self::$circular_dependencies_pairs = array_merge(
+ self::$circular_dependencies_pairs,
+ self::check_for_circular_dependencies( array( $dependent_slug ), $dependencies )
+ );
+ }
+
+ return self::$circular_dependencies_pairs;
+ }
+
+ /**
+ * Checks for circular dependencies.
+ *
+ * @since 6.5.0
+ *
+ * @param array $dependents Array of dependent plugins.
+ * @param array $dependencies Array of plugins dependencies.
+ * @return array A circular dependency pairing, or an empty array if none exists.
+ */
+ protected static function check_for_circular_dependencies( $dependents, $dependencies ) {
+ $circular_dependencies_pairs = array();
+
+ // Check for a self-dependency.
+ $dependents_location_in_its_own_dependencies = array_intersect( $dependents, $dependencies );
+ if ( ! empty( $dependents_location_in_its_own_dependencies ) ) {
+ foreach ( $dependents_location_in_its_own_dependencies as $self_dependency ) {
+ self::$circular_dependencies_slugs[] = $self_dependency;
+ $circular_dependencies_pairs[] = array( $self_dependency, $self_dependency );
+
+ // No need to check for itself again.
+ unset( $dependencies[ array_search( $self_dependency, $dependencies, true ) ] );
+ }
+ }
+
+ /*
+ * Check each dependency to see:
+ * 1. If it has dependencies.
+ * 2. If its list of dependencies includes one of its own dependents.
+ */
+ foreach ( $dependencies as $dependency ) {
+ // Check if the dependency is also a dependent.
+ $dependency_location_in_dependents = array_search( $dependency, self::$dependent_slugs, true );
+
+ if ( false !== $dependency_location_in_dependents ) {
+ $dependencies_of_the_dependency = self::$dependencies[ $dependency_location_in_dependents ];
+
+ foreach ( $dependents as $dependent ) {
+ // Check if its dependencies includes one of its own dependents.
+ $dependent_location_in_dependency_dependencies = array_search(
+ $dependent,
+ $dependencies_of_the_dependency,
+ true
+ );
+
+ if ( false !== $dependent_location_in_dependency_dependencies ) {
+ self::$circular_dependencies_slugs[] = $dependent;
+ self::$circular_dependencies_slugs[] = $dependency;
+ $circular_dependencies_pairs[] = array( $dependent, $dependency );
+
+ // Remove the dependent from its dependency's dependencies.
+ unset( $dependencies_of_the_dependency[ $dependent_location_in_dependency_dependencies ] );
+ }
+ }
+
+ $dependents[] = $dependency;
+
+ /*
+ * Now check the dependencies of the dependency's dependencies for the dependent.
+ *
+ * Yes, that does make sense.
+ */
+ $circular_dependencies_pairs = array_merge(
+ $circular_dependencies_pairs,
+ self::check_for_circular_dependencies( $dependents, array_unique( $dependencies_of_the_dependency ) )
+ );
+ }
+ }
+
+ return $circular_dependencies_pairs;
+ }
+
+ /**
+ * Converts a plugin filepath to a slug.
+ *
+ * @since 6.5.0
+ *
+ * @param string $plugin_file The plugin's filepath, relative to the plugins directory.
+ * @return string The plugin's slug.
+ */
+ protected static function convert_to_slug( $plugin_file ) {
+ if ( 'hello.php' === $plugin_file ) {
+ return 'hello-dolly';
+ }
+ return str_contains( $plugin_file, '/' ) ? dirname( $plugin_file ) : str_replace( '.php', '', $plugin_file );
+ }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesfunctionsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/functions.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-includes/functions.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3359,7 +3359,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * specification and the AV1-AVIF spec, see https://aomediacodec.github.io/av1-avif/v1.1.0.html#brands.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- // Divide the header string into 4 byte groups.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // Divide the header string into 4 byte groups.
</ins><span class="cx" style="display: block; padding: 0 10px"> $magic = str_split( $magic, 8 );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if (
</span></span></pre></div>
<a id="trunksrcwpincludesloadphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/load.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/load.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-includes/load.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -986,6 +986,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $network_plugins = is_multisite() ? wp_get_active_network_plugins() : false;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $invalid_plugins = array();
</ins><span class="cx" style="display: block; padding: 0 10px"> foreach ( $active_plugins as $plugin ) {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! validate_file( $plugin ) // $plugin must validate as file.
</span><span class="cx" style="display: block; padding: 0 10px"> && str_ends_with( $plugin, '.php' ) // $plugin must end with '.php'.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -994,9 +995,23 @@
</span><span class="cx" style="display: block; padding: 0 10px"> && ( ! $network_plugins || ! in_array( WP_PLUGIN_DIR . '/' . $plugin, $network_plugins, true ) )
</span><span class="cx" style="display: block; padding: 0 10px"> ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $plugins[] = WP_PLUGIN_DIR . '/' . $plugin;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ } else {
+ $invalid_plugins[] = $plugin;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( ! empty( $invalid_plugins ) ) {
+ $all_plugin_data = get_option( 'plugin_data', array() );
+
+ if ( ! empty( $all_plugin_data ) ) {
+ foreach ( $invalid_plugins as $invalid_plugin ) {
+ unset( $all_plugin_data[ $invalid_plugin ] );
+ }
+
+ update_option( 'plugin_data', $all_plugin_data );
+ }
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /*
</span><span class="cx" style="display: block; padding: 0 10px"> * Remove plugins from the list of active plugins when we're on an endpoint
</span><span class="cx" style="display: block; padding: 0 10px"> * that should be protected against WSODs and the plugin is paused.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1190,6 +1205,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'search-install-plugins', // Searching for a plugin in the plugin install screen.
</span><span class="cx" style="display: block; padding: 0 10px"> 'update-plugin', // Update an existing plugin.
</span><span class="cx" style="display: block; padding: 0 10px"> 'update-theme', // Update an existing theme.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'activate-plugin', // Activating an existing plugin.
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span></span></pre></div>
<a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2024-02-06 21:11:27 UTC (rev 57544)
+++ trunk/src/wp-settings.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -412,6 +412,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $GLOBALS['wp_plugin_paths'] = array();
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+// Load and initialize WP_Plugin_Dependencies.
+require_once ABSPATH . WPINC . '/class-wp-plugin-dependencies.php';
+if ( ! defined( 'WP_RUN_CORE_TESTS' ) ) {
+ WP_Plugin_Dependencies::initialize();
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> // Load must-use plugins.
</span><span class="cx" style="display: block; padding: 0 10px"> foreach ( wp_get_mu_plugins() as $mu_plugin ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $_wp_plugin_file = $mu_plugin;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -486,7 +492,91 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // Load active plugins.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+$all_plugin_data = get_option( 'plugin_data', array() );
+$failed_plugins = array();
+$update_plugin_data = false;
</ins><span class="cx" style="display: block; padding: 0 10px"> foreach ( wp_get_active_and_valid_plugins() as $plugin ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $plugin_file = str_replace( trailingslashit( WP_PLUGIN_DIR ), '', $plugin );
+ if ( ! isset( $all_plugin_data[ $plugin_file ] ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ $all_plugin_data[ $plugin_file ] = get_plugin_data( WP_PLUGIN_DIR . "/$plugin_file" );
+
+ $update_plugin_data = true;
+ }
+
+ $plugin_headers = $all_plugin_data[ $plugin_file ];
+ $errors = array();
+ $requirements = array(
+ 'requires' => ! empty( $plugin_headers['RequiresWP'] ) ? $plugin_headers['RequiresWP'] : '',
+ 'requires_php' => ! empty( $plugin_headers['RequiresPHP'] ) ? $plugin_headers['RequiresPHP'] : '',
+ 'requires_plugins' => ! empty( $plugin_headers['RequiresPlugins'] ) ? $plugin_headers['RequiresPlugins'] : '',
+ );
+ $compatible_wp = is_wp_version_compatible( $requirements['requires'] );
+ $compatible_php = is_php_version_compatible( $requirements['requires_php'] );
+ $dependencies_met = ! WP_Plugin_Dependencies::has_unmet_dependencies( $plugin_file );
+
+ $php_update_message = '</p><p>' . sprintf(
+ /* translators: %s: URL to Update PHP page. */
+ __( '<a href="%s">Learn more about updating PHP</a>.' ),
+ esc_url( wp_get_update_php_url() )
+ );
+
+ $annotation = wp_get_update_php_annotation();
+
+ if ( $annotation ) {
+ $php_update_message .= '</p><p><em>' . $annotation . '</em>';
+ }
+
+ if ( ! $dependencies_met ) {
+ $errors[] = sprintf(
+ /* translators: %s: The plugin's name. */
+ _x( '%s has unmet dependencies.', 'plugin' ),
+ $plugin_headers['Name']
+ );
+ }
+
+ if ( ! $compatible_wp && ! $compatible_php ) {
+ $errors[] = sprintf(
+ /* translators: 1: Current WordPress version, 2: Current PHP version, 3: Plugin name, 4: Required WordPress version, 5: Required PHP version. */
+ _x( '<strong>Error:</strong> Current versions of WordPress (%1$s) and PHP (%2$s) do not meet minimum requirements for %3$s. The plugin requires WordPress %4$s and PHP %5$s.', 'plugin' ),
+ get_bloginfo( 'version' ),
+ PHP_VERSION,
+ $plugin_headers['Name'],
+ $requirements['requires'],
+ $requirements['requires_php']
+ ) . $php_update_message;
+ } elseif ( ! $compatible_php ) {
+ $errors[] = sprintf(
+ /* translators: 1: Current PHP version, 2: Plugin name, 3: Required PHP version. */
+ _x( '<strong>Error:</strong> Current PHP version (%1$s) does not meet minimum requirements for %2$s. The plugin requires PHP %3$s.', 'plugin' ),
+ PHP_VERSION,
+ $plugin_headers['Name'],
+ $requirements['requires_php']
+ ) . $php_update_message;
+ } elseif ( ! $compatible_wp ) {
+ $errors[] = sprintf(
+ /* translators: 1: Current WordPress version, 2: Plugin name, 3: Required WordPress version. */
+ _x( '<strong>Error:</strong> Current WordPress version (%1$s) does not meet minimum requirements for %2$s. The plugin requires WordPress %3$s.', 'plugin' ),
+ get_bloginfo( 'version' ),
+ $plugin_headers['Name'],
+ $requirements['requires']
+ );
+ }
+
+ if ( ! empty( $errors ) ) {
+ $failed_plugins[ $plugin_file ] = '';
+ foreach ( $errors as $error ) {
+ $failed_plugins[ $plugin_file ] .= wp_get_admin_notice(
+ $error,
+ array(
+ 'type' => 'error',
+ 'dismissible' => true,
+ )
+ );
+ }
+ continue;
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> wp_register_plugin_realpath( $plugin );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $_wp_plugin_file = $plugin;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -504,6 +594,24 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> unset( $plugin, $_wp_plugin_file );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+if ( $update_plugin_data ) {
+ update_option( 'plugin_data', $all_plugin_data );
+}
+
+if ( ! empty( $failed_plugins ) ) {
+ add_action(
+ 'admin_notices',
+ function () use ( $failed_plugins ) {
+ global $pagenow;
+
+ if ( 'index.php' === $pagenow || 'plugins.php' === $pagenow ) {
+ echo implode( '', $failed_plugins );
+ }
+ }
+ );
+}
+unset( $failed_plugins );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> // Load pluggable functions.
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/pluggable.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/pluggable-deprecated.php';
</span></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependenciesbasephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/base.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/base.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/base.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,126 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test case for the Plugin Dependencies tests.
+ *
+ * @package WP_Plugin_Dependencies
+ *
+ * Abstracts the common properties and tasks for the Plugin Dependencies tests.
+ */
+abstract class WP_PluginDependencies_UnitTestCase extends WP_UnitTestCase {
+
+ /**
+ * Stores an instance of WP_Plugin_Dependencies
+ * for managing property visibility during tests.
+ *
+ * @var WP_Plugin_Dependencies
+ */
+ protected static $instance;
+
+ /**
+ * Stores a list of static properties and their default values.
+ * for resetting after each test runs.
+ *
+ * @var array
+ */
+ protected static $static_properties = array(
+ 'plugins' => null,
+ 'plugin_dirnames' => null,
+ 'dependencies' => null,
+ 'dependency_slugs' => null,
+ 'dependent_slugs' => null,
+ 'dependency_api_data' => null,
+ 'dependency_filepaths' => null,
+ 'circular_dependencies_pairs' => null,
+ 'circular_dependencies_slugs' => null,
+ );
+
+ /**
+ * An array of reflected class members.
+ *
+ * @var ReflectionMethod[]|ReflectionProperty[]
+ */
+ protected static $reflected_members = array();
+
+ /**
+ * Sets up the WP_Plugin_Dependencies instance before any tests run.
+ */
+ public static function set_up_before_class() {
+ parent::set_up_before_class();
+
+ self::$instance = new WP_Plugin_Dependencies();
+ }
+
+ /**
+ * Empties the '$reflected_members' property after all tests run.
+ */
+ public static function tear_down_after_class() {
+ self::$reflected_members = array();
+
+ parent::tear_down_after_class();
+ }
+
+ /**
+ * Resets all static properties to a default value after each test.
+ */
+ public function set_up() {
+ parent::set_up();
+
+ foreach ( self::$static_properties as $name => $default_value ) {
+ $this->set_property_value( $name, $default_value );
+ }
+ }
+
+ /**
+ * Temporarily modifies the accessibility of a property to change its value.
+ *
+ * @param string $property The property's name.
+ * @param mixed $value The new value.
+ */
+ public function set_property_value( $property, $value ) {
+ if ( ! isset( self::$reflected_members[ $property ] ) ) {
+ self::$reflected_members[ $property ] = new ReflectionProperty( self::$instance, $property );
+ }
+
+ self::$reflected_members[ $property ]->setAccessible( true );
+ self::$reflected_members[ $property ]->setValue( self::$instance, $value );
+ self::$reflected_members[ $property ]->setAccessible( false );
+ }
+
+ /**
+ * Temporarily modifies the accessibility of a property to get its value.
+ *
+ * @param string $property The property's name.
+ * @return mixed The value of the property.
+ */
+ public function get_property_value( $property ) {
+ if ( ! isset( self::$reflected_members[ $property ] ) ) {
+ self::$reflected_members[ $property ] = new ReflectionProperty( self::$instance, $property );
+ }
+
+ self::$reflected_members[ $property ]->setAccessible( true );
+ $value = self::$reflected_members[ $property ]->getValue( self::$instance );
+ self::$reflected_members[ $property ]->setAccessible( false );
+
+ return $value;
+ }
+
+ /**
+ * Temporarily modifies the accessibility of a method to invoke it
+ * and return its result.
+ *
+ * @param string $method The method's name.
+ * @param mixed ...$args Arguments for the method.
+ * @return mixed The result of the method call.
+ */
+ protected function call_method( $method, ...$args ) {
+ if ( ! isset( self::$reflected_members[ $method ] ) ) {
+ self::$reflected_members[ $method ] = new ReflectionMethod( self::$instance, $method );
+ }
+
+ self::$reflected_members[ $method ]->setAccessible( true );
+ $value = self::$reflected_members[ $method ]->invokeArgs( self::$instance, $args );
+ self::$reflected_members[ $method ]->setAccessible( false );
+
+ return $value;
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependenciesgetDependenciesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencies.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencies.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencies.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,40 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::get_dependencies() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::get_dependencies
+ */
+class Tests_Admin_WPPluginDependencies_GetDependencies extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that a plugin with no dependencies will return an empty array.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_an_empty_array_when_a_plugin_has_no_dependencies() {
+ $this->assertSame( array(), self::$instance::get_dependencies( 'dependent/dependent.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with dependencies will return an array of dependencies.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_an_array_of_dependencies_when_a_plugin_has_dependencies() {
+ $expected = array( 'dependency', 'dependency2' );
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => $expected )
+ );
+ $this->assertSame( $expected, self::$instance::get_dependencies( 'dependent/dependent.php' ) );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependenciesgetDependencyDataphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyData.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyData.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyData.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,73 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::get_dependency_data() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::get_dependency_data
+ * @covers WP_Plugin_Dependencies::get_dependency_api_data
+ */
+class Tests_Admin_WPPluginDependencies_GetDependencyData extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that dependency data is retrieved.
+ *
+ * @ticket 22316
+ *
+ * @global string $pagenow The filename of the current screen.
+ */
+ public function test_should_get_dependency_data() {
+ global $pagenow;
+
+ // Backup $pagenow.
+ $old_pagenow = $pagenow;
+
+ // Ensure is_admin() and screen checks pass.
+ $pagenow = 'plugins.php';
+ set_current_screen( 'plugins.php' );
+
+ $expected = array( 'name' => 'Dependency 1' );
+ $this->set_property_value( 'dependency_api_data', array( 'dependency' => $expected ) );
+
+ $actual = self::$instance::get_dependency_data( 'dependency' );
+
+ // Restore $pagenow.
+ $pagenow = $old_pagenow;
+
+ $this->assertSame( $expected, $actual );
+ }
+
+ /**
+ * Tests that false is returned when no dependency data exists.
+ *
+ * @ticket 22316
+ *
+ * @global string $pagenow The filename of the current screen.
+ */
+ public function test_should_return_false_when_no_dependency_data_exists() {
+ global $pagenow;
+
+ // Backup $pagenow.
+ $old_pagenow = $pagenow;
+
+ // Ensure is_admin() and screen checks pass.
+ $pagenow = 'plugins.php';
+ set_current_screen( 'plugins.php' );
+
+ $this->set_property_value( 'dependency_api_data', array() );
+
+ $actual = self::$instance::get_dependency_data( 'dependency' );
+
+ // Restore $pagenow.
+ $pagenow = $old_pagenow;
+
+ $this->assertFalse( $actual );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependenciesgetDependencyFilepathphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyFilepath.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyFilepath.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyFilepath.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,135 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::get_dependency_filepath() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::get_dependency_filepath
+ * @covers WP_Plugin_Dependencies::get_dependency_filepaths
+ * @covers WP_Plugin_Dependencies::get_plugin_dirnames
+ */
+class Tests_Admin_WPPluginDependencies_GetDependencyFilepath extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that the expected dependency filepaths are retrieved for installed dependencies.
+ *
+ * @ticket 22316
+ *
+ * @dataProvider data_get_dependency_filepath
+ *
+ * @param string[] $dependency_slug The dependency slug.
+ * @param string[] $plugins An array of plugin paths.
+ * @param string|false $expected The expected result.
+ */
+ public function test_should_return_filepaths_for_installed_dependencies( $dependency_slug, $plugins, $expected ) {
+ $this->set_property_value( 'plugins', $plugins );
+ $this->assertNull( $this->get_property_value( 'dependency_filepaths' ) );
+ self::$instance::initialize();
+
+ $this->assertSame(
+ $expected,
+ self::$instance::get_dependency_filepath( $dependency_slug ),
+ 'The incorrect filepath was returned.'
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_get_dependency_filepath() {
+ return array(
+ 'no plugins' => array(
+ 'dependency_slug' => 'dependency',
+ 'plugins' => array(),
+ 'expected' => false,
+ ),
+ 'a plugin that starts with slug/' => array(
+ 'dependency_slug' => 'dependency',
+ 'plugins' => array(
+ 'dependency-pro/dependency.php' => array( 'RequiresPlugins' => '' ),
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency' ),
+ ),
+ 'expected' => false,
+ ),
+ 'a plugin that ends with slug/' => array(
+ 'dependency_slugs' => 'dependency',
+ 'plugins' => array(
+ 'addon-for-dependency/dependency.php' => array( 'RequiresPlugins' => '' ),
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency' ),
+ ),
+ 'expected' => false,
+ ),
+ 'a plugin that does not exist' => array(
+ 'dependency_slugs' => 'dependency2',
+ 'plugins' => array(
+ 'dependency/dependency.php' => array( 'RequiresPlugins' => '' ),
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency2' ),
+ ),
+ 'expected' => false,
+ ),
+ 'a plugin that exists' => array(
+ 'dependency_slugs' => 'dependency',
+ 'plugins' => array(
+ 'dependency/dependency.php' => array( 'RequiresPlugins' => '' ),
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency' ),
+ ),
+ 'expected' => 'dependency/dependency.php',
+ ),
+ );
+ }
+
+ /**
+ * Tests that an existing value for dependency filepaths is returned.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_existing_value_for_dependency_filepaths() {
+ $expected = 'dependency/dependency.php';
+
+ $this->set_property_value( 'dependency_filepaths', array( 'dependency' => $expected ) );
+
+ /*
+ * If existing dependency filepaths are not returned,
+ * they'll be built from this data.
+ *
+ * This data is explicitly set to ensure that no
+ * test plugins ever interfere with this test.
+ */
+ $this->set_property_value(
+ 'dependency_slugs',
+ array( 'dependency', 'dependency2', 'dependency3' )
+ );
+
+ $this->set_property_value(
+ 'plugins',
+ array(
+ // This is flipped as paths are stored in the keys.
+ 'dependency/dependency.php' => array(),
+ 'dependency2/dependency2.php' => array(),
+ 'dependency3/dependency3.php' => array(),
+ )
+ );
+
+ $this->assertSame( $expected, self::$instance::get_dependency_filepath( 'dependency' ) );
+ }
+
+ /**
+ * Tests that an empty array is returned when
+ * no plugin directory names are stored.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_empty_array_for_no_plugin_dirnames() {
+ $this->set_property_value( 'dependency_slugs', array() );
+ $this->assertFalse( self::$instance::get_dependency_filepath( 'dependency' ) );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependenciesgetDependencyNamesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyNames.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyNames.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyNames.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,224 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::get_dependency_names() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::get_dependency_names
+ * @covers WP_Plugin_Dependencies::get_dependency_api_data
+ * @covers WP_Plugin_Dependencies::get_dependencies
+ * @covers WP_Plugin_Dependencies::get_dependency_filepaths
+ */
+class Tests_Admin_WPPluginDependencies_GetDependencyNames extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that dependency names are retrieved.
+ *
+ * @ticket 22316
+ *
+ * @global string $pagenow The filename of the current screen.
+ */
+ public function test_should_get_dependency_names() {
+ global $pagenow;
+
+ // Backup $pagenow.
+ $old_pagenow = $pagenow;
+
+ // Ensure is_admin() and screen checks pass.
+ $pagenow = 'plugins.php';
+ set_current_screen( 'plugins.php' );
+
+ $this->set_property_value(
+ 'plugins',
+ array( 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency, dependency2' ) )
+ );
+
+ self::$instance::initialize();
+
+ $this->set_property_value(
+ 'dependency_filepaths',
+ array(
+ 'dependency' => 'dependency/dependency.php',
+ 'dependency2' => 'dependency2/dependency2.php',
+ )
+ );
+
+ $this->set_property_value(
+ 'dependency_api_data',
+ array(
+ 'dependency' => array(
+ 'name' => 'Dependency 1',
+ ),
+ 'dependency2' => array(
+ 'name' => 'Dependency 2',
+ ),
+ )
+ );
+
+ $actual = self::$instance::get_dependency_names( 'dependent/dependent.php' );
+
+ // Restore $pagenow.
+ $pagenow = $old_pagenow;
+
+ $this->assertSame(
+ array(
+ 'dependency' => 'Dependency 1',
+ 'dependency2' => 'Dependency 2',
+ ),
+ $actual
+ );
+ }
+
+ /**
+ * Tests that dependency slugs are used if their name is not available.
+ *
+ * @ticket 22316
+ *
+ * @global string $pagenow The filename of the current screen.
+ */
+ public function test_should_use_dependency_name_from_file() {
+ global $pagenow;
+
+ // Backup $pagenow.
+ $old_pagenow = $pagenow;
+
+ // Ensure is_admin() and screen checks pass.
+ $pagenow = 'plugins.php';
+ set_current_screen( 'plugins.php' );
+
+ $this->set_property_value(
+ 'plugins',
+ array(
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency, dependency2' ),
+ 'dependency/dependency.php' => array(
+ 'Name' => 'Dependency 1',
+ 'RequiresPlugins' => '',
+ ),
+ 'dependency2/dependency2.php' => array(
+ 'Name' => 'Dependency 2',
+ 'RequiresPlugins' => '',
+ ),
+ )
+ );
+
+ self::$instance::initialize();
+
+ $this->set_property_value(
+ 'dependency_filepaths',
+ array(
+ 'dependency' => 'dependency/dependency.php',
+ 'dependency2' => 'dependency2/dependency2.php',
+ )
+ );
+
+ // The plugins are not in the Plugins repository.
+ $this->set_property_value( 'dependency_api_data', array() );
+
+ $actual = self::$instance::get_dependency_names( 'dependent/dependent.php' );
+
+ // Restore $pagenow.
+ $pagenow = $old_pagenow;
+
+ $this->assertSame(
+ array(
+ 'dependency' => 'Dependency 1',
+ 'dependency2' => 'Dependency 2',
+ ),
+ $actual
+ );
+ }
+
+ /**
+ * Tests that dependency slugs are used if their name is not available.
+ *
+ * @ticket 22316
+ *
+ * @global string $pagenow The filename of the current screen.
+ */
+ public function test_should_use_dependency_slugs() {
+ global $pagenow;
+
+ // Backup $pagenow.
+ $old_pagenow = $pagenow;
+
+ // Ensure is_admin() and screen checks pass.
+ $pagenow = 'plugins.php';
+ set_current_screen( 'plugins.php' );
+
+ $this->set_property_value(
+ 'plugins',
+ array( 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency, dependency2' ) )
+ );
+
+ self::$instance::initialize();
+
+ // The plugins are not in the Plugins repository.
+ $this->set_property_value( 'dependency_api_data', array() );
+
+ $actual = self::$instance::get_dependency_names( 'dependent/dependent.php' );
+
+ // Restore $pagenow.
+ $pagenow = $old_pagenow;
+
+ $this->assertSame(
+ array(
+ 'dependency' => 'dependency',
+ 'dependency2' => 'dependency2',
+ ),
+ $actual
+ );
+ }
+
+ /**
+ * Tests that `$dependency_api_data` is set when it's not already available.
+ *
+ * @ticket 22316
+ *
+ * @global string $pagenow The filename of the current screen.
+ */
+ public function test_should_set_dependency_data_when_not_already_available() {
+ global $pagenow;
+
+ // Backup $pagenow.
+ $old_pagenow = $pagenow;
+
+ // Ensure is_admin() and screen checks pass.
+ $pagenow = 'plugins.php';
+ set_current_screen( 'plugins.php' );
+
+ $this->set_property_value(
+ 'plugins',
+ array(
+ 'dependent/dependent.php' => array(
+ 'Name' => 'Dependent 1',
+ 'RequiresPlugins' => 'dependency',
+ ),
+ 'dependency/dependency.php' => array(
+ 'Name' => 'Dependency 1',
+ 'RequiresPlugins' => '',
+ ),
+ )
+ );
+
+ $this->set_property_value( 'dependency_slugs', array( 'dependency' ) );
+
+ set_site_transient( 'wp_plugin_dependencies_plugin_data', array( 'dependency' => false ) );
+ set_site_transient( 'wp_plugin_dependencies_plugin_timeout_dependency', true, 12 * HOUR_IN_SECONDS );
+ self::$instance::get_dependency_names( 'dependent' );
+
+ // Restore $pagenow.
+ $pagenow = $old_pagenow;
+
+ $this->assertSame(
+ array( 'dependency' => array( 'Name' => 'Dependency 1' ) ),
+ $this->get_property_value( 'dependency_api_data' )
+ );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependenciesgetDependentFilepathphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependentFilepath.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependentFilepath.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependentFilepath.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,75 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::get_dependent_filepath() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::get_dependent_filepath
+ * @covers WP_Plugin_Dependencies::get_plugin_dirnames
+ */
+class Tests_Admin_WPPluginDependencies_GetDependentFilepath extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that the expected dependent filepath is retrieved.
+ *
+ * @ticket 22316
+ *
+ * @dataProvider data_get_dependent_filepath
+ *
+ * @param string $dependent_slug The dependent slug.
+ * @param string[] $plugins An array of plugin data.
+ * @param string|false $expected The expected result.
+ */
+ public function test_should_return_filepaths_for_installed_dependents( $dependent_slug, $plugins, $expected ) {
+ $this->set_property_value( 'plugins', $plugins );
+ self::$instance::initialize();
+
+ $this->assertSame(
+ $expected,
+ self::$instance::get_dependent_filepath( $dependent_slug ),
+ 'The incorrect filepath was returned.'
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_get_dependent_filepath() {
+ return array(
+ 'a plugin that exists' => array(
+ 'dependent_slug' => 'dependent',
+ 'plugins' => array( 'dependent/dependent.php' => array( 'RequiresPlugins' => 'woocommerce' ) ),
+ 'expected' => 'dependent/dependent.php',
+ ),
+ 'no plugins' => array(
+ 'dependent_slug' => 'dependent',
+ 'plugins' => array(),
+ 'expected' => false,
+ ),
+ 'a plugin that starts with slug/' => array(
+ 'dependent_slug' => 'dependent',
+ 'plugins' => array( 'dependent-pro/dependent.php' => array( 'RequiresPlugins' => 'woocommerce' ) ),
+ 'expected' => false,
+ ),
+ 'a plugin that ends with slug/' => array(
+ 'dependent_slug' => 'dependent',
+ 'plugins' => array( 'not-dependent/not-dependent.php' => array( 'RequiresPlugins' => 'woocommerce' ) ),
+ 'expected' => false,
+ ),
+ 'a plugin that does not exist' => array(
+ 'dependent_slug' => 'dependent2',
+ 'plugins' => array( 'dependent/dependent.php' => array( 'RequiresPlugins' => 'woocommerce' ) ),
+ 'expected' => false,
+ ),
+ );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependenciesgetDependentNamesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependentNames.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependentNames.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependentNames.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,76 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::get_dependent_names() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::get_dependent_names
+ * @covers WP_Plugin_Dependencies::get_plugins
+ * @covers WP_Plugin_Dependencies::convert_to_slug
+ * @covers WP_Plugin_Dependencies::get_dependents
+ */
+class Tests_Admin_WPPluginDependencies_GetDependentNames extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that dependent names are retrieved.
+ *
+ * @ticket 22316
+ */
+ public function test_should_get_dependent_names() {
+ $this->set_property_value(
+ 'plugins',
+ array(
+ 'dependent/dependent.php' => array(
+ 'Name' => 'Dependent 1',
+ 'RequiresPlugins' => 'dependency',
+ ),
+ 'dependent2/dependent2.php' => array(
+ 'Name' => 'Dependent 2',
+ 'RequiresPlugins' => 'dependency',
+ ),
+ )
+ );
+
+ self::$instance::initialize();
+
+ $this->assertSame(
+ array( 'Dependent 1', 'Dependent 2' ),
+ self::$instance::get_dependent_names( 'dependency/dependency.php' )
+ );
+ }
+
+ /**
+ * Tests that dependent names are sorted.
+ *
+ * @ticket 22316
+ */
+ public function test_should_sort_dependent_names() {
+ $this->set_property_value(
+ 'plugins',
+ array(
+ 'dependent2/dependent2.php' => array(
+ 'Name' => 'Dependent 2',
+ 'RequiresPlugins' => 'dependency',
+ ),
+ 'dependent/dependent.php' => array(
+ 'Name' => 'Dependent 1',
+ 'RequiresPlugins' => 'dependency',
+ ),
+ )
+ );
+
+ self::$instance::initialize();
+
+ $this->assertSame(
+ array( 'Dependent 1', 'Dependent 2' ),
+ self::$instance::get_dependent_names( 'dependency/dependency.php' )
+ );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependenciesgetDependentsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependents.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependents.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependents.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,50 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::get_dependents() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::get_dependents
+ */
+class Tests_Admin_WPPluginDependencies_GetDependents extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that a plugin with no dependents will return an empty array.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_an_empty_array_when_a_plugin_has_no_dependents() {
+ self::$instance::initialize();
+ $this->assertSame(
+ array(),
+ self::$instance::get_dependents( 'dependency' )
+ );
+ }
+
+ /**
+ * Tests that a plugin with dependents will return an array of dependents.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_an_array_of_dependents_when_a_plugin_has_dependents() {
+ $this->set_property_value(
+ 'dependencies',
+ array(
+ 'dependent/dependent.php' => array( 'dependency' ),
+ 'dependent2/dependent2.php' => array( 'dependency' ),
+ )
+ );
+
+ $this->assertSame(
+ array( 'dependent/dependent.php', 'dependent2/dependent2.php' ),
+ self::$instance::get_dependents( 'dependency' )
+ );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependencieshasActiveDependentsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/hasActiveDependents.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/hasActiveDependents.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/hasActiveDependents.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,201 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::has_active_dependents() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::has_active_dependents
+ */
+class Tests_Admin_WPPluginDependencies_HasActiveDependents extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that a plugin with no dependents will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_false_when_a_plugin_has_no_dependents() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => array( 'dependency' ) )
+ );
+
+ update_option( 'active_plugins', array( 'dependent/dependent.php' ) );
+
+ $this->assertFalse( self::$instance::has_active_dependents( 'dependency2/dependency2.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with active dependents will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_active_dependents() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => array( 'dependency' ) )
+ );
+
+ update_option( 'active_plugins', array( 'dependent/dependent.php' ) );
+
+ $this->assertTrue( self::$instance::has_active_dependents( 'dependency/dependency.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with one inactive and one active dependent will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_one_inactive_and_one_active_dependent() {
+ $this->set_property_value(
+ 'dependencies',
+ array(
+ 'dependent2/dependent2.php' => array( 'dependency' ),
+ 'dependent/dependent.php' => array( 'dependency' ),
+ )
+ );
+
+ update_option( 'active_plugins', array( 'dependent/dependent.php' ) );
+
+ $this->assertTrue( self::$instance::has_active_dependents( 'dependency/dependency.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with one active and one inactive dependent will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_one_active_and_one_inactive_dependent() {
+ $this->set_property_value(
+ 'dependencies',
+ array(
+ 'dependent/dependent.php' => array( 'dependency' ),
+ 'dependent2/dependent2.php' => array( 'dependency' ),
+ )
+ );
+
+ update_option( 'active_plugins', array( 'dependent/dependent.php' ) );
+
+ $this->assertTrue( self::$instance::has_active_dependents( 'dependency/dependency.php' ) );
+ }
+
+ /**
+ * Tests that when a plugin with active dependents is earlier in the list,
+ * it will return true if a later plugin has no active dependents.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_the_earlier_plugin_has_active_dependents_but_the_later_plugin_does_not() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent2/dependent2.php' => array( 'dependency' ) )
+ );
+
+ $this->set_property_value(
+ 'dependencies',
+ array(
+ 'dependent/dependent.php' => array( 'dependency' ),
+ 'dependent2/dependent2.php' => array( 'dependency2' ),
+ )
+ );
+
+ update_option( 'active_plugins', array( 'dependent/dependent.php' ) );
+
+ $this->assertTrue( self::$instance::has_active_dependents( 'dependency/dependency.php' ) );
+ }
+
+ /**
+ * Tests that when a plugin with active dependents is later in the list,
+ * it will return true if an earlier plugin has no active dependents.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_the_later_plugin_has_active_dependents_but_the_earlier_plugin_does_not() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent2/dependent2.php' => array( 'dependency' ) )
+ );
+
+ $this->set_property_value(
+ 'dependencies',
+ array(
+ 'dependent/dependent.php' => array( 'dependency' ),
+ 'dependent2/dependent2.php' => array( 'dependency2' ),
+ )
+ );
+
+ update_option( 'active_plugins', array( 'dependent2/dependent2.php' ) );
+
+ $this->assertTrue( self::$instance::has_active_dependents( 'dependency2/dependency2.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with no dependents will return false.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_false_when_a_plugin_has_no_active_dependents() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => array( 'dependency' ) )
+ );
+
+ $this->assertFalse( self::$instance::has_active_dependents( 'dependency/dependency.php' ) );
+ }
+
+ /**
+ * Tests that when a plugin with no active dependents is earlier in the list,
+ * it will return false if a later plugin has active dependents.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_false_when_the_earlier_plugin_has_no_active_dependents_but_the_later_plugin_does() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent2/dependent2.php' => array( 'dependency' ) )
+ );
+
+ $this->set_property_value(
+ 'dependencies',
+ array(
+ 'dependent/dependent.php' => array( 'dependency' ),
+ 'dependent2/dependent2.php' => array( 'dependency2' ),
+ )
+ );
+
+ update_option( 'active_plugins', array( 'dependent2/dependent2.php' ) );
+
+ $this->assertFalse( self::$instance::has_active_dependents( 'dependency/dependency.php' ) );
+ }
+
+ /**
+ * Tests that when a plugin with no active dependents is later in the list,
+ * it will return false if an earlier plugin has active dependents.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_false_when_the_later_plugin_has_no_active_dependents_but_the_earlier_plugin_does() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent2/dependent2.php' => array( 'dependency' ) )
+ );
+
+ $this->set_property_value(
+ 'dependencies',
+ array(
+ 'dependent/dependent.php' => array( 'dependency' ),
+ 'dependent2/dependent2.php' => array( 'dependency2' ),
+ )
+ );
+
+ update_option( 'active_plugins', array( 'dependent/dependent.php' ) );
+
+ $this->assertFalse( self::$instance::has_active_dependents( 'dependency2/dependency2.php' ) );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependencieshasCircularDependencyphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/hasCircularDependency.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/hasCircularDependency.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/hasCircularDependency.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,127 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::has_circular_dependency() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::has_circular_dependency
+ * @covers WP_Plugin_Dependencies::get_circular_dependencies
+ * @covers WP_Plugin_Dependencies::check_for_circular_dependencies
+ */
+class Tests_Admin_WPPluginDependencies_HasCircularDependency extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that a plugin with a circular dependency will return true.
+ *
+ * @ticket 22316
+ *
+ * @dataProvider data_circular_dependencies
+ *
+ * @param string $plugin_to_check The plugin file of the plugin to check.
+ * @param array[] $plugins An array of plugins.
+ */
+ public function test_should_return_true_when_a_plugin_has_circular_dependency( $plugin_to_check, $plugins ) {
+ $this->set_property_value( 'plugins', $plugins );
+ self::$instance::initialize();
+
+ $this->assertTrue( self::$instance::has_circular_dependency( $plugin_to_check ) );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_circular_dependencies() {
+ return array(
+ 'a plugin that depends on itself' => array(
+ 'plugin_to_check' => 'dependency/dependency.php',
+ 'plugins' => array(
+ 'dependency/dependency.php' => array(
+ 'Name' => 'Dependency 1',
+ 'RequiresPlugins' => 'dependency',
+ ),
+ ),
+ ),
+ 'two plugins' => array(
+ 'plugin_to_check' => 'dependency/dependency.php',
+ 'plugins' => array(
+ 'dependency/dependency.php' => array(
+ 'Name' => 'Dependency 1',
+ 'RequiresPlugins' => 'dependency2',
+ ),
+ 'dependency2/dependency2.php' => array(
+ 'Name' => 'Dependency 2',
+ 'RequiresPlugins' => 'dependency',
+ ),
+ ),
+ ),
+ 'three plugins' => array(
+ 'plugin_to_check' => 'dependency/dependency.php',
+ 'plugins' => array(
+ 'dependency/dependency.php' => array(
+ 'Name' => 'Dependency 1',
+ 'RequiresPlugins' => 'dependency2',
+ ),
+ 'dependency2/dependency2.php' => array(
+ 'Name' => 'Dependency 2',
+ 'RequiresPlugins' => 'dependency3',
+ ),
+ 'dependency3/dependency3.php' => array(
+ 'Name' => 'Dependency 3',
+ 'RequiresPlugins' => 'dependency',
+ ),
+ ),
+ ),
+ 'four plugins' => array(
+ 'plugin_to_check' => 'dependency/dependency.php',
+ 'plugins' => array(
+ 'dependency/dependency.php' => array(
+ 'Name' => 'Dependency 1',
+ 'RequiresPlugins' => 'dependency4',
+ ),
+ 'dependency2/dependency2.php' => array(
+ 'Name' => 'Dependency 2',
+ 'RequiresPlugins' => 'dependency3',
+ ),
+ 'dependency3/dependency3.php' => array(
+ 'Name' => 'Dependency 3',
+ 'RequiresPlugins' => 'dependency',
+ ),
+ 'dependency4/dependency4.php' => array(
+ 'Name' => 'Dependency 4',
+ 'RequiresPlugins' => 'dependency2',
+ ),
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Tests that a plugin with no circular dependencies will return false.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_false_when_a_plugin_has_no_circular_dependency() {
+ $this->set_property_value(
+ 'plugins',
+ array(
+ 'dependency/dependency.php' => array(
+ 'Name' => 'Dependency 1',
+ 'RequiresPlugins' => 'dependency2',
+ ),
+ )
+ );
+
+ self::$instance::initialize();
+
+ $this->assertFalse( self::$instance::has_circular_dependency( 'dependent/dependent.php' ) );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependencieshasDependenciesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/hasDependencies.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/hasDependencies.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/hasDependencies.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,37 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::has_dependencies() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::has_dependencies
+ */
+class Tests_Admin_WPPluginDependencies_HasDependencies extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that a plugin with dependencies will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_dependencies() {
+ $this->set_property_value( 'dependencies', array( 'dependent/dependent.php' => array() ) );
+ $this->assertTrue( self::$instance::has_dependencies( 'dependent/dependent.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with no dependencies will return false.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_false_when_a_plugin_has_no_dependencies() {
+ $this->set_property_value( 'dependencies', array( 'dependent2/dependent2.php' => array() ) );
+ $this->assertFalse( self::$instance::has_dependencies( 'dependent/dependent.php' ) );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependencieshasDependentsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/hasDependents.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/hasDependents.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/hasDependents.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,58 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::has_dependents() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::has_dependents
+ * @covers WP_Plugin_Dependencies::convert_to_slug
+ */
+class Tests_Admin_WPPluginDependencies_HasDependents extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that a plugin with dependents will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_dependents() {
+ $this->set_property_value( 'dependency_slugs', array( 'dependent' ) );
+ $this->assertTrue( self::$instance::has_dependents( 'dependent/dependent.php' ) );
+ }
+
+ /**
+ * Tests that a single file plugin with dependents will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_single_file_plugin_has_dependents() {
+ $this->set_property_value( 'dependency_slugs', array( 'dependent' ) );
+ $this->assertTrue( self::$instance::has_dependents( 'dependent.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with no dependents will return false.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_false_when_a_plugin_has_no_dependents() {
+ $this->set_property_value( 'dependency_slugs', array( 'dependent2' ) );
+ $this->assertFalse( self::$instance::has_dependents( 'dependent/dependent.php' ) );
+ }
+
+ /**
+ * Tests that 'hello.php' is converted to 'hello-dolly'.
+ *
+ * @ticket 22316
+ */
+ public function test_should_convert_hellophp_to_hello_dolly() {
+ $this->set_property_value( 'dependency_slugs', array( 'hello-dolly' ) );
+ $this->assertTrue( self::$instance::has_dependents( 'hello.php' ) );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependencieshasUnmetDependenciesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/hasUnmetDependencies.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/hasUnmetDependencies.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/hasUnmetDependencies.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,146 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::has_unmet_dependencies() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::has_unmet_dependencies
+ */
+class Tests_Admin_WPPluginDependencies_HasUnmetDependencies extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that a plugin with no dependencies will return false.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_false_when_a_plugin_has_no_dependencies() {
+ $this->set_property_value( 'dependencies', array( 'dependent/dependent.php' => array( 'dependency' ) ) );
+ $this->assertFalse( self::$instance::has_unmet_dependencies( 'dependent2/dependent2.php' ) );
+ }
+
+ /**
+ * Tests that a plugin whose dependencies are installed and active will return false.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_false_when_a_plugin_has_no_unmet_dependencies() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => array( 'dependency' ) )
+ );
+
+ $this->set_property_value(
+ 'dependency_filepaths',
+ array( 'dependency' => 'dependency/dependency.php' )
+ );
+
+ update_option( 'active_plugins', array( 'dependency/dependency.php' ) );
+
+ $this->assertFalse( self::$instance::has_unmet_dependencies( 'dependent/dependent.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with a dependency that is not installed will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_a_dependency_that_is_not_installed() {
+ self::$instance::initialize();
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => array( 'dependency' ) )
+ );
+
+ $this->assertTrue( self::$instance::has_unmet_dependencies( 'dependent/dependent.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with a dependency that is inactive will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_a_dependency_that_is_inactive() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => array( 'dependency' ) )
+ );
+
+ $this->set_property_value(
+ 'dependency_filepaths',
+ array( 'dependency' => 'dependency/dependency.php' )
+ );
+
+ $this->assertTrue( self::$instance::has_unmet_dependencies( 'dependent/dependent.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with one dependency that is active and one dependency that is inactive will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_one_active_dependency_and_one_inactive_dependency() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => array( 'dependency', 'dependency2' ) )
+ );
+
+ $this->set_property_value(
+ 'dependency_filepaths',
+ array(
+ 'dependency' => 'dependency/dependency.php',
+ 'dependency2' => 'dependency2/dependency2.php',
+ )
+ );
+
+ update_option( 'active_plugins', array( 'dependency/dependency.php' ) );
+
+ $this->assertTrue( self::$instance::has_unmet_dependencies( 'dependent/dependent.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with one dependency that is active and one dependency that is not installed will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_one_active_dependency_and_one_that_is_not_installed() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => array( 'dependency', 'dependency2' ) )
+ );
+
+ $this->set_property_value(
+ 'dependency_filepaths',
+ array( 'dependency' => 'dependency/dependency.php' )
+ );
+
+ update_option( 'active_plugins', array( 'dependency/dependency.php' ) );
+
+ $this->assertTrue( self::$instance::has_unmet_dependencies( 'dependent/dependent.php' ) );
+ }
+
+ /**
+ * Tests that a plugin with one dependency that is inactive and one dependency that is not installed will return true.
+ *
+ * @ticket 22316
+ */
+ public function test_should_return_true_when_a_plugin_has_one_inactive_dependency_and_one_that_is_not_installed() {
+ $this->set_property_value(
+ 'dependencies',
+ array( 'dependent/dependent.php' => array( 'dependency', 'dependency2' ) )
+ );
+
+ $this->set_property_value(
+ 'dependency_filepaths',
+ array( 'dependency' => 'dependency/dependency.php' )
+ );
+
+ $this->assertTrue( self::$instance::has_unmet_dependencies( 'dependent/dependent.php' ) );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsadminplugindependenciesinitializephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/plugin-dependencies/initialize.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/plugin-dependencies/initialize.php (rev 0)
+++ trunk/tests/phpunit/tests/admin/plugin-dependencies/initialize.php 2024-02-06 23:44:09 UTC (rev 57545)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,336 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for the WP_Plugin_Dependencies::initialize() method.
+ *
+ * @package WordPress
+ */
+
+require_once __DIR__ . '/base.php';
+
+/**
+ * @group admin
+ * @group plugins
+ *
+ * @covers WP_Plugin_Dependencies::initialize
+ */
+class Tests_Admin_WPPluginDependencies_Initialize extends WP_PluginDependencies_UnitTestCase {
+
+ /**
+ * Tests that `$dependency_api_data` is set on certain screens.
+ *
+ * @ticket 22316
+ *
+ * @covers WP_Plugin_Dependencies::get_dependency_api_data
+ * @covers WP_Plugin_Dependencies::get_plugins
+ *
+ * @dataProvider data_screens
+ *
+ * @global string $pagenow The filename of the current screen.
+ *
+ * @param string $screen The screen file.
+ */
+ public function test_should_set_dependency_api_data_on_certain_screens( $screen ) {
+ global $pagenow;
+
+ // Backup $pagenow.
+ $old_pagenow = $pagenow;
+
+ // Ensure is_admin() and screen checks pass.
+ $pagenow = $screen;
+ set_current_screen( $screen );
+
+ self::$instance::initialize();
+
+ // Restore $pagenow.
+ $pagenow = $old_pagenow;
+
+ $dependency_api_data = $this->get_property_value( 'dependency_api_data' );
+
+ $this->assertIsArray( $dependency_api_data, '$dependency_api_data is not an array.' );
+ $this->assertEmpty( $dependency_api_data, '$dependency_api_data is not empty.' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_screens() {
+ return array(
+ 'plugins.php' => array(
+ 'screen' => 'plugins.php',
+ ),
+ 'plugin-install.php' => array(
+ 'screen' => 'plugin-install.php',
+ ),
+ );
+ }
+
+ /**
+ * Tests that `$dependency_api_data` is not set by default.
+ *
+ * @ticket 22316
+ *
+ * @covers WP_Plugin_Dependencies::get_dependency_api_data
+ */
+ public function test_should_not_set_dependency_api_data() {
+ self::$instance::initialize();
+
+ $dependency_api_data = $this->get_property_value( 'dependency_api_data' );
+
+ $this->assertNull( $dependency_api_data, '$dependency_api_data was set.' );
+ }
+
+ /**
+ * Tests that dependency slugs are loaded and sanitized.
+ *
+ * @ticket 22316
+ *
+ * @covers WP_Plugin_Dependencies::read_dependencies_from_plugin_headers
+ * @covers WP_Plugin_Dependencies::sanitize_dependency_slugs
+ *
+ * @dataProvider data_should_sanitize_slugs
+ *
+ * @param string $requires_plugins The unsanitized dependency slug(s).
+ * @param array $expected Optional. The sanitized dependency slug(s). Default empty array.
+ */
+ public function test_initialize_should_load_and_sanitize_dependency_slugs_from_plugin_headers( $requires_plugins, $expected = array() ) {
+ $this->set_property_value( 'plugins', array( 'dependent/dependent.php' => array( 'RequiresPlugins' => $requires_plugins ) ) );
+ self::$instance->initialize();
+ $this->assertSame( $expected, $this->get_property_value( 'dependency_slugs' ) );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_should_sanitize_slugs() {
+ return array(
+ // Valid slugs.
+ 'one dependency' => array(
+ 'requires_plugins' => 'hello-dolly',
+ 'expected' => array( 'hello-dolly' ),
+ ),
+ 'two dependencies in alphabetical order' => array(
+ 'requires_plugins' => 'hello-dolly, woocommerce',
+ 'expected' => array(
+ 'hello-dolly',
+ 'woocommerce',
+ ),
+ ),
+ 'two dependencies in reverse alphabetical order' => array(
+ 'requires_plugins' => 'woocommerce, hello-dolly',
+ 'expected' => array(
+ 'hello-dolly',
+ 'woocommerce',
+ ),
+ ),
+ 'two dependencies with a space' => array(
+ 'requires_plugins' => 'hello-dolly , woocommerce',
+ 'expected' => array(
+ 'hello-dolly',
+ 'woocommerce',
+ ),
+ ),
+ 'a repeated dependency' => array(
+ 'requires_plugins' => 'hello-dolly, woocommerce, hello-dolly',
+ 'expected' => array(
+ 'hello-dolly',
+ 'woocommerce',
+ ),
+ ),
+ 'a dependency with multiple dashes' => array(
+ 'requires_plugins' => 'this-is-a-valid-slug',
+ 'expected' => array( 'this-is-a-valid-slug' ),
+ ),
+ 'a dependency starting with numbers' => array(
+ 'requires_plugins' => '123slug',
+ 'expected' => array( '123slug' ),
+ ),
+ 'a dependency with a trailing comma' => array(
+ 'requires_plugins' => 'hello-dolly,',
+ 'expected' => array( 'hello-dolly' ),
+ ),
+ 'a dependency with a leading comma' => array(
+ 'requires_plugins' => ',hello-dolly',
+ 'expected' => array( 'hello-dolly' ),
+ ),
+ 'a dependency with leading and trailing commas' => array(
+ 'requires_plugins' => ',hello-dolly,',
+ 'expected' => array( 'hello-dolly' ),
+ ),
+ 'a dependency with a trailing comma and a space' => array(
+ 'requires_plugins' => 'hello-dolly, ',
+ 'expected' => array( 'hello-dolly' ),
+ ),
+
+ // Invalid or empty slugs.
+ 'no dependencies' => array(
+ 'requires_plugins' => '',
+ ),
+ 'a dependency with an underscore' => array(
+ 'requires_plugins' => 'hello_dolly',
+ ),
+ 'a dependency with a space' => array(
+ 'requires_plugins' => 'hello dolly',
+ ),
+ 'a dependency in quotes' => array(
+ 'requires_plugins' => '"hello-dolly"',
+ ),
+ 'two dependencies in quotes' => array(
+ 'requires_plugins' => '"hello-dolly, woocommerce"',
+ ),
+ 'a dependency with trailing dash' => array(
+ 'requires_plugins' => 'ending-dash-',
+ ),
+ 'a dependency with leading dash' => array(
+ 'requires_plugins' => '-slug',
+ ),
+ 'a dependency with double dashes' => array(
+ 'requires_plugins' => 'abc--123',
+ ),
+ 'cyrillic dependencies' => array(
+ 'requires_plugins' => 'я-делюсь',
+ ),
+ 'arabic dependencies' => array(
+ 'requires_plugins' => 'لينوكس-ويكى',
+ ),
+ 'chinese dependencies' => array(
+ 'requires_plugins' => '唐诗宋词chinese-poem,社交登录,腾讯微博一键登录,豆瓣秀-for-wordpress',
+ ),
+ 'symbol dependencies' => array(
+ 'requires_plugins' => '★-wpsymbols-★',
+ ),
+ );
+ }
+
+ /**
+ * Tests that dependent files are loaded and slugified.
+ *
+ * @ticket 22316
+ *
+ * @covers WP_Plugin_Dependencies::read_dependencies_from_plugin_headers
+ * @covers WP_Plugin_Dependencies::convert_to_slug
+ */
+ public function test_should_slugify_dependent_files() {
+ $plugins = get_plugins();
+
+ $expected_slugs = array();
+ foreach ( $plugins as $plugin_file => &$headers ) {
+ // Create the expected slugs.
+ if ( 'hello.php' === $plugin_file ) {
+ $slug = 'hello-dolly';
+ } else {
+ $slug = str_replace( '.php', '', explode( '/', $plugin_file )[0] );
+ }
+
+ $expected_slugs[ $plugin_file ] = $slug;
+
+ // While here, ensure the plugins are all dependents.
+ $headers['RequiresPlugins'] = 'dependency';
+ }
+ unset( $headers );
+
+ // Set the plugins property with the plugin data modified to make them dependents.
+ $this->set_property_value( 'plugins', $plugins );
+
+ self::$instance->initialize();
+ $this->assertSame( $expected_slugs, $this->get_property_value( 'dependent_slugs' ) );
+ }
+
+ /**
+ * Tests that dependents with unmet dependencies are deactivated.
+ *
+ * @ticket 22316
+ *
+ * @covers WP_Plugin_Dependencies::deactivate_dependents_with_unmet_dependencies
+ * @covers WP_Plugin_Dependencies::has_unmet_dependencies
+ * @covers WP_Plugin_Dependencies::get_active_dependents_in_dependency_tree
+ *
+ * @dataProvider data_should_only_deactivate_dependents_with_unmet_dependencies
+ *
+ * @param array $active_plugins An array of active plugin paths.
+ * @param array $plugins An array of installed plugins.
+ * @param array $expected The expected value of 'active_plugins' after initialization.
+ */
+ public function test_should_deactivate_dependents_with_uninstalled_dependencies( $active_plugins, $plugins, $expected ) {
+ update_option( 'active_plugins', $active_plugins );
+
+ $this->set_property_value( 'plugins', $plugins );
+ self::$instance::initialize();
+
+ $this->assertSame( $expected, array_values( get_option( 'active_plugins', array() ) ) );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_should_only_deactivate_dependents_with_unmet_dependencies() {
+ return array(
+ 'a dependent with an uninstalled dependency' => array(
+ 'active_plugins' => array( 'dependent/dependent.php' ),
+ 'plugins' => array(
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency' ),
+ ),
+ 'expected' => array(),
+ ),
+ 'a dependent with an inactive dependency' => array(
+ 'active_plugins' => array( 'dependent/dependent.php' ),
+ 'plugins' => array(
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency' ),
+ 'dependency/dependency.php' => array( 'RequiresPlugins' => '' ),
+ ),
+ 'expected' => array(),
+ ),
+ 'a dependent with two dependencies, one uninstalled, one inactive' => array(
+ 'active_plugins' => array( 'dependent/dependent.php' ),
+ 'plugins' => array(
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency, dependency2' ),
+ 'dependency2/dependency2.php' => array( 'RequiresPlugins' => '' ),
+ ),
+ 'expected' => array(),
+ ),
+ 'a dependent with a dependency that is installed and active' => array(
+ 'active_plugins' => array( 'dependent/dependent.php', 'dependency/dependency.php' ),
+ 'plugins' => array(
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency' ),
+ 'dependency/dependency.php' => array( 'RequiresPlugins' => '' ),
+ ),
+ 'expected' => array( 'dependent/dependent.php', 'dependency/dependency.php' ),
+ ),
+ 'one dependent with two dependencies that are installed and active' => array(
+ 'active_plugins' => array(
+ 'dependent/dependent.php',
+ 'dependency/dependency.php',
+ 'dependency2/dependency2.php',
+ ),
+ 'plugins' => array(
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency, dependency2' ),
+ 'dependency/dependency.php' => array( 'RequiresPlugins' => '' ),
+ 'dependency2/dependency2.php' => array( 'RequiresPlugins' => '' ),
+ ),
+ 'expected' => array(
+ 'dependent/dependent.php',
+ 'dependency/dependency.php',
+ 'dependency2/dependency2.php',
+ ),
+ ),
+ 'two dependents, one with an uninstalled dependency, and one with an active dependency' => array(
+ 'active_plugins' => array(
+ 'dependent/dependent.php',
+ 'dependent2/dependent2.php',
+ 'dependency2/dependency2.php',
+ ),
+ 'plugins' => array(
+ 'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency' ),
+ 'dependent2/dependent2.php' => array( 'RequiresPlugins' => 'dependency2' ),
+ 'dependency2/dependency2.php' => array( 'RequiresPlugins' => '' ),
+ ),
+ 'expected' => array( 'dependent2/dependent2.php', 'dependency2/dependency2.php' ),
+ ),
+ );
+ }
+}
</ins></span></pre>
</div>
</div>
</body>
</html>