Passed
Push — hans/valid-classes ( 1110df...3598c3 )
by Simon
11:18
created

DataObjectExtension::getMembers()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
cc 2
nc 2
nop 0
crap 2
1
<?php
2
3
4
namespace Firesphere\SolrSearch\Extensions;
5
6
use Exception;
7
use Firesphere\SolrSearch\Helpers\SolrLogger;
8
use Firesphere\SolrSearch\Models\DirtyClass;
9
use Firesphere\SolrSearch\Models\SolrLog;
10
use Firesphere\SolrSearch\Services\SolrCoreService;
11
use GuzzleHttp\Exception\GuzzleException;
12
use Psr\Log\LoggerInterface;
13
use SilverStripe\CMS\Model\SiteTree;
14
use SilverStripe\Control\Controller;
15
use SilverStripe\Core\Injector\Injector;
16
use SilverStripe\ORM\ArrayList;
17
use SilverStripe\ORM\DataExtension;
18
use SilverStripe\ORM\DataList;
19
use SilverStripe\ORM\DataObject;
20
use SilverStripe\ORM\ValidationException;
21
use SilverStripe\Security\LoginAttempt;
22
use SilverStripe\Security\Member;
23
use SilverStripe\Security\Security;
24
use SilverStripe\SiteConfig\SiteConfig;
25
use SilverStripe\Versioned\ChangeSet;
26
use SilverStripe\Versioned\ChangeSetItem;
27
use SilverStripe\Versioned\Versioned;
28
29
/**
30
 * Class \Firesphere\SolrSearch\Compat\DataObjectExtension
31
 *
32
 * Extend every DataObject with the option to update the index.
33
 *
34
 * @package Firesphere\SolrSearch\Extensions
35
 * @property DataObject|DataObjectExtension $owner
36
 */
