Passed
Push — master ( da6ddd...7266dd )
by Russell
02:37
created

VerifiableController::getVerificationStatus()   C

Complexity

Conditions 11
Paths 9

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 9
nop 2
dl 0
loc 57
rs 6.7915
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
class VerifiableController extends Controller
25
{
26
    /**
27
     * No local proof found. Evidence that the record has been tampered-with.
28
     *
29
     * @var string
30
     */
31
    const STATUS_LOCAL_PROOF_NONE = 'Local Proof Not Found';
32
33
    /**
34
     * Invalid local proof found. Evidence that the record has been tampered-with.
35
     *
36
     * @var string
37
     */
38
    const STATUS_LOCAL_PROOF_INVALID = 'Local Proof Invalid';
39
40
    /**
41
     * Invalid local hash found. Evidence that the record has been tampered-with.
42
     *
43
     * @var string
44
     */
45
    const STATUS_LOCAL_HASH_INVALID = 'Local Hash Invalid';
46
47
    /**
48
     * Invalid or no matching remote proof found. Evidence that the record has been tampered-with.
49
     *
50
     * @var string
51
     */
52
    const STATUS_REMOTE_HASH_INVALID_NO_DATA = 'Remote Hash Not Found';
53
54
    /**
55
     * Invalid remote hash found. Evidence that the record has been tampered-with.
56
     *
57
     * @var string
58
     */
59
    const STATUS_REMOTE_HASH_INVALID_NO_HASH = 'Remote Hash Not Found';
60
61
    /**
62
     * Invalid UUID. Evidence that the record has been tampered-with.
63
     *
64
     * @var string
65
     */
66
    const STATUS_UUID_INVALID = 'Invalid UUID';
67
68
    /**
69
     * All checks passed. Submitted hash is verified.
70
     *
71
     * @var string
72
     */
73
    const STATUS_VERIFIED = 'Verified';
74
75
    /**
76
     * All checks passed. But submitted hash is not yet verified.
77
     *
78
     * @var string
79
     */
80
    const STATUS_UNVERIFIED = 'Unverified';
81
82
    /**
83
     * All checks passed. The local proof has been validated, but is not yet confirmed.
84
     *
85
     * @var string
86
     */
87
    const STATUS_PENDING = 'Pending';
88
89
    /**
90
     * Some kind of upstream error.
91
     *
92
     * @var string
93
     */
94
    const STATUS_UPSTREAM_ERROR = 'Upstream Error';
95
96
    /**
97
     * @var array
98
     */
99
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
100
        'verifyhash',
101
    ];
102
103
    /**
104
     * Verify the integrity of arbitrary data by means of a single hash.
105
     *
106
     * Responds to URIs of the following prototype: /verifiable/verify/<model>/<ID>/<VID>
107
     * by echoing a JSON response for consumption by client-side logic.
108
     *
109
     * @param  HTTPRequest $request
110
     * @return void
111
     */
112
    public function verifyhash(HTTPRequest $request)
113
    {
114
        $class = $request->param('ClassName');
115
        $id = $request->param('ModelID');
116
        $version = $request->param('VersionID');
117
118
        if (empty($id) || !is_numeric($id) ||
119
                empty($version) || !is_numeric($version) ||
120
                empty($class)) {
121
            return $this->httpError(400, 'Bad request');
122
        }
123
124
        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

124
        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

124
        if (!$record = Versioned::get_version($class, /** @scrutinizer ignore-type */ $id, $version)) {
Loading history...
125
            return $this->httpError(400, 'Bad request');
126
        }
127
128
        try {
129
            $status = $this->getVerificationStatus($record, $record->getExtraByIndex());
130
        } catch (ValidationException $ex) {
131
            $status = self::STATUS_UPSTREAM_ERROR;
132
        }
133
134
        $response = json_encode([
135
            'RecordID' => "$record->RecordID",
136
            'Version' => "$record->Version",
137
            'Class' => get_class($record),
138
            'StatusNice' => $status,
139
            'StatusCode' => $this->getCodeMeta($status, 'code'),
140
            'StatusDefn' => $this->getCodeMeta($status, 'defn'),
141
            'SubmittedAt' => $record->dbObject('Proof')->getSubmittedAt(),
142
            'SubmittedTo' => $record->dbObject('Extra')->getStoreAsArray(),
143
        ], JSON_UNESCAPED_UNICODE);
144
145
        $this->renderJSON($response);
146
    }
