Issues (524)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

code/control/ApprovalsDispatcher.php (24 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
class ApprovalsDispatcher extends Dispatcher {
0 ignored issues
show
The property $allowed_actions is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $action_types is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
4
5
	const ACTION_APPROVALS = 'approvals';
6
7
	const ALLOW_APPROVAL = 'ALLOW_APPROVAL';
8
9
	const ALLOW_APPROVAL_BYPASS = 'ALLOW_APPROVAL_BYPASS';
10
11
	/**
12
	 * @var array
13
	 */
14
	private static $allowed_actions = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
15
		'submit',
16
		'cancel',
17
		'approve',
18
		'reject'
19
	];
20
21
	private static $dependencies = [
22
		'formatter' => '%$DeploynautAPIFormatter'
23
	];
24
25
	/**
26
	 * @var \DNProject
27
	 */
28
	protected $project = null;
29
30
	/**
31
	 * @var \DNEnvironment
32
	 */
33
	protected $environment = null;
34
35
	/**
36
	 * @var array
37
	 */
38
	private static $action_types = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
The property $action_types is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
39
		self::ACTION_APPROVALS
40
	];
41
42
	/**
43
	 * @param \DNEnvironment $environment
44
	 * @param \Member|null $member
45
	 * @return bool
46
	 */
47
	public static function can_approve(\DNEnvironment $environment, \Member $member = null) {
48
		if ($member === null) {
49
			$member = \Member::currentUser();
50
		}
51
		return $environment->Project()->allowed(self::ALLOW_APPROVAL, $member);
0 ignored issues
show
It seems like $member defined by \Member::currentUser() on line 49 can also be of type object<DataObject>; however, DNProject::allowed() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
52
	}
53
54
	/**
55
	 * @param \DNEnvironment $environment
56
	 * @param \Member|null $member
57
	 * @return bool
58
	 */
59
	public static function can_bypass_approval(\DNEnvironment $environment, \Member $member = null) {
60
		if ($member === null) {
61
			$member = \Member::currentUser();
62
		}
63
		// special case for non-Production environments: users who can deploy are able to bypass approval.
64
		if ($environment->Usage !== \DNEnvironment::PRODUCTION && $environment->canDeploy($member)) {
0 ignored issues
show
It seems like $member defined by \Member::currentUser() on line 61 can also be of type object<DataObject>; however, DNEnvironment::canDeploy() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
65
			return true;
66
		}
67
		return $environment->Project()->allowed(self::ALLOW_APPROVAL_BYPASS, $member);
0 ignored issues
show
It seems like $member defined by \Member::currentUser() on line 61 can also be of type object<DataObject>; however, DNProject::allowed() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
68
	}
69
70 View Code Duplication
	public function init() {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
71
		parent::init();
72
73
		$this->project = $this->getCurrentProject();
74
		if (!$this->project) {
75
			return $this->project404Response();
76
		}
77
78
		// Performs canView permission check by limiting visible projects
79
		$this->environment = $this->getCurrentEnvironment($this->project);
80
		if (!$this->environment) {
81
			return $this->environment404Response();
82
		}
83
	}
84
85
	/**
86
	 * @param \SS_HTTPRequest $request
87
	 * @return \SS_HTTPResponse
88
	 */
89
	public function submit(\SS_HTTPRequest $request) {
90
		if ($request->httpMethod() !== 'POST') {
91
			return $this->getAPIResponse(['message' => 'Method not allowed, requires POST'], 405);
92
		}
93
94
		$this->checkSecurityToken();
95
96
		$deployment = \DNDeployment::get()->byId($request->postVar('id'));
97
		$errorResponse = $this->validateDeployment($deployment);
0 ignored issues
show
$deployment is of type object<DataObject>|null, but the function expects a object<DNDeployment>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
98
		if ($errorResponse instanceof \SS_HTTPResponse) {
99
			return $errorResponse;
100
		}
101
102
		$approver = \Member::get()->byId($request->postVar('approver_id'));
103
		if ($approver && $approver->exists()) {
104
			if (!self::can_approve($this->environment, $approver)) {
0 ignored issues
show
$approver is of type object<DataObject>, but the function expects a null|object<Member>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
105
				return $this->getAPIResponse(['message' => 'The given approver does not have permissions to approve'], 403);
106
			}
107
			$deployment->ApproverID = $approver->ID;
108
		}
109
110
		// title and summary may have changed, ensure they are saved
111
		if ($request->postVar('title')) {
112
			$deployment->Title = $request->postVar('title');
113
		}
114
		if ($request->postVar('summary')) {
115
			$deployment->Summary = $request->postVar('summary');
0 ignored issues
show
The property Summary does not seem to exist. Did you mean summary_fields?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
116
		}
117
118
		try {
119
			$deployment->getMachine()->apply(\DNDeployment::TR_SUBMIT);
120
		} catch (\Exception $e) {
121
			return $this->getAPIResponse([
122
				'message' => $e->getMessage()
123
			], 400);
124
		}
125
126
		return $this->getAPIResponse([
127
			'message' => 'Deployment request has been submitted',
128
			'deployment' => $this->formatter->getDeploymentData($deployment)
0 ignored issues
show
The property formatter does not exist on object<ApprovalsDispatcher>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
129
		], 200);
130
	}
