Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,7 @@ class JsonapiView extends BaseApiView
*/
public function displayList(?array $items = null)
{
foreach (FieldsHelper::getFields('com_content.categories') as $field) {
$this->fieldsToRenderList[] = $field->name;
}
$this->registerApiFields(FieldsHelper::getFields('com_content.categories'), true);

return parent::displayList();
}
Expand All @@ -123,9 +121,7 @@ public function displayList(?array $items = null)
*/
public function displayItem($item = null)
{
foreach (FieldsHelper::getFields('com_content.categories') as $field) {
$this->fieldsToRenderItem[] = $field->name;
}
$this->registerApiFields(FieldsHelper::getFields('com_content.categories'), false);

if ($item === null) {
/** @var \Joomla\CMS\MVC\Model\AdminModel $model */
Expand Down Expand Up @@ -155,9 +151,7 @@ public function displayItem($item = null)
*/
protected function prepareItem($item)
{
foreach (FieldsHelper::getFields('com_content.categories', $item, true) as $field) {
$item->{$field->name} = $field->apivalue ?? $field->rawvalue;
}
$this->assignApiFieldValues(FieldsHelper::getFields('com_content.categories', $item, true), $item);

return parent::prepareItem($item);
}
Expand Down
12 changes: 3 additions & 9 deletions api/components/com_contact/src/View/Contacts/JsonapiView.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,7 @@ public function __construct($config = [])
*/
public function displayList(?array $items = null)
{
foreach (FieldsHelper::getFields('com_contact.contact') as $field) {
$this->fieldsToRenderList[] = $field->name;
}
$this->registerApiFields(FieldsHelper::getFields('com_contact.contact'), true);

return parent::displayList();
}
Expand All @@ -154,9 +152,7 @@ public function displayList(?array $items = null)
*/
public function displayItem($item = null)
{
foreach (FieldsHelper::getFields('com_contact.contact') as $field) {
$this->fieldsToRenderItem[] = $field->name;
}
$this->registerApiFields(FieldsHelper::getFields('com_contact.contact'), false);

if (Multilanguage::isEnabled()) {
$this->fieldsToRenderItem[] = 'languageAssociations';
Expand All @@ -177,9 +173,7 @@ public function displayItem($item = null)
*/
protected function prepareItem($item)
{
foreach (FieldsHelper::getFields('com_contact.contact', $item, true) as $field) {
$item->{$field->name} = $field->apivalue ?? $field->rawvalue;
}
$this->assignApiFieldValues(FieldsHelper::getFields('com_contact.contact', $item, true), $item);

if (Multilanguage::isEnabled() && !empty($item->associations)) {
$associations = [];
Expand Down
12 changes: 3 additions & 9 deletions api/components/com_content/src/View/Articles/JsonapiView.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,7 @@ public function __construct($config = [])
*/
public function displayList(?array $items = null)
{
foreach (FieldsHelper::getFields('com_content.article') as $field) {
$this->fieldsToRenderList[] = $field->name;
}
$this->registerApiFields(FieldsHelper::getFields('com_content.article'), true);

return parent::displayList();
}
Expand All @@ -166,9 +164,7 @@ public function displayItem($item = null)
{
$this->relationship[] = 'modified_by';

foreach (FieldsHelper::getFields('com_content.article') as $field) {
$this->fieldsToRenderItem[] = $field->name;
}
$this->registerApiFields(FieldsHelper::getFields('com_content.article'), false);

if (Multilanguage::isEnabled()) {
$this->fieldsToRenderItem[] = 'languageAssociations';
Expand Down Expand Up @@ -199,9 +195,7 @@ protected function prepareItem($item)
PluginHelper::importPlugin('content');
Factory::getApplication()->triggerEvent('onContentPrepare', ['com_content.article', &$item, &$item->params]);

foreach (FieldsHelper::getFields('com_content.article', $item, true) as $field) {
$item->{$field->name} = $field->apivalue ?? $field->rawvalue;
}
$this->assignApiFieldValues(FieldsHelper::getFields('com_content.article', $item, true), $item);

if (Multilanguage::isEnabled() && !empty($item->associations)) {
$associations = [];
Expand Down
12 changes: 3 additions & 9 deletions api/components/com_users/src/View/Users/JsonapiView.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@ class JsonapiView extends BaseApiView
*/
public function displayList(?array $items = null)
{
foreach (FieldsHelper::getFields('com_users.user') as $field) {
$this->fieldsToRenderList[] = $field->name;
}
$this->registerApiFields(FieldsHelper::getFields('com_users.user'), true);

return parent::displayList();
}
Expand All @@ -95,9 +93,7 @@ public function displayList(?array $items = null)
*/
public function displayItem($item = null)
{
foreach (FieldsHelper::getFields('com_users.user') as $field) {
$this->fieldsToRenderItem[] = $field->name;
}
$this->registerApiFields(FieldsHelper::getFields('com_users.user'), false);

return parent::displayItem();
}
Expand All @@ -117,9 +113,7 @@ protected function prepareItem($item)
throw new RouteNotFoundException('Item does not exist');
}

foreach (FieldsHelper::getFields('com_users.user', $item, true) as $field) {
$item->{$field->name} = $field->apivalue ?? $field->rawvalue;
}
$this->assignApiFieldValues(FieldsHelper::getFields('com_users.user', $item, true), $item);

return parent::prepareItem($item);
}
Expand Down
57 changes: 57 additions & 0 deletions components/com_content/src/Model/CategoryModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ public function getItems()
if ($limit >= 0) {
$this->_articles = $model->getItems();

// Adjust alphabetical ordering for specific locales when needed.
$this->applyLocaleAwareOrdering();

if ($this->_articles === false) {
$this->setError($model->getError());
}
Expand All @@ -281,6 +284,60 @@ public function getItems()
return $this->_articles;
}

/**
* Apply locale-aware ordering to the loaded articles when using alphabetical
* ordering and a locale which requires special collation (e.g. Danish).
*
* @return void
*
* @since 5.4.0
*/
protected function applyLocaleAwareOrdering(): void
{
if (empty($this->_articles) || !\class_exists('\\Collator')) {
return;
}

$app = Factory::getApplication();
$language = $app->getLanguage()->getTag();

// Only apply locale-aware sorting for Danish at this stage.
if (\stripos($language, 'da-') !== 0 && $language !== 'da') {
return;
}

$params = $this->state->get('params');
$articleOrderby = $params->get('orderby_sec', 'rdate');

if (!\in_array($articleOrderby, ['alpha', 'ralpha'], true)) {
return;
}

$collator = new \Collator('da_DK');

if (!$collator) {
return;
}

$ascending = $articleOrderby === 'alpha';

\usort(
$this->_articles,
static function ($a, $b) use ($collator, $ascending) {
$titleA = $a->title ?? '';
$titleB = $b->title ?? '';

$result = $collator->compare($titleA, $titleB);

if (!$ascending) {
$result = -$result;
}

return $result;
}
);
}

/**
* Build the orderby for the query
*
Expand Down
2 changes: 2 additions & 0 deletions language/en-GB/joomla.ini
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ JERROR_SENDING_EMAIL="Email could not be sent."
JERROR_SESSION_STARTUP="Error starting the session."
JERROR_TABLE_BIND_FAILED="hmm %s …"
JERROR_USERS_PROFILE_NOT_FOUND="User profile not found"
JERROR_PREVIEW_SHARED_SESSIONS_HINT_TITLE="Article preview is not available."
JERROR_PREVIEW_SHARED_SESSIONS_HINT_BODY="To preview unpublished content from the administrator, enable \u201cShared Sessions\u201d in Global Configuration \u2192 System \u2192 Session Settings so your administrator login is shared with the frontend."

JFIELD_ACCESS_DESC="Access level for this content."
JFIELD_ACCESS_LABEL="Access"
Expand Down
83 changes: 83 additions & 0 deletions libraries/src/MVC/View/JsonApiView.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,89 @@ protected function prepareItem($item)
return $item;
}

/**
* Get the effective API field key for a custom field, using the cf_ prefix
* when it would otherwise collide with an existing property or a core field.
*
* @param object|null $item The item being prepared, if available
* @param string $fieldName The original custom field name
*
* @return string
*
* @since 5.4.0
*/
protected function getApiFieldKey(?object $item, string $fieldName): string
{
if ($item !== null && property_exists($item, $fieldName)) {
return 'cf_' . $fieldName;
}

if (\in_array($fieldName, $this->fieldsToRenderItem, true)
|| \in_array($fieldName, $this->fieldsToRenderList, true)
) {
return 'cf_' . $fieldName;
}

return $fieldName;
}

/**
* Register custom fields for rendering in list or item responses, using
* a collision-safe key derived from the original field name.
*
* @param array $fields The list of custom field objects
* @param bool $forList True when registering for list views, false for item views
*
* @return void
*
* @since 5.4.0
*/
protected function registerApiFields(array $fields, bool $forList): void
{
foreach ($fields as $field) {
$fieldKey = $this->getApiFieldKey(null, $field->name);

if ($forList) {
if (!\in_array($fieldKey, $this->fieldsToRenderList, true)) {
$this->fieldsToRenderList[] = $fieldKey;
}
} else {
if (!\in_array($fieldKey, $this->fieldsToRenderItem, true)) {
$this->fieldsToRenderItem[] = $fieldKey;
}
}
}
}

/**
* Assign values of custom fields to the item and register them for
* rendering in both item and list responses using a collision-safe key.
*
* @param array $fields The list of custom field objects
* @param object $item The item being prepared
*
* @return void
*
* @since 5.4.0
*/
protected function assignApiFieldValues(array $fields, $item): void
{
foreach ($fields as $field) {
$value = $field->apivalue ?? $field->rawvalue ?? null;
$fieldKey = $this->getApiFieldKey($item, $field->name);

$item->{$fieldKey} = $value;

if (!\in_array($fieldKey, $this->fieldsToRenderItem, true)) {
$this->fieldsToRenderItem[] = $fieldKey;
}

if (!\in_array($fieldKey, $this->fieldsToRenderList, true)) {
$this->fieldsToRenderList[] = $fieldKey;
}
}
}

/**
* Encode square brackets in the URI query, according to JSON API specification.
*
Expand Down
12 changes: 12 additions & 0 deletions templates/system/error.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Factory;

/** @var Joomla\CMS\Document\ErrorDocument $this */

Expand All @@ -32,6 +33,13 @@

// Get the error code
$errorCode = $this->error->getCode();

$app = Factory::getApplication();
$input = $app->getInput();
$sharedSessionsEnabled = (bool) $app->get('shared_session', '0');
$isPreviewComponent = $input->getCmd('tmpl') === 'component';
$isArticleNotFound = $this->error->getMessage() === Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND');
$showSharedSessionsHint = $errorCode === 404 && !$sharedSessionsEnabled && $isPreviewComponent && $isArticleNotFound;
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
Expand All @@ -58,6 +66,10 @@
<li><?php echo Text::_('JERROR_LAYOUT_REQUESTED_RESOURCE_WAS_NOT_FOUND'); ?></li>
<li><?php echo Text::_('JERROR_LAYOUT_ERROR_HAS_OCCURRED_WHILE_PROCESSING_YOUR_REQUEST'); ?></li>
</ul>
<?php if ($showSharedSessionsHint) : ?>
<p><strong><?php echo Text::_('JERROR_PREVIEW_SHARED_SESSIONS_HINT_TITLE'); ?></strong></p>
<p><?php echo Text::_('JERROR_PREVIEW_SHARED_SESSIONS_HINT_BODY'); ?></p>
<?php endif; ?>
<p><strong><?php echo Text::_('JERROR_LAYOUT_PLEASE_TRY_ONE_OF_THE_FOLLOWING_PAGES'); ?></strong></p>
<ul>
<li><a href="<?php echo Uri::root(true); ?>/index.php"><?php echo Text::_('JERROR_LAYOUT_HOME_PAGE'); ?></a></li>
Expand Down
19 changes: 18 additions & 1 deletion tests/System/integration/api/com_content/Articles.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,24 @@ describe('Test that content API endpoint', () => {
.its('title')
.should('include', 'automated test article'));
});

it('keeps core article images when a custom field is named images', () => {
cy.db_createField({
title: 'images field',
name: 'images',
label: 'images',
context: 'com_content.article',
required: 0,
})
.then(() => cy.db_createArticle({
title: 'automated test article with images',
images: '{"image_intro":"images/sampledata/cassiopeia/images/headers/flowers.jpg","image_intro_alt":"","float_intro":"","image_intro_caption":"","image_fulltext":"images/sampledata/cassiopeia/images/headers/flowers.jpg","image_fulltext_alt":"","float_fulltext":"","image_fulltext_caption":""}',
}))
.then((article) => cy.api_get(`/content/articles/${article.id}`))
.then((response) => cy.wrap(response).its('body').its('data').its('attributes')
.its('images')
.its('image_intro')
.should('include', 'flowers.jpg'));
});
it('can create an article', () => {
cy.db_createCategory({ extension: 'com_content' })
.then((categoryId) => cy.api_post('/content/articles', {
Expand Down
Loading