Passed
Push — master ( 6d236c...fc8074 )
by Stefan
07:29
created

OptionParser   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 501
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 87
eloc 248
dl 0
loc 501
rs 2
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
C postProcessValidAttributes() 0 56 13
B checkUploadSanity() 0 29 10
C sendOptionsToDatabase() 0 33 12
A collateOptionArrays() 0 9 1
A postProcessCoordinates() 0 9 3
A processSubmittedFields() 0 59 4
A __construct() 0 4 1
A displaySummaryInUI() 0 25 5
F sanitiseInputs() 0 94 24
C furtherStringChecks() 0 56 14

How to fix   Complexity   

Complex Class

Complex classes like OptionParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OptionParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
namespace web\lib\admin;
24
25
use Exception;
26
27
?>
28
<?php
29
30
require_once dirname(dirname(dirname(dirname(__FILE__)))) . "/config/_config.php";
31
32
/**
33
 * This class parses HTML field input from POST and FILES and extracts valid and authorized options to be set.
34
 * 
35
 * @author Stefan Winter <[email protected]>
36
 */
37
class OptionParser extends \core\common\Entity {
38
39
    /**
40
     * an instance of the InputValidation class which we use heavily for syntax checks.
41
     * 
42
     * @var \web\lib\common\InputValidation
43
     */
44
    private $validator;
45
46
    /**
47
     * an instance of the UIElements() class to draw some UI widgets from.
48
     * 
49
     * @var UIElements
50
     */
51
    private $uiElements;
52
53
    /**
54
     * a handle for the Options singleton
55
     * 
56
     * @var \core\Options
57
     */
58
    private $optioninfoObject;
59
60
    /**
61
     * initialises the various handles.
62
     */
63
    public function __construct() {
64
        $this->validator = new \web\lib\common\InputValidation();
65
        $this->uiElements = new UIElements();
66
        $this->optioninfoObject = \core\Options::instance();
67
    }
68
69
    /**
70
     * Verifies whether an incoming upload was actually valid data
71
     * 
72
     * @param string $optiontype     for which option was the data uploaded
73
     * @param string $incomingBinary the uploaded data
74
     * @return boolean whether the data was valid
75
     */
76
    private function checkUploadSanity(string $optiontype, string $incomingBinary) {
77
        switch ($optiontype) {
78
            case "general:logo_file":
79
            case "fed:logo_file":
80
            case "internal:logo_from_url":
81
                // we check logo_file with ImageMagick
82
                return $this->validator->image($incomingBinary);
83
            case "eap:ca_file":
84
            // fall-through intended: both CA types are treated the same
85
            case "fed:minted_ca_file":
86
                // echo "Checking $optiontype with file $filename";
87
                $func = new \core\common\X509;
88
                $cert = $func->processCertificate($incomingBinary);
89
                if (is_array($cert)) { // could also be FALSE if it was incorrect incoming data
1 ignored issue
show
introduced by
The condition is_array($cert) is always false.
Loading history...
90
                    return TRUE;
91
                }
92
                // the certificate seems broken
93
                return FALSE;
94
            case "support:info_file":
95
                $info = new \finfo();
96
                $filetype = $info->buffer($incomingBinary, FILEINFO_MIME_TYPE);
97
98
                // we only take plain text files in UTF-8!
99
                if ($filetype == "text/plain" && iconv("UTF-8", "UTF-8", $incomingBinary) !== FALSE) {
100
                    return TRUE;
101
                }
102
                return FALSE;
103
            default:
104
                return FALSE;
105
        }
106
    }
107
108
    /**
109
     * Known-good options are sometimes converted, this function takes care of that.
110
     * 
111
     * Cases in point:
112
     * - CA import by URL reference: fetch cert from URL and store it as CA file instead
113
     * - Logo import by URL reference: fetch logo from URL and store it as logo file instead
114
     * - CA file: mangle the content so that *only* the valid content remains (raw input may contain line breaks or spaces which are valid, but some supplicants choke upon)
115
     * 
116
     * @param array $options the list of options we got
117
     * @param array $good    by-reference: the future list of actually imported options
118
     * @param array $bad     by-reference: the future list of submitted but rejected options
119
     * @return array the options, post-processed
120
     */
121
    private function postProcessValidAttributes(array $options, array &$good, array &$bad) {
122
        foreach ($options as $index => $iterateOption) {
123
            foreach ($iterateOption as $name => $optionPayload) {
124
                switch ($name) {
125
                    case "eap:ca_url": // eap:ca_url becomes eap:ca_file by downloading the file
126
                        $finalOptionname = "eap:ca_file";
127
                    // intentional fall-through, treatment identical to logo_url
128
                    case "general:logo_url": // logo URLs become logo files by downloading the file
129
                        $finalOptionname = $finalOptionname ?? "general:logo_file";
130
                        if (empty($optionPayload['content'])) {
131
                            break;
132
                        }
133
                        $bindata = \core\common\OutsideComm::downloadFile($optionPayload['content']);
134
                        unset($options[$index]);
135
                        if ($bindata === FALSE) {
136
                            $bad[] = $name;
137
                            break;
138
                        }
139
                        if ($this->checkUploadSanity($finalOptionname, $bindata)) {
140
                            $good[] = $name;
141
                            $options[] = [$finalOptionname => ['lang' => NULL, 'content' => base64_encode($bindata)]];
142
                        } else {
143
                            $bad[] = $name;
144
                        }
145
                        break;
146
                    case "eap:ca_file":
147
                    case "fed:minted_ca_file":
148
                        // CA files get split (PEM files can contain more than one CA cert)
149
                        // the data being processed here is always "good": 
150
                        // if it was eap:ca_file initially then its sanity was checked in step 1;
151
                        // if it was eap:ca_url then it was checked after we downloaded it
152
                        if (empty($optionPayload['content'])) {
153
                            break;
154
                        }
155
                        if (preg_match('/^ROWID-/', $optionPayload['content'])) {
156
                            // accounted for, already in DB
157
                            $good[] = $name;
158
                            break;
159
                        }
160
                        $content = base64_decode($optionPayload['content']);
161
                        unset($options[$index]);
162
                        $x509 = new \core\common\X509();
163
                        $cAFiles = $x509->splitCertificate($content);
164
                        foreach ($cAFiles as $cAFile) {
165
                            $options[] = [$name => ['lang' => NULL, 'content' => base64_encode($x509->pem2der($cAFile))]];
166
                        }
167
                        $good[] = $name;
168
                        break;
169
                    default:
170
                        $good[] = $name; // all other options were checked and are sane in step 1 already
171
                        break;
172
                }
173
            }
174
        }
175
176
        return $options;
177
    }
178
179
    /**
180
     * extracts a coordinate pair from _POST (if any) and returns it in our 
181
     * standard attribute notation
182
     * 
183
     * @param array $postArray data as sent by POST
184
     * @param array $good      options which have been successfully parsed
185
     * @return array
186
     */
187
    private function postProcessCoordinates(array $postArray, array &$good) {
188
        if (!empty($postArray['geo_long']) && !empty($postArray['geo_lat'])) {
189
190
            $lat = $this->validator->coordinate($postArray['geo_lat']);
191
            $lon = $this->validator->coordinate($postArray['geo_long']);
192
            $good[] = ("general:geo_coordinates");
193
            return [0 => ["general:geo_coordinates" => ['lang' => NULL, 'content' => json_encode(["lon" => $lon, "lat" => $lat])]]];
194
        }
195
        return [];
196
    }
197
198
    /**
199
     * creates HTML code for a user-readable summary of the imports
200
     * @param array $good           list of actually imported options
201
     * @param array $bad            list of submitted but rejected options
202
     * @param array $mlAttribsWithC list of language-variant options
203
     * @return string HTML code
204
     */
205
    private function displaySummaryInUI(array $good, array $bad, array $mlAttribsWithC) {
206
        \core\common\Entity::intoThePotatoes();
207
        $retval = "";
208
        // don't do your own table - only the <tr>s here
209
        // list all attributes that were set correctly
210
        $listGood = array_count_values($good);
211
        $uiElements = new UIElements();
212
        foreach ($listGood as $name => $count) {
213
            /// number of times attribute is present, and its name
214
            /// Example: "5x Support E-Mail"
215
            $retval .= $this->uiElements->boxOkay(sprintf(_("%dx %s"), $count, $uiElements->displayName($name)));
216
        }
217
        // list all atributes that had errors
218
        $listBad = array_count_values($bad);
219
        foreach ($listBad as $name => $count) {
220
            $retval .= $this->uiElements->boxError(sprintf(_("%dx %s"), (int) $count, $uiElements->displayName($name)));
221
        }
222
        // list multilang without default
223
        foreach ($mlAttribsWithC as $attribName => $isitsetornot) {
224
            if ($isitsetornot == FALSE) {
225
                $retval .= $this->uiElements->boxWarning(sprintf(_("You did not set a 'default language' value for %s. This means we can only display this string for installers which are <strong>exactly</strong> in the language you configured. For the sake of all other languages, you may want to edit the profile again and populate the 'default/other' language field."), $uiElements->displayName($attribName)));
226
            }
227
        }
228
        \core\common\Entity::outOfThePotatoes();
229
        return $retval;
230
    }
231
232
    /**
233
     * Incoming data is in $_POST and possibly in $_FILES. Collate values into 
234
     * one array according to our name and numbering scheme.
235
     * 
236
     * @param array $postArray  _POST
237
     * @param array $filesArray _FILES
238
     * @return array
239
     */
240
    private function collateOptionArrays(array $postArray, array $filesArray) {
241
242
        $optionarray = $postArray['option'] ?? [];
243
        $valuearray = $postArray['value'] ?? [];
244
        $filesarray = $filesArray['value']['tmp_name'] ?? [];
245
246
        $iterator = array_merge($optionarray, $valuearray, $filesarray);
247
248
        return $iterator;
249
    }
250
251
    /**
252
     * The very end of the processing: clean input data gets sent to the database
253
     * for storage
254
     * 
255
     * @param mixed  $object            for which object are the options
256
     * @param array  $options           the options to store
257
     * @param array  $pendingattributes list of attributes which are already stored but may need to be deleted
258
     * @param string $device            when the $object is Profile, this indicates device-specific attributes
259
     * @param int    $eaptype           when the $object is Profile, this indicates eap-specific attributes
260
     * @return array list of attributes which were previously stored but are to be deleted now
261
     * @throws Exception
262
     */
263
    private function sendOptionsToDatabase($object, array $options, array $pendingattributes, string $device = NULL, int $eaptype = NULL) {
264
        $retval = [];
265
        foreach ($options as $iterateOption) {
266
            foreach ($iterateOption as $name => $optionPayload) {
267
                $optiontype = $this->optioninfoObject->optionType($name);
268
                // some attributes are in the DB and were only called by reference
269
                // keep those which are still referenced, throw the rest away
270
                if ($optiontype["type"] == \core\Options::TYPECODE_FILE && preg_match("/^ROWID-.*-([0-9]+)/", $optionPayload['content'], $retval)) {
271
                    unset($pendingattributes[$retval[1]]);
272
                    continue;
273
                }
274
                switch (get_class($object)) {
275
                    case 'core\\ProfileRADIUS':
276
                        if ($device !== NULL) {
277
                            $object->addAttributeDeviceSpecific($name, $optionPayload['lang'], $optionPayload['content'], $device);
278
                        } elseif ($eaptype !== NULL) {
279
                            $object->addAttributeEAPSpecific($name, $optionPayload['lang'], $optionPayload['content'], $eaptype);
280
                        } else {
281
                            $object->addAttribute($name, $optionPayload['lang'], $optionPayload['content']);
282
                        }
283
                        break;
284
                    case 'core\\IdP':
285
                    case 'core\\User':
286
                    case 'core\\Federation':
287
                    case 'core\\DeploymentManaged':
288
                        $object->addAttribute($name, $optionPayload['lang'], $optionPayload['content']);
289
                        break;
290
                    default:
291
                        throw new Exception("This type of object can't have options that are parsed by this file!");
292
                }
293
            }
294
        }
295
        return $pendingattributes;
296
    }
297
298
    /**
299
     * filters the input to find syntactically correctly submitted attributes
300
     * 
301
     * @param array $listOfEntries       list of POST and FILES entries
302
     * @param array $multilangAttrsWithC by-reference: future list of language-variant options and their "default lang" state
303
     * @param array $bad                 by-reference: future list of submitted but rejected options
304
     * @return array sanitised list of options
305
     * @throws Exception
306
     */
307
    private function sanitiseInputs(array $listOfEntries, array &$multilangAttrsWithC, array &$bad) {
308
        $retval = [];
309
        foreach ($listOfEntries as $objId => $objValueRaw) {
310
// pick those without dash - they indicate a new value        
311
            if (preg_match('/^S[0123456789]*$/', $objId)) {
312
                $objValue = $this->validator->optionName(preg_replace('/#.*$/', '', $objValueRaw));
313
                $optioninfo = $this->optioninfoObject->optionType($objValue);
314
                $lang = NULL;
315
                if ($optioninfo["flag"] == "ML") {
316
                    if (isset($listOfEntries["$objId-lang"])) {
317
                        if (!isset($multilangAttrsWithC[$objValue])) { // on first sight, initialise the attribute as "no C language set"
318
                            $multilangAttrsWithC[$objValue] = FALSE;
319
                        }
320
                        $lang = $listOfEntries["$objId-lang"];
321
                        if ($lang == "") { // user forgot to select a language
322
                            $lang = "C";
323
                        }
324
                    } else {
325
                        $bad[] = $objValue;
326
                        continue;
327
                    }
328
                    // did we get a C language? set corresponding value to TRUE
329
                    if ($lang == "C") {
330
                        $multilangAttrsWithC[$objValue] = TRUE;
331
                    }
332
                }
333
334
                // many of the cases below condense due to identical treatment
335
                // except validator function to call and where in POST the
336
                // content is
337
                $validators = [
338
                    \core\Options::TYPECODE_TEXT => ["function" => "string", "field" => \core\Options::TYPECODE_TEXT, "extraarg" => [TRUE]],
339
                    \core\Options::TYPECODE_COORDINATES => ["function" => "coordJsonEncoded", "field" => \core\Options::TYPECODE_TEXT, "extraarg" => []],
340
                    \core\Options::TYPECODE_BOOLEAN => ["function" => "boolean", "field" => \core\Options::TYPECODE_BOOLEAN, "extraarg" => []],
341
                    \core\Options::TYPECODE_INTEGER => ["function" => "integer", "field" => \core\Options::TYPECODE_INTEGER, "extraarg" => []],
342
                ];
343
344
                switch ($optioninfo["type"]) {
345
                    case \core\Options::TYPECODE_TEXT:
346
                    case \core\Options::TYPECODE_COORDINATES:
347
                    case \core\Options::TYPECODE_INTEGER:
348
                        $varName = "$objId-" . $validators[$optioninfo['type']]['field'];
349
                        if (!empty($listOfEntries[$varName])) {
350
                            $content = call_user_func_array([$this->validator, $validators[$optioninfo['type']]['function']], array_merge([$listOfEntries[$varName]], $validators[$optioninfo['type']]['extraarg']));
351
                            break;
352
                        }
353
                        continue 2;
354
                    case \core\Options::TYPECODE_BOOLEAN:
355
                        $varName = "$objId-" . \core\Options::TYPECODE_BOOLEAN;
356
                        if (!empty($listOfEntries[$varName])) {
357
                            $contentValid = $this->validator->boolean($listOfEntries[$varName]);
358
                            if ($contentValid) {
359
                                $content = "on";
360
                            } else {
361
                                $bad[] = $objValue;
362
                                continue 2;
363
                            }
364
                            break;
365
                        }
366
                        continue 2;
367
                    case \core\Options::TYPECODE_STRING:
368
                        $previsionalContent = $listOfEntries["$objId-" . \core\Options::TYPECODE_STRING];
369
                        if (!empty($previsionalContent)) {
370
                            $content = $this->furtherStringChecks($objValue, $previsionalContent, $bad);
371
                            if ($content === FALSE) {
372
                                continue 2;
373
                            }
374
                            break;
375
                        }
376
                        continue 2;
377
                    case \core\Options::TYPECODE_FILE:
378
                        if (!empty($listOfEntries["$objId-" . \core\Options::TYPECODE_STRING])) { // was already in, by ROWID reference, extract
379
                            // ROWID means it's a multi-line string (simple strings are inline in the form; so allow whitespace)
380
                            $content = $this->validator->string(urldecode($listOfEntries["$objId-" . \core\Options::TYPECODE_STRING]), TRUE);
381
                            break;
382
                        } else if (isset($listOfEntries["$objId-" . \core\Options::TYPECODE_FILE]) && ($listOfEntries["$objId-" . \core\Options::TYPECODE_FILE] != "")) { // let's do the download
383
                            $rawContent = \core\common\OutsideComm::downloadFile("file:///" . $listOfEntries["$objId-" . \core\Options::TYPECODE_FILE]);
384
385
                            if ($rawContent === FALSE || !$this->checkUploadSanity($objValue, $rawContent)) {
386
                                $bad[] = $objValue;
387
                                continue 2;
388
                            }
389
                            $content = base64_encode($rawContent);
390
                            break;
391
                        }
392
                        continue 2;
393
                    default:
394
                        throw new Exception("Internal Error: Unknown option type " . $objValue . "!");
395
                }
396
                // lang can be NULL here, if it's not a multilang attribute, or a ROWID reference. Never mind that.
397
                $retval[] = ["$objValue" => ["lang" => $lang, "content" => $content]];
398
            }
399
        }
400
        return $retval;
401
    }
402
403
    /**
404
     * 
405
     * @param string $attribute          which attribute was sent?
406
     * @param string $previsionalContent which content was sent?
407
     * @param array  $bad                list of malformed attributes, by-reference
408
     * @return string|false FALSE if value is not in expected format, else the content itself
409
     */
410
    private function furtherStringChecks($attribute, $previsionalContent, &$bad) {
411
        $content = FALSE;
412
        switch ($attribute) {
413
            case "media:consortium_OI":
414
                $content = $this->validator->consortiumOI($previsionalContent);
415
                if ($content === FALSE) {
416
                    $bad[] = $attribute;
417
                    return FALSE;
418
                }
419
                break;
420
            case "media:remove_SSID":
421
                $content = $this->validator->string($previsionalContent);
422
                if ($content == "eduroam") {
423
                    $bad[] = $attribute;
424
                    return FALSE;
425
                }
426
                break;
427
            case "media:force_proxy":
428
                $content = $this->validator->string($previsionalContent);
429
                $serverAndPort = explode(':', strrev($content), 2);
430
                if (count($serverAndPort) != 2) {
431
                    $bad[] = $attribute;
432
                    return FALSE;
433
                }
434
                $port = strrev($serverAndPort[0]);
435
                if (!is_numeric($port)) {
436
                    $bad[] = $attribute;
437
                    return FALSE;
438
                }
439
                break;
440
            case "support:url":
441
                $content = $this->validator->string($previsionalContent);
442
                if (preg_match("/^http/", $content) != 1) {
443
                    $bad[] = $attribute;
444
                    return FALSE;
445
                }
446
                break;
447
            case "support:email":
448
                $content = $this->validator->email($previsionalContent);
449
                if ($content === FALSE) {
450
                    $bad[] = $attribute;
451
                    return FALSE;
452
                }
453
                break;
454
            case "managedsp:operatorname":
455
                $content = $previsionalContent;
456
                if (!preg_match("/^1.*\..*/", $content)) {
457
                    $bad[] = $attribute;
458
                    return FALSE;
459
                }
460
                break;
461
            default:
462
                $content = $this->validator->string($previsionalContent);
463
                break;
464
        }
465
        return $content;
466
    }
467
468
    /**
469
     * The main function: takes all HTML field inputs, makes sense of them and stores valid data in the database
470
     * 
471
     * @param mixed  $object     The object for which attributes were submitted
472
     * @param array  $postArray  incoming attribute names and values as submitted with $_POST
473
     * @param array  $filesArray incoming attribute names and values as submitted with $_FILES
474
     * @param int    $eaptype    for eap-specific attributes (only used where $object is a ProfileRADIUS instance)
475
     * @param string $device     for device-specific attributes (only used where $object is a ProfileRADIUS instance)
476
     * @return string text to be displayed in UI with the summary of attributes added
477
     * @throws Exception
478
     */
479
    public function processSubmittedFields($object, array $postArray, array $filesArray, int $eaptype = NULL, string $device = NULL) {
480
481
        // construct new array with all non-empty options for later feeding into DB
482
        // $multilangAttrsWithC is a helper array to keep track of multilang 
483
        // options that were set in a specific language but are not 
484
        // accompanied by a "default" language setting
485
        // if there are some without C by the end of processing, we need to warn
486
        // the admin that this attribute is "invisible" in certain languages
487
        // attrib_name -> boolean
488
489
        $multilangAttrsWithC = [];
490
491
        // these two variables store which attributes were processed 
492
        // successfully vs. which were discarded because in some way malformed
493
494
        $good = [];
495
        $bad = [];
496
497
        // Step 1: collate option names, option values and uploaded files (by 
498
        // filename reference) into one array for later handling
499
500
        $iterator = $this->collateOptionArrays($postArray, $filesArray);
501
502
        // Step 2: sieve out malformed input
503
        $cleanData = $this->sanitiseInputs($iterator, $multilangAttrsWithC, $bad);
504
        // Step 3: now we have clean input data. Some attributes need special care:
505
        // URL-based attributes need to be downloaded to get their actual content
506
        // CA files may need to be split (PEM can contain multiple CAs 
507
508
        $optionsStep2 = $this->postProcessValidAttributes($cleanData, $good, $bad);
509
510
        // Step 4: coordinates do not follow the usual POST array as they are 
511
        // two values forming one attribute; extract those two as an extra step
512
513
        $options = array_merge($optionsStep2, $this->postProcessCoordinates($postArray, $good));
514
515
        // Step 5: push all the received options to the database. Keep mind of 
516
        // the list of existing database entries that are to be deleted.
517
        // 5a: first deletion step: purge all old content except file-based attributes;
518
        //     then take note of which file-based attributes are now stale
519
        if ($device === NULL && $eaptype === NULL) {
520
            $remaining = $object->beginflushAttributes();
521
            $killlist = $this->sendOptionsToDatabase($object, $options, $remaining);
522
        } elseif ($device !== NULL) {
523
            $remaining = $object->beginFlushMethodLevelAttributes(0, $device);
524
            $killlist = $this->sendOptionsToDatabase($object, $options, $remaining, $device);
525
        } else {
526
            $remaining = $object->beginFlushMethodLevelAttributes($eaptype, "");
527
            $killlist = $this->sendOptionsToDatabase($object, $options, $remaining, NULL, $eaptype);
528
        }
529
        // 5b: finally, kill the stale file-based attributes which are not wanted any more.
530
        $object->commitFlushAttributes($killlist);
531
532
        // finally: return HTML code that gives feedback about what we did. 
533
        // In some cases, callers won't actually want to display it; so simply
534
        // do not echo the return value. Reasons not to do this is if we working
535
        // e.g. from inside an overlay
536
537
        return $this->displaySummaryInUI($good, $bad, $multilangAttrsWithC);
538
    }
539
540
}
541