Passed
Push — hans/Index-all-fluent-options ( 42a725...2f1f10 )
by Simon
07:16
created

DataObjectExtension::onAfterDelete()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0987

Importance

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