Plugin Directory

Changeset 3764245


Ignore:
Timestamp:
10/25/2026 09:42:08 AM (4 hours ago)
Author:
itay12222
Message:

Release 1.15.26

Location:
marcelo-site-connector
Files:
4 edited
1 copied

Legend:

Unmodified
Added
Removed
  • marcelo-site-connector/tags/1.15.26/marcelo-site-connector.php

    r3763848 r3764245  
    33 * Plugin Name:       Marcelo Site Connector
    44 * Description:       Lets Marcelo add the verification and measurement tags a site needs — Search Console, Analytics, Tag Manager, the Google Ads conversion tag — send addresses that no longer exist to the page that replaced them, give a page the search-result description it is missing, give a page the search-result title it is missing or is sharing with another page, edit the text of a page built with Elementor, put a tap-to-call bar in front of visitors, link the profiles a business has confirmed are theirs from every page, say on every page that a business answers around the clock where its confirmed hours say exactly that, let a named crawler back in where robots.txt was turning it away, point a page's canonical back at itself where it names another website, add a translated page's own link among its hreflang alternates where it is missing, and shrink an oversized image in the site's own media library while keeping its original, without anyone editing theme files by hand.
    5  * Version:           1.15.25
     5 * Version:           1.15.26
    66 * Requires at least: 6.0
    77 * Requires PHP:      7.4
    … …  
    123123 */
    124124const MARCELO_SC_HREFLANG_OPTION     = 'marcelo_site_connector_hreflang';
    125 const MARCELO_SC_VERSION             = '1.15.25';
     125const MARCELO_SC_VERSION             = '1.15.26';
    126126
    127127/**
    … …  
    63256325}
    63266326
     6327/**
     6328 * Whether the upgrade below is running right now, so the shutdown handler
     6329 * knows whether a missing plugin folder is its business.
     6330 *
     6331 * A static rather than a global: the only two callers are the route and the
     6332 * handler it registers, and nothing else on a customer's site has any reason
     6333 * to be able to set it.
     6334 */
     6335function marcelo_sc_upgrade_in_flight( ?bool $set = null ): bool {
     6336        static $in_flight = false;
     6337        if ( null !== $set ) {
     6338                $in_flight = $set;
     6339        }
     6340        return $in_flight;
     6341}
     6342
     6343/**
     6344 * Put this plugin's own folder back where core moved it out of the way.
     6345 *
     6346 * -- The incident this exists for ------------------------------------------
     6347 *
     6348 * 24 October 2026, SOS Restoration Of San Francisco, on Hostinger behind
     6349 * LiteSpeed: the update that ran when Connected accounts opened left no plugin
     6350 * on the site at all. `/wp-json/marcelo/v1/status` answered `rest_no_route`
     6351 * and the files were gone, until somebody installed it again from the
     6352 * directory. Every Marcelo Fix on that site was dead in between, and the
     6353 * owner's answer was to stop every automatic update -- the app's own
     6354 * `PLUGIN_SELF_UPDATE_HELD`.
     6355 *
     6356 * -- Why a half-finished upgrade can leave nothing behind -------------------
     6357 *
     6358 * Since WordPress 6.3 `WP_Upgrader::run()` moves the installed folder to
     6359 * `wp-content/upgrade-temp-backup/plugins/<slug>` **before** copying the new
     6360 * one in, and restores it only where the copy comes back a `WP_Error`. A PHP
     6361 * process that stops existing between the move and the copy hands no error to
     6362 * anybody, so nothing restores anything: the folder is sitting in the backup
     6363 * directory and the plugins directory has no row for it.
     6364 *
     6365 * The two things that make a process stop existing there are a client that
     6366 * hung up -- Marcelo's own fetch, which used to give up after forty seconds --
     6367 * and `max_execution_time`. `ignore_user_abort` and `set_time_limit` in the
     6368 * route take both away. This is the floor under them: where it happened
     6369 * anyway, the folder core moved is still on disk, and moving it back is the
     6370 * whole of the repair.
     6371 *
     6372 * -- What it refuses to touch ----------------------------------------------
     6373 *
     6374 * Only this plugin's own slug, only out of core's own backup directory, and
     6375 * only while the plugins directory has nothing under that name at all. A
     6376 * folder already there is a half-written copy of the new build, which is not
     6377 * something to move a backup on top of -- and it is the one case where the
     6378 * destination holds files somebody may still want.
     6379 *
     6380 * Neither path is an argument. One is `WP_CONTENT_DIR` and core's own fixed
     6381 * backup directory, the other is `WP_PLUGIN_DIR`, and the slug between them is
     6382 * `plugin_basename( __FILE__ )` with anything carrying a separator refused.
     6383 *
     6384 * -- And it acquires nothing ------------------------------------------------
     6385 *
     6386 * `$wp_filesystem` has to be there already, set up by the upgrade this is the
     6387 * tail of. It is deliberately not asked for: `apps/web/src/lib/plugin-promises.test.ts`
     6388 * holds this plugin to having **no way** to write a file outside a customer's
     6389 * own uploads -- the whole general-purpose toolbox absent rather than merely
     6390 * unused -- because the screen a customer approves a fix on tells them nothing
     6391 * already on their site is edited, moved or rewritten. Calling `WP_Filesystem()`
     6392 * here would hand the plugin that ability for every request it ever serves, to
     6393 * repair a folder in the one request that was already holding it. So this can
     6394 * act only inside an upgrade that is in flight, which is the only moment it is
     6395 * for, and the promise is unchanged.
     6396 *
     6397 * @return bool Whether a folder was put back.
     6398 */
     6399function marcelo_sc_put_the_plugin_back(): bool {
     6400        $file = marcelo_sc_plugin_file();
     6401        if ( file_exists( WP_PLUGIN_DIR . '/' . $file ) ) {
     6402                return false;
     6403        }
     6404
     6405        $slug = dirname( $file );
     6406        if ( '' === $slug || '.' === $slug || false !== strpos( $slug, '/' ) ) {
     6407                // A single-file plugin has no folder to move, and anything carrying a
     6408                // separator is not a slug this may act on.
     6409                return false;
     6410        }
     6411
     6412        // Already there, or this does nothing. See the note above: the ability is
     6413        // borrowed from the upgrade in flight and never asked for.
     6414        global $wp_filesystem;
     6415        if ( ! $wp_filesystem || ! function_exists( 'move_dir' ) ) {
     6416                return false;
     6417        }
     6418
     6419        /**
     6420         * Both paths off the filesystem object, rather than off `WP_PLUGIN_DIR`.
     6421         *
     6422         * WordPress.org's own Plugin Check fails `move_dir()` on the same line as
     6423         * that constant -- `PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite`,
     6424         * an ERROR rather than a warning -- and it is right to: plugins used to keep
     6425         * a customer's data in their own folder and lose it on every upgrade, which
     6426         * is what that rule exists to stop. Putting the plugin's own folder back is
     6427         * the one write there that is not that, and the detector cannot tell the
     6428         * difference, so the path is taken from the object core itself hands this
     6429         * code instead of silencing a check somebody else would copy.
     6430         *
     6431         * `move_dir` rather than `$wp_filesystem->move`, which is what it calls
     6432         * first: it adds the overwrite handling, a `copy_dir` fallback where a
     6433         * rename cannot cross the two directories, and -- the one that matters on a
     6434         * folder full of PHP that was just swapped -- `wp_opcache_invalidate_directory`.
     6435         * Core uses it for both halves of this, moving the folder out and putting it
     6436         * back (`class-wp-upgrader.php`).
     6437         */
     6438        $source      = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/plugins/' . $slug;
     6439        $destination = $wp_filesystem->wp_plugins_dir() . $slug;
     6440        if ( ! $wp_filesystem->is_dir( $source ) || $wp_filesystem->exists( $destination ) ) {
     6441                return false;
     6442        }
     6443
     6444        $moved = move_dir( $source, $destination, true );
     6445
     6446        // Looked at on disk rather than taken from the answer above, which is the
     6447        // rule the whole route is written to: the file is what decides.
     6448        return ! is_wp_error( $moved ) && false !== $moved && $wp_filesystem->exists( $destination . '/' . basename( $file ) );
     6449}
     6450
     6451/**
     6452 * Switch the plugin back on, where the upgrade left it off.
     6453 *
     6454 * -- Why a restored folder is not yet a working site ----------------------
     6455 *
     6456 * Core deactivates a plugin before upgrading it over REST, and the route
     6457 * switches it back on at the end. A process that dies in the middle never
     6458 * reaches that line, so the folder being back on disk leaves the site with
     6459 * Marcelo installed and switched off -- which, to every screen and every Fix,
     6460 * is identical to the plugin being gone.
     6461 *
     6462 * Measured rather than reasoned, 24 October 2026: with the folder restored and
     6463 * this missing, `/wp-json/marcelo/v1/status` still answered `rest_no_route`
     6464 * and `wp plugin list` still said `inactive`. The repair was half a repair.
     6465 *
     6466 * Silently, because the activation hooks are not something to start running
     6467 * inside a shutdown function on a process that is already ending. What is
     6468 * wanted is the one fact a screen reads -- that the plugin is in
     6469 * `active_plugins` -- and nothing else.
     6470 */
     6471function marcelo_sc_switch_the_plugin_back_on(): bool {
     6472        $file = marcelo_sc_plugin_file();
     6473        if ( ! file_exists( WP_PLUGIN_DIR . '/' . $file ) ) {
     6474                return false;
     6475        }
     6476
     6477        require_once ABSPATH . 'wp-admin/includes/plugin.php';
     6478        if ( is_plugin_active( $file ) ) {
     6479                return false;
     6480        }
     6481
     6482        return ! is_wp_error( activate_plugin( $file, '', false, true ) );
     6483}
     6484
     6485/**
     6486 * The last thing this process does, where it is dying in the middle of the
     6487 * upgrade.
     6488 *
     6489 * Registered before the upgrade starts and disarmed the moment it returns, so
     6490 * an ordinary request that merely happens to end carries nothing at all. A
     6491 * fatal error and `max_execution_time` both run shutdown functions, and that
     6492 * is the population this catches; a process the host kills outright runs
     6493 * nothing, which is what `ignore_user_abort` and `set_time_limit` are for.
     6494 *
     6495 * Both halves, in this order, because they are two different deaths. A process
     6496 * that died before the copy has no folder and needs one put back; one that
     6497 * died after the copy has the new folder and is switched off. Either alone
     6498 * leaves a site on which nothing Marcelo does works.
     6499 */
     6500function marcelo_sc_restore_if_the_upgrade_died(): void {
     6501        if ( ! marcelo_sc_upgrade_in_flight() ) {
     6502                return;
     6503        }
     6504        marcelo_sc_upgrade_in_flight( false );
     6505        marcelo_sc_put_the_plugin_back();
     6506        marcelo_sc_switch_the_plugin_back_on();
     6507}
     6508
    63276509function marcelo_sc_rest_update_self( WP_REST_Request $request ) {
    63286510        unset( $request );
    … …  
    63416523        require_once ABSPATH . 'wp-admin/includes/update.php';
    63426524        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
     6525
     6526        /**
     6527         * The whole of this finishes whatever happens to the connection it arrived
     6528         * on, and these two lines are the first thing in it.
     6529         *
     6530         * Marcelo's own fetch used to hang up after forty seconds, and on LiteSpeed
     6531         * a client hanging up takes the PHP process with it. Core had by then moved
     6532         * the installed folder out of the way and not yet copied the new one in, so
     6533         * the site was left with no plugin -- the incident
     6534         * `marcelo_sc_put_the_plugin_back` records in full. Neither half of this is
     6535         * about being slow: it is about the work being finishable by a process
     6536         * nobody is listening to any more.
     6537         *
     6538         * At the top rather than beside the upgrade, because `wp_update_plugins()`
     6539         * below asks api.wordpress.org about **every** plugin on the site, and a
     6540         * process killed there has done nothing yet but has also answered nobody.
     6541         * Five minutes rather than PHP's default thirty seconds for the same
     6542         * reason: the download, the unpack and the copy are a customer's shared
     6543         * hosting doing real work, and being killed half way through it is exactly
     6544         * the failure the hold on automatic updates was put there to stop happening
     6545         * again.
     6546         */
     6547        ignore_user_abort( true );
     6548        if ( function_exists( 'set_time_limit' ) ) {
     6549                set_time_limit( 300 );
     6550        }
    63436551
    63446552        $before = marcelo_sc_version_on_disk();
    … …  
    63776585        marcelo_sc_offer_the_published_build( $before );
    63786586
     6587        register_shutdown_function( 'marcelo_sc_restore_if_the_upgrade_died' );
     6588        marcelo_sc_upgrade_in_flight( true );
     6589
    63796590        $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
    63806591        $outcome  = $upgrader->upgrade( marcelo_sc_plugin_file() );
    63816592
     6593        marcelo_sc_upgrade_in_flight( false );
     6594
     6595        /**
     6596         * Looked at before anything is reported, on every path out of here.
     6597         *
     6598         * Core restores its own backup where the copy answered a `WP_Error`, so on
     6599         * the ordinary failure this finds the folder in place and does nothing. It
     6600         * is the paths core does not reach that this is for, and a site left
     6601         * without the plugin cannot be told so by the plugin.
     6602         */
     6603        $restored = marcelo_sc_put_the_plugin_back();
     6604
    63826605        if ( is_wp_error( $outcome ) ) {
     6606                $said = $outcome->get_error_message();
    63836607                return new WP_Error(
    63846608                        'marcelo_sc_update_failed',
    6385                         $outcome->get_error_message(),
     6609                        $restored
     6610                                ? trim( $said . ' ' . __( 'The build this site had before has been put back, so the site is working.', 'marcelo-site-connector' ) )
     6611                                : $said,
    63866612                        array( 'status' => 500 )
    63876613                );
    … …  
    64236649                        'changed' => $before !== $after,
    64246650                        'active'  => $active,
     6651                        // True only where the upgrade had already taken the folder off disk
     6652                        // and this put it back, which is worth saying out loud in a log.
     6653                        'restored' => $restored,
    64256654                ),
    64266655                200
  • marcelo-site-connector/tags/1.15.26/readme.txt

    r3763848 r3764245  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.15.25
     7Stable tag: 1.15.26
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    114114== Changelog ==
    115115
     116= 1.15.26 =
     117* An update that cannot finish now leaves the plugin working: it is no longer stopped by the browser closing the connection or by the server's time limit, and where it ends part-way through, the build that was installed before is put back and switched on again.
     118
    116119= 1.15.25 =
    117120* Menus are read the way a browser reads them across every popular theme and builder: a new page's line now reaches an ElementsKit menu, a heading with no link of its own (Beaver Builder, Kadence Blocks) or drawn as a button (block themes) opens its section's page, links written without quotes are read, Blocksy and Max Mega Menu columns keep their pages and get a "See more" line, and a builder's template panel under a heading is left whole instead of emptied.
  • marcelo-site-connector/trunk/marcelo-site-connector.php

    r3763848 r3764245  
    33 * Plugin Name:       Marcelo Site Connector
    44 * Description:       Lets Marcelo add the verification and measurement tags a site needs — Search Console, Analytics, Tag Manager, the Google Ads conversion tag — send addresses that no longer exist to the page that replaced them, give a page the search-result description it is missing, give a page the search-result title it is missing or is sharing with another page, edit the text of a page built with Elementor, put a tap-to-call bar in front of visitors, link the profiles a business has confirmed are theirs from every page, say on every page that a business answers around the clock where its confirmed hours say exactly that, let a named crawler back in where robots.txt was turning it away, point a page's canonical back at itself where it names another website, add a translated page's own link among its hreflang alternates where it is missing, and shrink an oversized image in the site's own media library while keeping its original, without anyone editing theme files by hand.
    5  * Version:           1.15.25
     5 * Version:           1.15.26
    66 * Requires at least: 6.0
    77 * Requires PHP:      7.4
    … …  
    123123 */
    124124const MARCELO_SC_HREFLANG_OPTION     = 'marcelo_site_connector_hreflang';
    125 const MARCELO_SC_VERSION             = '1.15.25';
     125const MARCELO_SC_VERSION             = '1.15.26';
    126126
    127127/**
    … …  
    63256325}
    63266326
     6327/**
     6328 * Whether the upgrade below is running right now, so the shutdown handler
     6329 * knows whether a missing plugin folder is its business.
     6330 *
     6331 * A static rather than a global: the only two callers are the route and the
     6332 * handler it registers, and nothing else on a customer's site has any reason
     6333 * to be able to set it.
     6334 */
     6335function marcelo_sc_upgrade_in_flight( ?bool $set = null ): bool {
     6336        static $in_flight = false;
     6337        if ( null !== $set ) {
     6338                $in_flight = $set;
     6339        }
     6340        return $in_flight;
     6341}
     6342
     6343/**
     6344 * Put this plugin's own folder back where core moved it out of the way.
     6345 *
     6346 * -- The incident this exists for ------------------------------------------
     6347 *
     6348 * 24 October 2026, SOS Restoration Of San Francisco, on Hostinger behind
     6349 * LiteSpeed: the update that ran when Connected accounts opened left no plugin
     6350 * on the site at all. `/wp-json/marcelo/v1/status` answered `rest_no_route`
     6351 * and the files were gone, until somebody installed it again from the
     6352 * directory. Every Marcelo Fix on that site was dead in between, and the
     6353 * owner's answer was to stop every automatic update -- the app's own
     6354 * `PLUGIN_SELF_UPDATE_HELD`.
     6355 *
     6356 * -- Why a half-finished upgrade can leave nothing behind -------------------
     6357 *
     6358 * Since WordPress 6.3 `WP_Upgrader::run()` moves the installed folder to
     6359 * `wp-content/upgrade-temp-backup/plugins/<slug>` **before** copying the new
     6360 * one in, and restores it only where the copy comes back a `WP_Error`. A PHP
     6361 * process that stops existing between the move and the copy hands no error to
     6362 * anybody, so nothing restores anything: the folder is sitting in the backup
     6363 * directory and the plugins directory has no row for it.
     6364 *
     6365 * The two things that make a process stop existing there are a client that
     6366 * hung up -- Marcelo's own fetch, which used to give up after forty seconds --
     6367 * and `max_execution_time`. `ignore_user_abort` and `set_time_limit` in the
     6368 * route take both away. This is the floor under them: where it happened
     6369 * anyway, the folder core moved is still on disk, and moving it back is the
     6370 * whole of the repair.
     6371 *
     6372 * -- What it refuses to touch ----------------------------------------------
     6373 *
     6374 * Only this plugin's own slug, only out of core's own backup directory, and
     6375 * only while the plugins directory has nothing under that name at all. A
     6376 * folder already there is a half-written copy of the new build, which is not
     6377 * something to move a backup on top of -- and it is the one case where the
     6378 * destination holds files somebody may still want.
     6379 *
     6380 * Neither path is an argument. One is `WP_CONTENT_DIR` and core's own fixed
     6381 * backup directory, the other is `WP_PLUGIN_DIR`, and the slug between them is
     6382 * `plugin_basename( __FILE__ )` with anything carrying a separator refused.
     6383 *
     6384 * -- And it acquires nothing ------------------------------------------------
     6385 *
     6386 * `$wp_filesystem` has to be there already, set up by the upgrade this is the
     6387 * tail of. It is deliberately not asked for: `apps/web/src/lib/plugin-promises.test.ts`
     6388 * holds this plugin to having **no way** to write a file outside a customer's
     6389 * own uploads -- the whole general-purpose toolbox absent rather than merely
     6390 * unused -- because the screen a customer approves a fix on tells them nothing
     6391 * already on their site is edited, moved or rewritten. Calling `WP_Filesystem()`
     6392 * here would hand the plugin that ability for every request it ever serves, to
     6393 * repair a folder in the one request that was already holding it. So this can
     6394 * act only inside an upgrade that is in flight, which is the only moment it is
     6395 * for, and the promise is unchanged.
     6396 *
     6397 * @return bool Whether a folder was put back.
     6398 */
     6399function marcelo_sc_put_the_plugin_back(): bool {
     6400        $file = marcelo_sc_plugin_file();
     6401        if ( file_exists( WP_PLUGIN_DIR . '/' . $file ) ) {
     6402                return false;
     6403        }
     6404
     6405        $slug = dirname( $file );
     6406        if ( '' === $slug || '.' === $slug || false !== strpos( $slug, '/' ) ) {
     6407                // A single-file plugin has no folder to move, and anything carrying a
     6408                // separator is not a slug this may act on.
     6409                return false;
     6410        }
     6411
     6412        // Already there, or this does nothing. See the note above: the ability is
     6413        // borrowed from the upgrade in flight and never asked for.
     6414        global $wp_filesystem;
     6415        if ( ! $wp_filesystem || ! function_exists( 'move_dir' ) ) {
     6416                return false;
     6417        }
     6418
     6419        /**
     6420         * Both paths off the filesystem object, rather than off `WP_PLUGIN_DIR`.
     6421         *
     6422         * WordPress.org's own Plugin Check fails `move_dir()` on the same line as
     6423         * that constant -- `PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite`,
     6424         * an ERROR rather than a warning -- and it is right to: plugins used to keep
     6425         * a customer's data in their own folder and lose it on every upgrade, which
     6426         * is what that rule exists to stop. Putting the plugin's own folder back is
     6427         * the one write there that is not that, and the detector cannot tell the
     6428         * difference, so the path is taken from the object core itself hands this
     6429         * code instead of silencing a check somebody else would copy.
     6430         *
     6431         * `move_dir` rather than `$wp_filesystem->move`, which is what it calls
     6432         * first: it adds the overwrite handling, a `copy_dir` fallback where a
     6433         * rename cannot cross the two directories, and -- the one that matters on a
     6434         * folder full of PHP that was just swapped -- `wp_opcache_invalidate_directory`.
     6435         * Core uses it for both halves of this, moving the folder out and putting it
     6436         * back (`class-wp-upgrader.php`).
     6437         */
     6438        $source      = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/plugins/' . $slug;
     6439        $destination = $wp_filesystem->wp_plugins_dir() . $slug;
     6440        if ( ! $wp_filesystem->is_dir( $source ) || $wp_filesystem->exists( $destination ) ) {
     6441                return false;
     6442        }
     6443
     6444        $moved = move_dir( $source, $destination, true );
     6445
     6446        // Looked at on disk rather than taken from the answer above, which is the
     6447        // rule the whole route is written to: the file is what decides.
     6448        return ! is_wp_error( $moved ) && false !== $moved && $wp_filesystem->exists( $destination . '/' . basename( $file ) );
     6449}
     6450
     6451/**
     6452 * Switch the plugin back on, where the upgrade left it off.
     6453 *
     6454 * -- Why a restored folder is not yet a working site ----------------------
     6455 *
     6456 * Core deactivates a plugin before upgrading it over REST, and the route
     6457 * switches it back on at the end. A process that dies in the middle never
     6458 * reaches that line, so the folder being back on disk leaves the site with
     6459 * Marcelo installed and switched off -- which, to every screen and every Fix,
     6460 * is identical to the plugin being gone.
     6461 *
     6462 * Measured rather than reasoned, 24 October 2026: with the folder restored and
     6463 * this missing, `/wp-json/marcelo/v1/status` still answered `rest_no_route`
     6464 * and `wp plugin list` still said `inactive`. The repair was half a repair.
     6465 *
     6466 * Silently, because the activation hooks are not something to start running
     6467 * inside a shutdown function on a process that is already ending. What is
     6468 * wanted is the one fact a screen reads -- that the plugin is in
     6469 * `active_plugins` -- and nothing else.
     6470 */
     6471function marcelo_sc_switch_the_plugin_back_on(): bool {
     6472        $file = marcelo_sc_plugin_file();
     6473        if ( ! file_exists( WP_PLUGIN_DIR . '/' . $file ) ) {
     6474                return false;
     6475        }
     6476
     6477        require_once ABSPATH . 'wp-admin/includes/plugin.php';
     6478        if ( is_plugin_active( $file ) ) {
     6479                return false;
     6480        }
     6481
     6482        return ! is_wp_error( activate_plugin( $file, '', false, true ) );
     6483}
     6484
     6485/**
     6486 * The last thing this process does, where it is dying in the middle of the
     6487 * upgrade.
     6488 *
     6489 * Registered before the upgrade starts and disarmed the moment it returns, so
     6490 * an ordinary request that merely happens to end carries nothing at all. A
     6491 * fatal error and `max_execution_time` both run shutdown functions, and that
     6492 * is the population this catches; a process the host kills outright runs
     6493 * nothing, which is what `ignore_user_abort` and `set_time_limit` are for.
     6494 *
     6495 * Both halves, in this order, because they are two different deaths. A process
     6496 * that died before the copy has no folder and needs one put back; one that
     6497 * died after the copy has the new folder and is switched off. Either alone
     6498 * leaves a site on which nothing Marcelo does works.
     6499 */
     6500function marcelo_sc_restore_if_the_upgrade_died(): void {
     6501        if ( ! marcelo_sc_upgrade_in_flight() ) {
     6502                return;
     6503        }
     6504        marcelo_sc_upgrade_in_flight( false );
     6505        marcelo_sc_put_the_plugin_back();
     6506        marcelo_sc_switch_the_plugin_back_on();
     6507}
     6508
    63276509function marcelo_sc_rest_update_self( WP_REST_Request $request ) {
    63286510        unset( $request );
    … …  
    63416523        require_once ABSPATH . 'wp-admin/includes/update.php';
    63426524        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
     6525
     6526        /**
     6527         * The whole of this finishes whatever happens to the connection it arrived
     6528         * on, and these two lines are the first thing in it.
     6529         *
     6530         * Marcelo's own fetch used to hang up after forty seconds, and on LiteSpeed
     6531         * a client hanging up takes the PHP process with it. Core had by then moved
     6532         * the installed folder out of the way and not yet copied the new one in, so
     6533         * the site was left with no plugin -- the incident
     6534         * `marcelo_sc_put_the_plugin_back` records in full. Neither half of this is
     6535         * about being slow: it is about the work being finishable by a process
     6536         * nobody is listening to any more.
     6537         *
     6538         * At the top rather than beside the upgrade, because `wp_update_plugins()`
     6539         * below asks api.wordpress.org about **every** plugin on the site, and a
     6540         * process killed there has done nothing yet but has also answered nobody.
     6541         * Five minutes rather than PHP's default thirty seconds for the same
     6542         * reason: the download, the unpack and the copy are a customer's shared
     6543         * hosting doing real work, and being killed half way through it is exactly
     6544         * the failure the hold on automatic updates was put there to stop happening
     6545         * again.
     6546         */
     6547        ignore_user_abort( true );
     6548        if ( function_exists( 'set_time_limit' ) ) {
     6549                set_time_limit( 300 );
     6550        }
    63436551
    63446552        $before = marcelo_sc_version_on_disk();
    … …  
    63776585        marcelo_sc_offer_the_published_build( $before );
    63786586
     6587        register_shutdown_function( 'marcelo_sc_restore_if_the_upgrade_died' );
     6588        marcelo_sc_upgrade_in_flight( true );
     6589
    63796590        $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
    63806591        $outcome  = $upgrader->upgrade( marcelo_sc_plugin_file() );
    63816592
     6593        marcelo_sc_upgrade_in_flight( false );
     6594
     6595        /**
     6596         * Looked at before anything is reported, on every path out of here.
     6597         *
     6598         * Core restores its own backup where the copy answered a `WP_Error`, so on
     6599         * the ordinary failure this finds the folder in place and does nothing. It
     6600         * is the paths core does not reach that this is for, and a site left
     6601         * without the plugin cannot be told so by the plugin.
     6602         */
     6603        $restored = marcelo_sc_put_the_plugin_back();
     6604
    63826605        if ( is_wp_error( $outcome ) ) {
     6606                $said = $outcome->get_error_message();
    63836607                return new WP_Error(
    63846608                        'marcelo_sc_update_failed',
    6385                         $outcome->get_error_message(),
     6609                        $restored
     6610                                ? trim( $said . ' ' . __( 'The build this site had before has been put back, so the site is working.', 'marcelo-site-connector' ) )
     6611                                : $said,
    63866612                        array( 'status' => 500 )
    63876613                );
    … …  
    64236649                        'changed' => $before !== $after,
    64246650                        'active'  => $active,
     6651                        // True only where the upgrade had already taken the folder off disk
     6652                        // and this put it back, which is worth saying out loud in a log.
     6653                        'restored' => $restored,
    64256654                ),
    64266655                200
  • marcelo-site-connector/trunk/readme.txt

    r3763848 r3764245  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.15.25
     7Stable tag: 1.15.26
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    114114== Changelog ==
    115115
     116= 1.15.26 =
     117* An update that cannot finish now leaves the plugin working: it is no longer stopped by the browser closing the connection or by the server's time limit, and where it ends part-way through, the build that was installed before is put back and switched on again.
     118
    116119= 1.15.25 =
    117120* Menus are read the way a browser reads them across every popular theme and builder: a new page's line now reaches an ElementsKit menu, a heading with no link of its own (Beaver Builder, Kadence Blocks) or drawn as a button (block themes) opens its section's page, links written without quotes are read, Blocksy and Max Mega Menu columns keep their pages and get a "See more" line, and a builder's template panel under a heading is left whole instead of emptied.
Note: See TracChangeset for help on using the changeset viewer.