1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19

20
/**
21
 * Runs SonarQube Scanner.
22
 *
23
 * @author  Bernhard Mendl <mail@bernhard-mendl.de>
24
 * @package phing.tasks.ext.sonar
25
 * @see     http://www.sonarqube.org
26
 */
27
class SonarTask extends Task
28
{
29
    public const EXIT_SUCCESS = 0;
30

31
    /**
32
     *
33
     * @var string|null
34
     */
35
    private $executable = null;
36

37
    /**
38
     *
39
     * @var string
40
     */
41
    private $errors = 'false';
42

43
    /**
44
     *
45
     * @var string
46
     */
47
    private $debug = 'false';
48

49
    /**
50
     *
51
     * @var string|null
52
     */
53
    private $configuration = null;
54

55
    /**
56
     *
57
     * @var array Nested *Property* elements.
58
     * @see Property
59
     */
60
    private $propertyElements = [];
61

62
    /**
63
     * The command-line options passed to the SonarQube Scanner executable.
64
     *
65
     * @var array
66
     */
67
    private $commandLineOptions = [];
68

69
    /**
70
     * Map containing SonarQube's "analysis parameters".
71
     *
72
     * Map keys are SonarQube parameter names. Map values are parameter values.
73
     * See {@link http://docs.sonarqube.org/display/SONAR/Analysis+Parameters}.
74
     *
75
     * @var array
76
     */
77
    private $properties = [];
78

79
    /**
80
     * Sets the path of the SonarQube Scanner executable.
81
     *
82
     * If the SonarQube Scanner is included in the PATH environment variable,
83
     * the file name is sufficient.
84
     *
85
     * @param  string $executable
86
     * @return void
87
     */
88 1
    public function setExecutable($executable)
89
    {
90 1
        $this->executable = (string) $executable;
91

92 1
        $message = sprintf("Set executable to [%s].", $this->executable);
93 1
        $this->log($message, Project::MSG_DEBUG);
94
    }
95

96
    /**
97
     * Sets or unsets the "--errors" flag of SonarQube Scanner.
98
     *
99
     * @param  string $errors
100
     *            Allowed values are "true"/"false", "yes"/"no", or "1"/"0".
101
     * @return void
102
     */
103 1
    public function setErrors($errors)
104
    {
105 1
        $this->errors = strtolower((string) $errors);
106

107 1
        $message = sprintf("Set errors flag to [%s].", $this->errors);
108 1
        $this->log($message, Project::MSG_DEBUG);
109
    }
110

111
    /**
112
     * Sets or unsets the "--debug" flag of SonarQube Scanner.
113
     *
114
     * @param  string $debug
115
     *            Allowed values are "true"/"false", "yes"/"no", or "1"/"0".
116
     * @return void
117
     */
118 1
    public function setDebug($debug)
119
    {
120 1
        $this->debug = strtolower((string) $debug);
121

122 1
        $message = sprintf("Set debug flag to [%s].", $this->debug);
123 1
        $this->log($message, Project::MSG_DEBUG);
124
    }
125

126
    /**
127
     * Sets the path of a configuration file for SonarQube Scanner.
128
     *
129
     * @param  string $configuration
130
     * @return void
131
     */
132 1
    public function setConfiguration($configuration)
133
    {
134 1
        $this->configuration = (string) $configuration;
135

136 1
        $message = sprintf("Set configuration to [%s].", $this->configuration);
137 1
        $this->log($message, Project::MSG_DEBUG);
138
    }
139

140
    /**
141
     * Adds a nested Property element.
142
     *
143
     * @param  SonarProperty $property
144
     * @return void
145
     */
146 1
    public function addProperty(SonarProperty $property)
147
    {
148 1
        $this->propertyElements[] = $property;
149

150 1
        $message = sprintf("Added property: [%s] = [%s].", $property->getName(), $property->getValue());
151 1
        $this->log($message, Project::MSG_DEBUG);
152
    }
153

154
    /**
155
     * {@inheritdoc}
156
     *
157
     * @see Task::init()
158
     */
159 1
    public function init()
160
    {
161 1
        $this->checkExecAllowed();
162
    }
163

164
    /**
165
     * {@inheritdoc}
166
     *
167
     * @see Task::main()
168
     */
169 1
    public function main()
170
    {
171 1
        $this->validateErrors();
172 1
        $this->validateDebug();
173 1
        $this->validateConfiguration();
174 1
        $this->validateProperties();
175 1
        $this->validateExecutable();
176

177 0
        $command = sprintf('%s %s', escapeshellcmd($this->executable), $this->constructOptionsString());
178

179 0
        $message = sprintf('Executing: [%s]', $command);
180 0
        $this->log($message, Project::MSG_VERBOSE);
181

182 0
        exec($command, $output, $returnCode);
183

184 0
        foreach ($output as $line) {
185 0
            $this->log($line);
186
        }
187

188 0
        if ($returnCode !== self::EXIT_SUCCESS) {
189 0
            throw new BuildException('Execution of SonarQube Scanner failed.');
190
        }
191
    }
192

193
    /**
194
     * Constructs command-line options string for SonarQube Scanner.
195
     *
196
     * @return string
197
     */
198 0
    private function constructOptionsString()
199
    {
200 0
        $options = implode(' ', $this->commandLineOptions);
201

202 0
        foreach ($this->properties as $name => $value) {
203 0
            $arg = sprintf('%s=%s', $name, $value);
204 0
            $options .= ' -D ' . escapeshellarg($arg);
205
        }
206

207 0
        return $options;
208
    }
209

210
    /**
211
     * Check whether PHP function 'exec()' is available.
212
     *
213
     * @throws BuildException
214
     * @return void
215
     */
216 1
    private function checkExecAllowed()
217
    {
218 1
        if (!function_exists('exec') || !is_callable('exec')) {
219 0
            $message = 'Cannot execute SonarQube Scanner because calling PHP function exec() is not permitted by PHP configuration.';
220 0
            throw new BuildException($message);
221
        }
222
    }
223

224
    /**
225
     *
226
     * @throws BuildException
227
     * @return void
228
     */
229 1
    private function validateExecutable()
230
    {
231 1
        if (($this->executable === null) || ($this->executable === '')) {
232 1
            $message = 'You must specify the path of the SonarQube Scanner using the "executable" attribute.';
233 1
            throw new BuildException($message);
234
        }
235

236
        // Note that executable is used as argument here.
237 1
        $escapedExecutable = escapeshellarg($this->executable);
238

239 1
        if ($this->isWindows()) {
240 0
            $message = 'Assuming a Windows system. Looking for SonarQube Scanner ...';
241 0
            $command = 'where ' . $escapedExecutable;
242
        } else {
243 1
            $message = 'Assuming a Linux or Mac system. Looking for SonarQube Scanner ...';
244 1
            $command = 'which ' . $escapedExecutable;
245
        }
246

247 1
        $this->log($message, Project::MSG_VERBOSE);
248 1
        unset($output);
249 1
        exec($command, $output, $returnCode);
250

251 1
        if ($returnCode !== self::EXIT_SUCCESS) {
252 1
            $message = sprintf('Cannot find SonarQube Scanner: [%s].', $this->executable);
253 1
            throw new BuildException($message);
254
        }
255

256
        // Verify that executable is indeed SonarQube Scanner ...
257 1
        $escapedExecutable = escapeshellcmd($this->executable);
258 1
        unset($output);
259 1
        exec($escapedExecutable . ' --version', $output, $returnCode);
260

261 1
        if ($returnCode !== self::EXIT_SUCCESS) {
262 0
            $message = sprintf(
263 0
                'Could not check version string. Executable appears not to be SonarQube Scanner: [%s].',
264 0
                $this->executable
265
            );
266 0
            throw new BuildException($message);
267
        }
268

269 1
        $isOk = false;
270 1
        foreach ($output as $line) {
271 1
            if (preg_match('/SonarQube Scanner \d+\\.\d+/', $line) === 1) {
272 0
                $isOk = true;
273 0
                break;
274
            }
275
        }
276

277 1
        if ($isOk) {
278 0
            $message = sprintf('Found SonarQube Scanner: [%s].', $this->executable);
279 0
            $this->log($message, Project::MSG_VERBOSE);
280
        } else {
281 1
            $message = sprintf(
282 1
                'Could not find name of SonarQube Scanner in version string. Executable appears not to be SonarQube Scanner: [%s].',
283 1
                $this->executable
284
            );
285 1
            throw new BuildException($message);
286
        }
287
    }
288

289
    /**
290
     *
291
     * @throws BuildException
292
     * @return void
293
     */
294 1
    private function validateErrors()
295
    {
296 1
        if (($this->errors === '1') || ($this->errors === 'true') || ($this->errors === 'yes')) {
297 0
            $errors = true;
298 1
        } elseif (($this->errors === '0') || ($this->errors === 'false') || ($this->errors === 'no')) {
299 1
            $errors = false;
300
        } else {
301 1
            throw new BuildException('Expected a boolean value.');
302
        }
303

304 1
        if ($errors) {
305 0
            $this->commandLineOptions[] = '--errors';
306
        }
307
    }
308

309
    /**
310
     *
311
     * @throws BuildException
312
     * @return void
313
     */
314 1
    private function validateDebug()
315
    {
316 1
        if (($this->debug === '1') || ($this->debug === 'true') || ($this->debug === 'yes')) {
317 0
            $debug = true;
318 1
        } elseif (($this->debug === '0') || ($this->debug === 'false') || ($this->debug === 'no')) {
319 1
            $debug = false;
320
        } else {
321 1
            throw new BuildException('Expected a boolean value.');
322
        }
323

324 1
        if ($debug) {
325 0
            $this->commandLineOptions[] = '--debug';
326
        }
327
    }
328

329
    /**
330
     *
331
     * @throws BuildException
332
     * @return void
333
     */
334 1
    private function validateConfiguration()
335
    {
336 1
        if (($this->configuration === null) || ($this->configuration === '')) {
337
            // NOTE: Ignore an empty configuration. This allows for
338
            // using Phing properties as attribute values, e.g.
339
            // <sonar ... configuration="{sonar.config.file}">.
340 1
            return;
341
        }
342

343 1
        if (!@file_exists($this->configuration)) {
344 1
            $message = sprintf('Cannot find configuration file [%s].', $this->configuration);
345 1
            throw new BuildException($message);
346
        }
347

348 0
        if (!@is_readable($this->configuration)) {
349 0
            $message = sprintf('Cannot read configuration file [%s].', $this->configuration);
350 0
            throw new BuildException($message);
351
        }
352

353
        // TODO: Maybe check file type?
354
    }
355

356
    /**
357
     *
358
     * @throws BuildException
359
     * @return void
360
     */
361 1
    private function validateProperties()
362
    {
363 1
        $this->properties = $this->parseConfigurationFile();
364

365 1
        foreach ($this->propertyElements as $property) {
366 1
            $name = $property->getName();
367 1
            $value = $property->getValue();
368

369 1
            if ($name === null || $name === '') {
370 1
                throw new BuildException('Property name must not be null or empty.');
371
            }
372

373 1
            if (array_key_exists($name, $this->properties)) {
374 0
                $message = sprintf(
375 0
                    'Property [%s] overwritten: old value [%s], new value [%s].',
376 0
                    $name,
377 0
                    $this->properties[$name],
378 0
                    $value
379
                );
380 0
                $this->log($message, Project::MSG_WARN);
381
            }
382

383 1
            $this->properties[$name] = $value;
384
        }
385

386
        // Check if all properties required by SonarQube Scanner are set ...
387
        $requiredProperties = [
388 1
            'sonar.projectKey',
389
            'sonar.projectName',
390
            'sonar.projectVersion',
391
            'sonar.sources'
392
        ];
393 1
        $intersection = array_intersect($requiredProperties, array_keys($this->properties));
394 1
        if (count($intersection) < count($requiredProperties)) {
395 0
            $message = 'SonarQube Scanner misses some parameters. The following properties are mandatory: ' . implode(
396 0
                ', ',
397 0
                $requiredProperties
398 0
            ) . '.';
399 0
            throw new BuildException($message);
400
        }
401
    }
402

403
    /**
404
     *
405
     * @return array
406
     */
407 1
    private function parseConfigurationFile()
408
    {
409 1
        if (($this->configuration === null) || ($this->configuration === '')) {
410 1
            return [];
411
        }
412

413 0
        $parser = new SonarConfigurationFileParser($this->configuration, $this->project);
414 0
        return $parser->parse();
415
    }
416

417
    /**
418
     *
419
     * @return boolean
420
     */
421 1
    private function isWindows()
422
    {
423 1
        return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
424
    }
425
}

Read our documentation on viewing source code .

Loading