Issues (126)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

code/CloudFileExtension.php (1 issue)

Upgrade to new PHP Analysis Engine

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
0 ignored issues
show
This class has a complexity of 67 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

You can also find more detailed suggestions for refactoring in the “Code” section of your repository.

Loading history...
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) {
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