Passed
Push — sheepy/elevation-configuration ( f8fadb...77a4b0 )
by Marco
07:42
created

DataObjectExtension::onAfterDelete()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0175

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 9
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 18
ccs 7
cts 8
cp 0.875
crap 3.0175
rs 9.9666
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 Psr\SimpleCache\InvalidArgumentException;
21
use ReflectionException;
22
use SilverStripe\CMS\Model\SiteTree;
23
use SilverStripe\Control\Controller;
24
use SilverStripe\Core\Injector\Injector;
25
use SilverStripe\ORM\ArrayList;
26
use SilverStripe\ORM\DataExtension;
27
use SilverStripe\ORM\DataObject;
28
use SilverStripe\ORM\ValidationException;
29
use SilverStripe\Security\InheritedPermissionsExtension;
30
use SilverStripe\Security\Member;
31
use SilverStripe\SiteConfig\SiteConfig;
32
use SilverStripe\Versioned\Versioned;
33
34
/**
35
 * Class \Firesphere\SolrSearch\Compat\DataObjectExtension
36
 *
37
 * Extend every DataObject with the option to update the index.
38
 *
39
 * @package Firesphere\SolrSearch\Extensions
40
 * @property DataObject|DataObjectExtension $owner
41
 */
42
class DataObjectExtension extends DataExtension
43
{
44
    /**
45
     * @var array Cached permission list
46
     */
47
    public static $cachedClasses;
48
    /**
49
     * @var SiteConfig Current siteconfig
50
     */
51
    protected static $siteConfig;
52
53
    /**
54
     * Push the item to solr if it is not versioned
55
     * Update the index after write.
56
     *
57
     * @throws ValidationException
58
     * @throws GuzzleException
59
     * @throws ReflectionException
60
     * @throws InvalidArgumentException
61
     */
62 85
    public function onAfterWrite()
63
    {
64
        /** @var DataObject $owner */
65 85
        $owner = $this->owner;
66
67 85
        if ($this->shouldPush() && !$owner->hasExtension(Versioned::class)) {
68 85
            $this->pushToSolr($owner);
69
        }
70 85
    }
71
72
    /**
73
     * Reindex this owner object in Solr
74
     * This is a simple stub for the push method, for semantic reasons
75
     * It should never be called on Objects that are not a valid class for any Index
76
     * It does not check if the class is valid to be pushed to Solr
77
     *
78
     * @throws GuzzleException
79
     * @throws ReflectionException
80
     * @throws ValidationException
81
     * @throws InvalidArgumentException
82
     */
83
    public function doReindex()
84
    {
85
        $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

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