Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2f8baf4
deprecated
RobertDeutz Aug 21, 2025
f4d6479
move functionality to trait
RobertDeutz Aug 21, 2025
feb8b20
update interface
RobertDeutz Aug 21, 2025
e537bd9
remove func after moving
RobertDeutz Aug 21, 2025
2b6349e
use new concept
RobertDeutz Aug 21, 2025
00c65a3
typos and cs fixes
RobertDeutz Aug 21, 2025
2b55887
cs fix
RobertDeutz Aug 21, 2025
d8dad29
change function names
RobertDeutz Aug 21, 2025
62dd93e
Update administrator/components/com_contenthistory/src/Model/HistoryM…
rdeutz Aug 25, 2025
14c3f9c
Update libraries/src/Versioning/VersionableModelTrait.php
rdeutz Aug 25, 2025
bdde2cd
Update libraries/src/Versioning/VersionableModelTrait.php
rdeutz Aug 25, 2025
8c4b935
Merge branch '6.0-dev' into PR45955
LadySolveig Aug 29, 2025
13cbe81
codestyle
LadySolveig Aug 29, 2025
6ea97a7
Remove event dispatcher in compat plugin constructor (#45998)
laoneo Aug 29, 2025
1cb90fd
[6.0] Don't insert duplicate records in update SQL scripts when they …
richard67 Aug 29, 2025
e424a92
Update joomla/filesystem to 4.1.0 (#46002)
richard67 Aug 29, 2025
2d80bbc
[6.0] Fix incorrect language tag comparison (#45947)
Fedik Aug 30, 2025
e6084eb
[6.0] Add CSS to Cassiopeia for the refactored mod_menu (#45930)
drmenzelit Aug 30, 2025
02877be
[6.0] No htmlhelper for js (#45925)
dgrammatiko Aug 30, 2025
432faad
[6.0] Fix language auto-loading without Application in CMSPlugin (#45…
Fedik Aug 30, 2025
2ebcff3
[5.4] Autoupdate email groups (#45721)
chmst Aug 28, 2025
e7a3f92
[5.4] Remove duplicate string (#46008)
brianteeman Aug 29, 2025
44276c8
[5.4] Custom Logging Description (#46004)
brianteeman Aug 29, 2025
d8d3e76
[5.4] Move associations alert (#46011)
brianteeman Aug 29, 2025
5c017bf
Add check if fields exist in versioning (#46009)
bembelimen Aug 29, 2025
b539bb4
[5.4] Start/End Featured (#46003)
brianteeman Aug 29, 2025
eb184f4
Update libraries/src/Versioning/VersionableModelTrait.php
richard67 Aug 31, 2025
9ac8f9f
Merge branch '6.0-dev' into version-fixes
rdeutz Sep 2, 2025
9e61bf1
Merge branch '6.0-dev' into version-fixes
rdeutz Sep 3, 2025
2bc287f
current
RobertDeutz Sep 4, 2025
1eb47fe
cs fix
RobertDeutz Sep 11, 2025
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 @@ -123,6 +123,8 @@
$this->event_after_change_featured ??= 'onContentAfterChangeFeatured';

$this->setUpWorkflow('com_content.article');

$this->setIgnoreChanges(['checked_out', 'checked_out_time', 'tagsHelper', 'version', 'articletext', 'rules']);
}

/**
Expand Down Expand Up @@ -853,7 +855,7 @@
$table = $this->getTable('Featured', 'Administrator');

// Trigger the before change state event.
$eventResult = Factory::getApplication()->getDispatcher()->dispatch(

Check failure on line 858 in administrator/components/com_content/src/Model/ArticleModel.php

View workflow job for this annotation

GitHub Actions / Run PHPstan

Ignored error pattern #^Call to method getDispatcher\(\) of deprecated interface Joomla\\CMS\\Application\\EventAwareInterface\: 4\.3 will be removed in 7\.0 This interface will be removed without replacement as the Joomla 3\.x compatibility layer will be removed$# (method.deprecatedInterface) in path /__w/joomla-cms/joomla-cms/administrator/components/com_content/src/Model/ArticleModel.php is expected to occur 2 times, but occurred 3 times.
$this->event_before_change_featured,
AbstractEvent::create(
$this->event_before_change_featured,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Joomla\CMS\Table\ContentHistory;
use Joomla\CMS\Table\ContentType;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Versioning\VersionableModelInterface;
use Joomla\Database\ParameterType;
use Joomla\Database\QueryInterface;

Expand Down Expand Up @@ -377,13 +378,27 @@ protected function getSha1Hash()
{
$result = false;
$item_id = Factory::getApplication()->getInput()->getCmd('item_id', '');
$typeAlias = explode('.', $item_id);
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/' . $typeAlias[0] . '/tables');

[$extension, $type, $id] = explode('.', $item_id);

$app = Factory::getApplication();

$model = $app->bootComponent($extension)->getMVCFactory()->createModel($type, 'Administrator');

if ($model instanceof VersionableModelInterface) {
$item = $model->getItem($id);
$result = $model->getSha1($item);

return $result;
}

// Legacy code for history concept before 6.0.0, deprecated 6.0.0 will be removed with 8.0.0
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/' . $extension . '/tables');
$typeTable = $this->getTable('ContentType');
$typeTable->load(['type_alias' => $typeAlias[0] . '.' . $typeAlias[1]]);
$typeTable->load(['type_alias' => $extension . '.' . $type]);
$contentTable = $typeTable->getContentTable();

if ($contentTable && $contentTable->load($typeAlias[2])) {
if ($contentTable && $contentTable->load($id)) {
$helper = new CMSHelper();

$dataObject = $helper->getDataObject($contentTable);
Expand Down
25 changes: 1 addition & 24 deletions libraries/src/MVC/Model/AdminModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
use Joomla\CMS\Tag\TaggableTableInterface;
use Joomla\CMS\UCM\UCMType;
use Joomla\CMS\Versioning\VersionableModelInterface;
use Joomla\CMS\Versioning\Versioning;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
Expand Down Expand Up @@ -1449,12 +1448,11 @@ public function save($data)
// Merge table data and data so that we write all data to the history
$tableData = ArrayHelper::fromObject($table);

$historyData = array_merge($tableData, $data);
$historyData = array_merge($data, $tableData);

// We have to set the key for new items, would be always 0 otherwise
$historyData[$key] = $this->getState($this->getName() . '.id');


$this->saveHistory($historyData, $context);
}

Expand Down Expand Up @@ -1745,25 +1743,4 @@ protected function redirectToAssociations($data)

return true;
}

/**
* Method to save the history.
*
* @param array $data The form data.
* @param string $context The model context.
*
* @return boolean True on success, False on error.
*
* @since 6.0.0
*/
protected function saveHistory(array $data, string $context)
{
$id = $this->getState($this->getName() . '.id');

$versionNote = \array_key_exists('version_note', $data) ? $data['version_note'] : '';

$result = Versioning::store($context, $id, ArrayHelper::toObject($data), $versionNote);

return $result;
}
}
12 changes: 12 additions & 0 deletions libraries/src/Versioning/VersionableModelInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,16 @@ interface VersionableModelInterface
* @since 6.0.0
*/
public function loadHistory(int $historyId);

/**
* Method to save the history.
*
* @param array $data The form data.
* @param string $context The model context.
*
* @return boolean True on success, False on error.
*
* @since __DEPLOY_VERSION__
*/
public function saveHistory(array $data, string $context);
}
251 changes: 251 additions & 0 deletions libraries/src/Versioning/VersionableModelTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@

namespace Joomla\CMS\Versioning;

use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Event\AbstractEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Table\ContentHistory;
use Joomla\CMS\Table\ContentType;
use Joomla\CMS\Workflow\WorkflowServiceInterface;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;

// phpcs:disable PSR1.Files.SideEffects
Expand All @@ -23,6 +31,22 @@
*/
trait VersionableModelTrait
{
/**
* Fields to be ignored when calculating the hash.
*
* @var array
* @since __DEPLOY_VERSION__
*/
protected $ignoreChanges = [];

/**
* Fields to be converted to int when calculating the hash.
*
* @var array
* @since __DEPLOY_VERSION__
*/
protected $convertToInt = [];

/**
* Method to get the item id from the version history table.
*
Expand Down Expand Up @@ -50,6 +74,7 @@ public function getItemIdFromHistory($historyId)
return false;
}


/**
* Method to get the version data from the version history table.
*
Expand Down Expand Up @@ -143,4 +168,230 @@ public function loadHistory(int $historyId)

return true;
}

/**
* Utility method to get the hash after removing selected values. This lets us detect changes other than
* modified date (which will change on every save).
*
* @param mixed $data Either an object or an array
*
* @return string SHA1 hash on success. Empty string on failure.
*
* @since __DEPLOY_VERSION__
*/
public function getSha1($data)
{
$object = \is_object($data) ? $data : ArrayHelper::toObject($data);

foreach ($this->ignoreChanges as $remove) {
if (property_exists($object, $remove)) {
unset($object->$remove);
}
}

// Convert integers, booleans, and nulls to strings to get a consistent hash value
foreach ($object as $name => $value) {
if (\is_object($value)) {
// Go one level down for JSON column values
foreach ($value as $subName => $subValue) {
$object->$subName = \is_int($subValue) || \is_bool($subValue) || $subValue === null ? (string) $subValue : $subValue;
}
} else {
$object->$name = \is_int($value) || \is_bool($value) || $value === null ? (string) $value : $value;
}
}

// Work around empty values
foreach ($this->convertToInt as $convert) {
if (isset($object->$convert)) {
$object->$convert = (int) $object->$convert;
}
}

if (isset($object->review_time)) {
$object->review_time = (int) $object->review_time;
}

return sha1(json_encode($object));
}

/**
* Setter for the value
*
* @param array $ignoreChanges
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
public function setIgnoreChanges(array $ignoreChanges): void
{
$this->ignoreChanges = $ignoreChanges;
}

/**
* Setter for the value
*
* @param array $convertToInt
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
public function setConvertToInt(array $convertToInt): void
{
$this->convertToInt = $convertToInt;
}

/**
* Method to save the history.
*
* @param array $data The form data.
* @param string $context The model context.
*
* @return boolean True on success, False on error.
*
* @since __DEPLOY_VERSION__
*/
public function saveHistory(array $data, string $context)
{
$id = $this->getState($this->getName() . '.id');

$versionNote = '';

if (\array_key_exists('version_note', $data)) {
$versionNote = $data['version_note'];
unset($data['version_note']);
}

foreach ($this->ignoreChanges as $ignore) {
if (\array_key_exists($ignore, $data)) {
unset($data[$ignore]);
}
}

$item = $this->getItem($id);

$hash = $this->getSha1($item);

$result = $this->storeHistory($context, $id, ArrayHelper::toObject($data), $versionNote, $hash);

return $result;
}

/**
* Method to delete the history for an item.
*
* @param string $typeAlias Typealias of the component
* @param integer $id ID of the content item to delete
*
* @return boolean true on success, otherwise false.
*
* @since __DEPLOY_VERSION__
*/
public function deleteHistory($typeAlias, $id)
{
$db = $this->getDatabase();
$itemid = $typeAlias . '.' . $id;
$query = $db->createQuery();
$query->delete($db->quoteName('#__history'))
->where($db->quoteName('item_id') . ' = :item_id')
->bind(':item_id', $itemid, ParameterType::STRING);
$db->setQuery($query);

return $db->execute();
}

/**
* Method to save a version snapshot to the content history table.
*
* @param string $typeAlias Typealias of the content type
* @param integer $id ID of the content item
* @param mixed $data Array or object of data that can be
* en- and decoded into JSON
* @param string $note Note for the version to store
* @param string $hash
*
* @return boolean True on success, otherwise false.
*
* @since __DEPLOY_VERSION__
* @throws \Exception
*/
public function storeHistory(string $typeAlias, int $id, mixed $data, string $note = '', string $hash = '')
{
$typeTable = new ContentType($this->getDatabase());
$typeTable->load(['type_alias' => $typeAlias]);

$historyTable = new ContentHistory($this->getDatabase());
$historyTable->item_id = $typeAlias . '.' . $id;

[$extension, $type] = explode('.', $typeAlias);

// Don't store unless we have a non-zero item id
if (!$historyTable->item_id) {
return true;
}

// We should allow workflow items interact with the versioning
$component = Factory::getApplication()->bootComponent($extension);

if ($component instanceof WorkflowServiceInterface && $component->isWorkflowActive($typeAlias)) {
PluginHelper::importPlugin('workflow');

// Pre-processing by observers
$event = AbstractEvent::create(
'onContentVersioningPrepareTable',
[
'subject' => $historyTable,
'extension' => $typeAlias,
]
);

Factory::getApplication()->getDispatcher()->dispatch('onContentVersioningPrepareTable', $event);
}

// Fix for null ordering - set to 0 if null
if (\is_object($data)) {
if (property_exists($data, 'ordering') && $data->ordering === null) {
$data->ordering = 0;
}
} elseif (\is_array($data)) {
if (\array_key_exists('ordering', $data) && $data['ordering'] === null) {
$data['ordering'] = 0;
}
}

$historyTable->version_data = json_encode($data);
$historyTable->version_note = $note;

// Don't save if hash already exists and same version note
$historyTable->sha1_hash = $hash;

$historyRow = $historyTable->getHashMatch();

if ($historyRow) {
if (!$note || ($historyRow->version_note === $note)) {
return true;
}

// Update existing row to set version note
$historyTable->version_id = $historyRow->version_id;
}

$result = $historyTable->store();

// Load history_limit config from extension.
$context = $type ?? '';

$maxVersionsContext = ComponentHelper::getParams($extension)->get('history_limit_' . $context, 0);
$maxVersions = ComponentHelper::getParams($extension)->get('history_limit', 0);

if ($maxVersionsContext) {
$historyTable->deleteOldVersions($maxVersionsContext);
} elseif ($maxVersions) {
$historyTable->deleteOldVersions($maxVersions);
}

return $result;
}
}
Loading
Loading