EncryptedDBFile::sendDecryptedFile()   B
last analyzed

Complexity

Conditions 7
Paths 12

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 20
c 4
b 1
f 0
dl 0
loc 31
rs 8.6666
cc 7
nc 12
nop 0
1
<?php
2
3
namespace LeKoala\Encrypt;
4
5
use Exception;
6
use SilverStripe\Assets\File;
7
use SilverStripe\Control\Director;
8
use SilverStripe\ORM\DataExtension;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Versioned\Versioned;
11
use ParagonIE\CipherSweet\CipherSweet;
12
use ParagonIE\CipherSweet\EncryptedFile;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, LeKoala\Encrypt\EncryptedFile. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
13
use SilverStripe\Assets\Flysystem\FlysystemAssetStore;
14
15
/**
16
 * Safe and encrypted content file
17
 * Also make sure that files are not public assets! => use htaccess
18
 * @property bool $Encrypted
19
 * @property File&EncryptedDBFile $owner
20
 */
21
class EncryptedDBFile extends DataExtension
22
{
23
    /**
24
     * @var EncryptedFile
25
     */
26
    protected static $encryptionEngine;
27
28
    /**
29
     * @var array<string,string>
30
     */
31
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
32
        "Encrypted" => "Boolean",
33
    ];
34
35
    /**
36
     * @return string|bool
37
     */
38
    public function getDecryptionLink()
39
    {
40
        $data = [
41
            "ID" => $this->owner->ID,
42
            "Hash" => substr($this->owner->File->Hash, 0, 10),
43
        ];
44
        $url = "__decrypt/?" . http_build_query($data);
45
        return Director::absoluteURL($url);
46
    }
47
48
    /**
49
     * Check if the actual file on the filesystem is encrypted
50
     * You might also use the Encrypted field that should be accurate
51
     *
52
     * @return boolean
53
     */
54
    public function isEncrypted()
55
    {
56
        $stream = $this->owner->getStream();
57
        if (!$stream) {
0 ignored issues
show
introduced by
$stream is of type resource, thus it always evaluated to false.
Loading history...
58
            return false;
59
        }
60
        $encFile = EncryptHelper::getEncryptedFileInstance();
61
        return $encFile->isStreamEncrypted($stream);
62
    }
63
64
    /**
65
     * @param boolean $forceStatus
66
     * @param boolean $write
67
     * @return boolean
68
     */
69
    public function updateEncryptionStatus($forceStatus = null, $write = true)
70
    {
71
        if ($forceStatus !== null) {
72
            $this->owner->Encrypted = (bool)$forceStatus;
73
        } else {
74
            if ($this->isEncrypted()) {
75
                $this->owner->Encrypted = true;
76
            } else {
77
                $this->owner->Encrypted = false;
78
            }
79
        }
80
        if ($write) {
81
            if (class_exists(Versioned::class) && $this->owner->hasExtension(Versioned::class)) {
82
                $this->owner->writeWithoutVersion();
83
            } else {
84
                $this->owner->write();
85
            }
86
        }
87
        return $this->owner->Encrypted;
88
    }
89
90
    /**
91
     * Output file using regular php
92
     * Does not send headers, see EncryptHelper::sendDecryptedFile
93
     *
94
     * @throws Exception
95
     * @return void
96
     */
97
    public function sendDecryptedFile()
98
    {
99
        if (ob_get_level()) {
100
            ob_end_clean();
101
        }
102
        $stream = $this->owner->getStream();
103
        if (!$stream) {
0 ignored issues
show
introduced by
$stream is of type resource, thus it always evaluated to false.
Loading history...
104
            throw new Exception("File not found");
105
        }
106
        if ($this->owner->Encrypted) {
107
            $encFile = EncryptHelper::getEncryptedFileInstance();
108
            $output = fopen('php://temp', 'w+b');
109
            if (!$output) {
110
                throw new Exception("Failed to open output stream");
111
            }
112
113
            // We need to decrypt stream
114
            if ($encFile->isStreamEncrypted($stream)) {
115
                $success = $encFile->decryptStream($stream, $output);
116
                if (!$success) {
117
                    throw new Exception("Failed to decrypt stream");
118
                }
119
120
                // Rewind first
121
                rewind($output);
122
                fpassthru($output);
123
            } else {
124
                fpassthru($stream);
125
            }
126
        } else {
127
            fpassthru($stream);
128
        }
129
    }
130
131
    /**
132
     * Files are not encrypted automatically
133
     * Call this method to encrypt them
134
     *
135
     * @throws Exception
136
     * @param bool $write
137
     * @return bool
138
     */
139
    public function encryptFileIfNeeded($write = true)
140
    {
141
        // Already mark as encrypted
142
        if ($this->owner->Encrypted) {
143
            return true;
144
        }
145
        if (!$this->owner->exists()) {
146
            throw new Exception("File does not exist");
147
        }
148
        $stream = $this->owner->getStream();
149
        if (!$stream) {
0 ignored issues
show
introduced by
$stream is of type resource, thus it always evaluated to false.
Loading history...
150
            throw new Exception("Failed to get stream");
151
        }
152
153
        $encFile = EncryptHelper::getEncryptedFileInstance();
154
        $isEncrypted = $encFile->isStreamEncrypted($stream);
155
156
        // It's not yet encrypted
157
        if (!$isEncrypted) {
158
            // php://temp is not a file path, it's a pseudo protocol that always creates a new random temp file when used.
159
            $output = fopen('php://temp', 'wb');
160
            if (!$output) {
161
                throw new Exception("Failed to decrypt stream");
162
            }
163
            $success =  $encFile->encryptStream($stream, $output);
164
            if (!$success) {
165
                throw new Exception("Failed to encrypt stream");
166
            }
167
            // dont forget to rewind the stream !
168
            rewind($output);
169
170
            // This is really ugly, see https://github.com/silverstripe/silverstripe-assets/issues/467
171
            $configFlag = FlysystemAssetStore::config()->keep_empty_dirs;
172
            Config::modify()->set(FlysystemAssetStore::class, 'keep_empty_dirs', true);
173
            $fileResult = $this->owner->setFromStream($output, $this->owner->getFilename());
174
            // Mark as encrypted in db
175
            $this->updateEncryptionStatus(true, $write);
176
            Config::modify()->set(FlysystemAssetStore::class, 'keep_empty_dirs', $configFlag);
177
178
            return true;
179
        }
180
181
        if ($this->owner->Encrypted != $isEncrypted) {
182
            $this->updateEncryptionStatus($isEncrypted, $write);
183
        }
184
185
        return $isEncrypted;
186
    }
187
}
188