Passed
Push — master ( bc16c5...fce4c6 )
by Simon
06:43
created

DataObjectExtension::clearIDs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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