Issues (84)

src/EncryptedDBFile.php (1 issue)

Labels
1
<?php
2
3
namespace LeKoala\Encrypt;
4
5
use Exception;
6
use SilverStripe\Assets\File;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Core\Config\Config;
9
use SilverStripe\Versioned\Versioned;
10
use ParagonIE\CipherSweet\CipherSweet;
11
use ParagonIE\CipherSweet\EncryptedFile;
0 ignored issues
show
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...
12
use SilverStripe\Assets\Flysystem\FlysystemAssetStore;
13
use SilverStripe\Core\Extension;
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 Extension
22
{
23
    /**
24
     * @var EncryptedFile
25
     */
26
    protected static $encryptionEngine;
27
28
    /**
29
     * @var array<string,string>
30
     */
31
    private static $db = [
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) {
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) {
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) {
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,
159
            // it's a pseudo protocol that always creates a new random temp file when used.
160
            $output = fopen('php://temp', 'wb');
161
            if (!$output) {
162
                throw new Exception("Failed to decrypt stream");
163
            }
164
            $success =  $encFile->encryptStream($stream, $output);
165
            if (!$success) {
166
                throw new Exception("Failed to encrypt stream");
167
            }
168
            // dont forget to rewind the stream !
169
            rewind($output);
170
171
            // This is really ugly, see https://github.com/silverstripe/silverstripe-assets/issues/467
172
            $configFlag = FlysystemAssetStore::config()->keep_empty_dirs;
173
            Config::modify()->set(FlysystemAssetStore::class, 'keep_empty_dirs', true);
174
            $fileResult = $this->owner->setFromStream($output, $this->owner->getFilename());
175
            // Mark as encrypted in db
176
            $this->updateEncryptionStatus(true, $write);
177
            Config::modify()->set(FlysystemAssetStore::class, 'keep_empty_dirs', $configFlag);
178
179
            return true;
180
        }
181
182
        if ($this->owner->Encrypted != $isEncrypted) {
183
            $this->updateEncryptionStatus($isEncrypted, $write);
184
        }
185
186
        return $isEncrypted;
187
    }
188
}
189