Passed
Pull Request — master (#202)
by Simon
07:00 queued 04:41
created

DataObjectExtension::getGroupViewPermissions()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 18
nc 6
nop 1
dl 0
loc 24
ccs 18
cts 18
cp 1
crap 6
rs 9.0444
c 0
b 0
f 0
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
     * @var ArrayList|Member[] List of all the members in the system
54
     */
55
    protected static $memberList;
56
57
    /**
58
     * Push the item to solr if it is not versioned
59
     * Update the index after write.
60
     *
61
     * @throws ValidationException
62
     * @throws GuzzleException
63
     * @throws ReflectionException
64
     * @throws InvalidArgumentException
65
     */
66 84
    public function onAfterWrite()
67
    {
68
        /** @var DataObject $owner */
69 84
        $owner = $this->owner;
70
71 84
        if ($this->shouldPush() && !$owner->hasExtension(Versioned::class)) {
72 84
            $this->pushToSolr($owner);
73
        }
74 84
    }
75
76
    /**
77
     * Reindex this owner object in Solr
78
     * This is a simple stub for the push method, for semantic reasons
79
     * It should never be called on Objects that are not a valid class for any Index
80
     * It does not check if the class is valid to be pushed to Solr
81
     *
82
     * @throws GuzzleException
83
     * @throws ReflectionException
84
     * @throws ValidationException
85
     */
86
    public function doReindex()
87
    {
88
        $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

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