This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * This is added to all File objects for shared functionality. |
||
4 | * |
||
5 | * @author Mark Guinn <[email protected]> |
||
6 | * @date 01.10.2014 |
||
7 | * @package cloudassets |
||
8 | */ |
||
9 | class CloudFileExtension extends DataExtension |
||
10 | { |
||
11 | private static $db = array( |
||
12 | 'CloudStatus' => "Enum('Local,Live,Error','Local')", |
||
13 | 'CloudSize' => 'Int', |
||
14 | 'CloudMetaJson' => 'Text', // saves any bucket or file-type specific information |
||
15 | ); |
||
16 | |||
17 | /** @var File|CloudFileExtension */ |
||
18 | protected $owner; |
||
19 | |||
20 | private $inUpdate = false; |
||
21 | |||
22 | |||
23 | /** |
||
24 | * Handle renames |
||
25 | */ |
||
26 | public function onBeforeWrite() |
||
27 | { |
||
28 | $bucket = CloudAssets::inst()->map($this->owner->getFilename()); |
||
29 | if ($bucket) { |
||
30 | if (!$this->owner->isChanged('Filename')) { |
||
31 | return; |
||
32 | } |
||
33 | |||
34 | $changedFields = $this->owner->getChangedFields(); |
||
35 | $pathBefore = $changedFields['Filename']['before']; |
||
36 | $pathAfter = $changedFields['Filename']['after']; |
||
37 | |||
38 | // If the file or folder didn't exist before, don't rename - its created |
||
39 | if (!$pathBefore) { |
||
40 | return; |
||
41 | } |
||
42 | |||
43 | // Tell the remote to rename the file (or delete and recreate or whatever) |
||
44 | if ($this->owner->hasMethod('onBeforeCloudRename')) { |
||
45 | $this->owner->onBeforeCloudRename($pathBefore, $pathAfter); |
||
46 | } |
||
47 | CloudAssets::inst()->getLogger()->info("CloudAssets: Renaming $pathBefore to $pathAfter"); |
||
48 | $bucket->rename($this->owner, $pathBefore, $pathAfter); |
||
49 | if ($this->owner->hasMethod('onAfterCloudRename')) { |
||
50 | $this->owner->onAfterCloudRename($pathBefore, $pathAfter); |
||
51 | } |
||
52 | } |
||
53 | } |
||
54 | |||
55 | |||
56 | /** |
||
57 | * Update cloud status any time the file is written |
||
58 | */ |
||
59 | public function onAfterWrite() |
||
60 | { |
||
61 | $this->updateCloudStatus(); |
||
62 | } |
||
63 | |||
64 | |||
65 | /** |
||
66 | * Delete the file from the cloud (if it was ever there) |
||
67 | */ |
||
68 | public function onAfterDelete() |
||
69 | { |
||
70 | $bucket = CloudAssets::inst()->map($this->owner->getFilename()); |
||
71 | if ($bucket && !Config::inst()->get('CloudAssets', 'uploads_disabled')) { |
||
72 | if ($this->owner->hasMethod('onBeforeCloudDelete')) { |
||
73 | $this->owner->onBeforeCloudDelete(); |
||
74 | } |
||
75 | |||
76 | try { |
||
77 | CloudAssets::inst()->getLogger()->info("CloudAssets: deleting {$this->owner->getFilename()}"); |
||
78 | $bucket->delete($this->owner); |
||
79 | } catch (Exception $e) { |
||
80 | CloudAssets::inst()->getLogger()->error("CloudAssets: Failed bucket delete: " . $e->getMessage() . " for " . $this->owner->getFullPath()); |
||
81 | } |
||
82 | |||
83 | if ($this->owner->hasMethod('onAfterCloudDelete')) { |
||
84 | $this->owner->onAfterCloudDelete(); |
||
85 | } |
||
86 | } |
||
87 | } |
||
88 | |||
89 | |||
90 | /** |
||
91 | * Performs two functions: |
||
92 | * 1. Wraps this object in CloudFile (etc) by changing the classname if it should be and is not |
||
93 | * 2. Uploads the file to the cloud storage if it doesn't contain the placeholder |
||
94 | * |
||
95 | * @return File |
||
96 | */ |
||
97 | public function updateCloudStatus() |
||
98 | { |
||
99 | if ($this->inUpdate) { |
||
100 | return; |
||
101 | } |
||
102 | $this->inUpdate = true; |
||
103 | $cloud = CloudAssets::inst(); |
||
104 | |||
105 | // does this file fall under a cloud bucket? |
||
106 | $bucket = $cloud->map($this->owner->getFilename()); |
||
107 | if ($bucket) { |
||
108 | // does this file need to be wrapped? |
||
109 | $wrapClass = $cloud->getWrapperClass($this->owner->ClassName); |
||
110 | if (!empty($wrapClass)) { |
||
111 | if ($wrapClass != $this->owner->ClassName) { |
||
112 | $cloud->getLogger()->debug("CloudAssets: wrapping {$this->owner->ClassName} to $wrapClass. ID={$this->owner->ID}"); |
||
113 | $this->owner->ClassName = $wrapClass; |
||
114 | $this->owner->write(); |
||
115 | $wrapped = DataObject::get($wrapClass)->byID($this->owner->ID); |
||
116 | if ($wrapped->hasMethod('onAfterCloudWrap')) { |
||
117 | $wrapped->onAfterCloudWrap(); |
||
118 | } |
||
119 | } else { |
||
120 | $wrapped = $this->owner; |
||
121 | } |
||
122 | |||
123 | // does this file need to be uploaded to storage? |
||
124 | if ($wrapped->canBeInCloud() && $wrapped->isCloudPutNeeded() && !Config::inst()->get('CloudAssets', 'uploads_disabled')) { |
||
125 | try { |
||
126 | if ($wrapped->hasMethod('onBeforeCloudPut')) { |
||
127 | $wrapped->onBeforeCloudPut(); |
||
128 | } |
||
129 | $cloud->getLogger()->debug("CloudAssets: uploading file ".$wrapped->getFilename()); |
||
130 | $bucket->put($wrapped); |
||
131 | |||
132 | $wrapped->setCloudMeta('LastPut', time()); |
||
133 | $wrapped->CloudStatus = 'Live'; |
||
134 | $wrapped->CloudSize = filesize($this->owner->getFullPath()); |
||
135 | $wrapped->write(); |
||
136 | |||
137 | $wrapped->convertToPlaceholder(); |
||
138 | if ($wrapped->hasMethod('onAfterCloudPut')) { |
||
139 | $wrapped->onAfterCloudPut(); |
||
140 | } |
||
141 | } catch (Exception $e) { |
||
142 | $wrapped->CloudStatus = 'Error'; |
||
143 | $wrapped->write(); |
||
144 | $cloud->getLogger()->error("CloudAssets: Failed bucket upload: " . $e->getMessage() . " for " . $wrapped->getFullPath()); |
||
145 | // Fail silently for now. This will cause the local copy to be served. |
||
146 | } |
||
147 | } elseif ($wrapped->CloudStatus !== 'Live' && $wrapped->containsPlaceholder()) { |
||
148 | // If this is a duplicate file, update the status |
||
149 | // This shouldn't happen ever and won't happen often but when it does this will be helpful |
||
150 | $dup = File::get()->filter(array( |
||
151 | 'Filename' => $wrapped->Filename, |
||
152 | 'CloudStatus' => 'Live', |
||
153 | ))->first(); |
||
154 | |||
155 | if ($dup && $dup->exists()) { |
||
156 | $cloud->getLogger()->warn("CloudAssets: fixing status for duplicate file: {$wrapped->ID} and {$dup->ID}"); |
||
157 | $wrapped->CloudStatus = $dup->CloudStatus; |
||
158 | $wrapped->CloudSize = $dup->CloudSize; |
||
159 | $wrapped->CloudMetaJson = $dup->CloudMetaJson; |
||
160 | $wrapped->write(); |
||
161 | } |
||
162 | } |
||
163 | |||
164 | $this->inUpdate = false; |
||
165 | return $wrapped; |
||
166 | } |
||
167 | } |
||
168 | |||
169 | $this->inUpdate = false; |
||
170 | return $this->owner; |
||
171 | } |
||
172 | |||
173 | |||
174 | /** |
||
175 | * @return bool |
||
176 | */ |
||
177 | public function canBeInCloud() |
||
178 | { |
||
179 | if ($this->owner instanceof Folder) { |
||
180 | return false; |
||
181 | } |
||
182 | if (!file_exists($this->owner->getFullPath())) { |
||
183 | return false; |
||
184 | } |
||
185 | return true; |
||
186 | } |
||
187 | |||
188 | |||
189 | /** |
||
190 | * @return bool |
||
191 | */ |
||
192 | public function containsPlaceholder() |
||
193 | { |
||
194 | $placeholder = Config::inst()->get('CloudAssets', 'file_placeholder'); |
||
195 | $path = $this->owner->getFullPath(); |
||
196 | |||
197 | // check the size first to avoid reading crazy huge files into memory |
||
198 | return (file_exists($path) && filesize($path) == strlen($placeholder) && file_get_contents($path) == $placeholder); |
||
199 | } |
||
200 | |||
201 | |||
202 | /** |
||
203 | * Wipes out the contents of this file and replaces with placeholder text |
||
204 | */ |
||
205 | public function convertToPlaceholder() |
||
206 | { |
||
207 | $bucket = $this->getCloudBucket(); |
||
208 | if ($bucket && !$bucket->isLocalCopyEnabled()) { |
||
209 | $path = $this->owner->getFullPath(); |
||
210 | CloudAssets::inst()->getLogger()->debug("CloudAssets: converting $path to placeholder"); |
||
211 | Filesystem::makeFolder(dirname($path)); |
||
212 | file_put_contents($path, Config::inst()->get('CloudAssets', 'file_placeholder')); |
||
213 | clearstatcache(); |
||
214 | } |
||
215 | |||
216 | return $this->owner; |
||
217 | } |
||
218 | |||
219 | |||
220 | /** |
||
221 | * @return CloudBucket |
||
222 | */ |
||
223 | public function getCloudBucket() |
||
224 | { |
||
225 | return CloudAssets::inst()->map($this->owner); |
||
226 | } |
||
227 | |||
228 | |||
229 | /** |
||
230 | * @param int $linkType [optional] - see CloudBucket::LINK_XXX constants |
||
231 | * @return string |
||
232 | */ |
||
233 | public function getCloudURL($linkType = CloudBucket::LINK_SMART) |
||
234 | { |
||
235 | $bucket = $this->getCloudBucket(); |
||
236 | return $bucket ? $bucket->getLinkFor($this->owner, $linkType) : ''; |
||
237 | } |
||
238 | |||
239 | |||
240 | /** |
||
241 | * @param string $key [optional] - if not present returns the whole array |
||
242 | * @return array |
||
243 | */ |
||
244 | public function getCloudMeta($key = null) |
||
245 | { |
||
246 | $data = json_decode($this->owner->CloudMetaJson, true); |
||
247 | if (empty($data) || !is_array($data)) { |
||
248 | $data = array(); |
||
249 | } |
||
250 | |||
251 | if (!empty($key)) { |
||
252 | return isset($data[$key]) ? $data[$key] : null; |
||
253 | } else { |
||
254 | return $data; |
||
255 | } |
||
256 | } |
||
257 | |||
258 | |||
259 | /** |
||
260 | * @param string|array $key - passing an array as the first argument replaces the meta data entirely |
||
261 | * @param mixed $val |
||
262 | * @return File - chainable |
||
263 | */ |
||
264 | public function setCloudMeta($key, $val = null) |
||
265 | { |
||
266 | if (is_array($key)) { |
||
267 | $data = $key; |
||
268 | } else { |
||
269 | $data = $this->getCloudMeta(); |
||
270 | $data[$key] = $val; |
||
271 | } |
||
272 | |||
273 | $this->owner->CloudMetaJson = json_encode($data); |
||
274 | return $this->owner; |
||
275 | } |
||
276 | |||
277 | |||
278 | /** |
||
279 | * If this file is stored in the cloud, downloads the cloud |
||
280 | * copy and replaces whatever is local. |
||
281 | */ |
||
282 | public function downloadFromCloud() |
||
283 | { |
||
284 | if ($this->owner->CloudStatus === 'Live') { |
||
285 | $bucket = $this->owner->getCloudBucket(); |
||
286 | if ($bucket) { |
||
287 | $contents = $bucket->getContents($this->owner); |
||
288 | $path = $this->owner->getFullPath(); |
||
289 | Filesystem::makeFolder(dirname($path)); |
||
290 | CloudAssets::inst()->getLogger()->debug("CloudAssets: downloading $path from cloud (size=".strlen($contents).")"); |
||
291 | // if there was an error and we overwrote the local file with empty or null, it could delete the remote |
||
292 | // file as well. Better to err on the side of not writing locally when we should than that. |
||
293 | if (!empty($contents)) { |
||
294 | file_put_contents($path, $contents); |
||
295 | } |
||
296 | } |
||
297 | } |
||
298 | } |
||
299 | |||
300 | |||
301 | /** |
||
302 | * If the file is present in the database and the cloud but not |
||
303 | * locally, create a placeholder for it. This can happen in a lot |
||
304 | * of cases such as load balanced servers and local development. |
||
305 | */ |
||
306 | public function createLocalIfNeeded() |
||
307 | { |
||
308 | if ($this->owner->CloudStatus === 'Live') { |
||
309 | $bucket = $this->getCloudBucket(); |
||
310 | if ($bucket && $bucket->isLocalCopyEnabled()) { |
||
311 | if (!file_exists($this->owner->getFullPath()) || $this->containsPlaceholder()) { |
||
312 | try { |
||
313 | $this->downloadFromCloud(); |
||
314 | } catch (Exception $e) { |
||
315 | // I'm not sure what the correct behaviour is here |
||
316 | // Pretty sure it'd be better to have a broken image |
||
317 | // link than a 500 error though. |
||
318 | CloudAssets::inst()->getLogger()->error("CloudAssets: Failed bucket download: " . $e->getMessage() . " for " . $this->owner->getFullPath()); |
||
319 | } |
||
320 | } |
||
321 | } else { |
||
322 | if (!file_exists($this->owner->getFullPath())) { |
||
323 | $this->convertToPlaceholder(); |
||
324 | } |
||
325 | } |
||
326 | } |
||
327 | } |
||
328 | |||
329 | |||
330 | /** |
||
331 | * @return bool |
||
332 | */ |
||
333 | public function isCloudPutNeeded() |
||
334 | { |
||
335 | // we never want to upload the placeholder |
||
336 | if ($this->containsPlaceholder()) { |
||
337 | return false; |
||
338 | } |
||
339 | |||
340 | // we never want to upload an empty file |
||
341 | $path = $this->owner->getFullPath(); |
||
342 | if (!file_exists($path)) { |
||
343 | return false; |
||
344 | } |
||
345 | |||
346 | // we always want to upload if it's the first time |
||
347 | $lastPut = $this->getCloudMeta('LastPut'); |
||
348 | if (!$lastPut) { |
||
0 ignored issues
–
show
|
|||
349 | return true; |
||
350 | } |
||
351 | |||
352 | // additionally, we want to upload if the file has been changed or replaced |
||
353 | $mtime = filemtime($path); |
||
354 | if ($mtime > $lastPut) { |
||
355 | return true; |
||
356 | } |
||
357 | |||
358 | return false; |
||
359 | } |
||
360 | |||
361 | |||
362 | /** |
||
363 | * Returns true if the local file is not available |
||
364 | * @return bool |
||
365 | */ |
||
366 | public function isLocalMissing() |
||
367 | { |
||
368 | return !file_exists($this->owner->getFullPath()) || $this->containsPlaceholder(); |
||
369 | } |
||
370 | } |
||
371 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.