PageRenderTime 60ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/filesystem/AssetControlExtension.php

http://github.com/silverstripe/sapphire
PHP | 299 lines | 142 code | 34 blank | 123 comment | 19 complexity | 1bbc692ccd2c133f052fb3b9e5b92d58 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. <?php
  2. namespace SilverStripe\Filesystem;
  3. use Injector;
  4. use SilverStripe\Filesystem\Storage\AssetStore;
  5. use SilverStripe\Filesystem\Storage\DBFile;
  6. use SilverStripe\ORM\DataObject;
  7. use SilverStripe\ORM\Versioning\Versioned;
  8. use SilverStripe\ORM\DataExtension;
  9. use SilverStripe\Security\Member;
  10. /**
  11. * This class provides the necessary business logic to ensure that any assets attached
  12. * to a record are safely deleted, published, or protected during certain operations.
  13. *
  14. * This class will respect the canView() of each object, and will use it to determine
  15. * whether or not public users can access attached assets. Public and live records
  16. * will have their assets promoted to the public store.
  17. *
  18. * Assets which exist only on non-live stages will be protected.
  19. *
  20. * Assets which are no longer referenced will be flushed via explicit delete calls
  21. * to the underlying filesystem.
  22. *
  23. * @property DataObject|Versioned $owner A {@see DataObject}, potentially decorated with {@see Versioned} extension.
  24. */
  25. class AssetControlExtension extends DataExtension
  26. {
  27. /**
  28. * When archiving versioned dataobjects, should assets be archived with them?
  29. * If false, assets will be deleted when the dataobject is archived.
  30. * If true, assets will be instead moved to the protected store, and can be
  31. * restored when the dataobject is restored from archive.
  32. *
  33. * Note that this does not affect the archiving of the actual database record in any way,
  34. * only the physical file.
  35. *
  36. * Unversioned dataobjects will ignore this option and always delete attached
  37. * assets on deletion.
  38. *
  39. * @config
  40. * @var bool
  41. */
  42. private static $keep_archived_assets = false;
  43. /**
  44. * Ensure that deletes records remove their underlying file assets, without affecting
  45. * other staged records.
  46. */
  47. public function onAfterDelete()
  48. {
  49. // Prepare blank manipulation
  50. $manipulations = new AssetManipulationList();
  51. // Add all assets for deletion
  52. $this->addAssetsFromRecord($manipulations, $this->owner, AssetManipulationList::STATE_DELETED);
  53. // Whitelist assets that exist in other stages
  54. $this->addAssetsFromOtherStages($manipulations);
  55. // Apply visibility rules based on the final manipulation
  56. $this->processManipulation($manipulations);
  57. }
  58. /**
  59. * Ensure that changes to records flush overwritten files, and update the visibility
  60. * of other assets.
  61. */
  62. public function onBeforeWrite()
  63. {
  64. // Prepare blank manipulation
  65. $manipulations = new AssetManipulationList();
  66. // Mark overwritten object as deleted
  67. if($this->owner->isInDB()) {
  68. $priorRecord = DataObject::get(get_class($this->owner))->byID($this->owner->ID);
  69. if($priorRecord) {
  70. $this->addAssetsFromRecord($manipulations, $priorRecord, AssetManipulationList::STATE_DELETED);
  71. }
  72. }
  73. // Add assets from new record with the correct visibility rules
  74. $state = $this->getRecordState($this->owner);
  75. $this->addAssetsFromRecord($manipulations, $this->owner, $state);
  76. // Whitelist assets that exist in other stages
  77. $this->addAssetsFromOtherStages($manipulations);
  78. // Apply visibility rules based on the final manipulation
  79. $this->processManipulation($manipulations);
  80. }
  81. /**
  82. * Check default state of this record
  83. *
  84. * @param DataObject $record
  85. * @return string One of AssetManipulationList::STATE_* constants
  86. */
  87. protected function getRecordState($record) {
  88. if($this->isVersioned()) {
  89. // Check stage this record belongs to
  90. $stage = $record->getSourceQueryParam('Versioned.stage') ?: Versioned::get_stage();
  91. // Non-live stages are automatically non-public
  92. if($stage !== Versioned::LIVE) {
  93. return AssetManipulationList::STATE_PROTECTED;
  94. }
  95. }
  96. // Check if canView permits anonymous viewers
  97. return $record->canView(Member::create())
  98. ? AssetManipulationList::STATE_PUBLIC
  99. : AssetManipulationList::STATE_PROTECTED;
  100. }
  101. /**
  102. * Given a set of asset manipulations, trigger any necessary publish, protect, or
  103. * delete actions on each asset.
  104. *
  105. * @param AssetManipulationList $manipulations
  106. */
  107. protected function processManipulation(AssetManipulationList $manipulations)
  108. {
  109. // When deleting from stage then check if we should archive assets
  110. $archive = $this->owner->config()->keep_archived_assets;
  111. // Publish assets
  112. $this->publishAll($manipulations->getPublicAssets());
  113. // Protect assets
  114. $this->protectAll($manipulations->getProtectedAssets());
  115. // Check deletion policy
  116. $deletedAssets = $manipulations->getDeletedAssets();
  117. if ($archive && $this->isVersioned()) {
  118. // Archived assets are kept protected
  119. $this->protectAll($deletedAssets);
  120. } else {
  121. // Otherwise remove all assets
  122. $this->deleteAll($deletedAssets);
  123. }
  124. }
  125. /**
  126. * Checks all stages other than the current stage, and check the visibility
  127. * of assets attached to those records.
  128. *
  129. * @param AssetManipulationList $manipulation Set of manipulations to add assets to
  130. */
  131. protected function addAssetsFromOtherStages(AssetManipulationList $manipulation)
  132. {
  133. // Skip unversioned or unsaved assets
  134. if(!$this->isVersioned() || !$this->owner->isInDB()) {
  135. return;
  136. }
  137. // Unauthenticated member to use for checking visibility
  138. $baseClass = $this->owner->baseClass();
  139. $baseTable = $this->owner->baseTable();
  140. $filter = array("\"{$baseTable}\".\"ID\"" => $this->owner->ID);
  141. $stages = $this->owner->getVersionedStages(); // {@see Versioned::getVersionedStages}
  142. foreach ($stages as $stage) {
  143. // Skip current stage; These should be handled explicitly
  144. if($stage === Versioned::get_stage()) {
  145. continue;
  146. }
  147. // Check if record exists in this stage
  148. $record = Versioned::get_one_by_stage($baseClass, $stage, $filter);
  149. if (!$record) {
  150. continue;
  151. }
  152. // Check visibility of this record, and record all attached assets
  153. $state = $this->getRecordState($record);
  154. $this->addAssetsFromRecord($manipulation, $record, $state);
  155. }
  156. }
  157. /**
  158. * Given a record, add all assets it contains to the given manipulation.
  159. * State can be declared for this record, otherwise the underlying DataObject
  160. * will be queried for canView() to see if those assets are public
  161. *
  162. * @param AssetManipulationList $manipulation Set of manipulations to add assets to
  163. * @param DataObject $record Record
  164. * @param string $state One of AssetManipulationList::STATE_* constant values.
  165. */
  166. protected function addAssetsFromRecord(AssetManipulationList $manipulation, DataObject $record, $state)
  167. {
  168. // Find all assets attached to this record
  169. $assets = $this->findAssets($record);
  170. if (empty($assets)) {
  171. return;
  172. }
  173. // Add all assets to this stage
  174. foreach ($assets as $asset) {
  175. $manipulation->addAsset($asset, $state);
  176. }
  177. }
  178. /**
  179. * Return a list of all tuples attached to this dataobject
  180. * Note: Variants are excluded
  181. *
  182. * @param DataObject $record to search
  183. * @return array
  184. */
  185. protected function findAssets(DataObject $record)
  186. {
  187. // Search for dbfile instances
  188. $files = array();
  189. foreach ($record->db() as $field => $db) {
  190. $fieldObj = $record->$field;
  191. if(!is_object($fieldObj) || !($record->$field instanceof DBFile)) {
  192. continue;
  193. }
  194. // Omit variant and merge with set
  195. $next = $record->dbObject($field)->getValue();
  196. unset($next['Variant']);
  197. if ($next) {
  198. $files[] = $next;
  199. }
  200. }
  201. // De-dupe
  202. return array_map("unserialize", array_unique(array_map("serialize", $files)));
  203. }
  204. /**
  205. * Determine if {@see Versioned) extension rules should be applied to this object
  206. *
  207. * @return bool
  208. */
  209. protected function isVersioned()
  210. {
  211. return $this->owner->has_extension('SilverStripe\\ORM\\Versioning\\Versioned') && class_exists('SilverStripe\\ORM\\Versioning\\Versioned');
  212. }
  213. /**
  214. * Delete all assets in the tuple list
  215. *
  216. * @param array $assets
  217. */
  218. protected function deleteAll($assets)
  219. {
  220. if (empty($assets)) {
  221. return;
  222. }
  223. $store = $this->getAssetStore();
  224. foreach ($assets as $asset) {
  225. $store->delete($asset['Filename'], $asset['Hash']);
  226. }
  227. }
  228. /**
  229. * Move all assets in the list to the public store
  230. *
  231. * @param array $assets
  232. */
  233. protected function publishAll($assets)
  234. {
  235. if (empty($assets)) {
  236. return;
  237. }
  238. $store = $this->getAssetStore();
  239. foreach ($assets as $asset) {
  240. $store->publish($asset['Filename'], $asset['Hash']);
  241. }
  242. }
  243. /**
  244. * Move all assets in the list to the protected store
  245. *
  246. * @param array $assets
  247. */
  248. protected function protectAll($assets)
  249. {
  250. if (empty($assets)) {
  251. return;
  252. }
  253. $store = $this->getAssetStore();
  254. foreach ($assets as $asset) {
  255. $store->protect($asset['Filename'], $asset['Hash']);
  256. }
  257. }
  258. /**
  259. * @return AssetStore
  260. */
  261. protected function getAssetStore()
  262. {
  263. return Injector::inst()->get('AssetStore');
  264. }
  265. }