Passed
Push — master ( cb0392...409c3f )
by Andreas
09:17
created

autocomplete::verify_request()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.5923

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 0
dl 0
loc 16
ccs 6
cts 9
cp 0.6667
crap 4.5923
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright CONTENT CONTROL GmbH, http://www.contentcontrol-berlin.de
4
 */
5
6
namespace midcom\datamanager\helper;
7
8
use midcom;
9
use midcom_connection;
10
use midcom_core_query;
11
use midcom_core_account;
12
use midcom_db_person;
13
use midcom_error;
14
use midcom_helper_reflector;
15
use midcom_baseclasses_components_configuration;
16
17
/**
18
 * Experimental autocomplete helper
19
 */
20
class autocomplete
21
{
22
    /**
23
     * The request data we're working on
24
     */
25
    private array $request;
26
27 2
    public function __construct(array $data)
28
    {
29 2
        $this->request = $data;
30 2
        $this->verify_request();
31
    }
32
33 2
    private function verify_request()
34
    {
35 2
        if (!class_exists($this->request['class'])) {
36
            throw new midcom_error("Class {$this->request['class']} could not be loaded");
37
        }
38
39 2
        if (empty($this->request['searchfields'])) {
40
            throw new midcom_error("No fields to search for defined");
41
        }
42
43 2
        if (empty($this->request["term"])) {
44
            throw new midcom_error("Empty query string.");
45
        }
46
47 2
        $this->request['titlefield'] ??= null;
48 2
        $this->request['result_headers'] ??= [];
49
    }
50
51 1
    private function prepare_qb()
52
    {
53 1
        $qb = $this->request['class']::new_query_builder();
54
55 1
        if (!empty($this->request['constraints'])) {
56
            $this->apply_constraints($qb, $this->request['constraints']);
57
        }
58
59 1
        $constraints = $this->get_search_constraints();
60 1
        if (!empty($constraints)) {
61 1
            $qb->begin_group('OR');
62 1
            $this->apply_constraints($qb, $constraints);
63 1
            $qb->end_group();
64
        }
65
66 1
        if (!empty($this->request['orders'])) {
67
            ksort($this->request['orders']);
68
            foreach ($this->request['orders'] as $data) {
69
                foreach ($data as $field => $order) {
70
                    $qb->add_order($field, $order);
71
                }
72
            }
73
        }
74 1
        return $qb;
75
    }
76
77 1
    private function apply_constraints(midcom_core_query $query, array $constraints)
78
    {
79 1
        foreach ($constraints as $key => $data) {
80 1
            if (   !array_key_exists('value', $data)
81 1
                || empty($data['field'])
82 1
                || empty($data['op'])) {
83
                debug_add("Constraint #{$key} is not correctly defined, skipping", MIDCOM_LOG_WARN);
84
                continue;
85
            }
86 1
            if ($data['field'] === 'username') {
87
                midcom_core_account::add_username_constraint($query, $data['op'], $data['value']);
88
            } else {
89 1
                $query->add_constraint($data['field'], $data['op'], $data['value']);
90
            }
91
        }
92
    }
93
94 1
    private function get_search_constraints() : array
95
    {
96 1
        $constraints = [];
97 1
        $query = $this->request["term"];
98 1
        if (preg_match('/^%+$/', $query)) {
99
            debug_add('query is all wildcards, don\'t waste time in adding LIKE constraints');
100
            return $constraints;
101
        }
102
103 1
        $reflector = new midcom_helper_reflector($this->request['class']);
104
105 1
        foreach ($this->request['searchfields'] as $field) {
106 1
            $field_type = $reflector->get_midgard_type($field);
107 1
            $operator = 'LIKE';
108 1
            if (str_contains($field, '.')) {
109
                //TODO: This should be resolved properly
110
                $field_type = MGD_TYPE_STRING;
111
            }
112
            switch ($field_type) {
113 1
                case MGD_TYPE_GUID:
114
                case MGD_TYPE_STRING:
115
                case MGD_TYPE_LONGTEXT:
116 1
                    $query = $this->get_querystring();
117 1
                    break;
118
                case MGD_TYPE_INT:
119
                case MGD_TYPE_UINT:
120
                case MGD_TYPE_FLOAT:
121
                    $operator = '=';
122
                    break;
123
                default:
124
                    debug_add("can't handle field type " . $field_type, MIDCOM_LOG_WARN);
125
                    continue 2;
126
            }
127 1
            debug_add("adding search (ORed) constraint: {$field} {$operator} '{$query}'");
128 1
            $constraints[] = [
129 1
                'field' => $field,
130 1
                'op' => $operator,
131 1
                'value' => $query
132 1
            ];
133
        }
134 1
        return $constraints;
135
    }
136
137 2
    public function get_querystring() : string
138
    {
139 2
        $query = $this->request["term"];
140 2
        $wildcard_query = $query;
141 2
        if (   isset($this->request['auto_wildcards'])
142 2
            && !str_contains($query, '%')) {
143 1
            switch ($this->request['auto_wildcards']) {
144 1
                case 'start':
145
                    $wildcard_query = '%' . $query;
146
                    break;
147 1
                case 'end':
148 1
                    $wildcard_query = $query . '%';
149 1
                    break;
150
                case 'both':
151
                    $wildcard_query = '%' . $query . '%';
152
                    break;
153
                default:
154
                    debug_add("Don't know how to handle auto_wildcards value '" . $this->request['auto_wildcards'] . "'", MIDCOM_LOG_WARN);
155
                    break;
156
            }
157
        }
158 2
        $wildcard_query = str_replace("*", "%", $wildcard_query);
159 2
        return preg_replace('/%+/', '%', $wildcard_query);
160
    }
161
162 1
    public function get_objects() : array
163
    {
164 1
        return $this->prepare_qb()->execute();
165
    }
166
167 1
    public function get_results() : array
168
    {
169 1
        if (empty($this->request["id_field"])) {
170
            throw new midcom_error("Empty ID field.");
171
        }
172
173 1
        $results = $this->get_objects();
174 1
        $items = [];
175
176 1
        foreach ($results as $object) {
177 1
            $item = [
178 1
                'id' => $object->{$this->request['id_field']},
179 1
                'label' => self::create_item_label($object, $this->request['result_headers'], $this->request['titlefield']),
180 1
            ];
181 1
            if (!empty($this->request['result_headers'])) {
182 1
                $item['description'] = self::build_label($object, array_column($this->request['result_headers'], 'name'));
183
            }
184 1
            if (!empty($this->request['categorize_by_parent_label'])) {
185
                $item['category'] = '';
186
                if ($parent = $object->get_parent()) {
187
                    $item['category'] = midcom_helper_reflector::get($parent)->get_object_label($parent);
188
                }
189
            }
190 1
            $item['value'] = $item['label'];
191
192 1
            $items[] = $item;
193
        }
194 1
        usort($items, [$this, 'sort_items']);
195
196 1
        return $items;
197
    }
198
199
    public static function sort_items($a, $b) : int
200
    {
201
        if (isset($a['category'])) {
202
            $cmp = strnatcasecmp($a['category'], $b['category']);
203
            if ($cmp != 0) {
204
                return $cmp;
205
            }
206
        }
207
        return strnatcasecmp($a['label'], $b['label']);
208
    }
209
210 74
    public static function add_head_elements(bool $creation_mode_enabled = false, bool $sortable = false)
211
    {
212 74
        $head = midcom::get()->head;
213 74
        $head->add_stylesheet(MIDCOM_STATIC_URL . "/stock-icons/font-awesome-4.7.0/css/font-awesome.min.css");
214 74
        $head->add_stylesheet(MIDCOM_STATIC_URL . '/midcom.datamanager/autocomplete.css');
215
216 74
        $components = ['menu', 'autocomplete'];
217 74
        if ($sortable) {
218 2
            $components[] = 'mouse';
219 2
            $components[] = 'sortable';
220
        }
221 74
        if ($creation_mode_enabled) {
222 23
            $components = array_merge($components, ['mouse', 'draggable', 'resizable', 'button', 'dialog']);
223 23
            $head->add_jsfile(MIDCOM_STATIC_URL . '/midcom.workflow/workflow.js');
224
        }
225 74
        $head->enable_jquery_ui($components);
226 74
        $head->add_jsfile(MIDCOM_STATIC_URL . '/midcom.datamanager/autocomplete.js');
227
    }
228
229 7
    public static function get_widget_config(string $type) : array
230
    {
231 7
        $handler_url = midcom_connection::get_url('self') . 'midcom-exec-midcom.datamanager/autocomplete.php';
232
233 7
        $widget_config = midcom_baseclasses_components_configuration::get('midcom.datamanager', 'config')->get('clever_classes');
234 7
        $config = $widget_config[$type];
235 7
        $config['handler_url'] = $handler_url;
236 7
        return $config;
237
    }
238
239 39
    public static function create_item_label($object, array $result_headers, ?string $titlefield) : string
240
    {
241 39
        if (!empty($titlefield)) {
242 32
            if (is_callable($titlefield)) {
243
                return $titlefield($object);
244
            }
245 32
            if ($label = self::build_label($object, (array) $titlefield)) {
246 26
                return $label;
247
            }
248
        }
249 17
        return midcom_helper_reflector::get($object)->get_object_label($object) ?:
250 9
            self::build_label($object, array_column($result_headers, 'name')) ?:
251 17
            get_class($object) . ' #' . $object->id;
252
    }
253
254 36
    private static function build_label($object, array $fields) : string
255
    {
256 36
        $label = [];
257 36
        foreach ($fields as $field) {
258 36
            if ($value = self::get_property_string($object, $field)) {
259 34
                $label[] = $value;
260
            }
261
        }
262 36
        return implode(', ', $label);
263
    }
264
265 36
    private static function get_property_string($object, string $field) : string
266
    {
267 36
        if (preg_match('/^metadata\.(.+)$/', $field, $regs)) {
268
            $date_fields = ['created', 'revised', 'published', 'schedulestart', 'scheduleend', 'imported', 'exported', 'approved'];
269
            $person_fields = ['creator', 'revisor', 'approver', 'locker'];
270
            $metadata_property = $regs[1];
271
            $value = $object->metadata->$metadata_property;
272
273
            if (in_array($metadata_property, $date_fields)) {
274
                $formatter = midcom::get()->i18n->get_l10n()->get_formatter();
275
                return $value ? $formatter->datetime($value, \IntlDateFormatter::SHORT, \IntlDateFormatter::MEDIUM) : '';
276
            }
277
            if (in_array($metadata_property, $person_fields)) {
278
                if ($value) {
279
                    $person = new midcom_db_person($value);
280
                    return self::sanitize_label($person->name);
281
                }
282
                return '';
283
            }
284
            return self::sanitize_label($value);
285
        }
286 36
        if (   $field == 'username'
287 36
            && $object instanceof midcom_db_person) {
288
            $account = new midcom_core_account($object);
289
            return self::sanitize_label($account->get_username());
290
        }
291 36
        return self::sanitize_label($object->$field);
292
    }
293
294 36
    private static function sanitize_label($input) : string
295
    {
296 36
        return trim(strip_tags((string) $input));
297
    }
298
}