* @license GNU General Public License version 2 or later; see LICENSE */ namespace Joomla\CMS\Application; use Joomla\Application\Web\WebClient; use Joomla\CMS\Access\Exception\AuthenticationFailed; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Router\ApiRouter; use Joomla\CMS\Router\Exception\RouteNotFoundException; use Joomla\CMS\Uri\Uri; use Joomla\DI\Container; use Joomla\Input\Json as JInputJson; use Joomla\Registry\Registry; use Negotiation\Accept; use Negotiation\Exception\InvalidArgument; use Negotiation\Negotiator; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! API Application class * * @since 4.0.0 */ final class ApiApplication extends CMSApplication { /** * Maps extension types to their * * @var array * @since 4.0.0 */ protected $formatMapper = array(); /** * The authentication plugin type * * @var string * @since 4.0.0 */ protected $authenticationPluginType = 'api-authentication'; /** * Class constructor. * * @param JInputJson $input An optional argument to provide dependency injection for the application's input * object. If the argument is a JInput object that object will become the * application's input object, otherwise a default input object is created. * @param Registry $config An optional argument to provide dependency injection for the application's config * object. If the argument is a Registry object that object will become the * application's config object, otherwise a default config object is created. * @param WebClient $client An optional argument to provide dependency injection for the application's client * object. If the argument is a WebClient object that object will become the * application's client object, otherwise a default client object is created. * @param Container $container Dependency injection container. * * @since 4.0.0 */ public function __construct(JInputJson $input = null, Registry $config = null, WebClient $client = null, Container $container = null) { // Register the application name $this->name = 'api'; // Register the client ID $this->clientId = 3; // Execute the parent constructor parent::__construct($input, $config, $client, $container); $this->addFormatMap('application/json', 'json'); $this->addFormatMap('application/vnd.api+json', 'jsonapi'); // Set the root in the URI based on the application name Uri::root(null, str_ireplace('/' . $this->getName(), '', Uri::base(true))); } /** * Method to run the application routines. * * Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly. * * @return void * * @since 4.0.0 */ protected function doExecute() { // Initialise the application $this->initialiseApp(); // Mark afterInitialise in the profiler. JDEBUG ? $this->profiler->mark('afterInitialise') : null; // Route the application $this->route(); // Mark afterApiRoute in the profiler. JDEBUG ? $this->profiler->mark('afterApiRoute') : null; // Dispatch the application $this->dispatch(); // Mark afterDispatch in the profiler. JDEBUG ? $this->profiler->mark('afterDispatch') : null; } /** * Adds a mapping from a content type to the format stored. Note the format type cannot be overwritten. * * @param string $contentHeader The content header * @param string $format The content type format * * @return void * * @since 4.0.0 */ public function addFormatMap($contentHeader, $format) { if (!\array_key_exists($contentHeader, $this->formatMapper)) { $this->formatMapper[$contentHeader] = $format; } } /** * Rendering is the process of pushing the document buffers into the template * placeholders, retrieving data from the document and pushing it into * the application response buffer. * * @return void * * @since 4.0.0 * * @note Rendering should be overridden to get rid of the theme files. */ protected function render() { // Render the document $this->setBody($this->document->render($this->allowCache())); } /** * Method to send the application response to the client. All headers will be sent prior to the main application output data. * * @param array $options An optional argument to enable CORS. (Temporary) * * @return void * * @since 4.0.0 */ protected function respond($options = array()) { // Set the Joomla! API signature $this->setHeader('X-Powered-By', 'JoomlaAPI/1.0', true); $forceCORS = (int) $this->get('cors'); if ($forceCORS) { /** * Enable CORS (Cross-origin resource sharing) * Obtain allowed CORS origin from Global Settings. * Set to * (=all) if not set. */ $allowedOrigin = $this->get('cors_allow_origin', '*'); $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin, true); $this->setHeader('Access-Control-Allow-Headers', 'Authorization'); if ($this->input->server->getString('HTTP_ORIGIN', null) !== null) { $this->setHeader('Access-Control-Allow-Origin', $this->input->server->getString('HTTP_ORIGIN'), true); $this->setHeader('Access-Control-Allow-Credentials', 'true', true); } } // Parent function can be overridden later on for debugging. parent::respond(); } /** * Gets the name of the current template. * * @param boolean $params True to return the template parameters * * @return string|\stdClass * * @since 4.0.0 */ public function getTemplate($params = false) { // The API application should not need to use a template if ($params) { $template = new \stdClass(); $template->template = 'system'; $template->params = new Registry(); $template->inheritable = 0; $template->parent = ''; return $template; } return 'system'; } /** * Route the application. * * Routing is the process of examining the request environment to determine which * component should receive the request. The component optional parameters * are then set in the request object to be processed when the application is being * dispatched. * * @return void * * @since 4.0.0 */ protected function route() { $router = $this->getContainer()->get(ApiRouter::class); // Trigger the onBeforeApiRoute event. PluginHelper::importPlugin('webservices'); $this->triggerEvent('onBeforeApiRoute', array(&$router, $this)); $caught404 = false; $method = $this->input->getMethod(); try { $this->handlePreflight($method, $router); $route = $router->parseApiRoute($method); } catch (RouteNotFoundException $e) { $caught404 = true; } /** * Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't * tell us otherwise it uses the plain JSON API */ $priorities = array('application/vnd.api+json'); if (!$caught404 && \array_key_exists('format', $route['vars'])) { $priorities = $route['vars']['format']; } $negotiator = new Negotiator(); try { $mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities); } catch (InvalidArgument $e) { $mediaType = null; } // If we can't find a match bail with a 406 - Not Acceptable if ($mediaType === null) { throw new Exception\NotAcceptable('Could not match accept header', 406); } /** @var $mediaType Accept */ $format = $mediaType->getValue(); if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) { $format = $this->formatMapper[$mediaType->getValue()]; } $this->input->set('format', $format); if ($caught404) { throw $e; } $this->input->set('option', $route['vars']['component']); $this->input->set('controller', $route['controller']); $this->input->set('task', $route['task']); foreach ($route['vars'] as $key => $value) { if ($key !== 'component') { if ($this->input->getMethod() === 'POST') { $this->input->post->set($key, $value); } else { $this->input->set($key, $value); } } } $this->triggerEvent('onAfterApiRoute', array($this)); if (!isset($route['vars']['public']) || $route['vars']['public'] === false) { if (!$this->login(array('username' => ''), array('silent' => true, 'action' => 'core.login.api'))) { throw new AuthenticationFailed(); } } } /** * Handles preflight requests. * * @param String $method The REST verb * * @param ApiRouter $router The API Routing object * * @return void * * @since 4.0.0 */ protected function handlePreflight($method, $router) { /** * If not an OPTIONS request or CORS is not enabled, * there's nothing useful to do here. */ if ($method !== 'OPTIONS' || !(int) $this->get('cors')) { return; } // Extract routes matching current route from all known routes. $matchingRoutes = $router->getMatchingRoutes(); // Extract exposed methods from matching routes. $matchingRoutesMethods = array_unique( array_reduce( $matchingRoutes, function ($carry, $route) { return array_merge($carry, $route->getMethods()); }, [] ) ); /** * Obtain allowed CORS origin from Global Settings. * Set to * (=all) if not set. */ $allowedOrigin = $this->get('cors_allow_origin', '*'); /** * Obtain allowed CORS headers from Global Settings. * Set to sensible default if not set. */ $allowedHeaders = $this->get('cors_allow_headers', 'Content-Type,X-Joomla-Token'); /** * Obtain allowed CORS methods from Global Settings. * Set to methods exposed by current route if not set. */ $allowedMethods = $this->get('cors_allow_methods', implode(',', $matchingRoutesMethods)); // No use to go through the regular route handling hassle, // so let's simply output the headers and exit. $this->setHeader('status', '204'); $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin); $this->setHeader('Access-Control-Allow-Headers', $allowedHeaders); $this->setHeader('Access-Control-Allow-Methods', $allowedMethods); $this->sendHeaders(); $this->close(); } /** * Returns the application Router object. * * @return ApiRouter * * @since 4.0.0 * @deprecated 5.0 Inject the router or load it from the dependency injection container */ public function getApiRouter() { return $this->getContainer()->get(ApiRouter::class); } /** * Dispatch the application * * @param string $component The component which is being rendered. * * @return void * * @since 4.0.0 */ public function dispatch($component = null) { // Get the component if not set. if (!$component) { $component = $this->input->get('option', null); } // Load the document to the API $this->loadDocument(); // Set up the params $document = Factory::getDocument(); // Register the document object with Factory Factory::$document = $document; $contents = ComponentHelper::renderComponent($component); $document->setBuffer($contents, 'component'); // Trigger the onAfterDispatch event. PluginHelper::importPlugin('system'); $this->triggerEvent('onAfterDispatch'); } }