ProjectController   D
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 25

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 33
c 2
b 1
f 0
lcom 1
cbo 25
dl 0
loc 429
ccs 0
cts 244
cp 0
rs 4.6999

11 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 7 1
B view() 0 34 4
A build() 0 21 3
A delete() 0 12 1
A builds() 0 10 1
A getLatestBuildsHtml() 0 19 3
B add() 0 51 5
B edit() 0 61 7
B projectForm() 0 92 2
A githubRepositories() 0 9 1
B getReferenceValidator() 0 37 5
1
<?php
2
/**
3
 * PHPCI - Continuous Integration for PHP.
4
 *
5
 * @copyright    Copyright 2014, Block 8 Limited.
6
 * @license      https://github.com/Block8/PHPCI/blob/master/LICENSE.md
7
 *
8
 * @link         https://www.phptesting.org/
9
 */
10
11
namespace PHPCI\Controller;
12
13
use b8;
14
use b8\Form;
15
use b8\Exception\HttpException\NotFoundException;
16
use b8\Store;
17
use PHPCI;
18
use PHPCI\BuildFactory;
19
use PHPCI\Helper\Github;
20
use PHPCI\Helper\Lang;
21
use PHPCI\Helper\SshKey;
22
use PHPCI\Service\BuildService;
23
use PHPCI\Service\ProjectService;
24
25
/**
26
 * Project Controller - Allows users to create, edit and view projects.
27
 *
28
 * @author       Dan Cryer <[email protected]>
29
 */
