Passed
Push — master ( 50ed2d...29d863 )
by Russell
05:06
created

VerifiableExtension::sourceMode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
1
<?php
2
3
/**
4
 * @author  Russell Michell 2018 <[email protected]>
5
 * @package silverstripe-verifiable
6
 */
7
8
namespace PhpTek\Verifiable\Model;
9
10
use SilverStripe\ORM\DataExtension;
11
use PhpTek\Verifiable\ORM\Fieldtype\ChainpointProof;
12
use SilverStripe\Forms\FieldList;
13
use SilverStripe\Forms\FormAction;
14
use SilverStripe\Forms\LiteralField;
15
use SilverStripe\Forms\DropdownField;
16
use SilverStripe\Forms\HiddenField;
17
use PhpTek\JSONText\ORM\FieldType\JSONText;
18
use SilverStripe\View\Requirements;
19
use SilverStripe\Versioned\Versioned;
20
use SilverStripe\ORM\DB;
21
22
/**
23
 * By attaching this extension to any {@link DataObject} subclass, it will therefore
24
 * be "verifiable aware". Declaring a `verify()` method on it, will automatically
25
 * make whatever the method returns, into that which is hashed and anchored to
26
 * the backend.
27
 *
28
 * If no `verify()` method is detected, the fallback is to assume that selected
29
 * fields on your data model should be combined and hashed. For this to work,
30
 * declare a `verifiable_fields` array in YML config. All subsequent publish actions
31
 * will be passed through here via {@link $this->onBeforeWrite()}.
32
 *
33
 * This {@link DataExtension} also provides a single field to which all verified
34
 * and verifiable chainpoint proofs are stored in a queryable JSON-aware field.
35
 */
36
class VerifiableExtension extends DataExtension
37
{
38
    // This data-model is using the default "verifiable_fields" mode
39
    const SOURCE_MODE_FIELD = 1;
40
    // This data-model is using the custom "verify" function mode
41
    const SOURCE_MODE_FUNC = 2;
42
43
    /**
44
     * Declares a JSON-aware {@link DBField} where all chainpoint proofs are stored.
45
     *
46
     * @var array
47
     * @config
48
     */
49
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
50
        'Proof' => ChainpointProof::class,
51
        'Extra' => JSONText::class,
52
        'VerifiableFields' => JSONText::class,
53
    ];
54
55
    /**
56
     * When no `verify()` method is found on decorated objects, this is the list
57
     * of fields who's values will be hashed and committed to the current backend.
58
     *
59
     * @var array
60
     * @config
61
     */
62
    private static $verifiable_fields = [];
0 ignored issues
show
introduced by
The private property $verifiable_fields is not used, and could be removed.
Loading history...
63
64
    /**
65
     * Which source mode are we using?
66
     *
67
     * @return int
68
     */
69
    public function sourceMode()
70
    {
71
        if (method_exists($this->getOwner(), 'verify')) {
72
            return self::SOURCE_MODE_FUNC;
73
        }
74
75
        return self::SOURCE_MODE_FIELD;
76
    }
77
78
    /**
79
     * After each publish action, userland data coming from either a custom `verify()`
80
     * method or `$verifiable_fields` config, is compiled into a string, hashed and
81
     * submitted to the current backend.
82
     *
83
     * Note: We update the versions table manually to avoid double publish problem
84
     * where a DO is marked internally as "changed".
85
     *
86
     * @return void
87
     */
88
    public function onAfterPublish()
89
    {
90
        $owner = $this->getOwner();
91
        $latest = Versioned::get_latest_version(get_class($owner), $owner->ID);
92
        $table = sprintf('%s_Versions', $latest->config()->get('table_name'));
93
94
        // Save the verifiable_fields to the xxx_Versioned table _before_ calling
95
        // source() which itself, makes use of this data
96
        DB::query(sprintf(''
97
            . ' UPDATE "%s"'
98
            . ' SET "VerifiableFields" = \'%s\''
99
            . ' WHERE "RecordID" = %d AND "Version" = %d',
100
            $table,
101
            json_encode($owner->config()->get('verifiable_fields')),
102
            $latest->ID,
103
            $latest->Version
104
        ));
105
106
        $this->service->setExtra();
0 ignored issues
show
Bug Best Practice introduced by
The property service does not exist on PhpTek\Verifiable\Model\VerifiableExtension. Did you maybe forget to declare it?
Loading history...
107
        $verifiable = $this->source();
108
        $doAnchor = (count($verifiable) && $owner->exists());
109
110
        if ($doAnchor && $proofData = $this->service->call('write', $verifiable)) {
111
            if (is_array($proofData)) {
112
                $proofData = json_encode($proofData);
113
            }
114
115
            DB::query(sprintf(''
116
                . ' UPDATE "%s"'
117
                . ' SET "Proof" = \'%s\','
118
                . '     "Extra" = \'%s\''
119
                . ' WHERE "RecordID" = %d AND "Version" = %d',
120
                $table,
121
                $proofData,
122
                json_encode($this->service->getExtra()),
123
                $latest->ID,
124
                $latest->Version
125
            ));
126
        }
127
    }
