getRootNode(); $node ->children() ->scalarNode('data_dir') ->isRequired() ->validate() ->ifTrue(function ($value) { return !file_exists($value); }) ->thenInvalid('Data directory does not exist') ->end() ->end() ->scalarNode('plugin_dir') ->isRequired() ->validate() ->ifTrue(function ($value) { return !file_exists($value); }) ->thenInvalid('Plugin directory does not exist') ->end() ->end() ->append($this->getUserNode()) ->append($this->getTimesheetNode()) ->append($this->getInvoiceNode()) ->append($this->getLanguagesNode()) ->append($this->getCalendarNode()) ->append($this->getThemeNode()) ->append($this->getIndustryNode()) ->append($this->getDashboardNode()) ->append($this->getWidgetsNode()) ->append($this->getDefaultsNode()) ->append($this->getPermissionsNode()) ->append($this->getLdapNode()) ->append($this->getSamlNode()) ->end() ->end(); return $treeBuilder; } protected function getTimesheetNode() { $builder = new TreeBuilder('timesheet'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->children() ->scalarNode('default_begin') ->defaultValue('now') ->end() ->booleanNode('duration_only') ->setDeprecated() ->end() ->scalarNode('mode') ->defaultValue('default') ->end() ->booleanNode('markdown_content') ->defaultValue(false) ->end() ->arrayNode('rounding') ->requiresAtLeastOneElement() ->useAttributeAsKey('key') ->arrayPrototype() ->children() ->arrayNode('days') ->useAttributeAsKey('key') ->scalarPrototype()->end() ->defaultValue(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']) ->end() ->integerNode('begin') ->defaultValue(1) ->end() ->integerNode('end') ->defaultValue(1) ->end() ->integerNode('duration') ->defaultValue(0) ->end() ->scalarNode('mode') ->defaultValue('default') ->validate() ->ifTrue(function ($value) { $class = 'App\\Timesheet\\Rounding\\' . ucfirst($value) . 'Rounding'; if (class_exists($class)) { $rounding = new $class(); return !($rounding instanceof RoundingInterface); } return false; }) ->thenInvalid('Chosen rounding mode is invalid') ->end() ->end() ->end() ->end() ->defaultValue([ 'default' => [ 'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'], 'begin' => 1, 'end' => 1, 'duration' => 0, 'mode' => 'default' ] ]) ->end() ->arrayNode('rates') ->requiresAtLeastOneElement() ->useAttributeAsKey('key') ->arrayPrototype() ->children() ->arrayNode('days') ->requiresAtLeastOneElement() ->useAttributeAsKey('key') ->isRequired() ->scalarPrototype()->end() ->defaultValue([]) ->end() ->floatNode('factor') ->isRequired() ->defaultValue(1) ->validate() ->ifTrue(function ($value) { return $value <= 0; }) ->thenInvalid('A rate factor smaller or equals 0 is not allowed') ->end() ->end() ->end() ->end() ->defaultValue([]) ->end() ->arrayNode('active_entries') ->addDefaultsIfNotSet() ->children() ->integerNode('soft_limit') ->defaultValue(1) ->validate() ->ifTrue(function ($value) { return $value <= 0; }) ->thenInvalid('The soft_limit must be at least 1') ->end() ->end() ->integerNode('hard_limit') ->defaultValue(1) ->validate() ->ifTrue(function ($value) { return $value <= 0; }) ->thenInvalid('The hard_limit must be at least 1') ->end() ->end() ->end() ->end() ->arrayNode('rules') ->addDefaultsIfNotSet() ->children() ->booleanNode('allow_future_times') ->defaultTrue() ->end() ->end() ->end() ->end() ; return $node; } protected function getInvoiceNode() { $builder = new TreeBuilder('invoice'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->addDefaultsIfNotSet() ->children() ->arrayNode('defaults') ->scalarPrototype()->end() ->defaultValue([ 'var/invoices/', 'templates/invoice/renderer/' ]) ->end() ->arrayNode('documents') ->requiresAtLeastOneElement() ->scalarPrototype()->end() ->defaultValue([]) ->end() ->end() ; return $node; } protected function getLanguagesNode() { $builder = new TreeBuilder('languages'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->useAttributeAsKey('name', false) // see https://github.com/symfony/symfony/issues/18988 ->arrayPrototype() ->children() ->scalarNode('date_time_type')->defaultValue('yyyy-MM-dd HH:mm')->end() // for DateTimeType ->scalarNode('date_type')->defaultValue('yyyy-MM-dd')->end() // for DateType ->scalarNode('date')->defaultValue('Y-m-d')->end() // for display via twig ->scalarNode('date_time')->defaultValue('m-d H:i')->end() // for display via twig ->scalarNode('duration')->defaultValue('%%h:%%m h')->end() // for display via twig ->scalarNode('time')->defaultValue('H:i')->end() // for display via twig ->booleanNode('24_hours')->defaultTrue()->end() // for DateTimeType JS component ->end() ->end() ; return $node; } protected function getCalendarNode() { $builder = new TreeBuilder('calendar'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->addDefaultsIfNotSet() ->children() ->booleanNode('week_numbers')->defaultTrue()->end() ->integerNode('day_limit')->defaultValue(4)->end() ->scalarNode('slot_duration')->defaultValue('00:30:00')->end() ->arrayNode('businessHours') ->addDefaultsIfNotSet() ->children() ->arrayNode('days') ->requiresAtLeastOneElement() ->integerPrototype()->end() ->defaultValue([1, 2, 3, 4, 5]) ->end() ->scalarNode('begin')->defaultValue('08:00')->end() ->scalarNode('end')->defaultValue('20:00')->end() ->end() ->end() ->arrayNode('visibleHours') ->addDefaultsIfNotSet() ->children() ->scalarNode('begin')->defaultValue('00:00')->end() ->scalarNode('end')->defaultValue('23:59')->end() ->end() ->end() ->arrayNode('google') ->addDefaultsIfNotSet() ->children() ->scalarNode('api_key')->defaultNull()->end() ->arrayNode('sources') ->requiresAtLeastOneElement() ->useAttributeAsKey('key') ->arrayPrototype() ->children() ->scalarNode('id')->isRequired()->end() ->scalarNode('color')->defaultValue('#ccc')->end() ->end() ->end() ->end() ->end() ->end() ->booleanNode('weekends')->defaultTrue()->end() ->end() ; return $node; } protected function getThemeNode() { $builder = new TreeBuilder('theme'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->addDefaultsIfNotSet() ->children() ->integerNode('active_warning') ->defaultValue(3) ->setDeprecated('The node "%node%" at path "%path%" is deprecated, please use "kimai.timesheet.active_entries.soft_limit" instead.') ->end() ->scalarNode('box_color') ->defaultValue('green') ->setDeprecated('The node "%node%" at path "%path%" was removed, please delete it from your config.') ->end() ->scalarNode('select_type') ->defaultValue('selectpicker') ->setDeprecated() ->end() ->booleanNode('auto_reload_datatable') ->defaultFalse() ->end() ->booleanNode('tags_create') ->defaultTrue() ->end() ->booleanNode('show_about') ->defaultTrue() ->end() ->arrayNode('chart') ->addDefaultsIfNotSet() ->children() ->scalarNode('background_color')->defaultValue('rgba(0,115,183,0.7)')->end() ->scalarNode('border_color')->defaultValue('#3b8bba')->end() ->scalarNode('grid_color')->defaultValue('rgba(0,0,0,.05)')->end() ->scalarNode('height')->defaultValue('200')->end() ->end() ->end() ->arrayNode('branding') ->addDefaultsIfNotSet() ->children() ->scalarNode('logo') ->defaultNull() ->end() ->scalarNode('mini') ->defaultNull() ->end() ->scalarNode('company') ->defaultNull() ->end() ->scalarNode('title') ->defaultNull() ->end() ->scalarNode('translation') ->defaultNull() ->end() ->end() ->end() ->integerNode('autocomplete_chars') ->defaultValue(3) ->end() ->end() ; return $node; } protected function getIndustryNode() { $builder = new TreeBuilder('industry'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->addDefaultsIfNotSet() ->children() ->scalarNode('translation')->defaultNull()->end() ->end() ; return $node; } protected function getUserNode() { $builder = new TreeBuilder('user'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->addDefaultsIfNotSet() ->children() ->booleanNode('registration') ->defaultTrue() ->end() ->booleanNode('password_reset') ->defaultTrue() ->end() ->end() ; return $node; } protected function getWidgetsNode() { $builder = new TreeBuilder('widgets'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->requiresAtLeastOneElement() ->useAttributeAsKey('key') ->arrayPrototype() ->addDefaultsIfNotSet() ->children() ->scalarNode('title')->isRequired()->end() ->scalarNode('query')->isRequired()->end() ->booleanNode('user')->defaultFalse()->end() ->scalarNode('begin')->end() ->scalarNode('end')->end() ->scalarNode('icon')->defaultValue('')->end() ->scalarNode('color')->defaultValue('')->end() ->scalarNode('type')->defaultValue(Counter::class)->end() ->end() ->end() ; return $node; } protected function getDashboardNode() { $builder = new TreeBuilder('dashboard'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->requiresAtLeastOneElement() ->useAttributeAsKey('key') ->arrayPrototype() ->addDefaultsIfNotSet() ->children() ->scalarNode('type')->defaultValue(CompoundRow::class)->end() ->integerNode('order')->defaultValue(0)->end() ->scalarNode('title')->end() ->scalarNode('permission')->defaultNull()->end() ->arrayNode('widgets') ->isRequired() ->performNoDeepMerging() ->scalarPrototype() ->end() ->end() ->end() ->end() ; return $node; } protected function getDefaultsNode() { $builder = new TreeBuilder('defaults'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->addDefaultsIfNotSet() ->children() ->arrayNode('customer') ->addDefaultsIfNotSet() ->children() ->scalarNode('timezone')->defaultNull()->end() ->scalarNode('country')->defaultValue('DE')->end() ->scalarNode('currency')->defaultValue(Customer::DEFAULT_CURRENCY)->end() ->end() ->end() ->arrayNode('user') ->addDefaultsIfNotSet() ->children() ->scalarNode('timezone')->defaultNull()->end() ->scalarNode('language')->defaultValue(User::DEFAULT_LANGUAGE)->end() ->scalarNode('theme')->defaultNull()->end() ->scalarNode('currency')->defaultValue(Customer::DEFAULT_CURRENCY)->end() ->end() ->end() ->end() ; return $node; } protected function getPermissionsNode() { $builder = new TreeBuilder('permissions'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->addDefaultsIfNotSet() ->children() ->arrayNode('sets') ->requiresAtLeastOneElement() ->useAttributeAsKey('key') ->arrayPrototype() ->useAttributeAsKey('key') ->isRequired() ->scalarPrototype()->end() ->defaultValue([]) ->end() ->end() ->arrayNode('maps') ->requiresAtLeastOneElement() ->useAttributeAsKey('key') ->arrayPrototype() ->useAttributeAsKey('key') ->isRequired() ->scalarPrototype()->end() ->defaultValue([]) ->end() ->end() ->arrayNode('roles') ->requiresAtLeastOneElement() ->useAttributeAsKey('key') ->arrayPrototype() ->isRequired() ->scalarPrototype()->end() ->defaultValue([]) ->end() ->defaultValue([ 'ROLE_USER' => [], 'ROLE_TEAMLEAD' => [], 'ROLE_ADMIN' => [], 'ROLE_SUPER_ADMIN' => [], ]) ->end() ->end() ; return $node; } protected function getLdapNode() { $treeBuilder = new TreeBuilder('ldap'); $node = $treeBuilder->getRootNode(); $node ->addDefaultsIfNotSet() ->children() ->arrayNode('connection') ->addDefaultsIfNotSet() ->children() ->scalarNode('host')->defaultNull()->end() ->scalarNode('port')->defaultValue(389)->end() ->scalarNode('useStartTls')->defaultFalse()->end() ->scalarNode('useSsl')->defaultFalse()->end() ->scalarNode('username')->end() ->scalarNode('password')->end() ->scalarNode('bindRequiresDn')->defaultTrue()->end() ->scalarNode('baseDn')->end() ->scalarNode('accountCanonicalForm')->end() ->scalarNode('accountDomainName')->end() ->scalarNode('accountDomainNameShort')->end() ->scalarNode('accountFilterFormat') ->defaultNull() ->validate() ->ifTrue(static function ($v) { if (empty($v)) { return false; } if ($v[0] !== '(' || (substr_count($v, '(') !== substr_count($v, ')'))) { return true; } return (substr_count($v, '%s') !== 1); }) ->thenInvalid('The accountFilterFormat must be enclosed by a matching number of parentheses "()" and contain one "%%s" replacer for the username') ->end() ->end() ->scalarNode('allowEmptyPassword')->end() ->scalarNode('optReferrals')->end() ->scalarNode('tryUsernameSplit')->end() ->scalarNode('networkTimeout')->end() ->end() ->validate() ->ifTrue(static function ($v) { return $v['useSsl'] && $v['useStartTls']; }) ->thenInvalid('The ldap.connection.useSsl and ldap.connection.useStartTls options are mutually exclusive.') ->end() ->end() ->arrayNode('user') ->addDefaultsIfNotSet() ->children() ->scalarNode('baseDn')->defaultNull()->end() ->scalarNode('filter') ->defaultValue('') ->validate() ->ifTrue(static function ($v) { if (empty($v)) { return false; } if ($v[0] !== '(' || (substr_count($v, '(') !== substr_count($v, ')'))) { return true; } return (stripos($v, '%s') !== false); }) ->thenInvalid('The ldap.user.filter must be enclosed by a matching number of parentheses "()" and must NOT contain a "%%s" replacer') ->end() ->end() ->scalarNode('attributesFilter')->defaultValue('(objectClass=*)')->end() ->scalarNode('usernameAttribute')->defaultValue('uid')->end() ->arrayNode('attributes') ->defaultValue([]) ->arrayPrototype() ->children() ->scalarNode('ldap_attr')->isRequired()->cannotBeEmpty()->end() ->scalarNode('user_method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ->end() ->end() ->arrayNode('role') ->addDefaultsIfNotSet() ->children() ->scalarNode('baseDn')->defaultNull()->end() ->scalarNode('filter')->end() ->scalarNode('usernameAttribute')->defaultValue('dn')->end() ->scalarNode('nameAttribute')->defaultValue('cn')->end() ->scalarNode('userDnAttribute')->defaultValue('member')->end() ->arrayNode('groups') ->defaultValue([]) ->arrayPrototype() ->children() ->scalarNode('ldap_value')->isRequired()->cannotBeEmpty()->end() ->scalarNode('role')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ->end() ->end() ->end() ->validate() ->ifTrue(static function ($v) { return null !== $v['connection']['host'] && !extension_loaded('ldap'); }) ->thenInvalid('LDAP is activated, but the LDAP PHP extension is not loaded.') ->end() ->validate() ->ifTrue(static function ($v) { return null !== $v['connection']['host'] && empty($v['user']['baseDn']); }) ->thenInvalid('The "ldap.user.baseDn" config must be set if LDAP is activated.') ->end() ; return $node; } protected function getSamlNode() { $builder = new TreeBuilder('saml'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); $node ->addDefaultsIfNotSet() ->children() ->booleanNode('activate') ->defaultFalse() ->end() ->scalarNode('title') ->defaultValue('Login with SAML') ->end() ->arrayNode('roles') ->addDefaultsIfNotSet() ->children() ->scalarNode('attribute') ->defaultNull() ->end() ->arrayNode('mapping') ->defaultValue([]) ->arrayPrototype() ->children() ->scalarNode('saml')->isRequired()->cannotBeEmpty()->end() ->scalarNode('kimai')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ->end() ->end() ->arrayNode('mapping') ->defaultValue([]) ->arrayPrototype() ->children() ->scalarNode('saml')->isRequired()->cannotBeEmpty()->end() ->scalarNode('kimai')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ->arrayNode('connection') ->addDefaultsIfNotSet() ->children() ->scalarNode('baseurl')->end() ->booleanNode('strict')->end() ->booleanNode('debug')->end() ->arrayNode('idp') ->children() ->scalarNode('entityId')->end() ->scalarNode('x509cert')->end() ->arrayNode('singleSignOnService') ->children() ->scalarNode('url')->end() ->scalarNode('binding')->end() ->end() ->end() ->arrayNode('singleLogoutService') ->children() ->scalarNode('url')->end() ->scalarNode('binding')->end() ->end() ->end() ->scalarNode('certFingerprint')->end() ->scalarNode('certFingerprintAlgorithm')->end() ->arrayNode('x509certMulti') ->children() ->arrayNode('signing') ->prototype('scalar')->end() ->end() ->arrayNode('encryption') ->prototype('scalar')->end() ->end() ->end() ->end() ->end() ->end() ->arrayNode('sp') ->children() ->scalarNode('entityId')->end() ->scalarNode('NameIDFormat')->end() ->scalarNode('x509cert')->end() ->scalarNode('privateKey')->end() ->arrayNode('assertionConsumerService') ->children() ->scalarNode('url')->end() ->scalarNode('binding')->end() ->end() ->end() ->arrayNode('attributeConsumingService') ->children() ->scalarNode('serviceName')->end() ->scalarNode('serviceDescription')->end() ->arrayNode('requestedAttributes') ->prototype('array') ->children() ->scalarNode('name')->end() ->booleanNode('isRequired')->defaultValue(false)->end() ->scalarNode('nameFormat')->end() ->scalarNode('friendlyName')->end() ->arrayNode('attributeValue')->end() ->end() ->end() ->end() ->end() ->end() ->arrayNode('singleLogoutService') ->children() ->scalarNode('url')->end() ->scalarNode('binding')->end() ->end() ->end() ->end() ->end() ->arrayNode('security') ->children() ->booleanNode('nameIdEncrypted')->end() ->booleanNode('authnRequestsSigned')->end() ->booleanNode('logoutRequestSigned')->end() ->booleanNode('logoutResponseSigned')->end() ->booleanNode('wantMessagesSigned')->end() ->booleanNode('wantAssertionsSigned')->end() ->booleanNode('wantAssertionsEncrypted')->end() ->booleanNode('wantNameId')->end() ->booleanNode('wantNameIdEncrypted')->end() ->variableNode('requestedAuthnContext') ->validate() ->ifTrue(function ($v) { return !is_bool($v) && !is_array($v); }) ->thenInvalid('Must be an array or a bool.') ->end() ->end() ->booleanNode('signMetadata')->end() ->booleanNode('wantXMLValidation')->end() ->booleanNode('lowercaseUrlencoding')->end() ->scalarNode('signatureAlgorithm')->end() ->scalarNode('digestAlgorithm')->end() ->scalarNode('entityManagerName')->end() ->end() ->end() ->arrayNode('contactPerson') ->children() ->arrayNode('technical') ->children() ->scalarNode('givenName')->end() ->scalarNode('emailAddress')->end() ->end() ->end() ->arrayNode('support') ->children() ->scalarNode('givenName')->end() ->scalarNode('emailAddress')->end() ->end() ->end() ->end() ->end() ->arrayNode('organization') ->prototype('array') ->children() ->scalarNode('name')->end() ->scalarNode('displayname')->end() ->scalarNode('url')->end() ->end() ->end() ->end() ->end() ->end() ->end() ->validate() ->ifTrue(static function ($v) { if (true !== $v['activate']) { return false; } $found = false; foreach ($v['mapping'] as $mapping) { if ($mapping['kimai'] === 'email') { $found = true; } } return !$found; }) ->thenInvalid('You need to configure a SAML mapping for the email attribute.') ->end() ; return $node; } }