Passed
Push — master ( c8c62a...ad1c3d )
by Simon
05:08
created

DataObjectExtension   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Test Coverage

Coverage 93.18%

Importance

Changes 13
Bugs 0 Features 1
Metric Value
eloc 95
dl 0
loc 280
ccs 82
cts 88
cp 0.9318
rs 9.6
c 13
b 0
f 1
wmc 35

11 Methods

Rating   Name   Duplication   Size   Complexity  
A onAfterWrite() 0 7 3
A doReindex() 0 3 1
A onAfterDelete() 0 18 3
A onAfterPublish() 0 6 2
A getDirtyClass() 0 14 3
A registerException() 0 20 1
A shouldPush() 0 7 3
A pushToSolr() 0 30 6
A clearIDs() 0 7 1
A getViewStatus() 0 28 6
A getGroupViewPermissions() 0 24 6
1
<?php
2
/**
3
 * class DataObjectExtension|Firesphere\SolrSearch\Extensions\DataObjectExtension Adds checking if changes should be
4
 * pushed to Solr
5
 *
6
 * @package Firesphere\SolrSearch\Extensions
7
 * @author Simon `Firesphere` Erkelens; Marco `Sheepy` Hermo
8
 * @copyright Copyright (c) 2018 - now() Firesphere & Sheepy
9
 */
10
11
namespace Firesphere\SolrSearch\Extensions;
12
13
use Exception;
14
use Firesphere\SolrSearch\Helpers\SolrLogger;
15
use Firesphere\SolrSearch\Models\DirtyClass;
16
use Firesphere\SolrSearch\Services\SolrCoreService;
17
use Firesphere\SolrSearch\Tests\DataObjectExtensionTest;
18
use GuzzleHttp\Exception\GuzzleException;
19
use Psr\Log\LoggerInterface;
20
use ReflectionException;
21
use SilverStripe\CMS\Model\SiteTree;
22
use SilverStripe\Control\Controller;
23
use SilverStripe\Core\Injector\Injector;
24
use SilverStripe\ORM\ArrayList;
25
use SilverStripe\ORM\DataExtension;
26
use SilverStripe\ORM\DataObject;
27
use SilverStripe\ORM\ValidationException;
28
use SilverStripe\Security\InheritedPermissionsExtension;
29
use SilverStripe\Security\Member;
30
use SilverStripe\SiteConfig\SiteConfig;
31
use SilverStripe\Versioned\Versioned;
32
33
/**
34
 * Class \Firesphere\SolrSearch\Compat\DataObjectExtension
35
 *
36
 * Extend every DataObject with the option to update the index.
37
 *
38
 * @package Firesphere\SolrSearch\Extensions
39
 * @property DataObject|DataObjectExtension $owner
40
 */
