1<?php
2
3final class ArcanistConfigurationEngine
4  extends Phobject {
5
6  private $workingCopy;
7  private $arguments;
8  private $toolset;
9
10  public function setWorkingCopy(ArcanistWorkingCopy $working_copy) {
11    $this->workingCopy = $working_copy;
12    return $this;
13  }
14
15  public function getWorkingCopy() {
16    return $this->workingCopy;
17  }
18
19  public function setArguments(PhutilArgumentParser $arguments) {
20    $this->arguments = $arguments;
21    return $this;
22  }
23
24  public function getArguments() {
25    if (!$this->arguments) {
26      throw new PhutilInvalidStateException('setArguments');
27    }
28    return $this->arguments;
29  }
30
31  public function newConfigurationSourceList() {
32    $list = new ArcanistConfigurationSourceList();
33
34    $list->addSource(new ArcanistDefaultsConfigurationSource());
35
36    $arguments = $this->getArguments();
37
38    // If the invoker has provided one or more configuration files with
39    // "--config-file" arguments, read those files instead of the system
40    // and user configuration files. Otherwise, read the system and user
41    // configuration files.
42
43    $config_files = $arguments->getArg('config-file');
44    if ($config_files) {
45      foreach ($config_files as $config_file) {
46        $list->addSource(new ArcanistFileConfigurationSource($config_file));
47      }
48    } else {
49      $system_path = $this->getSystemConfigurationFilePath();
50      $list->addSource(new ArcanistSystemConfigurationSource($system_path));
51
52      $user_path = $this->getUserConfigurationFilePath();
53      $list->addSource(new ArcanistUserConfigurationSource($user_path));
54    }
55
56
57    // If we're running in a working copy, load the ".arcconfig" and any
58    // local configuration.
59    $working_copy = $this->getWorkingCopy();
60    if ($working_copy) {
61      $project_path = $working_copy->getProjectConfigurationFilePath();
62      if ($project_path !== null) {
63        $list->addSource(new ArcanistProjectConfigurationSource($project_path));
64      }
65
66      $local_path = $working_copy->getLocalConfigurationFilePath();
67      if ($local_path !== null) {
68        $list->addSource(new ArcanistLocalConfigurationSource($local_path));
69      }
70    }
71
72    // If the invoker has provided "--config" arguments, parse those now.
73    $runtime_args = $arguments->getArg('config');
74    if ($runtime_args) {
75      $list->addSource(new ArcanistRuntimeConfigurationSource($runtime_args));
76    }
77
78    return $list;
79  }
80
81  private function getSystemConfigurationFilePath() {
82    if (phutil_is_windows()) {
83      return Filesystem::resolvePath(
84        'Phabricator/Arcanist/config',
85        getenv('ProgramData'));
86    } else {
87      return '/etc/arcconfig';
88    }
89  }
90
91  private function getUserConfigurationFilePath() {
92    if (phutil_is_windows()) {
93      return getenv('APPDATA').'/.arcrc';
94    } else {
95      return getenv('HOME').'/.arcrc';
96    }
97  }
98
99  public function newDefaults() {
100    $map = $this->newConfigOptionsMap();
101    return mpull($map, 'getDefaultValue');
102  }
103
104  public function newConfigOptionsMap() {
105    $extensions = $this->newEngineExtensions();
106
107    $map = array();
108    $alias_map = array();
109    foreach ($extensions as $extension) {
110      $options = $extension->newConfigurationOptions();
111
112      foreach ($options as $option) {
113        $key = $option->getKey();
114
115        $this->validateConfigOptionKey($key, $extension);
116
117        if (isset($map[$key])) {
118          throw new Exception(
119            pht(
120              'Configuration option ("%s") defined by extension "%s" '.
121              'conflicts with an existing option. Each option must have '.
122              'a unique key.',
123              $key,
124              get_class($extension)));
125        }
126
127        if (isset($alias_map[$key])) {
128          throw new Exception(
129            pht(
130              'Configuration option ("%s") defined by extension "%s" '.
131              'conflicts with an alias for another option ("%s"). The '.
132              'key and aliases of each option must be unique.',
133              $key,
134              get_class($extension),
135              $alias_map[$key]->getKey()));
136        }
137
138        $map[$key] = $option;
139
140        foreach ($option->getAliases() as $alias) {
141          $this->validateConfigOptionKey($alias, $extension, $key);
142
143          if (isset($map[$alias])) {
144            throw new Exception(
145              pht(
146                'Configuration option ("%s") defined by extension "%s" '.
147                'has an alias ("%s") which conflicts with an existing '.
148                'option. The key and aliases of each option must be '.
149                'unique.',
150                $key,
151                get_class($extension),
152                $alias));
153          }
154
155          if (isset($alias_map[$alias])) {
156            throw new Exception(
157              pht(
158                'Configuration option ("%s") defined by extension "%s" '.
159                'has an alias ("%s") which conflicts with the alias of '.
160                'another configuration option ("%s"). The key and aliases '.
161                'of each option must be unique.',
162                $key,
163                get_class($extension),
164                $alias,
165                $alias_map[$alias]->getKey()));
166          }
167
168          $alias_map[$alias] = $option;
169        }
170      }
171    }
172
173    return $map;
174  }
175
176  private function validateConfigOptionKey(
177    $key,
178    ArcanistConfigurationEngineExtension $extension,
179    $is_alias_of = null) {
180
181    $reserved = array(
182      // The presence of this key is used to detect old "~/.arcrc" files, so
183      // configuration options may not use it.
184      'config',
185    );
186    $reserved = array_fuse($reserved);
187
188    if (isset($reserved[$key])) {
189      throw new Exception(
190        pht(
191          'Extension ("%s") defines invalid configuration with key "%s". '.
192          'This key is reserved.',
193          get_class($extension),
194          $key));
195    }
196
197    $is_ok = preg_match('(^[a-z][a-z0-9._-]{2,}\z)', $key);
198    if (!$is_ok) {
199      if ($is_alias_of === null) {
200        throw new Exception(
201          pht(
202            'Extension ("%s") defines invalid configuration with key "%s". '.
203            'Configuration keys: may only contain lowercase letters, '.
204            'numbers, hyphens, underscores, and periods; must start with a '.
205            'letter; and must be at least three characters long.',
206            get_class($extension),
207            $key));
208      } else {
209        throw new Exception(
210          pht(
211            'Extension ("%s") defines invalid alias ("%s") for configuration '.
212            'key ("%s"). Configuration keys and aliases: may only contain '.
213            'lowercase letters, numbers, hyphens, underscores, and periods; '.
214            'must start with a letter; and must be at least three characters '.
215            'long.',
216            get_class($extension),
217            $key,
218            $is_alias_of));
219      }
220    }
221  }
222
223  private function newEngineExtensions() {
224    return id(new PhutilClassMapQuery())
225      ->setAncestorClass('ArcanistConfigurationEngineExtension')
226      ->setUniqueMethod('getExtensionKey')
227      ->setContinueOnFailure(true)
228      ->execute();
229  }
230
231}
232