No Code Attached Yet
avatar nikosdion
nikosdion
25 Dec 2022

Merry Christmas! This year Santa Claus brought me an esoteric bug in the Joomla API application.

Tagging @wilsonge as he's the only contributor of the code in play, therefore the only one who will understand what I am talking about.

The build-up to the bug

If you want to tell the Joomla API Application to provide a public API which handles requests with a different Accept header than the Joomla default of application/vnd.api+json you need to have the following in your webservices plugin:

$defaults = [
 'component' => 'com_example',
 'public' => true,
 'format' => [
  'application/ld+json',
  'application/json'
 ]
];

$router->addRoute(
  new Route(
    ['GET'], 'v1/example/whatever/:id', 'whatever.displayItem', ['id' => '[\d]+'], $defaults
  )
);

The format in the parameters is used in the ApiApplication::route() method to override the priorities of the Negotiator object:

$priorities = array('application/vnd.api+json');
if (!$caught404 && \array_key_exists('format', $route['vars'])) {
$priorities = $route['vars']['format'];
}
$negotiator = new Negotiator();

This works GREAT!

It works so well that once the negotiator settles for one of the provided priorities, ApiApplication correctly sets it as the negotiated content type:

/** @var $mediaType Accept */
$format = $mediaType->getValue();
if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) {
$format = $this->formatMapper[$mediaType->getValue()];
}
$this->input->set('format', $format);

This also works GREAT!

But.

The bug

Right away, Joomla overwrites the negotiated content type with the entire array I had to pass through my plugin ?

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);
}
}
}

Remember, $route['vars'] has everything I passed in the $default of my plugin. I had to pass an array keyed format with all my valid Accept headers, right? The Joomla API application uses the a request parameter called format of expected type string to figure out the content type and from there the Joomla Document type, right? The two things are named the same, have different expected data types (array vs string) which means that I get an immediate PHP error: Argument #1 ($type) must be of type string, array given

Right now you have to use the onAfterApiRoute plugin event handler to run the Negotiator AGAIN and set the format input variable AGAIN to work around the Joomla issue.

Solution ??

Add the following after line 284:

if ($key === 'format') {
  continue;
}
avatar nikosdion nikosdion - open - 25 Dec 2022
avatar joomla-cms-bot joomla-cms-bot - change - 25 Dec 2022
Labels Added: No Code Attached Yet
avatar joomla-cms-bot joomla-cms-bot - labeled - 25 Dec 2022
avatar wilsonge
wilsonge - comment - 26 Dec 2022

Well bugger :D been a bit sick last few days so might take me another day or two to get the PR in but I guess may as well skip all 4 input objects we set directly above for good measure. Glad to know it works otherwise tho :)

avatar nikosdion
nikosdion - comment - 26 Dec 2022

Oh, man, get well soon! It's nothing urgent. This will probably take months to complete. Once it's done you're gonna be surprised at what you can do with the Joomla API application ?

avatar carlitorweb
carlitorweb - comment - 26 Dec 2022

@nikosdion I wonder if is enough just set up in the onAfterApiRoute the follow:

$app->input->set('format', 'application/json');

without run the Negotiator again

avatar nikosdion
nikosdion - comment - 26 Dec 2022

@carlitorweb Not unless you want to break every other API application integration. This would have Joomla looking for JsonView in all API requests instead of JsonapiView.

You can of course check if it's your component and use domain-specific knowledge to assume a different content type for your own component's API application integration only, without running the negotiator. I am veering towards that direction, actually.

avatar carlitorweb
carlitorweb - comment - 26 Dec 2022

@nikosdion yes, sorry, I meant for a custom component, should be enough just add that.

avatar nikosdion
nikosdion - comment - 26 Dec 2022

Again, ONLY if you do that IF AND ONLY IF you have checked $input->getCmd('option') and it matches your component.

Custom component or not, the onAfterApiRoute event is called always. Let's not tell people to do something potentially stupid which will break the JSON API for everyone else. I have seen other 3PDs do… things in their plugins over the years. Give them half a malformed sentence to grip on and they'll cock up something major. I would classify this as being Joomla's fault for neither educating 3PDs, nor policing this "extension A breaks all other extensions" problem on the JED.

avatar wilsonge wilsonge - close - 26 Dec 2022
avatar wilsonge
wilsonge - comment - 26 Dec 2022

Managed to get this written up whilst I was on the plane home. Now 12 hour return home from girlfriends parents accomplished. I'm off to sleep. Apologies if there's anything dumb in the PR description but I'm sick and tired and running on fumes!

avatar wilsonge wilsonge - change - 26 Dec 2022
Status New Closed
Closed_Date 0000-00-00 00:00:00 2022-12-26 14:43:13
Closed_By wilsonge

Add a Comment

Login with GitHub to post a comment