Passed
Push — master ( f7650f...4e759b )
by Russell
12:30
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
     * All local checks passed. Submitted hash is currently pending.
88
     *
89
     * @var string
90
     */
91
    const STATUS_PENDING = 'Pending';
92
93
    /**
94
     * Some kind of upstream error.
95
     *
96
     * @var string
97
     */
98
    const STATUS_UPSTREAM_ERROR = 'Upstream Error';
99
100
    /**
101
     * @var array
102
     */
103
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
104
        'verifyhash',
105
    ];
106
107
    /**
108
     * Verify the integrity of arbitrary data by means of a single hash.
109
     *
110
     * Responds to URIs of the following prototype: /verifiable/verify/<model>/<ID>/<VID>
111
     * by echoing a JSON response for consumption by client-side logic.
112
     *
113
     * @param  HTTPRequest $request
114
     * @return void
115
     */
116
    public function verifyhash(HTTPRequest $request)
117
    {
118
        $class = $request->param('ClassName');
119
        $id = $request->param('ModelID');
120
        $version = $request->param('VersionID');
121
122
        if (empty($id) || !is_numeric($id) ||
123
                empty($version) || !is_numeric($version) ||
124
                empty($class)) {
125
            return $this->httpError(400, 'Bad request');
126
        }
127
128
        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

128
        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

128
        if (!$record = Versioned::get_version($class, /** @scrutinizer ignore-type */ $id, $version)) {
Loading history...
129
            return $this->httpError(400, 'Bad request');
130
        }
131
132
        try {
133
            $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

133
            $status = $this->getVerificationStatus($record, $record->/** @scrutinizer ignore-call */ getExtraByIndex());
Loading history...
134
        } catch (ValidationException $ex) {
135
            $status = self::STATUS_UPSTREAM_ERROR;
136
        }
137
138
        $response = json_encode([
139
            '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...
140
            '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...
141
            'Class' => get_class($record),
142
            'Status' => $status,
143
            '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

143
            'SubmittedAt' => $record->dbObject('Proof')->/** @scrutinizer ignore-call */ getSubmittedAt(),
Loading history...
144
            '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

144
            'SubmittedTo' => $record->dbObject('Extra')->/** @scrutinizer ignore-call */ getStoreAsArray(),
Loading history...
145
        ], JSON_UNESCAPED_UNICODE);
146
147
        $this->renderJSON($response);
148
    }
149
150
    /**
151
     * Gives us the current verification status of the given record. Takes into
152
     * account the state of the saved proof as well as by making a backend
153
     * verification call.
154
     *
155
     * For the ChainPoint Backend, the following process occurs:
156
     *
157
     * 1. Re-hash verifiable_fields as stored within the "Proof" field
158
     * 2. Assert that the record's "Proof" field is not empty
159
     * 3. Assert that the record's "Proof" field contains a valid proof
160
     * 4. Assert that the new hash exists in the record's "Proof" field
161
     * 5. Assert that hash_node_id for that proof returns a valid response from ChainPoint
162
     * 6. Assert that the returned data contains a matching hash for the new hash
163
     *
164
     * @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...
165
     * @param  array      $nodes
166
     * @return string
167
     */
168
    public function getVerificationStatus($record, $nodes)
169
    {
170
        // Set some extra data on the service. In this case, the actual chainpoint
171
        // node addresses, used to submit hashes for the given $record
172
        $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

172
        $this->verifiableService->/** @scrutinizer ignore-call */ 
173
                                  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...
173
174
        // Basic existence of proof (!!) check
175
        if (!$proof = $record->dbObject('Proof')) {
176
            return self::STATUS_LOCAL_PROOF_NONE;
177
        }
178
179
        // Basic proof validity check
180
        // @todo Beef this up to ensure that a basic regex is run over each to ensure it's all
181
        // not just gobbledygook
182
        if (!$proof->getHashIdNode() || !$proof->getHash() || !$proof->getSubmittedAt()) {
183
            return self::STATUS_LOCAL_PROOF_INVALID;
184
        }
185
186
        // Comparison check between locally stored proof, and re-hashed record data
187
        if ($proof->getHash() !== $reHash = $this->verifiableService->hash($record->normaliseData())) {
188
            return self::STATUS_LOCAL_HASH_INVALID;
189
        }
190
191
        // Remote verification check that local hash_node_id returns a valid response
192
        // Responds with a binary format proof
193
        $responseBinary = $this->verifiableService->read($proof->getHashIdNode());
194
195
        if ($responseBinary === '[]') {
196
            return self::STATUS_UUID_INVALID;
197
        }
198
199
        // Extract the "proof" component of the "binary" response to send for verification
200
        $responseBinaryProof = ChainpointProof::create()
201
                ->setValue($responseBinary)
202
                ->getProof();
203
        $responseVerify = $this->verifiableService->verify($responseBinaryProof);
204
205
        if ($responseVerify === '[]') {
206
            return self::STATUS_REMOTE_HASH_INVALID_NO_DATA;
207
        }
208
209
        // Compare returned hash matches the re-hash
210
        $responseProof = ChainpointProof::create()->setValue($responseVerify);
211
212
        if (!$responseProof->match($reHash)) {
213
            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...
214
        }
215
216
        if ($responseProof->getStatus() === 'verified') {
217
            return self::STATUS_VERIFIED;
218
        }
219
220
        return self::STATUS_UNVERIFIED;
221
    }
222
223
    /**
224
     * Properly return JSON, allowing consumers to render returned JSON correctly.
225
     *
226
     * @param  string $json
227
     * @return void
228
     */
229
    private function renderJSON(string $json)
230
    {
231
        header('Content-Type: application/json');
232
        echo $json;
233
        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...
234
    }
235
236
}
237