Issues (5)

src/Extensions/DataObjectExtension.php (2 issues)

1
<?php
2
/**
3
 * class DataObjectExtension|Firesphere\SolrSearch\Extensions\DataObjectExtension Adds checking if changes should be
4
 * pushed to Solr
5
 *
6
 * @package Firesphere\Solr\Search
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 Psr\Log\LoggerInterface;
19
use Psr\SimpleCache\InvalidArgumentException;
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\SiteConfig\SiteConfig;
30
use SilverStripe\Versioned\Versioned;
31
use Solarium\Exception\HttpException;
32
33
/**
34
 * Class \Firesphere\SolrSearch\Compat\DataObjectExtension
35
 *
36
 * Extend every DataObject with the option to update the index.
37
 *
38
 * @package Firesphere\Solr\Search
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
    /**
53
     * Push the item to solr if it is not versioned
54
     * Update the index after write.
55
     *
56
     * @throws ValidationException
57
     * @throws HTTPException
58
     * @throws ReflectionException
59
     * @throws InvalidArgumentException
60
     */
61 87
    public function onAfterWrite()
62
    {
63
        /** @var DataObject $owner */
64 87
        $owner = $this->owner;
65
66 87
        if ($this->shouldPush() && !$owner->hasExtension(Versioned::class)) {
67 85
            $this->pushToSolr($owner);
68
        }
69 87
    }
70
71
    /**
72
     * Should this write be pushed to Solr
73
     * @return bool
74
     */
75 87
    protected function shouldPush()
76
    {
77 87
        if (!Controller::has_curr()) {
78
            return false;
79
        }
80 87
        $request = Controller::curr()->getRequest();
81
82 87
        return (!($request->getURL() &&
83 87
            strpos('dev/build', $request->getURL()) !== false));
84
    }
85
86
    /**
87
     * Try to push the newly updated item to Solr
88
     *
89
     * @param DataObject $owner
90
     * @throws ValidationException
91
     * @throws HTTPException
92
     * @throws ReflectionException
93
     * @throws InvalidArgumentException
94
     */
95 85
    protected function pushToSolr(DataObject $owner)
96
    {
97 85
        $service = new SolrCoreService();
98 85
        if (!$service->isValidClass($owner->ClassName)) {
99 85
            return;
100
        }
101
102
        /** @var DataObject $owner */
103 3
        $record = $this->getDirtyClass(SolrCoreService::UPDATE_TYPE);
104
105 3
        $ids = json_decode($record->IDs, 1) ?: [];
106 3
        $mode = Versioned::get_reading_mode();
107
        try {
108 3
            Versioned::set_reading_mode(Versioned::LIVE);
109 3
            $service->setDebug(false);
110 3
            $type = SolrCoreService::UPDATE_TYPE;
111
            // If the object should not show in search, remove it
112 3
            if ($owner->ShowInSearch !== null && (bool)$owner->ShowInSearch === false) {
113 1
                $type = SolrCoreService::DELETE_TYPE;
114
            }
115 3
            $service->updateItems(ArrayList::create([$owner]), $type);
116
            // If we don't get an exception, mark the item as clean
117
            // Added bonus, array_flip removes duplicates
118 3
            $this->clearIDs($owner, $ids, $record);
119
            // @codeCoverageIgnoreStart
120
        } catch (Exception $error) {
121
            Versioned::set_reading_mode($mode);
122
            $this->registerException($ids, $record, $error);
123
        }
124
        // @codeCoverageIgnoreEnd
125 3
        Versioned::set_reading_mode($mode);
126 3
    }
127
128
    /**
129
     * Find or create a new DirtyClass for recording dirty IDs
130
     *
131
     * @param string $type
132
     * @return DirtyClass
133
     * @throws ValidationException
134
     */
135 7
    protected function getDirtyClass(string $type)
136
    {
137
        // Get the DirtyClass object for this item
138
        /** @var null|DirtyClass $record */
139 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...
140 7
        if (!$record || !$record->exists()) {
141 6
            $record = DirtyClass::create([
142 6
                'Class' => $this->owner->ClassName,
143 6
                'Type'  => $type,
144
            ]);
145 6
            $record->write();
146
        }
147
148 7
        return $record;
149
    }
150
151
    /**
152
     * Remove the owner ID from the dirty ID set
153
     *
154
     * @param DataObject $owner
155
     * @param array $ids
156
     * @param DirtyClass $record
157
     * @throws ValidationException
158
     */
159 7
    protected function clearIDs(DataObject $owner, array $ids, DirtyClass $record): void
160
    {
161 7
        $values = array_flip($ids);
162 7
        unset($values[$owner->ID]);
163
164 7
        $record->IDs = json_encode(array_keys($values));
165 7
        $record->write();
166 7
    }
167
168
    /**
169
     * Register the exception of the attempted index for later clean-up use
170
     *
171
     * @codeCoverageIgnore This is actually tested through reflection. See {@link DataObjectExtensionTest}
172
     * @param array $ids
173
     * @param DirtyClass $record
174
     * @param Exception $error
175
     * @throws ValidationException
176
     * @throws HTTPException
177
     */
178
    protected function registerException(array $ids, DirtyClass $record, Exception $error): void
179
    {
180
        /** @var DataObject $owner */
181
        $owner = $this->owner;
182
        $ids[] = $owner->ID;
183
        // If we don't get an exception, mark the item as clean
184
        $record->IDs = json_encode($ids);
185
        $record->write();
186
        $logger = Injector::inst()->get(LoggerInterface::class);
187
        $logger->warn(
188
            sprintf(
189
                'Unable to alter %s with ID %s',
190
                $owner->ClassName,
191
                $owner->ID
192
            )
193
        );
194
        $solrLogger = new SolrLogger();
195
        $solrLogger->saveSolrLog('Index');
196
197
        $logger->error($error->getMessage());
198
    }
199
200
    /**
201
     * Reindex this owner object in Solr
202
     * This is a simple stub for the push method, for semantic reasons
203
     * It should never be called on Objects that are not a valid class for any Index
204
     * It does not check if the class is valid to be pushed to Solr
205
     *
206
     * @throws HTTPException
207
     * @throws ReflectionException
208
     * @throws ValidationException
209
     * @throws InvalidArgumentException
210
     */
211 1
    public function doReindex()
212
    {
213 1
        $this->pushToSolr($this->owner);
0 ignored issues
show
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

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