1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* An extension that adds the ability to replace files with new uploads, and |
4
|
|
|
* view and roll back to existing versions. |
5
|
|
|
* |
6
|
|
|
* @package silverstripe-versionedfiles |
7
|
|
|
*/ |
8
|
|
|
class VersionedFileExtension extends DataExtension |
9
|
|
|
{ |
10
|
|
|
private static $has_one = array('CurrentVersion' => 'FileVersion'); |
11
|
|
|
private static $has_many = array('Versions' => 'FileVersion'); |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* @param FieldSet $fields |
15
|
|
|
*/ |
16
|
|
|
public function updateCMSFields(FieldList $fields) |
17
|
|
|
{ |
18
|
|
|
if ($this->owner instanceof Folder || !$this->owner->ID) { |
|
|
|
|
19
|
|
|
return; |
20
|
|
|
} |
21
|
|
|
|
22
|
|
|
$fields->addFieldToTab('Root.Main', new ReadonlyField( |
23
|
|
|
'VersionNumber', |
24
|
|
|
_t('VersionedFiles.CURRENTVERSION', 'Current Version') |
25
|
|
|
), 'Created'); |
26
|
|
|
|
27
|
|
|
|
28
|
|
|
// History |
29
|
|
|
|
30
|
|
|
$gridFieldConfig = GridFieldConfig::create()->addComponents( |
31
|
|
|
new GridFieldToolbarHeader(), |
32
|
|
|
//new GridFieldFilterHeader(), |
33
|
|
|
new GridFieldSortableHeader(), |
34
|
|
|
new GridFieldDataColumns(), |
35
|
|
|
new GridFieldPaginator(15), |
36
|
|
|
new GridFieldViewButton(), |
37
|
|
|
//new GridFieldDeleteAction(), |
38
|
|
|
new GridFieldDetailForm() |
39
|
|
|
); |
40
|
|
|
|
41
|
|
|
$gridField = new GridField('Versions', 'Versions', $this->owner->Versions(), $gridFieldConfig); |
42
|
|
|
$columns = $gridField->getConfig()->getComponentByType('GridFieldDataColumns'); |
43
|
|
|
$columns->setDisplayFields(array( |
44
|
|
|
'VersionNumber' => _t('VersionedFiles.VERSIONNUMBER', 'Version Number'), |
45
|
|
|
'Creator.Name' => _t('VersionedFiles.CREATOR', 'Creator'), |
46
|
|
|
'Created' => _t('VersionedFiles.DATE', 'Date'), |
47
|
|
|
'Link' => _t('VersionedFiles.LINK', 'Link'), |
48
|
|
|
'IsCurrent' => _t('VersionedFiles.ISCURRENT', 'Is Current') |
49
|
|
|
)); |
50
|
|
|
|
51
|
|
|
$columns->setFieldCasting(array( |
52
|
|
|
'Created' => 'Date->Nice' |
53
|
|
|
)); |
54
|
|
|
|
55
|
|
|
$columns->setFieldFormatting(array( |
56
|
|
|
'Link' => '<a href=\"$URL\" target=\"_blank\">$Name</a>', |
57
|
|
|
'IsCurrent' => '{$IsCurrent()->Nice()}', |
58
|
|
|
'Created' => '{$obj(\'Created\')->Nice()}' |
59
|
|
|
)); |
60
|
|
|
|
61
|
|
|
// history |
62
|
|
|
|
63
|
|
|
$versions = $this->owner->Versions( |
64
|
|
|
sprintf('"VersionNumber" <> %d', $this->getVersionNumber()) |
65
|
|
|
); |
66
|
|
|
|
67
|
|
|
if ($versions && $versions->Count() && $this->owner->canEdit()) { |
68
|
|
|
$fields->addFieldToTab('Root.History', new HeaderField('RollbackHeader', _t('VersionedFiles.ROLLBACKPREVVERSION', 'Rollback to a Previous Version'))); |
69
|
|
|
$fields->addFieldToTab('Root.History', $versionDropdown = new DropdownField('PreviousVersion', '', $versions->map('VersionNumber'))); |
70
|
|
|
$versionDropdown->setEmptyString(_t('VersionedFiles.SELECTAVERSION', '(Select a Version)')); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
$fields->addFieldToTab('Root.History', $gridField); |
74
|
|
|
|
75
|
|
|
// Replace |
76
|
|
|
if (!$this->owner->config()->get('disableReplaceTab')) { |
77
|
|
|
if (!$this->owner->canEdit()) { |
78
|
|
|
return; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
$folder = $this->owner->Parent(); |
|
|
|
|
82
|
|
|
$uploadField = new VersionedFileUploadField('ReplacementFile', ''); |
83
|
|
|
$uploadField->setConfig('previewMaxWidth', 40); |
84
|
|
|
$uploadField->setConfig('previewMaxHeight', 30); |
85
|
|
|
$uploadField->setConfig('allowedMaxFileNumber', 1); |
86
|
|
|
$uploadField->addExtraClass('ss-assetuploadfield'); |
87
|
|
|
$uploadField->removeExtraClass('ss-uploadfield'); |
88
|
|
|
$uploadField->setTemplate('VersionedFileUploadField'); |
89
|
|
|
$uploadField->currentVersionFile = $this->owner; |
|
|
|
|
90
|
|
|
$uploadField->relationAutoSetting = false; |
91
|
|
|
$uploadField->setOverwriteWarning(false); |
92
|
|
|
|
93
|
|
|
if ($folder->exists() && $folder->getFilename()) { |
94
|
|
|
$path = preg_replace('/^' . ASSETS_DIR . '\//', '', $folder->getFilename()); |
95
|
|
|
$uploadField->setFolderName($path); |
96
|
|
|
} else { |
97
|
|
|
$uploadField->setFolderName(ASSETS_DIR); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
// set the valid extensions to only that of the original file |
101
|
|
|
$ext = strtolower($this->owner->Extension); |
|
|
|
|
102
|
|
|
$uploadField->getValidator()->setAllowedExtensions(array($ext)); |
103
|
|
|
|
104
|
|
|
// css / js requirements for asset admin style file uploader |
105
|
|
|
Requirements::javascript(FRAMEWORK_DIR . '/javascript/AssetUploadField.js'); |
106
|
|
|
Requirements::css(FRAMEWORK_DIR . '/css/AssetUploadField.css'); |
107
|
|
|
|
108
|
|
|
$fields->addFieldToTab('Root.Replace', $uploadField); |
109
|
|
|
|
110
|
|
|
|
111
|
|
|
$sameTypeMessage = sprintf(_t( |
112
|
|
|
'VersionedFiles.SAMETYPEMESSAGE', |
113
|
|
|
'You may only replace this file with another of the same type: .%s' |
114
|
|
|
), $this->owner->getExtension()); |
|
|
|
|
115
|
|
|
|
116
|
|
|
$fields->addFieldToTab('Root.Replace', new LiteralField('SameTypeMessage', "<p>$sameTypeMessage</p>")); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
return; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Creates the initial version when the file is created, as well as updating |
124
|
|
|
* the version records when the parent file is moved. |
125
|
|
|
*/ |
126
|
|
|
public function onAfterWrite() |
127
|
|
|
{ |
128
|
|
|
$changed = $this->owner->getChangedFields(true, 2); |
129
|
|
|
|
130
|
|
|
if (!$this->owner instanceof Folder && !$this->owner->CurrentVersionID) { |
|
|
|
|
131
|
|
|
$this->createVersion(); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
if (array_key_exists('Filename', $changed)) { |
135
|
|
|
if ($changed['Filename']['before'] == null) { |
136
|
|
|
return; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
$oldDirname = '/' . trim(dirname($changed['Filename']['before']), '/'); |
140
|
|
|
$newDirname = '/' . trim(dirname($changed['Filename']['after']), '/'); |
141
|
|
|
|
142
|
|
|
if ($oldDirname == $newDirname) { |
143
|
|
|
return; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
// First move the _versions directory across. |
147
|
|
|
$versionsDir = FileVersion::VERSION_FOLDER . '/' . $this->owner->ID; |
|
|
|
|
148
|
|
|
$oldVersions = BASE_PATH . $oldDirname . '/' . $versionsDir; |
149
|
|
|
$newVersions = BASE_PATH . $newDirname . '/' . $versionsDir; |
150
|
|
|
|
151
|
|
|
if (is_dir($oldVersions)) { |
152
|
|
|
if (!is_dir($newVersions)) { |
153
|
|
|
mkdir($newVersions, Config::inst()->get('Filesystem', 'folder_create_mask'), true); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
if (!is_dir($newVersions)) { |
157
|
|
|
rename($oldVersions, $newVersions); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
// Then update individual version records to point to the new |
162
|
|
|
// location. |
163
|
|
|
foreach ($this->owner->Versions() as $version) { |
164
|
|
|
if (strpos($version->Filename, $oldDirname) === 0) { |
165
|
|
|
$version->Filename = $newDirname . substr($version->Filename, strlen($oldDirname)); |
166
|
|
|
$version->write(); |
167
|
|
|
} |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Since AssetAdmin does not use {@link onBeforeWrite}, onAfterUpload is |
174
|
|
|
* also needed. |
175
|
|
|
* |
176
|
|
|
* @uses onAfterWrite() |
177
|
|
|
*/ |
178
|
|
|
public function onAfterUpload() |
179
|
|
|
{ |
180
|
|
|
$this->onAfterWrite(); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Deletes all saved version of the file as well as the file itself. |
186
|
|
|
*/ |
187
|
|
|
public function onBeforeDelete() |
188
|
|
|
{ |
189
|
|
|
$currentVersion = $this->owner->CurrentVersion(); |
190
|
|
|
|
191
|
|
|
// make sure there actually is a current version, otherwise we're going |
192
|
|
|
// to end up deleting a bunch of incorrect stuff! |
193
|
|
|
if ($currentVersion && $currentVersion->ID > 0) { |
194
|
|
|
$folder = dirname($this->owner->CurrentVersion()->getFullPath()); |
195
|
|
|
|
196
|
|
|
if ($versions = $this->owner->Versions()) { |
197
|
|
|
foreach ($versions as $version) { |
198
|
|
|
$version->delete(); |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
if (is_dir($folder)) { |
203
|
|
|
Filesystem::removeFolder($folder); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
// If the _versions folder is now empty - because there are no other versions of any assets - then we can |
207
|
|
|
// delete the _versions folder |
208
|
|
|
$folder = dirname($folder); // We want the parent of $folder, as we just deleted $folder |
209
|
|
|
if (is_dir($folder)) { |
210
|
|
|
$dir = opendir($folder); |
211
|
|
|
$foundOtherFile = false; |
212
|
|
|
|
213
|
|
|
while ($file = readdir($dir)) { |
214
|
|
|
if (($file == '.' || $file == '..')) { |
215
|
|
|
continue; |
216
|
|
|
} |
217
|
|
|
$foundOtherFile = true; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
if (!$foundOtherFile) { |
221
|
|
|
// No other files found, so we can delete the _versions folder |
222
|
|
|
rmdir($folder); |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* @return int |
231
|
|
|
*/ |
232
|
|
|
public function getVersionNumber() |
233
|
|
|
{ |
234
|
|
|
if ($this->owner->CurrentVersionID) { |
|
|
|
|
235
|
|
|
return $this->owner->CurrentVersion()->VersionNumber; |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* @param int $version |
242
|
|
|
*/ |
243
|
|
|
public function setVersionNumber($version) |
244
|
|
|
{ |
245
|
|
|
$fileVersion = DataObject::get_one('FileVersion', sprintf( |
246
|
|
|
'"FileID" = %d AND "VersionNumber" = %d', $this->owner->ID, $version |
|
|
|
|
247
|
|
|
)); |
248
|
|
|
|
249
|
|
|
if (!$fileVersion) { |
250
|
|
|
throw new Exception( |
251
|
|
|
"Could not get version #$version of file #{$this->owner->ID}" |
252
|
|
|
); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
$versionPath = Controller::join_links( |
256
|
|
|
Director::baseFolder(), $fileVersion->Filename |
257
|
|
|
); |
258
|
|
|
$currentPath = $this->owner->getFullPath(); |
259
|
|
|
|
260
|
|
|
if (!copy($versionPath, $currentPath)) { |
261
|
|
|
throw new Exception( |
262
|
|
|
"Could not replace file #{$this->owner->ID} with version #$version." |
263
|
|
|
); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
$this->owner->CurrentVersionID = $fileVersion->ID; |
|
|
|
|
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Handles rolling back to a selected version on save. |
271
|
|
|
* |
272
|
|
|
* @param int $version |
273
|
|
|
*/ |
274
|
|
|
public function savePreviousVersion($version) |
275
|
|
|
{ |
276
|
|
|
if (!is_numeric($version)) { |
277
|
|
|
return; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
try { |
281
|
|
|
$this->setVersionNumber($version); |
282
|
|
|
$this->owner->write(); |
283
|
|
|
} catch (Exception $e) { |
284
|
|
|
throw new ValidationException(new ValidationResult( |
285
|
|
|
false, |
286
|
|
|
"Could not replace file #{$this->owner->ID} with version #$version." |
|
|
|
|
287
|
|
|
)); |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Creates a new file version and sets it as the current version. |
294
|
|
|
* |
295
|
|
|
* @return boolean Whether file version is created |
296
|
|
|
*/ |
297
|
|
|
public function createVersion() |
298
|
|
|
{ |
299
|
|
|
if (!file_exists($this->owner->getFullPath())) { |
300
|
|
|
return false; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
$version = new FileVersion(); |
304
|
|
|
$version->FileID = $this->owner->ID; |
|
|
|
|
305
|
|
|
$version->write(); |
306
|
|
|
|
307
|
|
|
$this->owner->CurrentVersionID = $version->ID; |
|
|
|
|
308
|
|
|
return (bool) $this->owner->write(); |
309
|
|
|
} |
310
|
|
|
} |
311
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.