/libraries/src/Table/ContentHistory.php

https://github.com/joomla/joomla-cms · PHP · 219 lines · 117 code · 25 blank · 77 comment · 14 complexity · f482a5d0d83b0d0a3be2cfa980601906 MD5 · raw file

  1. <?php
  2. /**
  3. * Joomla! Content Management System
  4. *
  5. * @copyright (C) 2013 Open Source Matters, Inc. <https://www.joomla.org>
  6. * @license GNU General Public License version 2 or later; see LICENSE.txt
  7. */
  8. namespace Joomla\CMS\Table;
  9. use Joomla\CMS\Factory;
  10. use Joomla\Database\DatabaseDriver;
  11. use Joomla\Database\ParameterType;
  12. /**
  13. * Content History table.
  14. *
  15. * @since 3.2
  16. */
  17. class ContentHistory extends Table
  18. {
  19. /**
  20. * Array of object fields to unset from the data object before calculating SHA1 hash. This allows us to detect a meaningful change
  21. * in the database row using the hash. This can be read from the #__content_types content_history_options column.
  22. *
  23. * @var array
  24. * @since 3.2
  25. */
  26. public $ignoreChanges = array();
  27. /**
  28. * Array of object fields to convert to integers before calculating SHA1 hash. Some values are stored differently
  29. * when an item is created than when the item is changed and saved. This works around that issue.
  30. * This can be read from the #__content_types content_history_options column.
  31. *
  32. * @var array
  33. * @since 3.2
  34. */
  35. public $convertToInt = array();
  36. /**
  37. * Constructor
  38. *
  39. * @param DatabaseDriver $db A database connector object
  40. *
  41. * @since 3.1
  42. */
  43. public function __construct(DatabaseDriver $db)
  44. {
  45. parent::__construct('#__history', 'version_id', $db);
  46. $this->ignoreChanges = array(
  47. 'modified_by',
  48. 'modified_user_id',
  49. 'modified',
  50. 'modified_time',
  51. 'checked_out',
  52. 'checked_out_time',
  53. 'version',
  54. 'hits',
  55. 'path',
  56. );
  57. $this->convertToInt = array('publish_up', 'publish_down', 'ordering', 'featured');
  58. }
  59. /**
  60. * Overrides Table::store to set modified hash, user id, and save date.
  61. *
  62. * @param boolean $updateNulls True to update fields even if they are null.
  63. *
  64. * @return boolean True on success.
  65. *
  66. * @since 3.2
  67. */
  68. public function store($updateNulls = false)
  69. {
  70. $this->set('character_count', \strlen($this->get('version_data')));
  71. $typeTable = Table::getInstance('ContentType', 'JTable', array('dbo' => $this->getDbo()));
  72. $typeAlias = explode('.', $this->item_id);
  73. array_pop($typeAlias);
  74. $typeTable->load(array('type_alias' => implode('.', $typeAlias)));
  75. if (!isset($this->sha1_hash)) {
  76. $this->set('sha1_hash', $this->getSha1($this->get('version_data'), $typeTable));
  77. }
  78. // Modify author and date only when not toggling Keep Forever
  79. if ($this->get('keep_forever') === null) {
  80. $this->set('editor_user_id', Factory::getUser()->id);
  81. $this->set('save_date', Factory::getDate()->toSql());
  82. }
  83. return parent::store($updateNulls);
  84. }
  85. /**
  86. * Utility method to get the hash after removing selected values. This lets us detect changes other than
  87. * modified date (which will change on every save).
  88. *
  89. * @param mixed $jsonData Either an object or a string with json-encoded data
  90. * @param ContentType $typeTable Table object with data for this content type
  91. *
  92. * @return string SHA1 hash on success. Empty string on failure.
  93. *
  94. * @since 3.2
  95. */
  96. public function getSha1($jsonData, ContentType $typeTable)
  97. {
  98. $object = \is_object($jsonData) ? $jsonData : json_decode($jsonData);
  99. if (isset($typeTable->content_history_options) && \is_object(json_decode($typeTable->content_history_options))) {
  100. $options = json_decode($typeTable->content_history_options);
  101. $this->ignoreChanges = $options->ignoreChanges ?? $this->ignoreChanges;
  102. $this->convertToInt = $options->convertToInt ?? $this->convertToInt;
  103. }
  104. foreach ($this->ignoreChanges as $remove) {
  105. if (property_exists($object, $remove)) {
  106. unset($object->$remove);
  107. }
  108. }
  109. // Convert integers, booleans, and nulls to strings to get a consistent hash value
  110. foreach ($object as $name => $value) {
  111. if (\is_object($value)) {
  112. // Go one level down for JSON column values
  113. foreach ($value as $subName => $subValue) {
  114. $object->$subName = \is_int($subValue) || \is_bool($subValue) || $subValue === null ? (string) $subValue : $subValue;
  115. }
  116. } else {
  117. $object->$name = \is_int($value) || \is_bool($value) || $value === null ? (string) $value : $value;
  118. }
  119. }
  120. // Work around empty values
  121. foreach ($this->convertToInt as $convert) {
  122. if (isset($object->$convert)) {
  123. $object->$convert = (int) $object->$convert;
  124. }
  125. }
  126. if (isset($object->review_time)) {
  127. $object->review_time = (int) $object->review_time;
  128. }
  129. return sha1(json_encode($object));
  130. }
  131. /**
  132. * Utility method to get a matching row based on the hash value and id columns.
  133. * This lets us check to make sure we don't save duplicate versions.
  134. *
  135. * @return string SHA1 hash on success. Empty string on failure.
  136. *
  137. * @since 3.2
  138. */
  139. public function getHashMatch()
  140. {
  141. $db = $this->_db;
  142. $itemId = $this->get('item_id');
  143. $sha1Hash = $this->get('sha1_hash');
  144. $query = $db->getQuery(true);
  145. $query->select('*')
  146. ->from($db->quoteName('#__history'))
  147. ->where($db->quoteName('item_id') . ' = :item_id')
  148. ->where($db->quoteName('sha1_hash') . ' = :sha1_hash')
  149. ->bind(':item_id', $itemId, ParameterType::STRING)
  150. ->bind(':sha1_hash', $sha1Hash);
  151. $query->setLimit(1);
  152. $db->setQuery($query);
  153. return $db->loadObject();
  154. }
  155. /**
  156. * Utility method to remove the oldest versions of an item, saving only the most recent versions.
  157. *
  158. * @param integer $maxVersions The maximum number of versions to save. All others will be deleted.
  159. *
  160. * @return boolean true on success, false on failure.
  161. *
  162. * @since 3.2
  163. */
  164. public function deleteOldVersions($maxVersions)
  165. {
  166. $result = true;
  167. // Get the list of version_id values we want to save
  168. $db = $this->_db;
  169. $itemId = $this->get('item_id');
  170. $query = $db->getQuery(true);
  171. $query->select($db->quoteName('version_id'))
  172. ->from($db->quoteName('#__history'))
  173. ->where($db->quoteName('item_id') . ' = :item_id')
  174. ->where($db->quoteName('keep_forever') . ' != 1')
  175. ->bind(':item_id', $itemId, ParameterType::STRING)
  176. ->order($db->quoteName('save_date') . ' DESC ');
  177. $query->setLimit((int) $maxVersions);
  178. $db->setQuery($query);
  179. $idsToSave = $db->loadColumn(0);
  180. // Don't process delete query unless we have at least the maximum allowed versions
  181. if (\count($idsToSave) === (int) $maxVersions) {
  182. // Delete any rows not in our list and and not flagged to keep forever.
  183. $query = $db->getQuery(true);
  184. $query->delete($db->quoteName('#__history'))
  185. ->where($db->quoteName('item_id') . ' = :item_id')
  186. ->whereNotIn($db->quoteName('version_id'), $idsToSave)
  187. ->where($db->quoteName('keep_forever') . ' != 1')
  188. ->bind(':item_id', $itemId, ParameterType::STRING);
  189. $db->setQuery($query);
  190. $result = (bool) $db->execute();
  191. }
  192. return $result;
  193. }
  194. }