147
148
    /**
149
     * Return data used for verifiable statuses.
150
     *
151
     * @param  string $status
152
     * @param  string $key
153
     * @return mixed
154
     */
155
    private function getCodeMeta($status, $key)
156
    {
157
        $refl = new \ReflectionClass(__CLASS__);
158
        $const = array_search($status, $refl->getConstants());
159
        $keyJson = file_get_contents(realpath(__DIR__) . '/../../statuses.json');
160
        $keyMap = json_decode($keyJson, true);
161
        $defn = '';
162
163
        foreach ($keyMap as $map) {
164
            if (isset($map[$const])) {
165
                $defn = $map[$const];
166
            }
167
        }
168
169
        $data = [
170
            'code' => $const,
171
            'defn' => $defn,
172
        ];
173
174
        return $data[$key] ?? $data;
175
176
    }
177
178
    /**
179
     * Gives us the current verification status of the given record. Takes into
180
     * account the state of the saved proof as well as by making a backend
181
     * verification call.
182
     *
183
     * For the ChainPoint Backend, the following process occurs:
184
     *
185
     * 1. Re-hash verifiable_fields as stored within the "Proof" field
186
     * 2. Assert that the record's "Proof" field is not empty
187
     * 3. Assert that the record's "Proof" field contains a valid proof
188
     * 4. Assert that the new hash exists in the record's "Proof" field
189
     * 5. Assert that hash_node_id for that proof returns a valid response from ChainPoint
190
     * 6. Assert that the returned data contains a matching hash for the new hash
191
     *
192
     * @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...
193
     * @param  array      $nodes
194
     * @return string
195
     */
196
    public function getVerificationStatus($record, $nodes)
197
    {
198
        // Set some extra data on the service. In this case, the actual chainpoint
199
        // node addresses, used to submit hashes for the given $record
200
        $this->verifiableService->setExtra($nodes);
0 ignored issues
show
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...
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

200
        $this->verifiableService->/** @scrutinizer ignore-call */ 
201
                                  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...
201
202
        // Basic existence of proof (!!) check
203
        if (!$proof = $record->dbObject('Proof')) {
204
            return self::STATUS_LOCAL_PROOF_NONE;
205
        }
206
207
        // Basic proof validity check
208
        // @todo Beef this up to ensure that a basic regex is run over each to ensure it's all
209
        // not just gobbledygook
210
        if (!$proof->getHashIdNode() || !$proof->getHash() || !$proof->getSubmittedAt()) {
211
            return self::STATUS_LOCAL_PROOF_INVALID;
212
        }
213
214
        // Comparison check between locally stored proof, and re-hashed record data
215
        if ($proof->getHash() !== $reHash = $this->verifiableService->hash($record->source())) {
216
            return self::STATUS_LOCAL_HASH_INVALID;
217
        }
218
219
        // Remote verification check that local hash_node_id returns a valid response
220
        // Responds with a binary format proof
221
        $responseBinary = $this->verifiableService->call('read', $proof->getHashIdNode());
222
223
        if ($responseBinary === '[]') {
224
            return self::STATUS_UUID_INVALID;
225
        }
226
227
        // Extract the "proof" component of the "binary" response to send for verification
228
        $responseBinaryProof = ChainpointProof::create()
229
                ->setValue($responseBinary)
230
                ->getProof();
231
        $responseVerify = $this->verifiableService->call('verify', $responseBinaryProof);
232
233
        if ($responseVerify === '[]') {
234
            return self::STATUS_REMOTE_HASH_INVALID_NO_DATA;
235
        }
236
237
        // Compare returned hash matches the re-hash
238
        $responseProof = ChainpointProof::create()->setValue($responseVerify);
239
240
        if (!$responseProof->match($reHash)) {
241
            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...
242
        }
243
244
        if ($responseProof->getStatus() === 'verified') {
245
            if (!$responseProof->isComplete()) {
246
                return self::STATUS_PENDING;
247
            }
248
249
            return self::STATUS_VERIFIED;
250
        }
251
252
        return self::STATUS_UNVERIFIED;
253
    }
254
255
    /**
256
     * Properly return JSON, allowing consumers to render returned JSON correctly.
257
     *
258
     * @param  string $json
259
     * @return void
260
     */
261
    private function renderJSON(string $json)
262
    {
263
        header('Content-Type: application/json');
264
        echo $json;
265
        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...
266
    }
267
268
}
269