Issues (12)

Security Analysis    no request data  

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.

ConsignmentService/ConsignmentService.php (2 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
declare(strict_types=1);
4
5
namespace Loevgaard\DandomainConsignmentBundle\ConsignmentService;
6
7
use Doctrine\Common\Persistence\ManagerRegistry;
8
use Doctrine\ORM\EntityManager;
9
use Doctrine\ORM\OptimisticLockException;
10
use Doctrine\ORM\ORMException;
11
use Doctrine\ORM\QueryBuilder;
12
use Loevgaard\DandomainConsignment\Entity\Generated\ReportInterface;
13
use Loevgaard\DandomainConsignment\Entity\Report;
14
use Loevgaard\DandomainConsignment\Repository\ReportRepository;
15
use Loevgaard\DandomainConsignmentBundle\Event\ReportNotDeliveredEvent;
16
use Loevgaard\DandomainConsignmentBundle\Exception\InvalidBarCodeException;
17
use Loevgaard\DandomainConsignmentBundle\Exception\InvalidVendorNumberException;
18
use Loevgaard\DandomainFoundation\Entity\Generated\ManufacturerInterface;
19
use Loevgaard\DandomainStock\Entity\Generated\StockMovementInterface;
20
use Loevgaard\DandomainStock\Entity\StockMovement;
21
use Psr\Log\LoggerInterface;
22
use Psr\Log\NullLogger;
23
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
24
use Symfony\Component\OptionsResolver\OptionsResolver;
25
26
abstract class ConsignmentService implements ConsignmentServiceInterface
27
{
28
    /**
29
     * @var EntityManager
30
     */
31
    protected $entityManager;
32
33
    /**
34
     * @var ReportRepository
35
     */
36
    protected $reportRepository;
37
38
    /**
39
     * @var EventDispatcherInterface
40
     */
41
    protected $eventDispatcher;
42
43
    /**
44
     * The directory where report files will be saved.
45
     *
46
     * @var string
47
     */
48
    protected $reportDir;
49
50
    /**
51
     * @var LoggerInterface
52
     */
53
    protected $logger;
54
55
    /**
56
     * @var ManufacturerInterface
57
     */
58
    protected $manufacturer;
59
60
    /**
61
     * Contains the included product ids.
62
     *
63
     * @var array
64
     */
65
    protected $includedProductIds;
66
67
    /**
68
     * Contains the excluded product ids.
69
     *
70
     * @var array
71
     */
72
    protected $excludedProductIds;
73
74
    public function __construct(ManagerRegistry $managerRegistry, ReportRepository $reportRepository, EventDispatcherInterface $eventDispatcher, string $reportDir)
75
    {
76
        $this->entityManager = $managerRegistry->getManager();
77
        $this->reportRepository = $reportRepository;
78
        $this->eventDispatcher = $eventDispatcher;
79
        $this->reportDir = rtrim($reportDir, '/');
80
        $this->logger = new NullLogger();
81
82
        if (!is_dir($this->reportDir)) {
83
            throw new \InvalidArgumentException('The report dir given is not a directory');
84
        }
85
86
        if (!is_writable($this->reportDir)) {
87
            throw new \InvalidArgumentException('The report dir given is not writable');
88
        }
89
    }
90
91
    /**
92
     * @param array $options
93
     *
94
     * @return ReportInterface
95
     *
96
     * @throws ORMException
97
     */
98
    public function generateReport(array $options = []): ReportInterface
99
    {
100
        // resolve options
101
        $resolver = new OptionsResolver();
102
        $this->configureOptions($resolver);
103
        $options = $resolver->resolve($options);
104
105
        $report = new Report();
106
        $report->setManufacturer($this->manufacturer);
107
108
        $this->reportRepository->persist($report);
109
110
        try {
111
            if ($options['valid_bar_codes']) {
112
                $this->validateBarCodes();
113
            }
114
115
            if ($options['valid_vendor_numbers']) {
116
                $this->validateVendorNumbers();
117
            }
118
119
            $qb = $this->queryBuilder($options);
120
121
            /** @var StockMovementInterface[] $stockMovements */
122
            $stockMovements = $qb->getQuery()->getResult();
123
124
            if (!count($stockMovements)) {
125
                throw new \Exception('No stock movements applicable for this report');
126
            }
127
128
            $lastStockMovement = null;
129
            foreach ($stockMovements as $stockMovement) {
130
                $report->addStockMovement($stockMovement);
131
132
                $lastStockMovement = $stockMovement;
133
            }
134
135
            if ($lastStockMovement && $options['update_last_stock_movement']) {
136
                $this->manufacturer->setConsignmentLastStockMovement($lastStockMovement);
137
            }
138
139
            $report->markAsSuccess();
140
        } catch (\Exception $e) {
141
            $report->markAsError($e->getMessage());
142
        }
143
144
        $this->reportRepository->flush();
145
146
        return $report;
147
    }
148
149
    /**
150
     * @param ReportInterface $report
151
     * @param array           $options
152
     *
153
     * @return \SplFileObject
154
     *
155
     * @throws ORMException
156
     * @throws OptimisticLockException
157
     */
158
    public function generateReportFile(ReportInterface $report, array $options = []): \SplFileObject
159
    {
160
        $file = $this->getFile();
161
162
        foreach ($report->getStockMovements() as $stockMovement) {
163
            $file->fputcsv([
164
                $stockMovement->getQuantity(),
165
                $stockMovement->getProduct()->getBarCodeNumber(),
166
            ]);
167
        }
168
169
        $report->setFile($file);
170
171
        $this->reportRepository->flush();
172
173
        return $file;
174
    }
175
176
    /**
177
     * @param ReportInterface $report
178
     * @param array           $options
179
     *
180
     * @return bool
181
     *
182
     * @throws ORMException
183
     * @throws OptimisticLockException
184
     */
185
    public function deliverReport(ReportInterface $report, array $options = []): bool
186
    {
187
        if (!$report->isDeliverable()) {
188
            $this->eventDispatcher->dispatch(ReportNotDeliveredEvent::NAME, new ReportNotDeliveredEvent($report));
189
190
            return false;
191
        }
192
193
        $file = $report->getFile();
194
        if (!$file || !$file->isFile()) {
195
            $file = $this->generateReportFile($report);
196
        }
197
198
        $this->logger->info('File has been delivered to: '.$file->getPathname());
199
200
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
201
        Example of mailing the report
202
        -------
203
        $recipients = ['[email protected]'];
204
205
        $attachment = \Swift_Attachment::fromPath($file->getPathname());
206
        $attachment->setFilename('report-'.$report->getId().'.csv');
207
208
        $message = \Swift_Message::newInstance()
209
            ->attach($attachment)
210
            ->setSubject('Consignment report (id: '.$report->getId().')')
211
            ->setFrom('[email protected]', 'Your Business')
212
            ->setTo($recipients)
213
            ->setBody('See the attached file.', 'text/plain')
214
        ;
215
216
        $this->mailer->send($message);
217
        */
218
219
        return true;
220
    }
221
222
    public function queryBuilder(array $options = [], string $alias = 's'): QueryBuilder
223
    {
224
        // resolve options
225
        $resolver = new OptionsResolver();
226
        $this->configureOptions($resolver);
227
        $options = $resolver->resolve($options);
228
229
        $includedProductIds = $this->getIncludedProductIds();
230
231
        if (!count($includedProductIds)) {
232
            throw new \RuntimeException('No included product ids. Something is wrong');
233
        }
234
235
        $qb = $this->entityManager->createQueryBuilder();
236
        $qb->select('s, p')
237
            ->from('Loevgaard\DandomainStock\Entity\StockMovement', 's')
238
            ->join('s.product', 'p')
239
            ->andWhere($qb->expr()->in('p.id', ':includedProductIds'))
240
            ->andWhere($qb->expr()->in('s.type', ':stockMovementTypes'))
241
            ->addOrderBy('s.id', 'asc')
242
            ->setParameters([
243
                'includedProductIds' => $includedProductIds,
244
                'stockMovementTypes' => $options['stock_movement_types'],
245
            ]);
246
247
        if (!$options['include_complaints']) {
248
            $qb->andWhere('s.complaint = 0');
249
        }
250
251
        if ($options['use_last_stock_movement'] && $this->manufacturer->getConsignmentLastStockMovement()) {
252
            $qb->andWhere($qb->expr()->gt('s.id', ':lastStockMovementId'))
253
                ->setParameter('lastStockMovementId', $this->manufacturer->getConsignmentLastStockMovement()->getId());
254
        }
255
256
        if ($options['start_date']) {
257
            $qb->andWhere($qb->expr()->gte('s.createdAt', ':startDate'))
258
                ->setParameter('startDate', $options['start_date']);
259
        }
260
261
        if ($options['end_date']) {
262
            $qb->andWhere($qb->expr()->lte('s.createdAt', ':endDate'))
263
                ->setParameter('endDate', $options['end_date']);
264
        }
265
266
        return $qb;
267
    }
268
269
    public function getProductQueryBuilder(string $alias = 'p'): QueryBuilder
270
    {
271
        $excludedProductIds = $this->getExcludedProductIds();
272
273
        $qb = $this->entityManager->createQueryBuilder();
274
        $qb->select($alias)
275
            ->from('Loevgaard\DandomainFoundation\Entity\Product', $alias)
276
            ->where($qb->expr()->isMemberOf(':manufacturer', $alias.'.manufacturers'))
277
            ->setParameter('manufacturer', $this->manufacturer);
278
279
        if (!empty($excludedProductIds)) {
280
            $qb->andWhere($qb->expr()->notIn($alias.'.id', ':excluded'))
281
                ->setParameter('excluded', $excludedProductIds);
282
        }
283
284
        return $qb;
285
    }
286
287
    /**
288
     * @param LoggerInterface $logger
289
     *
290
     * @return ConsignmentServiceInterface
291
     */
292
    public function setLogger(LoggerInterface $logger): ConsignmentServiceInterface
293
    {
294
        $this->logger = $logger;
295
296
        return $this;
297
    }
298
299
    /**
300
     * @param ManufacturerInterface $manufacturer
301
     *
302
     * @return ConsignmentServiceInterface
303
     */
304
    public function setManufacturer(ManufacturerInterface $manufacturer): ConsignmentServiceInterface
305
    {
306
        $this->manufacturer = $manufacturer;
307
308
        return $this;
309
    }
310
311
    /**
312
     * This method should return an array of included product ids
313
     * It excludes the excluded product ids, by using the getProductQueryBuilder method.
314
     *
315
     * @return array
316
     */
317
    protected function getIncludedProductIds(): array
318
    {
319
        if (!$this->includedProductIds) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->includedProductIds of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
320
            $qb = $this->getProductQueryBuilder();
321
            $qb->select('p.id');
322
323
            $res = $qb->getQuery()->getArrayResult();
324
325
            $this->includedProductIds = array_map(function ($elm) {
326
                return array_values($elm)[0];
327
            }, $res);
328
        }
329
330
        return $this->includedProductIds;
331
    }
332
333
    /**
334
     * This method should return an array of excluded product ids.
335
     *
336
     * @return array
337
     */
338
    protected function getExcludedProductIds(): array
339
    {
340
        return [];
341
    }
342
343
    /**
344
     * This is a helper method which takes an array of product numbers and returns their respective product ids.
345
     *
346
     * @param array $numbers
347
     *
348
     * @return array
349
     */
350
    protected function getProductIdsFromProductNumbers(array $numbers): array
351
    {
352
        $qb = $this->entityManager->createQueryBuilder();
353
        $qb->select('p.id')
354
            ->from('Loevgaard\DandomainFoundation\Entity\Product', 'p')
355
            ->where($qb->expr()->in('p.number', ':numbers'))
356
            ->setParameter('numbers', $numbers);
357
358
        $res = $qb->getQuery()->getArrayResult();
359
360
        return array_map(function ($elm) {
361
            return array_values($elm)[0];
362
        }, $res);
363
    }
364
365
    /**
366
     * @throws InvalidBarCodeException
367
     */
368
    protected function validateBarCodes(): void
369
    {
370
        $qb = $this->queryBuilder();
371
        $qb->andWhere('p.validBarCode = 0');
372
373
        /** @var StockMovementInterface[] $stockMovements */
374
        $stockMovements = $qb->getQuery()->getResult();
375
376
        $c = count($stockMovements);
377
378 View Code Duplication
        if ($c) {
379
            $this->logger->emergency('There are '.$c.' stock movements with invalid bar codes');
380
            $productNumbers = [];
381
            foreach ($stockMovements as $stockMovement) {
382
                $productNumbers[] = $stockMovement->getProduct()->getNumber();
383
            }
384
385
            throw new InvalidBarCodeException('Products with invalid bar codes: '.join(', ', $productNumbers), $productNumbers);
386
        }
387
    }
388
389
    /**
390
     * @throws InvalidVendorNumberException
391
     */
392
    protected function validateVendorNumbers(): void
393
    {
394
        $qb = $this->queryBuilder();
395
        $qb->andWhere($qb->expr()->orX(
396
            $qb->expr()->eq('p.vendorNumber', ':empty'),
397
            $qb->expr()->isNull('p.vendorNumber')
398
        ))->setParameter(':empty', '');
399
400
        /** @var StockMovementInterface[] $stockMovements */
401
        $stockMovements = $qb->getQuery()->getResult();
402
403
        $c = count($stockMovements);
404
405 View Code Duplication
        if ($c) {
406
            $this->logger->critical('There are '.$c.' stock movements with invalid vendor numbers');
407
            $productNumbers = [];
408
            foreach ($stockMovements as $stockMovement) {
409
                $productNumbers[] = $stockMovement->getProduct()->getNumber();
410
            }
411
412
            throw new InvalidVendorNumberException('Products with invalid vendor numbers: '.join($productNumbers), $productNumbers);
413
        }
414
    }
415
416
    protected function getFile(string $extension = 'csv'): \SplFileObject
417
    {
418
        do {
419
            $filename = $this->reportDir.'/'.uniqid('consignment-', true).'.'.$extension;
420
        } while (file_exists($filename));
421
422
        return new \SplFileObject($filename, 'w+');
423
    }
424
425
    protected function configureOptions(OptionsResolver $resolver): void
426
    {
427
        $resolver->setDefaults([
428
            'valid_bar_codes' => false,
429
            'valid_vendor_numbers' => false,
430
            'update_last_stock_movement' => true,
431
            'stock_movement_types' => [
432
                StockMovement::TYPE_RETURN,
433
                StockMovement::TYPE_SALE,
434
                StockMovement::TYPE_REGULATION,
435
            ],
436
            'include_complaints' => false,
437
            'use_last_stock_movement' => true,
438
            'start_date' => null,
439
            'end_date' => null,
440
        ]);
441
    }
442
}
443