Make WordPress Core

Opened 6 years ago

Last modified 4 years ago

#45331 new defect (bug)

'rest_url_prefix' filter fails to impact flush_rewrite_rules() on plugin activation

Reported by: kestutisit's profile KestutisIT Owned by:
Milestone: Awaiting Review Priority: normal
Severity: major Version: 4.9.8
Component: Permalinks Keywords: needs-patch ux-feedback reporter-feedback
Focuses: administration, rest-api Cc:

Description

'rest_url_prefix' filter fails to impact flush_rewrite_rules() on plugin activation
=========================================================================

So here is the bug - 'flush_rewrite_rules' is called on plugin activation after 'rest_url_prefix' filter added with new API ENDPOINT,
but if I go to '<MY-DOMAIN>/<NEW-ENDPOINT>/' it gives me 404 error. And if I access '<MY-DOMAIN>/wp-json/' on Firefox Developer Edition in 'Header' section I see:
Link <http://<MY-DOMAIN>/<NEW-ENDPOINT>/>; rel="https://api.w.org/"
So the header is correct here. And it will fix the issue if I go to WP Settings -> Permalinks -> Save.
But that is a bug. As you won't have to instruct that to your plugin user, after his activation he will see that API is not working.

Install Controller class - 'flush_rewrite_rules' is called on plugin activation after 'rest_url_prefix' filter added with new API ENDPOINT:

<?php
namespace GreatestEverManager\Controllers\Admin;
final class InstallController
{
    <...>

    public function setCustomWP_RestAPI_Prefix()
    {
        // NOTE: Do not forget to do the same on install with flush_rewrite_rules(); after it.
        add_filter('rest_url_prefix', function() { return ConfigurationInterface::WP_REST_API_PREFIX; }, 10, 1);

        // NOTE: As there is no custom post types or custom taxonomies registration later, we perform rewrite rules flush right now
        flush_rewrite_rules();
    }
        
    <...>
}


Main Controller class:

<?php
namespace GreatestEverManager\Controllers;
use SoftwareLicenseManager\Models\Configuration\ConfigurationInterface;
<...>

final class MainController
{
    <...>
    public function __construct(ConfigurationInterface $paramConfWithoutRouting)
    {
        <...>
        if(!is_null($this->confWithoutRouting))
        {
            register_activation_hook($this->confWithoutRouting->getPluginPathWithFilename(), array(&$this, 'networkOrSingleActivate'));
            register_deactivation_hook($this->confWithoutRouting->getPluginPathWithFilename(), array(&$this, 'networkDeactivate'));
            <...>
        }
    }

    /**
     * Activate (enable+install or enable only) plugin for across the whole network
     * @note - 'get_sites' function requires WordPress 4.6 or newer!
     */
    public function networkOrSingleActivate()
    {
        if(is_multisite())
        {
            // A workaround until WP will get fixed
            // SHOULD be 'networkActivate' but WordPress does not yet support that feature,
            // so this means as long as the 'MULTISITE' constant is defined in wp-config, we use that method
            $this->multisiteActivate();
        } else
        {
            // A workaround until WP will get fixed
            $this->activate();
        }
    }

    public function activate()
    {
        try
        {
            <...>
            // Install plugin for single site
            $objInstaller = new \GreatestEverManager\Controllers\Admin\InstallController($conf, $lang, $conf->getBlogId());
            // Install
            <...>
            $objInstaller->setCustomWP_RestAPI_Prefix();
            <...>
        } catch (\Exception $e)
        {
            if(StaticValidator::inWPDebug())
            {
                // In WP activation we can kill the install only via 'trigger_error' with 'E_USER_ERROR' param
                $error = sprintf(static::LANG_ERROR_IN_METHOD_TEXT, __FUNCTION__, $e->getMessage());
                trigger_error($error, E_USER_ERROR);
            }
        }
    }

    public function run()
    {
        if($this->canProcess)
        {
            <...>
            add_filter('rest_url_prefix', function() { return ConfigurationInterface::WP_REST_API_PREFIX; }, 10, 1);
            add_action('rest_api_init', array(&$this, 'frontEndAPI_Callback'), 0);
            <...>
        }
    }
    <...>
}


Plugin main file (wp-content/plugins/GreatestEverManager/GreatestEverManager.php):

<?php
/**
 * Plugin Name: Greatest Ever Manager
 * <...>
 */
namespace GreatestEverManager;

require_once 'Models/Configuration/ConfigurationInterface.php';
require_once 'Models/Configuration/Configuration.php';
require_once 'Controllers/MainController.php';
<...>

use GreatestEverManager\Models\Configuration\Configuration;
use GreatestEverManager\Controllers\MainController;

if(!class_exists('GreatestEverManager\GreatestEverManager'))
{
    final class GreatestEverManager
    {
        // Configuration
        const REQUIRED_PHP_VERSION = '5.4.0';
        const REQUIRED_WP_VERSION = 4.6;
        const OLDEST_COMPATIBLE_PLUGIN_VERSION = 6.0;
        const PLUGIN_VERSION = 6.0;

        // Settings
        private static $params = array(
            'plugin_id' => 0,
            'plugin_prefix' => 'greatest_ever_manager_',
            'plugin_api_namespace' => 'gem/v1',
            'plugin_handle_prefix' => 'greatest-ever-manager-',
            <...>
        );
        private static $objConfiguration = NULL;
        private static $objMainController = NULL;
        <...>

        /**
         * @return Configuration
         */
        public static function getConfiguration()
        {
            if(is_null(static::$objConfiguration) || !(static::$objConfiguration instanceof Configuration))
            {
                // Create an instance of plugin configuration model
                static::$objConfiguration = new Configuration(
                    $GLOBALS['wpdb'],
                    get_current_blog_id(),
                    static::REQUIRED_PHP_VERSION, phpversion(),
                    static::REQUIRED_WP_VERSION, $GLOBALS['wp_version'],
                    static::OLDEST_COMPATIBLE_PLUGIN_VERSION, static::PLUGIN_VERSION,
                    __FILE__,
                    static::$params
                );
            }
            return static::$objConfiguration;
        }

        /**
         * Creates new or returns existing instance of plugin main controller
         * @return MainController
         */
        public static function getMainController()
        {
            if(is_null(static::$objMainController) || !(static::$objMainController instanceof MainController))
            {
                // NOTE: This is not passing by reference!
                static::$objMainController = new MainController(static::getConfiguration());
            }

            return static::$objMainController;
        }

        <...>
    }

    <...>

    // Run the plugin
    GreatestEverManager::getMainController()->run();
}


The coding pattern is S.O.L.I.D. MVC, Version 6, based on PSR-4 Autoloaders and PSR-2 Coding Standards.
To deeply inspect load process (without the REST_API part), you can inspect 'Expadandable FAQ' - SolidMVC boiler-plate plugin:
https://wordpress.org/plugins/expandable-faq/

Change History (3)

#1 @jrf
6 years ago

  • Focuses coding-standards removed

This ticket was mentioned in Slack in #core-restapi by desrosj. View the logs.


6 years ago

#3 @TimothyBlynJacobs
4 years ago

  • Keywords reporter-feedback added

AFAICT this happens because the rewrite rules have already been registered when a plugin is activated for the first time. This type of timing issue is common in WordPress. I would recommend running flush_rewrite_rules the next page load after your plugin is installed.

If that is incorrect, please provide a minimal, standalone plugin that demonstrates the issue.

Note: See TracTickets for help on using tickets.