Passed
Push — master ( a057b6...18c31d )
by Simon
05:55
created

DataObjectExtension::onAfterPublish()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 6
ccs 1
cts 1
cp 1
crap 2
rs 10
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 84
     * @throws GuzzleException
79
     * @throws ReflectionException
80 84
     * @throws ValidationException
81 84
     */
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
    protected function shouldPush()
92 84
    {
93
        return !(Controller::curr()->getRequest()->getURL() &&
94 84
            strpos('dev/build', Controller::curr()->getRequest()->getURL()) !== false);
95 84
    }
96 84
97
    /**
98
     * Try to push the newly updated item to Solr
99 3
     *
100
     * @param DataObject $owner
101 3
     * @throws ValidationException
102 3
     * @throws GuzzleException
103
     * @throws ReflectionException
104 3
     */
105 3
    protected function pushToSolr(DataObject $owner)
106 3
    {
107
        $service = new SolrCoreService();
108 3
        if (!$service->isValidClass($owner->ClassName)) {
109 1
            return;
110
        }
111 3
        /** @var DataObject $owner */
112
        $record = $this->getDirtyClass(SolrCoreService::UPDATE_TYPE);
113
114 3
        $ids = json_decode($record->IDs, 1) ?: [];
115
        $mode = Versioned::get_reading_mode();
116
        try {
117
            Versioned::set_reading_mode(Versioned::LIVE);
118
            $service->setDebug(false);
119
            $type = SolrCoreService::UPDATE_TYPE;
120
            // If the object should not show in search, remove it
121 3
            if ($owner->ShowInSearch !== null && (bool)$owner->ShowInSearch === false) {
122 3
                $type = SolrCoreService::DELETE_TYPE;
123
            }
124
            $service->updateItems(ArrayList::create([$owner]), $type);
125
            // If we don't get an exception, mark the item as clean
126
            // Added bonus, array_flip removes duplicates
127
            $this->clearIDs($owner, $ids, $record);
128
        } catch (Exception $error) {
129
            // @codeCoverageIgnoreStart
130
            Versioned::set_reading_mode($mode);
131
            $this->registerException($ids, $record, $error);
132 7
            // @codeCoverageIgnoreEnd
133
        }
134
        Versioned::set_reading_mode($mode);
135
    }
136 7
137 7
    /**
138 6
     * Find or create a new DirtyClass for recording dirty IDs
139 6
     *
140 6
     * @param string $type
141
     * @return DirtyClass
142 6
     * @throws ValidationException
143
     */
144
    protected function getDirtyClass($type)
145 7
    {
146
        // Get the DirtyClass object for this item
147
        /** @var null|DirtyClass $record */
148
        $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...
149
        if (!$record || !$record->exists()) {
150
            $record = DirtyClass::create([
151
                'Class' => $this->owner->ClassName,
152
                'Type'  => $type,
153
            ]);
154
            $record->write();
155
        }
156 7
157
        return $record;
158 7
    }
159 7
160
    /**
161 7
     * Remove the owner ID from the dirty ID set
162 7
     *
163 7
     * @param DataObject $owner
164
     * @param array $ids
165
     * @param DirtyClass $record
166
     * @throws ValidationException
167
     */
168
    protected function clearIDs(DataObject $owner, array $ids, DirtyClass $record): void
169
    {
170
        $values = array_flip($ids);
171
        unset($values[$owner->ID]);
172
173
        $record->IDs = json_encode(array_keys($values));
174
        $record->write();
175
    }
176
177
    /**
178
     * Register the exception of the attempted index for later clean-up use
179
     *
180
     * @codeCoverageIgnore This is actually tested through reflection. See {@link DataObjectExtensionTest}
181
     * @param array $ids
182
     * @param DirtyClass $record
183
     * @param Exception $error
184
     * @throws ValidationException
185
     * @throws GuzzleException
186
     */
187
    protected function registerException(array $ids, $record, Exception $error): void
188
    {
189
        /** @var DataObject $owner */
190
        $owner = $this->owner;
191
        $ids[] = $owner->ID;
192
        // If we don't get an exception, mark the item as clean
193
        $record->IDs = json_encode($ids);
194
        $record->write();
195
        $logger = Injector::inst()->get(LoggerInterface::class);
196
        $logger->warn(
197
            sprintf(
198
                'Unable to alter %s with ID %s',
199
                $owner->ClassName,
200
                $owner->ID
201
            )
202
        );
203
        $solrLogger = new SolrLogger();
204 4
        $solrLogger->saveSolrLog('Index');
205
206 4
        $logger->error($error->getMessage());
207
    }
