diff --git a/administrator/components/com_banners/src/Model/BannerModel.php b/administrator/components/com_banners/src/Model/BannerModel.php index 03e01ef025a29..9eab11d30dd15 100644 --- a/administrator/components/com_banners/src/Model/BannerModel.php +++ b/administrator/components/com_banners/src/Model/BannerModel.php @@ -16,6 +16,7 @@ use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\Table\Table; use Joomla\CMS\Table\TableInterface; +use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableModelTrait; use Joomla\Component\Categories\Administrator\Helper\CategoriesHelper; use Joomla\Database\ParameterType; @@ -29,7 +30,7 @@ * * @since 1.6 */ -class BannerModel extends AdminModel +class BannerModel extends AdminModel implements VersionableModelInterface { use VersionableModelTrait; diff --git a/administrator/components/com_banners/src/Model/ClientModel.php b/administrator/components/com_banners/src/Model/ClientModel.php index 6378292da6ab2..fb672b14f9487 100644 --- a/administrator/components/com_banners/src/Model/ClientModel.php +++ b/administrator/components/com_banners/src/Model/ClientModel.php @@ -13,6 +13,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\Table\Table; +use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableModelTrait; // phpcs:disable PSR1.Files.SideEffects @@ -24,7 +25,7 @@ * * @since 1.6 */ -class ClientModel extends AdminModel +class ClientModel extends AdminModel implements VersionableModelInterface { use VersionableModelTrait; diff --git a/administrator/components/com_categories/src/Model/CategoryModel.php b/administrator/components/com_categories/src/Model/CategoryModel.php index bb2c26b083b21..b4fb1b5290a27 100644 --- a/administrator/components/com_categories/src/Model/CategoryModel.php +++ b/administrator/components/com_categories/src/Model/CategoryModel.php @@ -26,6 +26,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Table\Category; use Joomla\CMS\UCM\UCMType; +use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableModelTrait; use Joomla\Component\Categories\Administrator\Helper\CategoriesHelper; use Joomla\Database\ParameterType; @@ -43,7 +44,7 @@ * * @since 1.6 */ -class CategoryModel extends AdminModel +class CategoryModel extends AdminModel implements VersionableModelInterface { use VersionableModelTrait; diff --git a/administrator/components/com_contact/src/Model/ContactModel.php b/administrator/components/com_contact/src/Model/ContactModel.php index 3c0128e293cba..f5d884f848622 100644 --- a/administrator/components/com_contact/src/Model/ContactModel.php +++ b/administrator/components/com_contact/src/Model/ContactModel.php @@ -18,6 +18,7 @@ use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\String\PunycodeHelper; +use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableModelTrait; use Joomla\Component\Categories\Administrator\Helper\CategoriesHelper; use Joomla\Database\ParameterType; @@ -33,7 +34,7 @@ * * @since 1.6 */ -class ContactModel extends AdminModel +class ContactModel extends AdminModel implements VersionableModelInterface { use VersionableModelTrait; diff --git a/administrator/components/com_content/src/Model/ArticleModel.php b/administrator/components/com_content/src/Model/ArticleModel.php index 48862d40276f9..106a7139a7634 100644 --- a/administrator/components/com_content/src/Model/ArticleModel.php +++ b/administrator/components/com_content/src/Model/ArticleModel.php @@ -29,6 +29,7 @@ use Joomla\CMS\Table\TableInterface; use Joomla\CMS\Tag\TaggableTableInterface; use Joomla\CMS\UCM\UCMType; +use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableModelTrait; use Joomla\CMS\Workflow\Workflow; use Joomla\Component\Categories\Administrator\Helper\CategoriesHelper; @@ -49,7 +50,7 @@ * @since 1.6 */ -class ArticleModel extends AdminModel implements WorkflowModelInterface +class ArticleModel extends AdminModel implements WorkflowModelInterface, VersionableModelInterface { use WorkflowBehaviorTrait; use VersionableModelTrait; diff --git a/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php b/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php index 255e527846da2..ecab911f422ed 100644 --- a/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php +++ b/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php @@ -18,6 +18,7 @@ use Joomla\Filesystem\File; use Joomla\Filesystem\Folder; use Joomla\Filesystem\Path; +use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; @@ -75,8 +76,18 @@ public static function decodeFields($jsonString) if (\is_object($object)) { foreach ($object as $name => $value) { - if (!\is_null($value) && $subObject = json_decode($value)) { - $object->$name = $subObject; + if (!\is_null($value)) { + if (\is_object($value)) { + $object->$name = ArrayHelper::fromObject($value); + continue; + } + + if (str_starts_with($value, '{')) { + $object->$name = json_decode($value); + continue; + } + + $object->$name = $value; } } } diff --git a/administrator/components/com_contenthistory/tmpl/compare/compare.php b/administrator/components/com_contenthistory/tmpl/compare/compare.php index 8f855b92c1ad8..56dc248d236f3 100644 --- a/administrator/components/com_contenthistory/tmpl/compare/compare.php +++ b/administrator/components/com_contenthistory/tmpl/compare/compare.php @@ -12,6 +12,7 @@ use Joomla\CMS\Language\Text; use Joomla\CMS\Session\Session; +use Joomla\Utilities\ArrayHelper; /** @var \Joomla\Component\Contenthistory\Administrator\View\Compare\HtmlView $this */ @@ -19,8 +20,8 @@ $version2 = $this->items[0]; $version1 = $this->items[1]; -$object1 = $version1->data; -$object2 = $version2->data; +$object1 = ArrayHelper::fromObject($version1->data); +$object2 = ArrayHelper::fromObject($version2->data); /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->getDocument()->getWebAssetManager(); @@ -43,40 +44,70 @@ - $value) : ?> - value) && isset($object2->$name->value) && $value->value != $object2->$name->value) : ?> - value)) : ?> - - - label; ?> - - - value as $subName => $subValue) : ?> - $name->value->$subName->value ?? ''; ?> - value || $newSubValue) : ?> - value != $newSubValue) : ?> + $value1) : ?> + + + + + + + + + + + + + + -   label; ?> - value, ENT_COMPAT, 'UTF-8'); ?> - + + + + + + + + + + + + + + + + + + + + + + + + + +   - + + + + + + + + + + +   + - - - - - label; ?> - - value); ?> - $name->value = is_object($object2->$name->value) ? json_encode($object2->$name->value) : $object2->$name->value; ?> - $name->value, ENT_COMPAT, 'UTF-8'); ?> -   - + - - + diff --git a/administrator/components/com_contenthistory/tmpl/preview/preview.php b/administrator/components/com_contenthistory/tmpl/preview/preview.php index fca9e66163ee0..ff46dd7cc5788 100644 --- a/administrator/components/com_contenthistory/tmpl/preview/preview.php +++ b/administrator/components/com_contenthistory/tmpl/preview/preview.php @@ -51,14 +51,27 @@ value = (\is_object($subValue->value) || \is_array($subValue->value)) ? \json_encode($subValue->value, \JSON_UNESCAPED_UNICODE) : $subValue->value; ?>   label; ?> - value; ?> + + value)) : ?> + value); ?> + + value; ?> + + + label; ?> - value; ?> + + value)) : ?> + value); ?> + + value; ?> + + diff --git a/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php b/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php index 193171d9d12ef..e710ec0c7e5c1 100644 --- a/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php +++ b/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php @@ -17,6 +17,7 @@ use Joomla\CMS\Language\Associations; use Joomla\CMS\Language\LanguageHelper; use Joomla\CMS\MVC\Model\AdminModel; +use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableModelTrait; use Joomla\Component\Categories\Administrator\Helper\CategoriesHelper; use Joomla\Registry\Registry; @@ -30,7 +31,7 @@ * * @since 1.6 */ -class NewsfeedModel extends AdminModel +class NewsfeedModel extends AdminModel implements VersionableModelInterface { use VersionableModelTrait; diff --git a/administrator/components/com_tags/src/Model/TagModel.php b/administrator/components/com_tags/src/Model/TagModel.php index b4e1901670743..f1b8b51b2bd89 100644 --- a/administrator/components/com_tags/src/Model/TagModel.php +++ b/administrator/components/com_tags/src/Model/TagModel.php @@ -15,6 +15,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableModelTrait; use Joomla\Registry\Registry; use Joomla\String\StringHelper; @@ -28,7 +29,7 @@ * * @since 3.1 */ -class TagModel extends AdminModel +class TagModel extends AdminModel implements VersionableModelInterface { use VersionableModelTrait; diff --git a/administrator/components/com_users/src/Model/NoteModel.php b/administrator/components/com_users/src/Model/NoteModel.php index 91443ab089e46..4f820fd251488 100644 --- a/administrator/components/com_users/src/Model/NoteModel.php +++ b/administrator/components/com_users/src/Model/NoteModel.php @@ -13,6 +13,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableModelTrait; // phpcs:disable PSR1.Files.SideEffects @@ -24,7 +25,7 @@ * * @since 2.5 */ -class NoteModel extends AdminModel +class NoteModel extends AdminModel implements VersionableModelInterface { use VersionableModelTrait; diff --git a/administrator/language/en-GB/joomla.ini b/administrator/language/en-GB/joomla.ini index db5d787f4508d..d91cba72dd62b 100644 --- a/administrator/language/en-GB/joomla.ini +++ b/administrator/language/en-GB/joomla.ini @@ -308,6 +308,7 @@ JFIELD_PLG_SEARCH_SEARCHLIMIT_LABEL="Search Limit" JFIELD_PUBLISHED_DESC="Set publication status." JFIELD_READMORE_DESC="Add a custom text instead of Read More." JFIELD_READMORE_LABEL="Read More Text" +JFIELD_RULES_LABEL="Group Permissions" JFIELD_SPACER_LABEL="
" JFIELD_TITLE_DESC="Title" JFIELD_VERSION_HISTORY_DESC="This button allows you to open a window to view older versions of this item." diff --git a/libraries/src/MVC/Model/AdminModel.php b/libraries/src/MVC/Model/AdminModel.php index 52e1a414efbaa..1d4c7fd8264c9 100644 --- a/libraries/src/MVC/Model/AdminModel.php +++ b/libraries/src/MVC/Model/AdminModel.php @@ -25,6 +25,8 @@ use Joomla\CMS\Table\TableInterface; 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; @@ -1443,6 +1445,19 @@ public function save($data) } } + if ($this instanceof VersionableModelInterface) { + // Merge table data and data so that we write all data to the history + $tableData = ArrayHelper::fromObject($table); + + $historyData = array_merge($tableData, $data); + + // 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); + } + if ($app->getInput()->get('task') == 'editAssociations') { return $this->redirectToAssociations($data); } @@ -1730,4 +1745,25 @@ 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 __DEPLOY_VERSION__ + */ + 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; + } } diff --git a/libraries/src/Versioning/VersionableControllerTrait.php b/libraries/src/Versioning/VersionableControllerTrait.php index 8e9d8a1e7eeaf..1acf83d697116 100644 --- a/libraries/src/Versioning/VersionableControllerTrait.php +++ b/libraries/src/Versioning/VersionableControllerTrait.php @@ -36,9 +36,9 @@ public function loadhistory() $table = $model->getTable(); $historyId = $this->input->getInt('version_id', null); - if (!$model->loadhistory($historyId, $table)) { - $this->setMessage($model->getError(), 'error'); + $id = $model->getItemIdFromHistory($historyId); + if (false === $id) { $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_list @@ -51,17 +51,13 @@ public function loadhistory() } // Determine the name of the primary key for the data. - if (empty($key)) { - $key = $table->getKeyName(); - } - - $recordId = $table->$key; + $key = $table->getKeyName(); // To avoid data collisions the urlVar may be different from the primary key. $urlVar = empty($this->urlVar) ? $key : $this->urlVar; // Access check. - if (!$this->allowEdit([$key => $recordId], $key)) { + if (!$this->allowEdit([$key => $id], $key)) { $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); $this->setRedirect( @@ -79,13 +75,21 @@ public function loadhistory() $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend($recordId, $urlVar), + . $this->getRedirectToItemAppend($id, $urlVar), false ) ); - if (!$table->check() || !$table->store()) { - $this->setMessage($table->getError(), 'error'); + if (!$model->loadhistory($historyId, $table)) { + $this->setMessage($model->getError(), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); return false; } diff --git a/libraries/src/Versioning/VersionableModelInterface.php b/libraries/src/Versioning/VersionableModelInterface.php new file mode 100644 index 0000000000000..010868ff52c4d --- /dev/null +++ b/libraries/src/Versioning/VersionableModelInterface.php @@ -0,0 +1,34 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Versioning; + +// phpcs:disable PSR1.Files.SideEffects + +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Interface for a versionable model. + * + * @since __DEPLOY_VERSION__ + */ +interface VersionableModelInterface +{ + /** + * Method to load a row for editing from the version history table. + * + * @param integer $historyId Key to the version history table. + * + * @return boolean False on failure or error, true otherwise. + * + * @since __DEPLOY_VERSION__ + */ + public function loadHistory(int $historyId); +} diff --git a/libraries/src/Versioning/VersionableModelTrait.php b/libraries/src/Versioning/VersionableModelTrait.php index 8cdae9be05ddd..916ee5d086ad5 100644 --- a/libraries/src/Versioning/VersionableModelTrait.php +++ b/libraries/src/Versioning/VersionableModelTrait.php @@ -9,9 +9,8 @@ namespace Joomla\CMS\Versioning; -use Joomla\CMS\Language\Text; +use Joomla\CMS\Factory; use Joomla\CMS\Table\ContentHistory; -use Joomla\CMS\Table\Table; use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects @@ -26,58 +25,95 @@ trait VersionableModelTrait { /** - * Method to load a row for editing from the version history table. + * Method to get the item id from the version history table. * - * @param integer $versionId Key to the version history table. - * @param Table $table Content table object being loaded. + * @param integer $historyId Key to the version history table. * - * @return boolean False on failure or error, true otherwise. + * @return integer False on failure or error, id otherwise. * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ - public function loadHistory($versionId, Table $table) + public function getItemIdFromHistory($historyId) { - // Only attempt to check the row in if it exists, otherwise do an early exit. - if (!$versionId) { + $rowArray = $this->getHistoryData($historyId); + + if (false === $rowArray) { return false; } - // Get an instance of the row to checkout. - $historyTable = new ContentHistory($this->getDbo()); + $table = $this->getTable(); + $key = $table->getKeyName(); + + if (isset($rowArray[$key])) { + return $rowArray[$key]; + } - if (!$historyTable->load($versionId)) { - $this->setError($historyTable->getError()); + return false; + } + /** + * Method to get the version data from the version history table. + * + * @param integer $historyId Key to the version history table. + * + * @return mixed False on failure or error, data otherwise. + * + * @since __DEPLOY_VERSION__ + */ + protected function getHistoryData($historyId) + { + // Get an instance of the row to checkout. + $historyTable = new ContentHistory($this->getDatabase()); + + if (!$historyTable->load($historyId)) { return false; } - $typeAlias = explode('.', $historyTable->item_id); - array_pop($typeAlias); - $rowArray = ArrayHelper::fromObject(json_decode($historyTable->version_data)); - $key = $table->getKeyName(); + return $rowArray; + } - if (implode('.', $typeAlias) != $this->typeAlias) { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_HISTORY_ID_MISMATCH')); + /** + * Method to get a version history table. + * + * @param integer $historyId Key to the version history table. + * + * @return mixed False on failure or error, table otherwise. + * + * @since __DEPLOY_VERSION__ + */ + protected function getHistoryTable($historyId) + { + if (empty($historyId)) { + return false; + } - if (isset($rowArray[$key])) { - $table->checkIn($rowArray[$key]); - } + // Get an instance of the row to checkout. + $historyTable = new ContentHistory($this->getDatabase()); + if (!$historyTable->load($historyId)) { return false; } - $this->setState('save_date', $historyTable->save_date); - $this->setState('version_note', $historyTable->version_note); + return $historyTable; + } - /** - * Load data from current version before replacing it with data from history to avoid error - * if there are some required keys missing in the history data - */ + /** + * Method to load a row for editing from the version history table. + * + * @param integer $historyId Key to the version history table. + * + * @return boolean False on failure or error, true otherwise. + * + * @since __DEPLOY_VERSION__ + */ + public function loadHistory(int $historyId) + { + $rowArray = $this->getHistoryData($historyId); - if (isset($rowArray[$key])) { - $table->load($rowArray[$key]); + if (false === $rowArray) { + return false; } // Fix null ordering when restoring history @@ -85,6 +121,16 @@ public function loadHistory($versionId, Table $table) $rowArray['ordering'] = 0; } - return $table->bind($rowArray); + [$extension, $type] = explode('.', $this->typeAlias); + + $app = Factory::getApplication(); + $app->setUserState($extension . '.edit.' . $type . '.data', $rowArray); + + $historyTable = $this->getHistoryTable($historyId); + + $this->setState('save_date', $historyTable->save_date); + $this->setState('version_note', $historyTable->version_note); + + return true; } } diff --git a/plugins/behaviour/versionable/src/Extension/Versionable.php b/plugins/behaviour/versionable/src/Extension/Versionable.php index 3f5f5bc3b974d..e1db352945e73 100644 --- a/plugins/behaviour/versionable/src/Extension/Versionable.php +++ b/plugins/behaviour/versionable/src/Extension/Versionable.php @@ -15,6 +15,7 @@ use Joomla\CMS\Event\Table\BeforeDeleteEvent; use Joomla\CMS\Helper\CMSHelper; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableTableInterface; use Joomla\CMS\Versioning\Versioning; use Joomla\Event\DispatcherInterface; @@ -97,21 +98,33 @@ public function onTableAfterStore(AfterStoreEvent $event) // Extract arguments /** @var VersionableTableInterface $table */ $table = $event['subject']; + + // We need to check this first because getTypeAlias is only available when VersionableTableInterface is implemented + if (!$table instanceof VersionableTableInterface) { + return; + } + $result = $event['result']; + $typeAlias = $table->getTypeAlias(); + [$component, $modelName] = explode('.', $typeAlias); + + $model = $this->getApplication()->bootComponent($component)->getMVCFactory()->createModel($modelName, 'Administrator'); + + if ($model instanceof VersionableModelInterface) { + return; + } + if (!$result) { return; } - if (!(\is_object($table) && $table instanceof VersionableTableInterface)) { + if (!(\is_object($table))) { return; } // Get the Tags helper and assign the parsed alias - $typeAlias = $table->getTypeAlias(); - $aliasParts = explode('.', $typeAlias); - - if ($aliasParts[0] === '' || !ComponentHelper::getParams($aliasParts[0])->get('save_history', 0)) { + if ($component === '' || !ComponentHelper::getParams($component)->get('save_history', 0)) { return; }