Passed
Push — hans/bugfix-dataresolving ( 542d23 )
by Simon
06:32
created

DataObjectExtension::shouldPush()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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