208 3
209 3
    /**
210
     * Push the item to Solr after publishing
211 4
     *
212
     * @throws ValidationException
213
     * @throws GuzzleException
214
     * @throws ReflectionException
215
     */
216
    public function onAfterPublish()
217
    {
218
        if ($this->shouldPush()) {
219 4
            /** @var DataObject $owner */
220
            $owner = $this->owner;
221
            $this->pushToSolr($owner);
222 4
        }
223
    }
224 4
225
    /**
226 4
     * Attempt to remove the item from Solr
227
     *
228
     * @throws ValidationException
229 4
     * @throws GuzzleException
230 4
     */
231
    public function onAfterDelete(): void
232
    {
233 4
        /** @var DataObject $owner */
234
        $owner = $this->owner;
235
        /** @var DirtyClass $record */
236
        $record = $this->getDirtyClass($owner, SolrCoreService::DELETE_TYPE);
0 ignored issues
show
Unused Code introduced by
The call to Firesphere\SolrSearch\Ex...ension::getDirtyClass() has too many arguments starting with Firesphere\SolrSearch\Se...oreService::DELETE_TYPE. ( Ignorable by Annotation )

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

236
        /** @scrutinizer ignore-call */ 
237
        $record = $this->getDirtyClass($owner, SolrCoreService::DELETE_TYPE);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
237
238
        $ids = json_decode($record->IDs, 1) ?: [];
239 4
240
        try {
241
            (new SolrCoreService())
242
                ->updateItems(ArrayList::create([$owner]), SolrCoreService::DELETE_TYPE);
243
            // If successful, remove it from the array
244
            // Added bonus, array_flip removes duplicates
245
            $this->clearIDs($owner, $ids, $record);
246 8
        } catch (Exception $error) {
247
            // @codeCoverageIgnoreStart
248
            $this->registerException($ids, $record, $error);
249
            // @codeCoverageIgnoreEnd
250 8
        }
251 8
    }
252 1
253
    /**
254
     * Get the view status for each member in this object
255
     *
256 8
     * @return array
257 1
     */
258
    public function getViewStatus(): array
259
    {
260
        // return as early as possible
261
        /** @var DataObject|SiteTree $owner */
262
        $owner = $this->owner;
263 8
        if (isset(static::$cachedClasses[$owner->ClassName])) {
264 1
            return static::$cachedClasses[$owner->ClassName];
265
        }
266
267 8
        // Make sure the siteconfig is loaded
268
        if (!static::$siteConfig) {
269 8
            static::$siteConfig = SiteConfig::current_site_config();
270 1
        }
271
        // Return false if it's not allowed to show in search
272
        // The setting needs to be explicitly false, to avoid any possible collision
273 8
        // with objects not having the setting, thus being `null`
274
        // Return immediately if the owner has ShowInSearch not being `null`
275
        if ($owner->ShowInSearch === false || $owner->ShowInSearch === 0) {
276
            return ['false'];
277
        }
278
279
        $permissions = $this->getGroupViewPermissions($owner);
280
281
        if (!$owner->hasExtension(InheritedPermissionsExtension::class)) {
282 8
            static::$cachedClasses[$owner->ClassName] = $permissions;
283
        }
284
285 8
        return $permissions;
286 8
    }
287 1
288 1
    /**
289 8
     * Determine the view permissions based on group settings
290 1
     *
291 1
     * @param DataObject|SiteTree|SiteConfig $owner
292 1
     * @return array
293 8
     */
294 8
    protected function getGroupViewPermissions($owner): array
295 8
    {
296 8
        // Switches are not ideal, but it's a lot more readable this way!
297 8
        switch ($owner->CanViewType) {
298 8
            case 'LoggedInUsers':
299 8
                $return = ['false', 'LoggedIn'];
300
                break;
301
            case 'OnlyTheseUsers':
302 1
                $return = ['false'];
303
                $return = array_merge($return, $owner->ViewerGroups()->column('Code'));
304
                break;
305 8
            case 'Inherit':
306
                $parent = !$owner->ParentID ? static::$siteConfig : $owner->Parent();
307
                $return = $this->getGroupViewPermissions($parent);
308
                break;
309
            case 'Anyone': // View is either not implemented, or it's "Anyone"
310
                $return = ['null'];
311
                break;
312
            default:
313
                // Default to "Anyone can view"
314
                $return = ['null'];
315
        }
316
317
        return $return;
318
    }
319
}
320