Issues (10)

src/Helpers/FindEditableObjects.php (2 issues)

Labels
Severity
1
<?php
2
3
namespace Sunnysideup\SiteWideSearch\Helpers;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Extensible;
10
use SilverStripe\Core\Injector\Injectable;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\ORM\DataList;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\UnsavedRelationList;
15
use Sunnysideup\CmsEditLinkField\Api\CMSEditLinkAPI;
16
17
class FindEditableObjects
18
{
19
    use Extensible;
20
    use Configurable;
21
    use Injectable;
22
23
    /**
24
     * @var string
25
     */
26
    private const CACHE_NAME = 'FindEditableObjectsCache';
27
28
    protected $additionalCacheName = '';
29
30
    protected $relationTypesCovered = [];
31
32
    protected $excludedClasses = [];
33
34
    /**
35
     * format is as follows:
36
     * ```php
37
     *      [
38
     *          'valid_methods_edit' => [
39
     *              ClassNameA => false, // tested and does not have any available methods
40
     *              ClassNameB => MethodName1, // tested found method MethodName1 that can be used.
41
     *              ClassNameC => MethodName2, // tested found method MethodName2 that can be used.
42
     *              ClassNameD => false, // tested and does not have any available methods
43
     *          ],
44
     *          'valid_methods_view' => [
45
     *              ClassNameA => false, // tested and does not have any available methods
46
     *              ClassNameB => MethodName1, // tested found method MethodName1 that can be used.
47
     *              ClassNameC => MethodName2, // tested found method MethodName2 that can be used.
48
     *              ClassNameD => false, // tested and does not have any available methods
49
     *          ],
50
     *          'valid_methods_view_links' => [
51
     *              [ClassNameX_IDY] => 'MyLinkView',
52
     *              [ClassNameX_IDZ] => 'MyLinkView',
53
     *          ],
54
     *          'valid_methods_edit_links' => [
55
     *              [ClassNameX_IDY] => 'MyLinkEdit',
56
     *              [ClassNameX_IDZ] => 'MyLinkEdit',
57
     *          ],
58
     *          'rels' =>
59
     *              'ClassNameY' => [
60
     *                  'MethodA' => RelationClassNameB,
61
     *                  'MethodC' => RelationClassNameD,
62
     *              ],
63
     *          ],
64
     *          'validMethods' => [
65
     *              'valid_methods_edit' => [
66
     *                  'A',
67
     *                  'B',
68
     *              ]
69
     *              'valid_methods_view' => [
70
     *                  'A',
71
     *                  'B',
72
     *              ]
73
     *          ]
74
     *     ]
75
     * ```
76
     * we use false to be able to use empty to work out if it has been tested before.
77
     *
78
     * @var array
79
     */
80
    protected $cache = [
81
        'valid_methods_edit',
82
        'valid_methods_view',
83
        'valid_methods_image',
84
        'valid_methods_view_links',
85
        'valid_methods_edit_links',
86
        'valid_methods_image_links',
87
        'rels',
88
        'validMethods' => [
89
            'valid_methods_edit' => [],
90
            'valid_methods_view' => [],
91
            'valid_methods_image' => [],
92
        ],
93
    ];
94
95
    private static $max_relation_depth = 3;
96
97
    private static $valid_methods_edit = [
98
        'CMSEditLink',
99
        'getCMSEditLink',
100
        'EditLink',
101
        'getEditLink',
102
    ];
103
104
    private static $valid_methods_view = [
105
        'getLink',
106
        'Link',
107
    ];
108
109
    private static $valid_methods_image = [
110
        'StripThumbnail',
111
        'CMSThumbnail',
112
        'getCMSThumbnail',
113
    ];
114
115
    public function getFileCache()
116
    {
117
        return Injector::inst()->get(Cache::class);
118
    }
119
120
    public function initCache(string $additionalCacheName): self
121
    {
122
        $this->additionalCacheName = $additionalCacheName;
123
        $this->cache = $this->getFileCache()->getCacheValues(self::CACHE_NAME . '_' . $this->additionalCacheName);
124
125
        return $this;
126
    }
127
128
    public function saveCache(): self
129
    {
130
        $this->getFileCache()->setCacheValues(self::CACHE_NAME . '_' . $this->additionalCacheName, $this->cache);
131
132
        return $this;
133
    }
134
135
    public function setExcludedClasses(array $excludedClasses): self
136
    {
137
        $this->excludedClasses = $excludedClasses;
138
139
        return $this;
140
    }
141
142
    /**
143
     * returns an link to an object that can be edited in the CMS.
144
     *
145
     * @param mixed $dataObject
146
     */
147
    public function getCMSEditLink($dataObject): string
148
    {
149
        return $this->getLinkInner($dataObject, 'valid_methods_edit');
150
    }
151
152
    /**
153
     * returns a link to an object that can be viewed.
154
     *
155
     * @param mixed $dataObject
156
     */
157
    public function getLink($dataObject): string
158
    {
159
        return $this->getLinkInner($dataObject, 'valid_methods_view');
160
    }
161
162
    /**
163
     * returns link to a thumbnail.
164
     *
165
     * @param mixed $dataObject
166
     */
167
    public function getCMSThumbnail($dataObject): string
168
    {
169
        return $this->getLinkInner($dataObject, 'valid_methods_image');
170
    }
171
172
    /**
173
     * returns an link to an object that can be viewed.
174
     *
175
     * @param mixed $dataObject
176
     */
177
    protected function getLinkInner($dataObject, string $type): string
178
    {
179
        $typeKey = $type . '_links';
180
        $key = $dataObject->ClassName . $dataObject->ID;
181
        $result = $this->cache[$typeKey][$key] ?? false;
182
        if (false === $result) {
183
            $this->relationTypesCovered = [];
184
            $result = $this->checkForValidMethods($dataObject, $type);
185
            $this->cache[$typeKey][$key] = $result;
186
        }
187
188
        return $result;
189
    }
190
191
    protected function checkForValidMethods($dataObject, string $type, ?int $relationDepth = 0): string
192
    {
193
        //too many iterations!
194
        if ($relationDepth > (int) $this->Config()->get('max_relation_depth')) {
195
            return '';
196
        }
197
198
        $validMethods = $this->getValidMethods($type);
199
200
        $this->relationTypesCovered[$dataObject->ClassName] = false;
201
202
        // quick return
203
        if (isset($this->cache[$type][$dataObject->ClassName]) && false !== $this->cache[$type][$dataObject->ClassName]) {
204
            $validMethod = $this->cache[$type][$dataObject->ClassName];
205
            if ($dataObject->hasMethod($validMethod)) {
206
                $link = $dataObject->{$validMethod}();
207
                if ($link) {
208
                    return $this->cleanupLink((string) $link);
209
                }
210
            }
211
            // last resort - is there a variable with this name?
212
            $link = $dataObject->{$validMethod};
213
            if ($link) {
214
                return $this->cleanupLink((string) $link);
215
            }
216
        }
217
218
        if ($this->classCanBeIncluded($dataObject->ClassName)) {
219
            if (empty($this->cache[$type][$dataObject->ClassName]) || false !== $this->cache[$type][$dataObject->ClassName]) {
220
                foreach ($validMethods as $validMethod) {
221
                    $outcome = null;
222
                    if ($dataObject->hasMethod($validMethod)) {
223
                        $outcome = $dataObject->{$validMethod}();
224
                    } elseif (! empty($dataObject->{$validMethod})) {
225
                        $outcome = $dataObject->{$validMethod};
226
                    }
227
228
                    if ($outcome) {
229
                        $this->cache[$type][$dataObject->ClassName] = $validMethod;
230
231
                        return $this->cleanupLink((string) $outcome);
232
                    }
233
                }
234
            }
235
236
            if ('valid_methods_edit' === $type && class_exists(CMSEditLinkAPI::class)) {
237
                $link = CMSEditLinkAPI::find_edit_link_for_object($dataObject);
238
                if ($link !== '' && $link !== '0') {
239
                    return $this->cleanupLink($link);
240
                }
241
            }
242
        }
243
244
        // there is no match for this one, but we can search relations ...
245
        $this->cache[$type][$dataObject->ClassName] = true;
246
        ++$relationDepth;
247
        foreach ($this->getRelations($dataObject) as $relationName => $relType) {
248
            $outcome = null;
249
            //TODO: no support for link through relations yet!
250
            if (is_array($relType)) {
251
                continue;
252
            }
253
254
            if (! isset($this->relationTypesCovered[$relType])) {
255
                $rels = null;
256
                if ($dataObject->hasMethod($relationName)) {
257
                    $rels = $dataObject->{$relationName}();
258
                } else {
259
                    user_error('Relation ' . print_r($relationName, 1) . ' does not exist on ' . $dataObject->ClassName . ' Relations are: ' . print_r($this->getRelations($dataObject), 1), E_USER_NOTICE);
0 ignored issues
show
Are you sure print_r($this->getRelations($dataObject), 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

259
                    user_error('Relation ' . print_r($relationName, 1) . ' does not exist on ' . $dataObject->ClassName . ' Relations are: ' . /** @scrutinizer ignore-type */ print_r($this->getRelations($dataObject), 1), E_USER_NOTICE);
Loading history...
Are you sure print_r($relationName, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

259
                    user_error('Relation ' . /** @scrutinizer ignore-type */ print_r($relationName, 1) . ' does not exist on ' . $dataObject->ClassName . ' Relations are: ' . print_r($this->getRelations($dataObject), 1), E_USER_NOTICE);
Loading history...
260
                }
261
                if ($rels) {
262
                    if ($rels instanceof DataList && ! $rels instanceof UnsavedRelationList) {
263
                        $rels = $rels->first();
264
                    }
265
                    if ($rels && $rels instanceof DataObject && $rels->exists()) {
266
                        $outcome = $this->checkForValidMethods($rels, $type, $relationDepth);
267
                    }
268
                }
269
            }
270
271
            if ($outcome) {
272
                return $this->cleanupLink((string) $outcome);
273
            }
274
        }
275
276
        return '';
277
    }
278
279
    protected function cleanupLink(?string $link = null): string
280
    {
281
        if ($link) {
282
            if ($link === ' ' || $link === '/' || $link === '0') {
283
                return '';
284
            }
285
286
            // it is a tag, not a link!
287
            if (strpos($link, '<') === 0) {
288
                return $link;
289
            }
290
            return Director::absoluteURL((string) $link);
291
        } else {
292
            return '';
293
        }
294
    }
295
296
297
    protected function getRelations($dataObject): array
298
    {
299
        if (! isset($this->cache['rels'][$dataObject->ClassName])) {
300
            $this->cache['rels'][$dataObject->ClassName] = array_merge(
301
                Config::inst()->get($dataObject->ClassName, 'belongs_to'),
302
                Config::inst()->get($dataObject->ClassName, 'has_one'),
303
                Config::inst()->get($dataObject->ClassName, 'has_many'),
304
                Config::inst()->get($dataObject->ClassName, 'belongs_many_many'),
305
                Config::inst()->get($dataObject->ClassName, 'many_many')
306
            );
307
            foreach ($this->cache['rels'][$dataObject->ClassName] as $key => $value) {
308
                if (! (is_string($value) && class_exists($value) && $this->classCanBeIncluded($value))) {
309
                    unset($this->cache['rels'][$dataObject->ClassName][$key]);
310
                }
311
            }
312
        }
313
314
        return $this->cache['rels'][$dataObject->ClassName];
315
    }
316
317
    protected function getValidMethods(string $type): array
318
    {
319
        if (! isset($this->cache['validMethods'][$type])) {
320
            $this->cache['validMethods'][$type] = $this->Config()->get($type);
321
        }
322
323
        return $this->cache['validMethods'][$type];
324
    }
325
326
    /**
327
     * it either is NOT in the excluded list or it is in the included list.
328
     */
329
    protected function classCanBeIncluded(string $dataObjectClassName): bool
330
    {
331
        if (count($this->excludedClasses) > 0) {
332
            if (! class_exists($dataObjectClassName)) {
333
                return false;
334
            }
335
            return ! in_array($dataObjectClassName, $this->excludedClasses, true);
336
        }
337
        user_error('Please set excludedClasses', E_USER_NOTICE);
338
        return false;
339
    }
340
}
341