Namespace

Drupal\subpathauto

File

src/PathProcessor.php

View source
<?php

namespace Drupal\subpathauto;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Url;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
use Symfony\Component\HttpFoundation\Request;

/**
 * Processes the inbound path using path alias lookups.
 */
class PathProcessor implements InboundPathProcessorInterface, OutboundPathProcessorInterface {
    
    /**
     * The path processor.
     *
     * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
     */
    protected $pathProcessor;
    
    /**
     * The language manager.
     *
     * @var \Drupal\Core\Language\LanguageManagerInterface
     */
    protected $languageManager;
    
    /**
     * The config factory.
     *
     * @var \Drupal\Core\Config\ConfigFactoryInterface
     */
    protected $configFactory;
    
    /**
     * The path validator.
     *
     * @var \Drupal\Core\Path\PathValidatorInterface
     */
    protected $pathValidator;
    
    /**
     * The module handler service.
     *
     * @var \Drupal\Core\Extension\ModuleHandlerInterface
     */
    protected $moduleHandler;
    
    /**
     * Whether it is recursive call or not.
     *
     * @var bool
     */
    protected $recursiveCall;
    
    /**
     * A boolean to hold whether the redirect module is installed.
     *
     * @var bool
     */
    protected $hasRedirectModuleSupport;
    