131
132
	/**
133
	 * @param \SS_HTTPRequest $request
134
	 * @return \SS_HTTPResponse
135
	 */
136
	public function cancel(\SS_HTTPRequest $request) {
137
		if ($request->httpMethod() !== 'POST') {
138
			return $this->getAPIResponse(['message' => 'Method not allowed, requires POST'], 405);
139
		}
140
141
		$this->checkSecurityToken();
142
143
		$deployment = \DNDeployment::get()->byId($request->postVar('id'));
144
		$errorResponse = $this->validateDeployment($deployment);
0 ignored issues
show
$deployment is of type object<DataObject>|null, but the function expects a object<DNDeployment>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
145
		if ($errorResponse instanceof \SS_HTTPResponse) {
146
			return $errorResponse;
147
		}
148
149
		// if the person cancelling is not the one who created the deployment, update the deployer
150
		if (\Member::currentUserID() !== $deployment->DeployerID) {
151
			$deployment->DeployerID = \Member::currentUserID();
152
		}
153
154
		try {
155
			$deployment->getMachine()->apply(\DNDeployment::TR_NEW);
156
		} catch (\Exception $e) {
157
			return $this->getAPIResponse([
158
				'message' => $e->getMessage()
159
			], 400);
160
		}
161
162
		return $this->getAPIResponse([
163
			'message' => 'Deployment request has been cancelled',
164
			'deployment' => $this->formatter->getDeploymentData($deployment)
0 ignored issues
show
The property formatter does not exist on object<ApprovalsDispatcher>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
165
		], 200);
166
	}
167
168
	/**
169
	 * @param \SS_HTTPRequest $request
170
	 * @return \SS_HTTPResponse
171
	 */
172
	public function approve(\SS_HTTPRequest $request) {
173
		if ($request->httpMethod() !== 'POST') {
174
			return $this->getAPIResponse(['message' => 'Method not allowed, requires POST'], 405);
175
		}
176
177
		$this->checkSecurityToken();
178
179
		$deployment = \DNDeployment::get()->byId($request->postVar('id'));
180
		$errorResponse = $this->validateDeployment($deployment);
0 ignored issues
show
$deployment is of type object<DataObject>|null, but the function expects a object<DNDeployment>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
181
		if ($errorResponse instanceof \SS_HTTPResponse) {
182
			return $errorResponse;
183
		}
184
185
		$canBypass = self::can_bypass_approval($this->environment);
186
		$canApprove = self::can_approve($this->environment);
187
188
		// ensure we have either bypass or approval permission of the logged in user
189
		if (!$canBypass || !$canBypass) {
190
			return $this->getAPIResponse(['message' => 'You are not authorised to approve or bypass this deployment'], 403);
191
		}
192
193
		// check for specific permission depending on the current state of the deployment:
194
		// submitted => approved requires approval permissions
195
		// new => approved requires bypass permissions.
196 View Code Duplication
		if ($deployment->State === \DNDeployment::STATE_SUBMITTED && !$canApprove) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
197
			return $this->getAPIResponse(['message' => 'You are not authorised to approve this deployment'], 403);
198
		}
199 View Code Duplication
		if ($deployment->State === \DNDeployment::STATE_NEW && !$canBypass) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
200
			return $this->getAPIResponse(['message' => 'You are not authorised to bypass approval of this deployment'], 403);
201
		}
202
203
		if ($deployment->State === \DNDeployment::STATE_NEW) {
204
			// Bypassing approval: Ensure that approver is not set. This may happen when someone has requested approval,
205
			// cancelled approval, then bypassed.
206
			$deployment->ApproverID = 0;
207
		} else {
208
			// if the current user is not the person who was selected for approval on submit, but they got
209
			// here because they still have permission, then change the approver to the current user
210
			if (\Member::currentUserID() !== $deployment->ApproverID) {
211
				$deployment->ApproverID = \Member::currentUserID();
212
			}
213
		}
