Passed
Push — master ( ffc3d5...a2ac04 )
by Russell
12:41
created

VerifiableController::renderJSON()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @author  Russell Michell 2018 <[email protected]>
5
 * @package silverstripe-verifiable
6
 */
7
8
namespace PhpTek\Verifiable\Controller;
9
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\HTTPRequest;
12
use SilverStripe\Versioned\Versioned;
13
use PhpTek\Verifiable\ORM\FieldType\ChainpointProof;
14
use SilverStripe\ORM\ValidationException;
15
16
/**
17
 * Accepts incoming requests for data verification e.g. from within the CMS
18
 * or framework's admin area, proxies them through {@link VerifiableService} and
19
 * sends them on their way.
20
 *
21
 * Will proxy validation requests to the currently configured backend for both
22
 * {@link SiteTree} and {@link DataObject} subclasses.
23
 *
24
 * @todo Take into account LastEdited and Created dates, outside of userland control
25
 * of verifiable_fields
26
 * @todo Rename to "VerifiableController"
27
 */
28
class VerifiableController extends Controller
29
{
30
    /**
31
     * No local proof found. Evidence that the record has been tampered-with.
32
     *
33
     * @var string
34
     */
35
    const STATUS_LOCAL_PROOF_NONE = 'Local Proof Not Found';
36
37
    /**
38
     * Invalid local proof found. Evidence that the record has been tampered-with.
39
     *
40
     * @var string
41
     */
42
    const STATUS_LOCAL_PROOF_INVALID = 'Local Proof Invalid';
43
44
    /**
45
     * Invalid local hash found. Evidence that the record has been tampered-with.
46
     *
47
     * @var string
48
     */
49
    const STATUS_LOCAL_HASH_INVALID = 'Local Hash Invalid';
50
51
    /**
52
     * Invalid or no matching remote proof found. Evidence that the record has been tampered-with.
53
     *
54
     * @var string
55
     */
56
    const STATUS_REMOTE_HASH_INVALID_NO_DATA = 'Remote Hash Not Found';
57
58
    /**
59
     * Invalid remote hash found. Evidence that the record has been tampered-with.
60
     *
61
     * @var string
62
     */
63
    const STATUS_REMOTE_HASH_INVALID_NO_HASH = 'Remote Hash Not Found';
64
65
    /**
66
     * Invalid UUID. Evidence that the record has been tampered-with.
67
     *
68
     * @var string
69
     */
70
    const STATUS_UUID_INVALID = 'Invalid UUID';
71
72
    /**
73
     * All checks passed. Submitted hash is verified.
74
     *
75
     * @var string
76
     */
77
    const STATUS_VERIFIED = 'Verified';
78
79
    /**
80
     * All checks passed. But submitted hash is not yet verified.
81
     *
82
     * @var string
83
     */
84
    const STATUS_UNVERIFIED = 'Unverified';
85
86
    /**
87
     * Some kind of upstream error.
88
     *
89
     * @var string
90
     */
91
    const STATUS_UPSTREAM_ERROR = 'Upstream Error';
92
93
    /**
94
     * @var array
95
     */
96
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
97
        'verifyhash',
98
    ];
99
100
    /**
101
     * Verify the integrity of arbitrary data by means of a single hash.
102
     *
103
     * Responds to URIs of the following prototype: /verifiable/verify/<model>/<ID>/<VID>
104
     * by echoing a JSON response for consumption by client-side logic.
105
     *
106
     * @param  HTTPRequest $request
107
     * @return void
108
     */
109
    public function verifyhash(HTTPRequest $request)
