Test Setup Failed
Push — master ( 194661...f85b2d )
by Stefan
06:51
created

OptionParser::sanitiseInputs()   D

Complexity

Conditions 23
Paths 43

Size

Total Lines 91
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 68
c 0
b 0
f 0
dl 0
loc 91
rs 4.1666
cc 23
nc 43
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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