41
class DataObjectExtension extends DataExtension
42
{
43
    /**
44
     * @var array Cached permission list
45
     */
46
    public static $cachedClasses;
47
    /**
48
     * @var SiteConfig Current siteconfig
49
     */
50
    protected static $siteConfig;
51
    /**
52
     * @var ArrayList|Member[] List of all the members in the system
53
     */
54
    protected static $memberList;
55
56
    /**
57
     * Push the item to solr if it is not versioned
58
     * Update the index after write.
59
     *
60
     * @throws ValidationException
61
     * @throws GuzzleException
62
     * @throws ReflectionException
63
     */
64 84
    public function onAfterWrite()
65
    {
66
        /** @var DataObject $owner */
67 84
        $owner = $this->owner;
68
69 84
        if ($this->shouldPush() && !$owner->hasExtension(Versioned::class)) {
70 84
            $this->pushToSolr($owner);
71
        }
72 84
    }
73
74
    /**
75
     * Reindex this owner object in Solr
76
     * This is a simple stub for the push method, for semantic reasons
77
     *
78
     * @throws GuzzleException
79
     * @throws ReflectionException
80
     * @throws ValidationException
81
     */
82
    public function doReindex()
83
    {
84
        $this->pushToSolr($this->owner);
0 ignored issues
show
Bug introduced by
It seems like $this->owner can also be of type Firesphere\SolrSearch\Ex...ons\DataObjectExtension; however, parameter $owner of Firesphere\SolrSearch\Ex...Extension::pushToSolr() does only seem to accept SilverStripe\ORM\DataObject, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

84
        $this->pushToSolr(/** @scrutinizer ignore-type */ $this->owner);
Loading history...
85
    }
86
87
    /**
88
     * Should this write be pushed to Solr
89
     * @return bool
90
     */
91 84
    protected function shouldPush()
92
    {
93 84
        if (!Controller::has_curr()) {
94
            return false;
95
        }
96 84
        return !(Controller::curr()->getRequest()->getURL() &&
97 84
            strpos('dev/build', Controller::curr()->getRequest()->getURL()) !== false);
98
    }
99
100
    /**
101
     * Try to push the newly updated item to Solr
102
     *
103
     * @param DataObject $owner
104
     * @throws ValidationException
105
     * @throws GuzzleException
106
     * @throws ReflectionException
107
     */
108 84
    protected function pushToSolr(DataObject $owner)
109
    {
110 84
        $service = new SolrCoreService();
111 84
        if (!$service->isValidClass($owner->ClassName)) {
112 84
            return;
113
        }
114
        /** @var DataObject $owner */
115 3
        $record = $this->getDirtyClass(SolrCoreService::UPDATE_TYPE);
116
117 3
        $ids = json_decode($record->IDs, 1) ?: [];
118 3
        $mode = Versioned::get_reading_mode();
119
        try {
120 3
            Versioned::set_reading_mode(Versioned::LIVE);
121 3
            $service->setDebug(false);
122 3
            $type = SolrCoreService::UPDATE_TYPE;
123
            // If the object should not show in search, remove it
124 3
            if ($owner->ShowInSearch !== null && (bool)$owner->ShowInSearch === false) {
125 1
                $type = SolrCoreService::DELETE_TYPE;
126
            }
127 3
            $service->updateItems(ArrayList::create([$owner]), $type);
128
            // If we don't get an exception, mark the item as clean
129
            // Added bonus, array_flip removes duplicates
130 3
            $this->clearIDs($owner, $ids, $record);
131
        } catch (Exception $error) {
132
            // @codeCoverageIgnoreStart
133
            Versioned::set_reading_mode($mode);
134
            $this->registerException($ids, $record, $error);
135
            // @codeCoverageIgnoreEnd
136
        }
137 3
        Versioned::set_reading_mode($mode);
138 3
    }
139
140
    /**
141
     * Find or create a new DirtyClass for recording dirty IDs
142
     *
143
     * @param string $type
144
     * @return DirtyClass
145
     * @throws ValidationException
146
     */
147 7
    protected function getDirtyClass($type)
148
    {
149
        // Get the DirtyClass object for this item
150
        /** @var null|DirtyClass $record */
151 7
        $record = DirtyClass::get()->filter(['Class' => $this->owner->ClassName, 'Type' => $type])->first();
0 ignored issues
show
Bug Best Practice introduced by
The property ClassName does not exist on Firesphere\SolrSearch\Ex...ons\DataObjectExtension. Did you maybe forget to declare it?
Loading history...
152 7
        if (!$record || !$record->exists()) {
153 6
            $record = DirtyClass::create([
154 6
                'Class' => $this->owner->ClassName,
155 6
                'Type'  => $type,
156
            ]);
157 6
            $record->write();
158
        }
159
160 7
        return $record;
161
    }
162
163
    /**
164
     * Remove the owner ID from the dirty ID set
165
     *
166
     * @param DataObject $owner
167
     * @param array $ids
168
     * @param DirtyClass $record
169
     * @throws ValidationException
170
     */
171 7
    protected function clearIDs(DataObject $owner, array $ids, DirtyClass $record): void
172
    {
173 7
        $values = array_flip($ids);
174 7
        unset($values[$owner->ID]);
175
176 7
        $record->IDs = json_encode(array_keys($values));
177 7
        $record->write();
178 7
    }
179
180
    /**
181
     * Register the exception of the attempted index for later clean-up use
182
     *
183
     * @codeCoverageIgnore This is actually tested through reflection. See {@link DataObjectExtensionTest}
184
     * @param array $ids
185
     * @param DirtyClass $record
186
     * @param Exception $error
187
     * @throws ValidationException
188
     * @throws GuzzleException
189
     */
190
    protected function registerException(array $ids, $record, Exception $error): void
191
    {
192
        /** @var DataObject $owner */
193
        $owner = $this->owner;
194
        $ids[] = $owner->ID;
195
        // If we don't get an exception, mark the item as clean
196
        $record->IDs = json_encode($ids);
197
        $record->write();
198
        $logger = Injector::inst()->get(LoggerInterface::class);
199
        $logger->warn(
200
            sprintf(
201
                'Unable to alter %s with ID %s',
202
                $owner->ClassName,
203
                $owner->ID
204
            )
205
        );
206
        $solrLogger = new SolrLogger();
207
        $solrLogger->saveSolrLog('Index');
208
209
        $logger->error($error->getMessage());
210
    }
211
212
    /**
213
     * Push the item to Solr after publishing
214
     *
215
     * @throws ValidationException
216
     * @throws GuzzleException
217
     * @throws ReflectionException
218
     */
219 4
    public function onAfterPublish()
220
    {
221 4
        if ($this->shouldPush()) {
222
            /** @var DataObject $owner */
223 3
            $owner = $this->owner;
224 3
            $this->pushToSolr($owner);
225
        }
226 4
    }
227
228
    /**
229
     * Attempt to remove the item from Solr
230
     *
231
     * @throws ValidationException
232
     * @throws GuzzleException
233
     */
234 4
    public function onAfterDelete(): void
235
    {
236
        /** @var DataObject $owner */
237 4
        $owner = $this->owner;
238
        /** @var DirtyClass $record */
239 4
        $record = $this->getDirtyClass(SolrCoreService::DELETE_TYPE);
240
241 4
        $ids = json_decode($record->IDs, 1) ?: [];
242
243
        try {
244 4
            (new SolrCoreService())
245 4
                ->updateItems(ArrayList::create([$owner]), SolrCoreService::DELETE_TYPE);
246
            // If successful, remove it from the array
247
            // Added bonus, array_flip removes duplicates
248 4
            $this->clearIDs($owner, $ids, $record);
249
        } catch (Exception $error) {
250
            // @codeCoverageIgnoreStart
251
            $this->registerException($ids, $record, $error);
252
            // @codeCoverageIgnoreEnd
253
        }
254 4
    }
255
256
    /**
257
     * Get the view status for each member in this object
258
     *
259
     * @return array
260
     */
261 8
    public function getViewStatus(): array
262
    {
263
        // return as early as possible
264
        /** @var DataObject|SiteTree $owner */
265 8
        $owner = $this->owner;
266 8
        if (isset(static::$cachedClasses[$owner->ClassName])) {
267 1
            return static::$cachedClasses[$owner->ClassName];
268
        }
269
270
        // Make sure the siteconfig is loaded
271 8
        if (!static::$siteConfig) {
272 1
            static::$siteConfig = SiteConfig::current_site_config();
273
        }
274
        // Return false if it's not allowed to show in search
275
        // The setting needs to be explicitly false, to avoid any possible collision
276
        // with objects not having the setting, thus being `null`
277
        // Return immediately if the owner has ShowInSearch not being `null`
278 8
        if ($owner->ShowInSearch === false || $owner->ShowInSearch === 0) {
279 1
            return ['false'];
280
        }
281
282 8
        $permissions = $this->getGroupViewPermissions($owner);
283
284 8
        if (!$owner->hasExtension(InheritedPermissionsExtension::class)) {
285 1
            static::$cachedClasses[$owner->ClassName] = $permissions;
286
        }
287
288 8
        return $permissions;
289
    }
290
291
    /**
292
     * Determine the view permissions based on group settings
293
     *
294
     * @param DataObject|SiteTree|SiteConfig $owner
295
     * @return array
296
     */
297 8
    protected function getGroupViewPermissions($owner): array
298
    {
299
        // Switches are not ideal, but it's a lot more readable this way!
300 8
        switch ($owner->CanViewType) {
301 8
            case 'LoggedInUsers':
302 1
                $return = ['false', 'LoggedIn'];
303 1
                break;
304 8
            case 'OnlyTheseUsers':
305 1
                $return = ['false'];
306 1
                $return = array_merge($return, $owner->ViewerGroups()->column('Code'));
307 1
                break;
308 8
            case 'Inherit':
309 8
                $parent = !$owner->ParentID ? static::$siteConfig : $owner->Parent();
310 8
                $return = $this->getGroupViewPermissions($parent);
311 8
                break;
312 8
            case 'Anyone': // View is either not implemented, or it's "Anyone"
313 8
                $return = ['null'];
314 8
                break;
315
            default:
316
                // Default to "Anyone can view"
317 1
                $return = ['null'];
318
        }
319
320 8
        return $return;
321
    }
322
}
323