128
129
    /**
130
     * Source the data that will end-up hashed and submitted. This method will
131
     * call a custom verify() method on all decorated objects if one is defined.
132
     * This provides a flexible public API for hashing and verifying pretty much
133
     * anything. But if no such method exists, the default is to take the value
134
     * of the YML config "verifiable_fields" array, hash and submit the values
135
     * of those DB fields. If no verifiable_fields are found or configured,
136
     * we just return an empty array and just stop.
137
     *
138
     * @param  DataObject $record
0 ignored issues
show
Bug introduced by
The type PhpTek\Verifiable\Model\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...
139
     * @return array
140
     */
141
    public function source($record = null) : array
142
    {
143
        $record = $record ?: $this->getOwner();
144
        $verifiable = [];
145
146
        if ($this->sourceMode() === self::SOURCE_MODE_FUNC) {
147
            $verifiable = (array) $record->verify();
148
        } else {
149
            // If the "VerifiableFields" DB field is not empty, it contains a cached
150
            // list of the field-names who's content should be sent for hashing.
151
            // This means the list of fields to be verified is now _relative_ to
152
            // the current version, thus any change made to YML config, will only
153
            // affect versions created _after_ that change.
154
            $verifiableFields = $record->config()->get('verifiable_fields');
155
156
            if ($cachedFields = $record->dbObject('VerifiableFields')->getStoreAsArray()) {
157
                $verifiableFields = $cachedFields;
158
            }
159
160
            foreach ($verifiableFields as $field) {
161
                if ($field === 'Proof') {
162
                    continue;
163
                }
164
165
                $verifiable[] = strip_tags((string) $record->getField($field));
166
            }
167
        }
168
169
        return $verifiable;
170
    }
171
172
    /**
173
     * Adds a "Verification" tab to the CMS.
174
     *
175
     * @param  FieldList $fields
176
     * @return void
177
     */
178
    public function updateCMSFields(FieldList $fields)
179
    {
180
        parent::updateCMSFields($fields);
181
182
        Requirements::css('phptek/verifiable: client/dist/css/verifiable.css');
183
        Requirements::javascript('phptek/verifiable: client/dist/js/verifiable.js');
184
185
        $owner = $this->getOwner();
186
        $list = $disabled = [];
187
        $versions = $owner->Versions()->sort('Version');
188
189
        foreach ($versions as $item) {
190
            if ($item->Version == 1) {
191
                $disabled[] = $item->Version;
192
            }
193
194
            $list[$item->Version] = sprintf('Version: %s (Created: %s)', $item->Version, $item->Created);
195
        }
196
197
        $fields->addFieldsToTab('Root.Verify', FieldList::create([
198
            LiteralField::create('Introduction', '<p class="message">Select a version'
199
                    . ' whose data you wish to verify, then select the "Verify"'
200
                    . ' button. After a few seconds, a verification status will be'
201
                    . ' displayed.</p>'),
202
            HiddenField::create('Type', null, get_class($owner)),
203
            DropdownField::create('Version', 'Version', $list)
204
                ->setEmptyString('-- Select One --')
205
                ->setDisabledItems($disabled),
206
            FormAction::create('doVerify', 'Verify')
207
                ->setUseButtonTag(true)
208
                ->addExtraClass('btn action btn-outline-primary ')
209
        ]));
210
    }
211
212
    /**
213
     * Get the contents of this model's "Extra" field by numeric index.
214
     *
215
     * @param  int $num
216
     * @return mixed array | int
217
     */
218
    public function getExtraByIndex(int $num = null)
219
    {
220
        $extra = $this->getOwner()->dbObject('Extra');
221
        $extra->setReturnType('array');
222
223
        if (!$num) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $num of type null|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
224
            return $extra->getStoreAsArray();
225
        }
226
227
        if (!empty($value = $extra->nth($num))) {
228
            return is_array($value) ? $value[0] : $value; // <-- stuuupId. Needs fixing in JSONText
229
        }
230
231
        return [];
232
    }
233
234
}
235