110
    {
111
        $class = $request->param('ClassName');
112
        $id = $request->param('ModelID');
113
        $version = $request->param('VersionID');
114
115
        if (empty($id) || !is_numeric($id) ||
116
                empty($version) || !is_numeric($version) ||
117
                empty($class)) {
118
            return $this->httpError(400, 'Bad request');
119
        }
120
121
        if (!$record = Versioned::get_version($class, $id, $version)) {
0 ignored issues
show
Bug introduced by
$version of type string is incompatible with the type integer expected by parameter $version of SilverStripe\Versioned\Versioned::get_version(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

121
        if (!$record = Versioned::get_version($class, $id, /** @scrutinizer ignore-type */ $version)) {
Loading history...
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of SilverStripe\Versioned\Versioned::get_version(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

121
        if (!$record = Versioned::get_version($class, /** @scrutinizer ignore-type */ $id, $version)) {
Loading history...
122
            return $this->httpError(400, 'Bad request');
123
        }
124
125
        try {
126
            $status = $this->getVerificationStatus($record, $record->getExtraByIndex());
0 ignored issues
show
Bug introduced by
The method getExtraByIndex() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

126
            $status = $this->getVerificationStatus($record, $record->/** @scrutinizer ignore-call */ getExtraByIndex());
Loading history...
127
        } catch (ValidationException $ex) {
128
            $status = self::STATUS_UPSTREAM_ERROR;
129
        }
130
131
        $response = json_encode([
132
            'RecordID' => "$record->RecordID",
0 ignored issues
show
Bug Best Practice introduced by
The property RecordID does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
133
            'Version' => "$record->Version",
0 ignored issues
show
Bug Best Practice introduced by
The property Version does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
134
            'Class' => get_class($record),
135
            'StatusNice' => $status,
136
            'StatusCode' => $this->getCodeMeta($status, 'code'),
137
            'StatusDefn' => $this->getCodeMeta($status, 'defn'),
138
            'SubmittedAt' => $record->dbObject('Proof')->getSubmittedAt(),
0 ignored issues
show
Bug introduced by
The method getSubmittedAt() does not exist on SilverStripe\ORM\FieldType\DBField. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

138
            'SubmittedAt' => $record->dbObject('Proof')->/** @scrutinizer ignore-call */ getSubmittedAt(),
Loading history...
139
            'SubmittedTo' => $record->dbObject('Extra')->getStoreAsArray(),
0 ignored issues
show
Bug introduced by
The method getStoreAsArray() does not exist on SilverStripe\ORM\FieldType\DBField. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

139
            'SubmittedTo' => $record->dbObject('Extra')->/** @scrutinizer ignore-call */ getStoreAsArray(),
Loading history...
140
        ], JSON_UNESCAPED_UNICODE);
141
142
        $this->renderJSON($response);
143
    }
144
145
    /**
146
     *
147
     * @param  string $key
148
     * @return mixed
149
     */
150
    private function getCodeMeta($status, $key)
151
    {
152
        $refl = new \ReflectionClass(__CLASS__);
153
        $const = array_search($status, $refl->getConstants());
154
        $keyJson = file_get_contents(realpath(__DIR__) . '/../../statuses.json');
155
        $keyMap = json_decode($keyJson, true);
156
        $defn = '';
157
158
        foreach ($keyMap as $map) {
159
            if (isset($map[$const])) {
160
                $defn = $map[$const];
161
            }
162
        }
163
164
        $data = [
165
            'code' => $const,
166
            'defn' => $defn,
167
        ];
168
169
        return isset($data[$key]) ? $data[$key] : $data;
170
171
    }
172
173
    /**
174
     * Gives us the current verification status of the given record. Takes into
175
     * account the state of the saved proof as well as by making a backend
176
     * verification call.
177
     *
178
     * For the ChainPoint Backend, the following process occurs:
179
     *
180
     * 1. Re-hash verifiable_fields as stored within the "Proof" field
181
     * 2. Assert that the record's "Proof" field is not empty
182
     * 3. Assert that the record's "Proof" field contains a valid proof
183
     * 4. Assert that the new hash exists in the record's "Proof" field
184
     * 5. Assert that hash_node_id for that proof returns a valid response from ChainPoint
185
     * 6. Assert that the returned data contains a matching hash for the new hash
186
     *
187
     * @param  DataObject $record
0 ignored issues
show
Bug introduced by
The type PhpTek\Verifiable\Controller\DataObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
188
     * @param  array      $nodes
189
     * @return string
190
     */
191
    public function getVerificationStatus($record, $nodes)
192
    {
193
        // Set some extra data on the service. In this case, the actual chainpoint
194
        // node addresses, used to submit hashes for the given $record
195
        $this->verifiableService->setExtra($nodes);
0 ignored issues
show
Bug introduced by
The method setExtra() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

195
        $this->verifiableService->/** @scrutinizer ignore-call */ 
196
                                  setExtra($nodes);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug Best Practice introduced by
The property verifiableService does not exist on PhpTek\Verifiable\Controller\VerifiableController. Since you implemented __get, consider adding a @property annotation.
Loading history...
196
197
        // Basic existence of proof (!!) check
198
        if (!$proof = $record->dbObject('Proof')) {
199
            return self::STATUS_LOCAL_PROOF_NONE;
200
        }
201
202
        // Basic proof validity check
203
        // @todo Beef this up to ensure that a basic regex is run over each to ensure it's all
204
        // not just gobbledygook
205
        if (!$proof->getHashIdNode() || !$proof->getHash() || !$proof->getSubmittedAt()) {
206
            return self::STATUS_LOCAL_PROOF_INVALID;
207
        }
208
209
        // Comparison check between locally stored proof, and re-hashed record data
210
        if ($proof->getHash() !== $reHash = $this->verifiableService->hash($record->normaliseData())) {
211
            return self::STATUS_LOCAL_HASH_INVALID;
212
        }
213
214
        // Remote verification check that local hash_node_id returns a valid response
215
        // Responds with a binary format proof
216
        $responseBinary = $this->verifiableService->read($proof->getHashIdNode());
217
218
        if ($responseBinary === '[]') {
219
            return self::STATUS_UUID_INVALID;
220
        }
221
222
        // Extract the "proof" component of the "binary" response to send for verification
223
        $responseBinaryProof = ChainpointProof::create()
224
                ->setValue($responseBinary)
225
                ->getProof();
226
        $responseVerify = $this->verifiableService->verify($responseBinaryProof);
227
228
        if ($responseVerify === '[]') {
229
            return self::STATUS_REMOTE_HASH_INVALID_NO_DATA;
230
        }
231
232
        // Compare returned hash matches the re-hash
233
        $responseProof = ChainpointProof::create()->setValue($responseVerify);
234
235
        if (!$responseProof->match($reHash)) {
236
            return self::STATUS_REMOTE_HASH_NO_HASH;
0 ignored issues
show
Bug introduced by
The constant PhpTek\Verifiable\Contro...TUS_REMOTE_HASH_NO_HASH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
237
        }
238
239
        if ($responseProof->getStatus() === 'verified') {
240
            return self::STATUS_VERIFIED;
241
        }
242
243
        return self::STATUS_UNVERIFIED;
244
    }
245
246
    /**
247
     * Properly return JSON, allowing consumers to render returned JSON correctly.
248
     *
249
     * @param  string $json
250
     * @return void
251
     */
252
    private function renderJSON(string $json)
253
    {
254
        header('Content-Type: application/json');
255
        echo $json;
256
        exit(0);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
257
    }
258
259
}
260