30
class ProjectController extends PHPCI\Controller
31
{
32
    /**
33
     * @var \PHPCI\Store\ProjectStore
34
     */
35
    protected $projectStore;
36
37
    /**
38
     * @var \PHPCI\Service\ProjectService
39
     */
40
    protected $projectService;
41
42
    /**
43
     * @var \PHPCI\Store\BuildStore
44
     */
45
    protected $buildStore;
46
47
    /**
48
     * @var \PHPCI\Service\BuildService
49
     */
50
    protected $buildService;
51
52
    /**
53
     * Initialise the controller, set up stores and services.
54
     */
55
    public function init()
56
    {
57
        $this->buildStore = Store\Factory::getStore('Build');
58
        $this->projectStore = Store\Factory::getStore('Project');
59
        $this->projectService = new ProjectService($this->projectStore);
0 ignored issues
show
Compatibility introduced by
$this->projectStore of type object<b8\Store> is not a sub-type of object<PHPCI\Store\ProjectStore>. It seems like you assume a child class of the class b8\Store to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
60
        $this->buildService = new BuildService($this->buildStore);
0 ignored issues
show
Compatibility introduced by
$this->buildStore of type object<b8\Store> is not a sub-type of object<PHPCI\Store\BuildStore>. It seems like you assume a child class of the class b8\Store to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
61
    }
62
63
    /**
64
     * View a specific project.
65
     */
66
    public function view($projectId)
67
    {
68
        $branch = $this->getParam('branch', '');
69
        $project = $this->projectStore->getById($projectId);
70
71
        if (empty($project)) {
72
            throw new NotFoundException(Lang::get('project_x_not_found', $projectId));
73
        }
74
75
        $per_page = 10;
76
        $page = $this->getParam('p', 1);
77
        $builds = $this->getLatestBuildsHtml($projectId, urldecode($branch), (($page - 1) * $per_page));
78
        $pages = $builds[1] == 0 ? 1 : ceil($builds[1] / $per_page);
79
80
        if ($page > $pages) {
81
            $response = new b8\Http\Response\RedirectResponse();
82
            $response->setHeader('Location', PHPCI_URL.'project/view/'.$projectId);
83
84
            return $response;
85
        }
86
87
        $this->view->builds = $builds[0];
88
        $this->view->total = $builds[1];
89
        $this->view->project = $project;
90
        $this->view->branch = urldecode($branch);
91
        $this->view->branches = $this->projectStore->getKnownBranches($projectId);
92
        $this->view->page = $page;
93
        $this->view->pages = $pages;
94
95
        $this->layout->title = $project->getTitle();
96
        $this->layout->subtitle = $this->view->branch;
97
98
        return $this->view->render();
99
    }
100
101
    /**
102
     * Create a new pending build for a project.
103
     */
104
    public function build($projectId, $branch = '')
0 ignored issues
show
Coding Style introduced by
build uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
105
    {
106
        /* @var \PHPCI\Model\Project $project */
107
        $project = $this->projectStore->getById($projectId);
108
109
        if (empty($branch)) {
110
            $branch = $project->getBranch();
111
        }
112
113
        if (empty($project)) {
114
            throw new NotFoundException(Lang::get('project_x_not_found', $projectId));
115
        }
116
117
        $email = $_SESSION['phpci_user']->getEmail();
118
        $build = $this->buildService->createBuild($project, null, urldecode($branch), $email);
119
120
        $response = new b8\Http\Response\RedirectResponse();
121
        $response->setHeader('Location', PHPCI_URL.'build/view/'.$build->getId());
122
123
        return $response;
124
    }
125
126
    /**
127
     * Delete a project.
128
     */
129
    public function delete($projectId)
130
    {
131
        $this->requireAdmin();
132
133
        $project = $this->projectStore->getById($projectId);
134
        $this->projectService->deleteProject($project);
0 ignored issues
show
Bug introduced by
It seems like $project defined by $this->projectStore->getById($projectId) on line 133 can be null; however, PHPCI\Service\ProjectService::deleteProject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
135
136
        $response = new b8\Http\Response\RedirectResponse();
137
        $response->setHeader('Location', PHPCI_URL);
138
139
        return $response;
140
    }
141
142
    /**
143
     * AJAX get latest builds.
144
     */
145
    public function builds($projectId)
146
    {
147
        $branch = $this->getParam('branch', '');
148
        $builds = $this->getLatestBuildsHtml($projectId, urldecode($branch));
149
150
        $this->response->disableLayout();
151
        $this->response->setContent($builds[0]);
152
153
        return $this->response;
154
    }
155
156
    /**
157
     * Render latest builds for project as HTML table.
158
     *
159
     * @param $projectId
160
     * @param string $branch A urldecoded branch name.
161
     * @param int    $start
162
     *
163
     * @return array
164
     */
165
    protected function getLatestBuildsHtml($projectId, $branch = '', $start = 0)
166
    {
167
        $criteria = array('project_id' => $projectId);
168
        if (!empty($branch)) {
169
            $criteria['branch'] = $branch;
170
        }
171
172
        $order = array('id' => 'DESC');
173
        $builds = $this->buildStore->getWhere($criteria, 10, $start, array(), $order);
174
        $view = new b8\View('BuildsTable');
175
176
        foreach ($builds['items'] as &$build) {
177
            $build = BuildFactory::getBuild($build);
178
        }
179
180
        $view->builds = $builds['items'];
181
182
        return array($view->render(), $builds['count']);
183
    }
184
185
    /**
186
     * Add a new project. Handles both the form, and processing.
187
     */
188
    public function add()
189
    {
190
        $this->layout->title = Lang::get('add_project');
191
        $this->requireAdmin();
192
193
        $method = $this->request->getMethod();
194
195
        $pub = null;
196
        $values = $this->getParams();
197
198
        if ($method != 'POST') {
199
            $sshKey = new SshKey();
200
            $key = $sshKey->generate();
201
202
            $values['key'] = $key['private_key'];
203
            $values['pubkey'] = $key['public_key'];
204
            $pub = $key['public_key'];
205
        }
206
207
        $form = $this->projectForm($values);
208
209
        if ($method != 'POST' || ($method == 'POST' && !$form->validate())) {
210
            $view = new b8\View('ProjectForm');
211
            $view->type = 'add';
212
            $view->project = null;
213
            $view->form = $form;
214
            $view->key = $pub;
215
216
            return $view->render();
217
        } else {
218
            $title = $this->getParam('title', 'New Project');
219
            $reference = $this->getParam('reference', null);
220
            $type = $this->getParam('type', null);
221
222
            $options = array(
223
                'ssh_private_key' => $this->getParam('key', null),
224
                'ssh_public_key' => $this->getParam('pubkey', null),
225
                'build_config' => $this->getParam('build_config', null),
226
                'allow_public_status' => $this->getParam('allow_public_status', 0),
227
                'branch' => $this->getParam('branch', null),
228
                'group' => $this->getParam('group_id', null),
229
            );
230
231
            $project = $this->projectService->createProject($title, $type, $reference, $options);
232
233
            $response = new b8\Http\Response\RedirectResponse();
234
            $response->setHeader('Location', PHPCI_URL.'project/view/'.$project->getId());
235
236
            return $response;
237
        }
238
    }
239
240
    /**
241
     * Edit a project. Handles both the form and processing.
242
     */
243
    public function edit($projectId)
244
    {
245
        $this->requireAdmin();
246
247
        $method = $this->request->getMethod();
248
        $project = $this->projectStore->getById($projectId);
249
250
        if (empty($project)) {
251
            throw new NotFoundException(Lang::get('project_x_not_found', $projectId));
252
        }
253
254
        $this->layout->title = $project->getTitle();
255
        $this->layout->subtitle = Lang::get('edit_project');
256
257
        $values = $project->getDataArray();
258
        $values['key'] = $values['ssh_private_key'];
259
        $values['pubkey'] = $values['ssh_public_key'];
260
261
        if ($values['type'] == 'gitlab') {
262
            $accessInfo = $project->getAccessInformation();
263
            $reference = $accessInfo['user'].'@'.$accessInfo['domain'].':'.$project->getReference().'.git';
264
            $values['reference'] = $reference;
265
        }
266
267
        if ($method == 'POST') {
268
            $values = $this->getParams();
269
        }
270
271
        $form = $this->projectForm($values, 'edit/'.$projectId);
272
273
        if ($method != 'POST' || ($method == 'POST' && !$form->validate())) {
274
            $view = new b8\View('ProjectForm');
275
            $view->type = 'edit';
276
            $view->project = $project;
277
            $view->form = $form;
278
            $view->key = $values['pubkey'];
279
280
            return $view->render();
281
        }
282
283
        $title = $this->getParam('title', Lang::get('new_project'));
284
        $reference = $this->getParam('reference', null);
285
        $type = $this->getParam('type', null);
286
287
        $options = array(
288
            'ssh_private_key' => $this->getParam('key', null),
289
            'ssh_public_key' => $this->getParam('pubkey', null),
290
            'build_config' => $this->getParam('build_config', null),
291
            'allow_public_status' => $this->getParam('allow_public_status', 0),
292
            'archived' => $this->getParam('archived', 0),
293
            'branch' => $this->getParam('branch', null),
294
            'group' => $this->getParam('group_id', null),
295
        );
296
297
        $project = $this->projectService->updateProject($project, $title, $type, $reference, $options);
298
299
        $response = new b8\Http\Response\RedirectResponse();
300
        $response->setHeader('Location', PHPCI_URL.'project/view/'.$project->getId());
301
302
        return $response;
303
    }
304
305
    /**
306
     * Create add / edit project form.
307
     */
308
    protected function projectForm($values, $type = 'add')
309
    {
310
        $form = new Form();
311
        $form->setMethod('POST');
312
        $form->setAction(PHPCI_URL.'project/'.$type);
313
        $form->addField(new Form\Element\Csrf('csrf'));
314
        $form->addField(new Form\Element\Hidden('pubkey'));
315
316
        $options = array(
317
            'choose' => Lang::get('select_repository_type'),
318
            'github' => Lang::get('github'),
319
            'bitbucket' => Lang::get('bitbucket'),
320
            'gitlab' => Lang::get('gitlab'),
321
            'remote' => Lang::get('remote'),
322
            'local' => Lang::get('local'),
323
            'hg' => Lang::get('hg'),
324
            'svn' => Lang::get('svn'),
325
            );
326
327
        $field = Form\Element\Select::create('type', Lang::get('where_hosted'), true);
328
        $field->setPattern('^(github|bitbucket|gitlab|remote|local|hg|svn)');
329
        $field->setOptions($options);
330
        $field->setClass('form-control')->setContainerClass('form-group');
331
        $form->addField($field);
332
333
        $container = new Form\ControlGroup('github-container');
334
        $container->setClass('github-container');
335
336
        $field = Form\Element\Select::create('github', Lang::get('choose_github'), false);
337
        $field->setClass('form-control')->setContainerClass('form-group');
338
        $container->addField($field);
339
        $form->addField($container);
340
341
        $field = Form\Element\Text::create('reference', Lang::get('repo_name'), true);
342
        $field->setValidator($this->getReferenceValidator($values));
343
        $field->setClass('form-control')->setContainerClass('form-group');
344
        $form->addField($field);
345
346
        $field = Form\Element\Text::create('title', Lang::get('project_title'), true);
347
        $field->setClass('form-control')->setContainerClass('form-group');
348
        $form->addField($field);
349
350
        $field = Form\Element\TextArea::create('key', Lang::get('project_private_key'), false);
351
        $field->setClass('form-control')->setContainerClass('form-group');
352
        $field->setRows(6);
353
        $form->addField($field);
354
355
        $field = Form\Element\TextArea::create('build_config', Lang::get('build_config'), false);
356
        $field->setClass('form-control')->setContainerClass('form-group');
357
        $field->setRows(6);
358
        $form->addField($field);
359
360
        $field = Form\Element\Text::create('branch', Lang::get('default_branch'), true);
361
        $field->setClass('form-control')->setContainerClass('form-group')->setValue('master');
362
        $form->addField($field);
363
364
        $field = Form\Element\Select::create('group_id', 'Project Group', true);
365
        $field->setClass('form-control')->setContainerClass('form-group')->setValue(1);
366
367
        $groups = array();
368
        $groupStore = b8\Store\Factory::getStore('ProjectGroup');
369
        $groupList = $groupStore->getWhere(array(), 100, 0, array(), array('title' => 'ASC'));
370
371
        foreach ($groupList['items'] as $group) {
372
            $groups[$group->getId()] = $group->getTitle();
373
        }
374
375
        $field->setOptions($groups);
376
        $form->addField($field);
377
378
        $field = Form\Element\Checkbox::create('allow_public_status', Lang::get('allow_public_status'), false);
379
        $field->setContainerClass('form-group');
380
        $field->setCheckedValue(1);
381
        $field->setValue(0);
382
        $form->addField($field);
383
384
        $field = Form\Element\Checkbox::create('archived', Lang::get('archived'), false);
385
        $field->setContainerClass('form-group');
386
        $field->setCheckedValue(1);
387
        $field->setValue(0);
388
        $form->addField($field);
389
390
        $field = new Form\Element\Submit();
391
        $field->setValue(Lang::get('save_project'));
392
        $field->setContainerClass('form-group');
393
        $field->setClass('btn-success');
394
        $form->addField($field);
395
396
        $form->setValues($values);
397
398
        return $form;
399
    }
400
401
    /**
402
     * Get an array of repositories from Github's API.
403
     */
404
    protected function githubRepositories()
405
    {
406
        $github = new Github();
407
408
        $response = new b8\Http\Response\JsonResponse();
409
        $response->setContent($github->getRepositories());
410
411
        return $response;
412
    }
413
414
    /**
415
     * Get the validator to use to check project references.
416
     *
417
     * @param $values
418
     *
419
     * @return callable
420
     */
421
    protected function getReferenceValidator($values)
422
    {
423
        return function ($val) use ($values) {
424
            $type = $values['type'];
425
426
            $validators = array(
427
                'hg' => array(
428
                    'regex' => '/^(https?):\/\//',
429
                    'message' => Lang::get('error_mercurial'),
430
                ),
431
                'remote' => array(
432
                    'regex' => '/^(git|https?):\/\//',
433
                    'message' => Lang::get('error_remote'),
434
                ),
435
                'gitlab' => array(
436
                    'regex' => '`^(.*)@(.*):(.*)/(.*)\.git`',
437
                    'message' => Lang::get('error_gitlab'),
438
                ),
439
                'github' => array(
440
                    'regex' => '/^[a-zA-Z0-9_\-]+\/[a-zA-Z0-9_\-\.]+$/',
441
                    'message' => Lang::get('error_github'),
442
                ),
443
                'bitbucket' => array(
444
                    'regex' => '/^[a-zA-Z0-9_\-]+\/[a-zA-Z0-9_\-\.]+$/',
445
                    'message' => Lang::get('error_bitbucket'),
446
                ),
447
            );
448
449
            if (in_array($type, $validators) && !preg_match($validators[$type]['regex'], $val)) {
450
                throw new \Exception($validators[$type]['message']);
451
            } elseif ($type == 'local' && !is_dir($val)) {
452
                throw new \Exception(Lang::get('error_path'));
453
            }
454
455
            return true;
456
        };
457
    }
458
}
459