214
215
		// title and summary may have changed, ensure they are saved
216
		if ($request->postVar('title')) {
217
			$deployment->Title = $request->postVar('title');
218
		}
219
		if ($request->postVar('summary')) {
220
			$deployment->Summary = $request->postVar('summary');
0 ignored issues
show
The property Summary does not seem to exist. Did you mean summary_fields?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
221
		}
222
223
		try {
224
			$deployment->getMachine()->apply(\DNDeployment::TR_APPROVE);
225
		} catch (\Exception $e) {
226
			return $this->getAPIResponse([
227
				'message' => $e->getMessage()
228
			], 400);
229
		}
230
231
		return $this->getAPIResponse([
232
			'message' => 'Deployment request has been approved',
233
			'deployment' => $this->formatter->getDeploymentData($deployment)
0 ignored issues
show
The property formatter does not exist on object<ApprovalsDispatcher>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
234
		], 200);
235
	}
236
237
	/**
238
	 * @param \SS_HTTPRequest $request
239
	 * @return \SS_HTTPResponse
240
	 */
241
	public function reject(\SS_HTTPRequest $request) {
242
		if ($request->httpMethod() !== 'POST') {
243
			return $this->getAPIResponse(['message' => 'Method not allowed, requires POST'], 405);
244
		}
245
246
		$this->checkSecurityToken();
247
248
		$deployment = \DNDeployment::get()->byId($request->postVar('id'));
249
		$errorResponse = $this->validateDeployment($deployment);
0 ignored issues
show
$deployment is of type object<DataObject>|null, but the function expects a object<DNDeployment>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
250
		if ($errorResponse instanceof \SS_HTTPResponse) {
251
			return $errorResponse;
252
		}
253
254
		// reject permissions are the same as can approve
255
		if (!self::can_approve($this->environment)) {
256
			return $this->getAPIResponse(['message' => 'You are not authorised to reject this deployment'], 403);
257
		}
258
259
		// if the current user is not the person who was selected for approval on submit, but they got
260
		// here because they still have permission, then change the approver to the current user
261
		if (\Member::currentUserID() !== $deployment->ApproverID) {
262
			$deployment->ApproverID = \Member::currentUserID();
263
		}
264
265
		if ($request->postVar('rejected_reason')) {
266
			$deployment->RejectedReason = $request->postVar('rejected_reason');
267
		}
268
269
		try {
270
			$deployment->getMachine()->apply(\DNDeployment::TR_REJECT);
271
		} catch (\Exception $e) {
272
			return $this->getAPIResponse([
273
				'message' => $e->getMessage()
274
			], 400);
275
		}
276
277
		return $this->getAPIResponse([
278
			'message' => 'Deployment request has been rejected',
279
			'deployment' => $this->formatter->getDeploymentData($deployment)
0 ignored issues
show
The property formatter does not exist on object<ApprovalsDispatcher>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
280
		], 200);
281
	}
282
283
	/**
284
	 * Check if a DNDeployment exists and do permission checks on it. If there is something wrong it will return
285
	 * an APIResponse with the error, otherwise null.
286
	 *
287
	 * @param \DNDeployment $deployment
288
	 *
289
	 * @return null|SS_HTTPResponse
290
	 */
291 View Code Duplication
	protected function validateDeployment($deployment) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
292
		if (!$deployment || !$deployment->exists()) {
293
			return $this->getAPIResponse(['message' => 'This deployment does not exist'], 404);
294
		}
295
		if ($deployment->EnvironmentID != $this->environment->ID) {
296
			return $this->getAPIResponse(['message' => 'This deployment does not belong to the environment'], 403);
297
		}
298
		if (!$deployment->canView()) {
299
			return $this->getAPIResponse(['message' => 'You are not authorised to view this deployment'], 403);
300
		}
301
		return null;
302
	}
303
304
	/**
305
	 * @param string $name
306
	 * @return array
307
	 */
308
	public function getModel($name = '') {
309
		return [];
310
	}
311
312
	/**
313
	 * @param string $action
314
	 * @return string
315
	 */
316
	public function Link($action = '') {
317
		return \Controller::join_links($this->environment->Link(), self::ACTION_APPROVALS, $action);
318
	}
319
320
}
321