    /**
     * Builds PathProcessor object.
     *
     * @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor
     *   The path processor.
     * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
     *   The language manager.
     * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
     *   The config factory.
     * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
     *   The module handler service.
     */
    public function __construct(InboundPathProcessorInterface $path_processor, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler = NULL) {
        $this->pathProcessor = $path_processor;
        $this->languageManager = $language_manager;
        $this->configFactory = $config_factory;
        $this->moduleHandler = $module_handler;
        if (empty($this->moduleHandler)) {
            @trigger_error('Calling PathProcessor::__construct() without the $module_handler argument is deprecated in subpathauto:8.x-1.2 and the $module_handler argument will be required in subpathauto:2.0. See https://www.drupal.org/project/subpathauto/issues/3175637', E_USER_DEPRECATED);
            $this->moduleHandler = \Drupal::service('module_handler');
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function processInbound($path, Request $request) {
        $request_path = $this->getPath($request->getPathInfo());
        // The path won't be processed if the path has been already modified by
        // a path processor (including this one), or if this is a recursive call
        // caused by ::isValidPath.
        if ($request_path !== $path || $this->recursiveCall) {
            return $path;
        }
        // Verify that the full path is not a redirect before checking its parts.
        $path = $this->checkRedirectedPath($request_path);
        $original_path = $path;
        $max_depth = $this->getMaxDepth();
        $subpath = [];
        $i = 0;
        while (($path_array = explode('/', ltrim($path, '/'))) && ($max_depth === 0 || $i < $max_depth)) {
            $i++;
            $subpath[] = array_pop($path_array);
            if (empty($path_array)) {
                break;
            }
            $path = '/' . implode('/', $path_array);
            $processed_path = $this->pathProcessor
                ->processInbound($path, $request);
            // If the path did not change, it might be that the alias is outdated.
            // Check if a redirect has been created in the meantime and if.
            if ($processed_path === $path) {
                $processed_path = $this->checkRedirectedPath($path);
                if ($path !== $processed_path) {
                    // Path $path is a redirect.
                    $processed_path = $this->pathProcessor
                        ->processInbound($processed_path, $request);
                }
            }
            if ($processed_path !== $path) {
                $path = $processed_path . '/' . implode('/', array_reverse($subpath));
                // Ensure that the path generated is valid. Call ::isValidPath to
                // trigger path processors one more time to ensure proposed new path is
                // valid. Since this method has generated the path, it should ignore all
                // recursive calls made for this method.
                $valid_path = $this->isValidPath($path);
                // Use generated path if it's valid, otherwise give up and return
                // original path to give other path processors chance to make their
                // modifications for the path.
                if ($valid_path) {
                    return $this->pathProcessor
                        ->processInbound($path, $request);
                }
                break;
            }
        }
        return $original_path;
    }
    
    /**
     * {@inheritdoc}
     */
    public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleableMetadata = NULL) {
        $original_path = $path;
        $subpath = [];
        $max_depth = $this->getMaxDepth();
        $i = 0;
        while (($path_array = explode('/', ltrim($path, '/'))) && ($max_depth === 0 || $i < $max_depth)) {
            $i++;
            $subpath[] = array_pop($path_array);
            if (empty($path_array)) {
                break;
            }
            $path = '/' . implode('/', $path_array);
            $processed_path = $this->pathProcessor
                ->processOutbound($path, $options, $request);
            if ($processed_path && $processed_path !== $path) {
                return $processed_path . '/' . implode('/', array_reverse($subpath));
            }
        }
        return $original_path;
    }
    
    /**
     * Helper function to handle multilingual paths.
     *
     * @param string $path_info
     *   Path that might contain language prefix.
     *
     * @return string
     *   Path without language prefix.
     */
    protected function getPath($path_info) {
        $prefixes = $this->getLanguageUrlPrefixes();
        $current_language_id = $this->languageManager
            ->getCurrentLanguage(LanguageInterface::TYPE_URL)
            ->getId();
        if (isset($prefixes[$current_language_id])) {
            $language_prefix = $prefixes[$current_language_id];
            $url_language_prefix = '/' . $language_prefix . '/';
            if (substr($path_info, 0, strlen($url_language_prefix)) == $url_language_prefix) {
                $path_info = '/' . substr($path_info, strlen($url_language_prefix));
            }
        }
        return rtrim(urldecode($path_info), '/');
    }
    
    /**
     * Checks if there is a redirect for the path and if so, returns the new path.
     *
     * @param string $path
     *   The path to check.
     *
     * @return string
     *   The new path.
     */
    protected function checkRedirectedPath(string $path) {
        if (!isset($this->hasRedirectModuleSupport)) {
            $this->hasRedirectModuleSupport = $this->moduleHandler
                ->moduleExists('redirect') && $this->configFactory
                ->get('subpathauto.settings')
                ->get('redirect_support');
        }
        if ($this->hasRedirectModuleSupport) {
            // Loads and check if the current path has a redirect.
            $redirects = \Drupal::service('redirect.repository')->findBySourcePath(ltrim($path, '/'));
            if (!empty($redirects)) {
                $redirect = reset($redirects)->getRedirect();
                $url = Url::fromUri($redirect['uri']);
                // If there is a redirect towards an external or unrouted source, don't
                // do anything as it's not relevant for constructing or deconstructing
                // an alias.
                if ($url->isExternal() || !$url->isRouted()) {
                    return $path;
                }
                // Return the internal path, the unaliased path of the URL.
                return '/' . $url->getInternalPath();
            }
        }
        return $path;
    }
    
    /**
     * Tests if path is valid.
     *
     * This method will call all of the path processors (including this one).
     * Sufficient protection against recursive calls is needed.
     *
     * @param string $path
     *   The path to be checked.
     *
     * @return bool
     *   Whether path is valid or not.
     */
    protected function isValidPath($path) {
        $this->recursiveCall = TRUE;
        $is_valid = (bool) $this->getPathValidator()
            ->getUrlIfValidWithoutAccessCheck($path);
        $this->recursiveCall = FALSE;
        return $is_valid;
    }
    
    /**
     * Gets the path validator.
     *
     * @return \Drupal\Core\Path\PathValidatorInterface
     *   The path validator.
     */
    protected function getPathValidator() {
        if (!$this->pathValidator) {
            $this->setPathValidator(\Drupal::service('path.validator'));
        }
        return $this->pathValidator;
    }
    
    /**
     * Sets the path validator.
     *
     * The path validator couldn't be injected to this class properly since it
     * would cause circular dependency.
     *
     * @param \Drupal\Core\Path\PathValidatorInterface $path_validator
     *   The path validator.
     *
     * @return $this
     */
    public function setPathValidator(PathValidatorInterface $path_validator) {
        $this->pathValidator = $path_validator;
        return $this;
    }
    
    /**
     * Gets the max depth that subpaths should be scanned through.
     *
     * @return int
     *   The maximum depth.
     */
    protected function getMaxDepth() {
        return $this->configFactory
            ->get('subpathauto.settings')
            ->get('depth');
    }
    
    /**
     * Language URL prefixes.
     *
     * @return array
     *   List of prefixes.
     */
    protected function getLanguageUrlPrefixes() {
        $config = $this->configFactory
            ->get('language.negotiation')
            ->get('url');
        if (isset($config['prefixes']) && $config['source'] == LanguageNegotiationUrl::CONFIG_PATH_PREFIX) {
            return $config['prefixes'];
        }
        return [];
    }

}

Classes

Title Deprecated Summary
PathProcessor Processes the inbound path using path alias lookups.