Passed
Push — hans/core-extraction ( ed2510...fc7dca )
by Simon
10:29 queued 08:21
created

DataObjectExtension::pushToSolr()   A

Complexity

Conditions 4
Paths 7

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.128

Importance

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