37
class DataObjectExtension extends DataExtension
38
{
39
    /**
40
     * canView cache
41
     *
42
     * @var array
43
     */
44
    public static $canViewClasses = [];
45
    /**
46
     * Member cache
47
     *
48
     * @var DataList
49
     */
50
    protected static $members;
51
    /**
52
     * Don't check these classes
53
     *
54
     * @var array
55
     */
56
    protected static $excludedClasses = [
57
        DirtyClass::class,
58
        ChangeSet::class,
59
        ChangeSetItem::class,
60
        SolrLog::class,
61
        LoginAttempt::class,
62
        Member::class,
63
        SiteConfig::class,
64
    ];
65
66
    /**
67
     * Push the item to solr if it is not versioned
68
     * Update the index after write.
69
     *
70
     * @throws ValidationException
71
     * @throws GuzzleException
72
     * @throws \ReflectionException
73
     */
74 77
    public function onAfterWrite()
75
    {
76
        /** @var DataObject $owner */
77 77
        $owner = $this->owner;
78 77
        if (Controller::curr()->getRequest()->getURL() &&
79 77
            strpos('dev/build', Controller::curr()->getRequest()->getURL()) !== false
80
        ) {
81 1
            return;
82
        }
83 77
        if (!$owner->hasExtension(Versioned::class)) {
84 77
            $this->pushToSolr($owner);
85
        }
86 77
    }
87
88
    /**
89
     * Try to push the newly updated item to Solr
90
     *
91
     * @param DataObject $owner
92
     * @throws ValidationException
93
     * @throws GuzzleException
94
     * @throws \ReflectionException
95
     */
96 77
    protected function pushToSolr(DataObject $owner): void
97
    {
98 77
        $service = new SolrCoreService();
99 77
        if (!$service->isValidClass($owner->ClassName)) {
100 77
            return;
101
        }
102
        /** @var DataObject $owner */
103 2
        $record = $this->getDirtyClass($owner, SolrCoreService::UPDATE_TYPE);
104
105 2
        $ids = json_decode($record->IDs, 1) ?: [];
106 2
        $mode = Versioned::get_reading_mode();
107
        try {
108 2
            Versioned::set_reading_mode(Versioned::DEFAULT_MODE);
109 2
            $service->setInDebugMode(false);
110 2
            $service->updateItems(ArrayList::create([$owner]), SolrCoreService::UPDATE_TYPE);
111
            // If we don't get an exception, mark the item as clean
112
            // Added bonus, array_flip removes duplicates
113 2
            $this->clearIDs($owner, $ids, $record);
114 2
            Versioned::set_reading_mode($mode);
115
        } catch (Exception $error) {
116
            Versioned::set_reading_mode($mode);
117
            $this->registerException($ids, $record, $error);
118
        }
119 2
    }
120
121
    /**
122
     * Find or create a new DirtyClass for recording dirty IDs
123
     *
124
     * @param DataObject $owner
125
     * @param string $type
126
     * @return DirtyClass
127
     * @throws ValidationException
128
     */
129 6
    protected function getDirtyClass(DataObject $owner, $type)
130
    {
131
        // Get the DirtyClass object for this item
132
        /** @var null|DirtyClass $record */
133 6
        $record = DirtyClass::get()->filter(['Class' => $owner->ClassName, 'Type' => $type])->first();
134 6
        if (!$record || !$record->exists()) {
135 5
            $record = DirtyClass::create([
136 5
                'Class' => $owner->ClassName,
137 5
                'Type'  => $type,
138
            ]);
139 5
            $record->write();
140
        }
141
142 6
        return $record;
143
    }
144
145
    /**
146
     * Remove the owner ID from the dirty ID set
147
     *
148
     * @param DataObject $owner
149
     * @param array $ids
150
     * @param DirtyClass $record
151
     * @throws ValidationException
152
     */
153 6
    protected function clearIDs(DataObject $owner, array $ids, DirtyClass $record): void
154
    {
155 6
        $values = array_flip($ids);
156 6
        unset($values[$owner->ID]);
157
158 6
        $record->IDs = json_encode(array_keys($values));
159 6
        $record->write();
160 6
    }
161
162
    /**
163
     * Register the exception of the attempted index for later clean-up use
164
     *
165
     * @param array $ids
166
     * @param $record
167
     * @param Exception $error
168
     * @throws ValidationException
169
     * @throws GuzzleException
170
     */
171
    protected function registerException(array $ids, $record, Exception $error): void
172
    {
173
        /** @var DataObject $owner */
174
        $owner = $this->owner;
175
        $ids[] = $owner->ID;
176
        // If we don't get an exception, mark the item as clean
177
        $record->IDs = json_encode($ids);
178
        $record->write();
179
        $logger = Injector::inst()->get(LoggerInterface::class);
180
        $logger->warn(
181
            sprintf(
182
                'Unable to alter %s with ID %s',
183
                $owner->ClassName,
184
                $owner->ID
185
            )
186
        );
187
        $solrLogger = new SolrLogger();
188
        $solrLogger->saveSolrLog('Index');
189
190
        $logger->error($error->getMessage());
191
    }
192
193
    /**
194
     * Push the item to Solr after publishing
195
     *
196
     * @throws ValidationException
197
     * @throws GuzzleException
198
     * @throws \ReflectionException
199
     */
200 2
    public function onAfterPublish()
201
    {
202
        /** @var DataObject $owner */
203 2
        $owner = $this->owner;
204 2
        $this->pushToSolr($owner);
205 2
    }
206
207
    /**
208
     * Attempt to remove the item from Solr
209
     *
210
     * @throws ValidationException
211
     * @throws GuzzleException
212
     */
213 4
    public function onAfterDelete(): void
214
    {
215
        /** @var DataObject $owner */
216 4
        $owner = $this->owner;
217
        /** @var DirtyClass $record */
218 4
        $record = $this->getDirtyClass($owner, SolrCoreService::DELETE_TYPE);
219
220 4
        $ids = json_decode($record->IDs, 1) ?: [];
221 4
        parent::onAfterDelete();
222
        try {
223 4
            (new SolrCoreService())->updateItems(ArrayList::create([$owner]), SolrCoreService::DELETE_TYPE);
224
            // If successful, remove it from the array
225
            // Added bonus, array_flip removes duplicates
226 4
            $this->clearIDs($owner, $ids, $record);
227
        } catch (Exception $error) {
228
            $this->registerException($ids, $record, $error);
229
        }
230 4
    }
231
232
    /**
233
     * Get the view status for each member in this object
234
     *
235
     * @return array
236
     */
237 7
    public function getViewStatus(): array
238
    {
239
        // Return empty if it's not allowed to show in search
240
        // The setting needs to be explicitly false, to avoid any possible collision
241
        // with objects not having the setting, thus being `null`
242
        /** @var DataObject|SiteTree $owner */
243 7
        $owner = $this->owner;
244
        // Return immediately if the owner has ShowInSearch not being `null`
245 7
        if ($owner->ShowInSearch === false || $owner->ShowInSearch === 0) {
246 1
            return [];
247
        }
248
249 7
        return self::$canViewClasses[$owner->ClassName] ?? $this->getMemberPermissions($owner);
250
    }
251
252
    /**
253
     * Get the view permissions for each member in the system
254
     *
255
     * @param DataObject|SiteTree $owner
256
     * @return array
257
     */
258 4
    protected function getMemberPermissions($owner): array
259
    {
260
        // Log out the current user to avoid collisions in permissions
261 4
        $currMember = Security::getCurrentUser();
262 4
        Security::setCurrentUser(null);
263
264 4
        if ($owner->canView(null)) {
265 3
            self::$canViewClasses[$owner->ClassName] = ['1-null'];
266
267
            // Anyone can view
268 3
            return ['1-null'];
269
        }
270
        // Return a default '0-0' to basically say "noboday can view"
271 2
        $return = ['0-0'];
272 2
        foreach (self::getMembers() as $member) {
273 2
            $return[] = sprintf('%s-%s', (int)$owner->canView($member), (int)$member->ID);
274
        }
275
276 2
        if (!$owner->hasField('ShowInSearch')) {
277 1
            self::$canViewClasses[$owner->ClassName] = $return;
278
        }
279
280 2
        Security::setCurrentUser($currMember);
281
282 2
        return $return;
283
    }
284
285
    /**
286
     * Get the static list of members
287
     *
288
     * @return DataList
289
     */
290 2
    protected static function getMembers()
291
    {
292 2
        if (empty(self::$members)) {
293 1
            self::$members = Member::get();
294
        }
295
296 2
        return self::$members;
297
    }
298
}
299