Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 360
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
WebInstallerOptions
0.00% covered (danger)
0.00%
0 / 360
0.00% covered (danger)
0.00%
0 / 12
3660
0.00% covered (danger)
0.00%
0 / 1
 execute
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 addPersonalizationOptions
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
2
 addModeOptions
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 addEmailOptions
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
6
 addSkinOptions
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 addExtensionOptions
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
240
 addFileOptions
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
6
 addAdvancedOptions
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 makeScreenshotsLink
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 makeMoreInfoLink
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 submitSkins
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 submit
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
420
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 *
19 * @file
20 * @ingroup Installer
21 */
22
23namespace MediaWiki\Installer;
24
25use MediaWiki\Html\Html;
26use MediaWiki\Specials\SpecialVersion;
27use Wikimedia\IPUtils;
28
29class WebInstallerOptions extends WebInstallerPage {
30
31    /**
32     * @return string|null
33     */
34    public function execute() {
35        if ( $this->getVar( '_SkipOptional' ) == 'skip' ) {
36            $this->submitSkins();
37            return 'skip';
38        }
39        if ( $this->parent->request->wasPosted() && $this->submit() ) {
40            return 'continue';
41        }
42
43        $this->startForm();
44        $this->addModeOptions();
45        $this->addEmailOptions();
46        $this->addSkinOptions();
47        $this->addExtensionOptions();
48        $this->addFileOptions();
49        $this->addPersonalizationOptions();
50        $this->addAdvancedOptions();
51        $this->endForm();
52
53        return null;
54    }
55
56    private function addPersonalizationOptions() {
57        $parent = $this->parent;
58        $this->addHTML(
59            $this->getFieldsetStart( 'config-personalization-settings' ) .
60            Html::rawElement( 'div', [
61                'class' => 'config-drag-drop'
62            ], wfMessage( 'config-logo-summary' )->parse() ) .
63            Html::openElement( 'div', [
64                'class' => 'config-personalization-options'
65            ] ) .
66            Html::hidden( 'config_LogoSiteName', $this->getVar( 'wgSitename' ) ) .
67            $parent->getTextBox( [
68                'var' => '_LogoIcon',
69                // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
70                'value' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
71                'label' => 'config-logo-icon',
72                'attribs' => [ 'dir' => 'ltr' ],
73                'help' => $parent->getHelpBox( 'config-logo-icon-help' )
74            ] ) .
75            $parent->getTextBox( [
76                'var' => '_LogoWordmark',
77                'label' => 'config-logo-wordmark',
78                'attribs' => [ 'dir' => 'ltr' ],
79                'help' => $parent->getHelpBox( 'config-logo-wordmark-help' )
80            ] ) .
81            $parent->getTextBox( [
82                'var' => '_LogoTagline',
83                'label' => 'config-logo-tagline',
84                'attribs' => [ 'dir' => 'ltr' ],
85                'help' => $parent->getHelpBox( 'config-logo-tagline-help' )
86            ] ) .
87            $parent->getTextBox( [
88                'var' => '_Logo1x',
89                'label' => 'config-logo-sidebar',
90                'attribs' => [ 'dir' => 'ltr' ],
91                'help' => $parent->getHelpBox( 'config-logo-sidebar-help' )
92            ] ) .
93            Html::openElement( 'div', [
94                'class' => 'logo-preview-area',
95                'data-main-page' => wfMessage( 'config-logo-preview-main' ),
96                'data-filedrop' => wfMessage( 'config-logo-filedrop' )
97            ] ) .
98            Html::closeElement( 'div' ) .
99            Html::closeElement( 'div' ) .
100            $this->getFieldsetEnd()
101        );
102    }
103
104    /**
105     * Wiki mode - user rights and copyright model.
106     * @return void
107     */
108    private function addModeOptions(): void {
109        $this->addHTML(
110            # User Rights
111            // getRadioSet() builds a set of labeled radio buttons.
112            // For grep: The following messages are used as the item labels:
113            // config-profile-wiki, config-profile-no-anon, config-profile-fishbowl, config-profile-private
114            $this->parent->getRadioSet( [
115                'var' => '_RightsProfile',
116                'label' => 'config-profile',
117                'itemLabelPrefix' => 'config-profile-',
118                'values' => array_keys( $this->parent->rightsProfiles ),
119            ] ) .
120            $this->parent->getInfoBox( wfMessage( 'config-profile-help' )->plain() ) .
121
122            # Licensing
123            // getRadioSet() builds a set of labeled radio buttons.
124            // For grep: The following messages are used as the item labels:
125            // config-license-cc-by, config-license-cc-by-sa, config-license-cc-by-nc-sa,
126            // config-license-cc-0, config-license-pd, config-license-gfdl,
127            // config-license-none
128            $this->parent->getRadioSet( [
129                'var' => '_LicenseCode',
130                'label' => 'config-license',
131                'itemLabelPrefix' => 'config-license-',
132                'values' => array_keys( $this->parent->licenses ),
133                'commonAttribs' => [ 'class' => 'licenseRadio' ],
134            ] ) .
135            $this->parent->getHelpBox( 'config-license-help' )
136        );
137    }
138
139    /**
140     * User email options.
141     * @return void
142     */
143    private function addEmailOptions(): void {
144        $emailwrapperStyle = $this->getVar( 'wgEnableEmail' ) ? '' : 'display: none';
145        $this->addHTML(
146            $this->getFieldsetStart( 'config-email-settings' ) .
147            $this->parent->getCheckBox( [
148                'var' => 'wgEnableEmail',
149                'label' => 'config-enable-email',
150                'attribs' => [ 'class' => 'showHideRadio', 'rel' => 'emailwrapper' ],
151            ] ) .
152            $this->parent->getHelpBox( 'config-enable-email-help' ) .
153            "<div id=\"emailwrapper\" style=\"$emailwrapperStyle\">" .
154            $this->parent->getTextBox( [
155                'var' => 'wgPasswordSender',
156                'label' => 'config-email-sender'
157            ] ) .
158            $this->parent->getHelpBox( 'config-email-sender-help' ) .
159            $this->parent->getCheckBox( [
160                'var' => 'wgEnableUserEmail',
161                'label' => 'config-email-user',
162            ] ) .
163            $this->parent->getHelpBox( 'config-email-user-help' ) .
164            $this->parent->getCheckBox( [
165                'var' => 'wgEnotifUserTalk',
166                'label' => 'config-email-usertalk',
167            ] ) .
168            $this->parent->getHelpBox( 'config-email-usertalk-help' ) .
169            $this->parent->getCheckBox( [
170                'var' => 'wgEnotifWatchlist',
171                'label' => 'config-email-watchlist',
172            ] ) .
173            $this->parent->getHelpBox( 'config-email-watchlist-help' ) .
174            $this->parent->getCheckBox( [
175                'var' => 'wgEmailAuthentication',
176                'label' => 'config-email-auth',
177            ] ) .
178            $this->parent->getHelpBox( 'config-email-auth-help' ) .
179            "</div>" .
180            $this->getFieldsetEnd()
181        );
182    }
183
184    /**
185     * Opt-in for bundled skins.
186     * @return void
187     */
188    private function addSkinOptions(): void {
189        $skins = $this->parent->findExtensions( 'skins' )->value;
190        '@phan-var array[] $skins';
191        $skinHtml = $this->getFieldsetStart( 'config-skins' );
192
193        $skinNames = array_map( 'strtolower', array_keys( $skins ) );
194        $chosenSkinName = $this->getVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) );
195
196        if ( $skins ) {
197            $radioButtons = $this->parent->getRadioElements( [
198                'var' => 'wgDefaultSkin',
199                'itemLabels' => array_fill_keys( $skinNames, 'config-skins-use-as-default' ),
200                'values' => $skinNames,
201                'value' => $chosenSkinName,
202            ] );
203
204            foreach ( $skins as $skin => $info ) {
205                if ( isset( $info['screenshots'] ) ) {
206                    $screenshotText = $this->makeScreenshotsLink( $skin, $info['screenshots'] );
207                } else {
208                    $screenshotText = htmlspecialchars( $skin );
209                }
210                $skinHtml .=
211                    '<div class="config-skins-item">' .
212                    $this->parent->getCheckBox( [
213                        'var' => "skin-$skin",
214                        'rawtext' => $screenshotText . $this->makeMoreInfoLink( $info ),
215                        'value' => $this->getVar( "skin-$skin", true ), // all found skins enabled by default
216                    ] ) .
217                    '<div class="config-skins-use-as-default">' . $radioButtons[strtolower( $skin )] . '</div>' .
218                    '</div>';
219            }
220        } else {
221            $skinHtml .=
222                Html::warningBox( wfMessage( 'config-skins-missing' )->plain(), 'config-warning-box' ) .
223                Html::hidden( 'config_wgDefaultSkin', $chosenSkinName );
224        }
225
226        $skinHtml .= $this->parent->getHelpBox( 'config-skins-help' ) .
227            $this->getFieldsetEnd();
228        $this->addHTML( $skinHtml );
229    }
230
231    /**
232     * Opt-in for bundled extensions.
233     * @return void
234     */
235    private function addExtensionOptions(): void {
236        global $wgLang;
237
238        $extensions = $this->parent->findExtensions()->value;
239        '@phan-var array[] $extensions';
240        $dependencyMap = [];
241
242        if ( $extensions ) {
243            $extHtml = $this->getFieldsetStart( 'config-extensions' );
244
245            $extByType = [];
246            $types = SpecialVersion::getExtensionTypes();
247            // Sort by type first
248            foreach ( $extensions as $ext => $info ) {
249                if ( !isset( $info['type'] ) || !isset( $types[$info['type']] ) ) {
250                    // We let extensions normally define custom types, but
251                    // since we aren't loading extensions, we'll have to
252                    // categorize them under other
253                    $info['type'] = 'other';
254                }
255                $extByType[$info['type']][$ext] = $info;
256            }
257
258            foreach ( $types as $type => $message ) {
259                if ( !isset( $extByType[$type] ) ) {
260                    continue;
261                }
262                $extHtml .= Html::element( 'h2', [], $message );
263                foreach ( $extByType[$type] as $ext => $info ) {
264                    $attribs = [
265                        'data-name' => $ext,
266                        'class' => 'config-ext-input cdx-checkbox__input'
267                    ];
268                    $labelAttribs = [];
269                    if ( isset( $info['requires']['extensions'] ) ) {
270                        $dependencyMap[$ext]['extensions'] = $info['requires']['extensions'];
271                        $labelAttribs['class'] = 'mw-ext-with-dependencies';
272                    }
273                    if ( isset( $info['requires']['skins'] ) ) {
274                        $dependencyMap[$ext]['skins'] = $info['requires']['skins'];
275                        $labelAttribs['class'] = 'mw-ext-with-dependencies';
276                    }
277                    if ( isset( $dependencyMap[$ext] ) ) {
278                        $links = [];
279                        // For each dependency, link to the checkbox for each
280                        // extension/skin that is required
281                        if ( isset( $dependencyMap[$ext]['extensions'] ) ) {
282                            foreach ( $dependencyMap[$ext]['extensions'] as $name ) {
283                                $links[] = Html::element(
284                                    'a',
285                                    [ 'href' => "#config_ext-$name" ],
286                                    $name
287                                );
288                            }
289                        }
290                        if ( isset( $dependencyMap[$ext]['skins'] ) ) {
291                            // @phan-suppress-next-line PhanTypeMismatchForeach Phan internal bug
292                            foreach ( $dependencyMap[$ext]['skins'] as $name ) {
293                                $links[] = Html::element(
294                                    'a',
295                                    [ 'href' => "#config_skin-$name" ],
296                                    $name
297                                );
298                            }
299                        }
300
301                        $text = wfMessage( 'config-extensions-requires' )
302                            ->rawParams( $ext, $wgLang->commaList( $links ) )
303                            ->escaped();
304                    } else {
305                        $text = $ext;
306                    }
307                    $extHtml .= $this->parent->getCheckBox( [
308                        'var' => "ext-$ext",
309                        'rawtext' => $text . $this->makeMoreInfoLink( $info ),
310                        'attribs' => $attribs,
311                        'labelAttribs' => $labelAttribs,
312                    ] );
313                }
314            }
315
316            $extHtml .= $this->parent->getHelpBox( 'config-extensions-help' ) .
317                $this->getFieldsetEnd();
318            $this->addHTML( $extHtml );
319            // Push the dependency map to the client side
320            $this->addHTML( Html::inlineScript(
321                'var extDependencyMap = ' . Html::encodeJsVar( $dependencyMap )
322            ) );
323        }
324    }
325
326    /**
327     * Image and file upload options.
328     * @return void
329     */
330    private function addFileOptions(): void {
331        // Having / in paths in Windows looks funny :)
332        $this->setVar( 'wgDeletedDirectory',
333            str_replace(
334                '/', DIRECTORY_SEPARATOR,
335                $this->getVar( 'wgDeletedDirectory' )
336            )
337        );
338
339        $uploadwrapperStyle = $this->getVar( 'wgEnableUploads' ) ? '' : 'display: none';
340        $this->addHTML(
341            # Uploading
342            $this->getFieldsetStart( 'config-upload-settings' ) .
343            $this->parent->getCheckBox( [
344                'var' => 'wgEnableUploads',
345                'label' => 'config-upload-enable',
346                'attribs' => [ 'class' => 'showHideRadio', 'rel' => 'uploadwrapper' ],
347                'help' => $this->parent->getHelpBox( 'config-upload-help' )
348            ] ) .
349            '<div id="uploadwrapper" style="' . $uploadwrapperStyle . '">' .
350            $this->parent->getTextBox( [
351                'var' => 'wgDeletedDirectory',
352                'label' => 'config-upload-deleted',
353                'attribs' => [ 'dir' => 'ltr' ],
354                'help' => $this->parent->getHelpBox( 'config-upload-deleted-help' )
355            ] ) .
356            '</div>'
357        );
358        $this->addHTML(
359            $this->parent->getCheckBox( [
360                'var' => 'wgUseInstantCommons',
361                'label' => 'config-instantcommons',
362                'help' => $this->parent->getHelpBox( 'config-instantcommons-help' )
363            ] ) .
364            $this->getFieldsetEnd()
365        );
366    }
367
368    /**
369     * System administration related options.
370     * @return void
371     */
372    private function addAdvancedOptions(): void {
373        $caches = [ 'none' ];
374        $cachevalDefault = 'none';
375
376        if ( count( $this->getVar( '_Caches' ) ) ) {
377            // A CACHE_ACCEL implementation is available
378            $caches[] = 'accel';
379            $cachevalDefault = 'accel';
380        }
381        $caches[] = 'memcached';
382
383        // We'll hide/show this on demand when the value changes, see config.js.
384        $cacheval = $this->getVar( '_MainCacheType' );
385        if ( !$cacheval ) {
386            // We need to set a default here; but don't hardcode it
387            // or we lose it every time we reload the page for validation
388            // or going back!
389            $cacheval = $cachevalDefault;
390        }
391        $hidden = ( $cacheval == 'memcached' ) ? '' : 'display: none';
392        $this->addHTML(
393            # Advanced settings
394            $this->getFieldsetStart( 'config-advanced-settings' ) .
395            # Object cache settings
396            // getRadioSet() builds a set of labeled radio buttons.
397            // For grep: The following messages are used as the item labels:
398            // config-cache-none, config-cache-accel, config-cache-memcached
399            $this->parent->getRadioSet( [
400                'var' => '_MainCacheType',
401                'label' => 'config-cache-options',
402                'itemLabelPrefix' => 'config-cache-',
403                'values' => $caches,
404                'value' => $cacheval,
405            ] ) .
406            $this->parent->getHelpBox( 'config-cache-help' ) .
407            "<div id=\"config-memcachewrapper\" style=\"$hidden\">" .
408            $this->parent->getTextArea( [
409                'var' => '_MemCachedServers',
410                'label' => 'config-memcached-servers',
411                'help' => $this->parent->getHelpBox( 'config-memcached-help' )
412            ] ) .
413            '</div>' .
414            $this->getFieldsetEnd()
415        );
416    }
417
418    /**
419     * @param string $name
420     * @param array $screenshots
421     * @return string HTML
422     */
423    private function makeScreenshotsLink( $name, $screenshots ) {
424        global $wgLang;
425        if ( count( $screenshots ) > 1 ) {
426            $links = [];
427            $counter = 1;
428
429            foreach ( $screenshots as $shot ) {
430                $links[] = Html::element(
431                    'a',
432                    [ 'href' => $shot, 'target' => '_blank' ],
433                    $wgLang->formatNum( $counter++ )
434                );
435            }
436            return wfMessage( 'config-skins-screenshots' )
437                ->rawParams( $name, $wgLang->commaList( $links ) )
438                ->escaped();
439        } else {
440            $link = Html::element(
441                'a',
442                [ 'href' => $screenshots[0], 'target' => '_blank' ],
443                wfMessage( 'config-screenshot' )->text()
444            );
445            return wfMessage( 'config-skins-screenshot', $name )->rawParams( $link )->escaped();
446        }
447    }
448
449    /**
450     * @param array $info
451     * @return string HTML
452     */
453    private function makeMoreInfoLink( $info ) {
454        if ( !isset( $info['url'] ) ) {
455            return '';
456        }
457        return ' ' . wfMessage( 'parentheses' )->rawParams(
458            Html::element(
459                'a',
460                [ 'href' => $info['url'] ],
461                wfMessage( 'config-ext-skins-more-info' )->text()
462            )
463        )->escaped();
464    }
465
466    /**
467     * If the user skips this installer page, we still need to set up the default skins, but ignore
468     * everything else.
469     *
470     * @return bool
471     */
472    public function submitSkins() {
473        $skins = array_keys( $this->parent->findExtensions( 'skins' )->value );
474        $this->parent->setVar( '_Skins', $skins );
475
476        if ( $skins ) {
477            $skinNames = array_map( 'strtolower', $skins );
478            $this->parent->setVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) );
479        }
480
481        return true;
482    }
483
484    /**
485     * @return bool
486     */
487    public function submit() {
488        $this->parent->setVarsFromRequest( [ '_RightsProfile', '_LicenseCode',
489            'wgEnableEmail', 'wgPasswordSender', 'wgEnableUploads',
490            '_Logo1x', '_LogoWordmark', '_LogoTagline', '_LogoIcon',
491            'wgEnableUserEmail', 'wgEnotifUserTalk', 'wgEnotifWatchlist',
492            'wgEmailAuthentication', '_MainCacheType', '_MemCachedServers',
493            'wgUseInstantCommons', 'wgDefaultSkin' ] );
494
495        $retVal = true;
496
497        if ( !array_key_exists( $this->getVar( '_RightsProfile' ), $this->parent->rightsProfiles ) ) {
498            $this->setVar( '_RightsProfile', array_key_first( $this->parent->rightsProfiles ) );
499        }
500
501        $code = $this->getVar( '_LicenseCode' );
502        if ( array_key_exists( $code, $this->parent->licenses ) ) {
503            // Messages:
504            // config-license-cc-by, config-license-cc-by-sa, config-license-cc-by-nc-sa,
505            // config-license-cc-0, config-license-pd, config-license-gfdl, config-license-none
506            $entry = $this->parent->licenses[$code];
507            $this->setVar( 'wgRightsText',
508                $entry['text'] ?? wfMessage( 'config-license-' . $code )->text() );
509            $this->setVar( 'wgRightsUrl', $entry['url'] );
510            $this->setVar( 'wgRightsIcon', $entry['icon'] );
511        } else {
512            $this->setVar( 'wgRightsText', '' );
513            $this->setVar( 'wgRightsUrl', '' );
514            $this->setVar( 'wgRightsIcon', '' );
515        }
516
517        $skinsAvailable = array_keys( $this->parent->findExtensions( 'skins' )->value );
518        $skinsToInstall = [];
519        foreach ( $skinsAvailable as $skin ) {
520            $this->parent->setVarsFromRequest( [ "skin-$skin" ] );
521            if ( $this->getVar( "skin-$skin" ) ) {
522                $skinsToInstall[] = $skin;
523            }
524        }
525        $this->parent->setVar( '_Skins', $skinsToInstall );
526
527        if ( !$skinsToInstall && $skinsAvailable ) {
528            $this->parent->showError( 'config-skins-must-enable-some' );
529            $retVal = false;
530        }
531        $defaultSkin = $this->getVar( 'wgDefaultSkin' );
532        $skinsToInstallLowercase = array_map( 'strtolower', $skinsToInstall );
533        if ( $skinsToInstall && !in_array( $defaultSkin, $skinsToInstallLowercase ) ) {
534            $this->parent->showError( 'config-skins-must-enable-default' );
535            $retVal = false;
536        }
537
538        $extsAvailable = array_keys( $this->parent->findExtensions()->value );
539        $extsToInstall = [];
540        foreach ( $extsAvailable as $ext ) {
541            $this->parent->setVarsFromRequest( [ "ext-$ext" ] );
542            if ( $this->getVar( "ext-$ext" ) ) {
543                $extsToInstall[] = $ext;
544            }
545        }
546        $this->parent->setVar( '_Extensions', $extsToInstall );
547
548        if ( $this->getVar( '_MainCacheType' ) == 'memcached' ) {
549            $memcServers = explode( "\n", $this->getVar( '_MemCachedServers' ) );
550            // FIXME: explode() will always result in an array of at least one string, even on null (when
551            // the string will be empty and you'll get a PHP warning), so this has never worked?
552            // @phan-suppress-next-line PhanImpossibleCondition
553            if ( !$memcServers ) {
554                $this->parent->showError( 'config-memcache-needservers' );
555                $retVal = false;
556            }
557
558            foreach ( $memcServers as $server ) {
559                $memcParts = explode( ":", $server, 2 );
560                if ( !isset( $memcParts[0] )
561                    || ( !IPUtils::isValid( $memcParts[0] )
562                        && ( gethostbyname( $memcParts[0] ) == $memcParts[0] ) )
563                ) {
564                    $this->parent->showError( 'config-memcache-badip', $memcParts[0] );
565                    $retVal = false;
566                } elseif ( !isset( $memcParts[1] ) ) {
567                    $this->parent->showError( 'config-memcache-noport', $memcParts[0] );
568                    $retVal = false;
569                } elseif ( $memcParts[1] < 1 || $memcParts[1] > 65535 ) {
570                    $this->parent->showError( 'config-memcache-badport', 1, 65535 );
571                    $retVal = false;
572                }
573            }
574        }
575
576        return $retVal;
577    }
578
579}