Standard   A
last analyzed

Complexity

Total Complexity 19

Size/Duplication

Total Lines 225
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 64
dl 0
loc 225
rs 10
c 2
b 0
f 0
wmc 19

6 Methods

Rating   Name   Duplication   Size   Complexity  
A header() 0 3 1
A body() 0 3 1
A checkAccess() 0 21 4
A addDownload() 0 34 5
A checkDownload() 0 38 4
A init() 0 36 4
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2016-2025
6
 * @package Client
7
 * @subpackage Html
8
 */
9
10
11
namespace Aimeos\Client\Html\Account\Download;
12
13
14
/**
15
 * Default implementation of account download HTML client.
16
 *
17
 * @package Client
18
 * @subpackage Html
19
 */
20
class Standard
21
	extends \Aimeos\Client\Html\Common\Client\Factory\Base
22
	implements \Aimeos\Client\Html\Common\Client\Factory\Iface
23
{
24
	/** client/html/account/download/name
25
	 * Class name of the used account download client implementation
26
	 *
27
	 * Each default HTML client can be replace by an alternative imlementation.
28
	 * To use this implementation, you have to set the last part of the class
29
	 * name as configuration value so the client factory knows which class it
30
	 * has to instantiate.
31
	 *
32
	 * For example, if the name of the default class is
33
	 *
34
	 *  \Aimeos\Client\Html\Account\Download\Standard
35
	 *
36
	 * and you want to replace it with your own version named
37
	 *
38
	 *  \Aimeos\Client\Html\Account\Download\Mydownload
39
	 *
40
	 * then you have to set the this configuration option:
41
	 *
42
	 *  client/html/account/download/name = Mydownload
43
	 *
44
	 * The value is the last part of your own class name and it's case sensitive,
45
	 * so take care that the configuration value is exactly named like the last
46
	 * part of the class name.
47
	 *
48
	 * The allowed characters of the class name are A-Z, a-z and 0-9. No other
49
	 * characters are possible! You should always start the last part of the class
50
	 * name with an upper case character and continue only with lower case characters
51
	 * or numbers. Avoid chamel case names like "MyDownload"!
52
	 *
53
	 * @param string Last part of the class name
54
	 * @since 2014.03
55
	 */
56
57
58
	/**
59
	 * Returns the HTML code for insertion into the body.
60
	 *
61
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
62
	 * @return string HTML code
63
	 */
64
	public function body( string $uid = '' ) : string
65
	{
66
		return '';
67
	}
68
69
70
	/**
71
	 * Returns the HTML string for insertion into the header.
72
	 *
73
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
74
	 * @return string|null String including HTML tags for the header on error
75
	 */
76
	public function header( string $uid = '' ) : ?string
77
	{
78
		return null;
79
	}
80
81
82
	/**
83
	 * Processes the input, e.g. store given values.
84
	 *
85
	 * A view must be available and this method doesn't generate any output
86
	 * besides setting view variables if necessary.
87
	 */
88
	public function init()
89
	{
90
		$context = $this->context();
91
92
		try
93
		{
94
			$view = $this->view();
95
			$id = $view->param( 'dl_id' );
96
97
			/** client/html/account/download/error/url/target
98
			 * Destination of the URL to redirect the customer if the file download isn't allowed
99
			 *
100
			 * The destination can be a page ID like in a content management system or the
101
			 * module of a software development framework. This "target" must contain or know
102
			 * the controller that should be called by the generated URL.
103
			 *
104
			 * @param string Destination of the URL
105
			 * @since 2019.04
106
			 */
107
			$target = $context->config()->get( 'client/html/account/download/error/url/target' );
108
109
			if( $this->checkAccess( $id ) === false ) {
110
				return $view->response()->withStatus( 401 )->withHeader( 'Location', $view->url( $target ) );
111
			}
112
113
			if( $this->checkDownload( $id ) === false ) {
114
				return $view->response()->withStatus( 403 )->withHeader( 'Location', $view->url( $target ) );
115
			}
116
117
			$this->addDownload( \Aimeos\MShop::create( $context, 'order/product/attribute' )->get( $id ) );
118
119
			parent::init();
120
		}
121
		catch( \Exception $e )
122
		{
123
			$this->logException( $e );
124
		}
125
	}
126
127
128
	/**
129
	 * Adds the necessary headers and the download content to the reponse object
130
	 *
131
	 * @param \Aimeos\MShop\Order\Item\Product\Attribute\Iface $item Order product attribute item with file reference
132
	 */
133
	protected function addDownload( \Aimeos\MShop\Order\Item\Product\Attribute\Iface $item )
134
	{
135
		$fs = $this->context()->fs( 'fs-secure' );
136
		$response = $this->view()->response();
137
		$value = (string) $item->getValue();
138
139
		if( $fs->has( $value ) )
140
		{
141
			$name = $item->getName();
142
143
			if( pathinfo( $name, PATHINFO_EXTENSION ) == null
144
					&& ( $ext = pathinfo( $value, PATHINFO_EXTENSION ) ) != null
145
			) {
146
				$name .= '.' . $ext;
0 ignored issues
show
Bug introduced by
Are you sure $ext of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

146
				$name .= '.' . /** @scrutinizer ignore-type */ $ext;
Loading history...
147
			}
148
149
			$response->withHeader( 'Content-Description', 'File Transfer' );
150
			$response->withHeader( 'Content-Type', 'application/octet-stream' );
151
			$response->withHeader( 'Content-Disposition', 'attachment; filename="' . $name . '"' );
152
			$response->withHeader( 'Content-Length', (string) $fs->size( $value ) );
153
			$response->withHeader( 'Cache-Control', 'must-revalidate' );
154
			$response->withHeader( 'Pragma', 'private' );
155
			$response->withHeader( 'Expires', '0' );
156
157
			$response->withBody( $response->createStream( $fs->reads( $value ) ) );
158
		}
159
		elseif( filter_var( $value, FILTER_VALIDATE_URL ) !== false )
160
		{
161
			$response->withHeader( 'Location', $value );
162
			$response->withStatus( 303 );
163
		}
164
		else
165
		{
166
			$response->withStatus( 404 );
167
		}
168
	}
169
170
171
	/**
172
	 * Checks if the customer is allowed to download the file
173
	 *
174
	 * @param string|null $id Unique order base product attribute ID referencing the download file
175
	 * @return bool True if download is allowed, false if not
176
	 */
177
	protected function checkAccess( ?string $id = null ) : bool
178
	{
179
		$context = $this->context();
180
181
		if( $id && ( $customerId = $context->user() ) )
182
		{
183
			$manager = \Aimeos\MShop::create( $context, 'order' );
184
185
			$filter = $manager->filter();
186
			$filter->add( $filter->and( [
187
				$filter->compare( '>=', 'order.statuspayment', \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED ),
188
				$filter->compare( '==', 'order.customerid', $customerId ),
189
				$filter->compare( '==', 'order.product.attribute.id', $id ),
190
			] ) )->slice( 0, 1 );
191
192
			if( !$manager->search( $filter )->isEmpty() ) {
193
				return true;
194
			}
195
		}
196
197
		return false;
198
	}
199
200
201
	/**
202
	 * Updates the download counter for the downloaded file
203
	 *
204
	 * @param string|null $id Unique order base product attribute ID referencing the download file
205
	 * @return bool True if download is allowed, false if not
206
	 */
207
	protected function checkDownload( ?string $id = null ) : bool
208
	{
209
		$context = $this->context();
210
211
		/** client/html/account/download/maxcount
212
		 * Maximum number of file downloads allowed for an ordered product
213
		 *
214
		 * This configuration setting enables you to limit the number of downloads
215
		 * of a product download file. The count is the maximum number for each
216
		 * bought product and customer, i.e. setting the count to "3" allows
217
		 * a customer to download the bought product file up to three times.
218
		 *
219
		 * The default value of null enforces no limit.
220
		 *
221
		 * @param integer Maximum number of downloads
222
		 * @since 2016.02
223
		 */
224
		$maxcnt = $context->config()->get( 'client/html/account/download/maxcount' );
225
226
		$cntl = \Aimeos\Controller\Frontend::create( $context, 'customer' );
227
		$item = $cntl->uses( ['order' => ['download']] )->get();
228
229
		if( ( $listItem = $item->getListItem( 'order', 'download', $id ) ) === null ) {
230
			$listItem = $cntl->createListItem()->setType( 'download' )->setRefId( $id );
231
		}
232
233
		$config = $listItem->getConfig();
234
		$count = (int) $listItem->getConfigValue( 'count', 0 );
235
236
		if( $maxcnt === null || $count < $maxcnt )
237
		{
238
			$config['count'] = $count++;
239
			$cntl->addListItem( 'order', $listItem->setConfig( $config ) )->store();
240
241
			return true;
242
		}
243
244
		return false;
245
	}
246
247
248
	/** client/html/account/download/decorators/excludes
249
	 * Excludes decorators added by the "common" option from the account download html client
250
	 *
251
	 * Decorators extend the functionality of a class by adding new aspects
252
	 * (e.g. log what is currently done), executing the methods of the underlying
253
	 * class only in certain conditions (e.g. only for logged in users) or
254
	 * modify what is returned to the caller.
255
	 *
256
	 * This option allows you to remove a decorator added via
257
	 * "client/html/common/decorators/default" before they are wrapped
258
	 * around the html client.
259
	 *
260
	 *  client/html/account/download/decorators/excludes = array( 'decorator1' )
261
	 *
262
	 * This would remove the decorator named "decorator1" from the list of
263
	 * common decorators ("\Aimeos\Client\Html\Common\Decorator\*") added via
264
	 * "client/html/common/decorators/default" to the html client.
265
	 *
266
	 * @param array List of decorator names
267
	 * @see client/html/common/decorators/default
268
	 * @see client/html/account/download/decorators/global
269
	 * @see client/html/account/download/decorators/local
270
	 */
271
272
	/** client/html/account/download/decorators/global
273
	 * Adds a list of globally available decorators only to the account download html client
274
	 *
275
	 * Decorators extend the functionality of a class by adding new aspects
276
	 * (e.g. log what is currently done), executing the methods of the underlying
277
	 * class only in certain conditions (e.g. only for logged in users) or
278
	 * modify what is returned to the caller.
279
	 *
280
	 * This option allows you to wrap global decorators
281
	 * ("\Aimeos\Client\Html\Common\Decorator\*") around the html client.
282
	 *
283
	 *  client/html/account/download/decorators/global = array( 'decorator1' )
284
	 *
285
	 * This would add the decorator named "decorator1" defined by
286
	 * "\Aimeos\Client\Html\Common\Decorator\Decorator1" only to the html client.
287
	 *
288
	 * @param array List of decorator names
289
	 * @see client/html/common/decorators/default
290
	 * @see client/html/account/download/decorators/excludes
291
	 * @see client/html/account/download/decorators/local
292
	 */
293
294
	/** client/html/account/download/decorators/local
295
	 * Adds a list of local decorators only to the account download html client
296
	 *
297
	 * Decorators extend the functionality of a class by adding new aspects
298
	 * (e.g. log what is currently done), executing the methods of the underlying
299
	 * class only in certain conditions (e.g. only for logged in users) or
300
	 * modify what is returned to the caller.
301
	 *
302
	 * This option allows you to wrap local decorators
303
	 * ("\Aimeos\Client\Html\Account\Decorator\*") around the html client.
304
	 *
305
	 *  client/html/account/download/decorators/local = array( 'decorator2' )
306
	 *
307
	 * This would add the decorator named "decorator2" defined by
308
	 * "\Aimeos\Client\Html\Account\Decorator\Decorator2" only to the html client.
309
	 *
310
	 * @param array List of decorator names
311
	 * @see client/html/common/decorators/default
312
	 * @see client/html/account/download/decorators/excludes
313
	 * @see client/html/account/download/decorators/global
314
	 */
315
}
316