Issues (15)

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.

src/Service/Loco.php (3 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
namespace Happyr\TranslationBundle\Service;
4
5
use Happyr\TranslationBundle\Exception\HappyrTranslationException;
6
use Happyr\TranslationBundle\Exception\HttpException;
7
use Happyr\TranslationBundle\Http\RequestManager;
8
use Happyr\TranslationBundle\Model\Message;
9
use Happyr\TranslationBundle\Translation\FilesystemUpdater;
10
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
11
use Symfony\Component\Translation\TranslatorInterface;
12
13
/**
14
 * @author Tobias Nyholm <[email protected]>
15
 */
16
class Loco implements TranslationServiceInterface
17
{
18
    const BASE_URL = 'https://localise.biz/api/';
19
20
    /**
21
     * @var RequestManager
22
     */
23
    private $requestManager;
24
25
    /**
26
     * @var array projects
27
     */
28
    private $projects;
29
30
    /**
31
     * @var FilesystemUpdater filesystemService
32
     */
33
    private $filesystemService;
34
35
    /**
36
     * @var TranslatorInterface filesystemService
37
     */
38
    private $translator;
39
40
    /**
41
     * @param TranslatorInterface $translator
42
     * @param RequestManager $requestManager
43
     * @param FilesystemUpdater $fs
44
     * @param array $projects
45
     */
46 1
    public function __construct(RequestManager $requestManager, FilesystemUpdater $fs, TranslatorInterface $translator, array $projects)
47
    {
48 1
        $this->translator = $translator;
49 1
        $this->requestManager = $requestManager;
50 1
        $this->projects = $projects;
51 1
        $this->filesystemService = $fs;
52 1
    }
53
54
    /**
55
     * @param $key
56
     * @param $method
57
     * @param $resource
58
     * @param null $body
59
     * @param string $type
60
     * @param array $extraQuery
61
     * @return array
62
     * @throws HttpException
63
     */
64
    protected function makeApiRequest($key, $method, $resource, $body = null, $type = 'form', $extraQuery = array())
65
    {
66
        $headers = array();
67
        if ($body !== null) {
68
            if ($type === 'form') {
69
                if (is_array($body)) {
70
                    $body = http_build_query($body);
71
                }
72
                $headers['Content-Type'] = 'application/x-www-form-urlencoded';
73
            } elseif ($type === 'json') {
74
                $body = json_encode($body);
75
                $headers['Content-Type'] = 'application/json';
76
            }
77
        }
78
79
        $query = array_merge($extraQuery, ['key' => $key]);
80
        $url = self::BASE_URL . $resource . '?' . http_build_query($query);
81
82
        return $this->requestManager->send($method, $url, $body, $headers);
83
    }
84
85
    /**
86
     * Fetch a translation form Loco.
87
     *
88
     * @param Message $message
89
     */
90
    public function fetchTranslation(Message $message, $updateFs = false)
91
    {
92
        $project = $this->getProject($message);
93
94
        try {
95
            $resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale());
96
            $response = $this->makeApiRequest($project['api_key'], 'GET', $resource);
97
        } catch (HttpException $e) {
98
            if ($e->getCode() === 404) {
99
                //Message does not exist
100
                return;
101
            }
102
            throw $e;
103
        }
104
105
        $logoTranslation = $response['translation'];
106
        $messageTranslation = $message->getTranslation();
107
        $message->setTranslation($logoTranslation);
108
109
        // update filesystem
110
        if ($updateFs && $logoTranslation !== $messageTranslation) {
111
            $this->filesystemService->updateMessageCatalog([$message]);
112
        }
113
114
        return $logoTranslation;
115
    }
116
117
    /**
118
     * Update the translation in Loco.
119
     *
120
     * @param Message $message
121
     */
122
    public function updateTranslation(Message $message)
123
    {
124
        $project = $this->getProject($message);
125
126
        try {
127
            $resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale());
128
            $this->makeApiRequest($project['api_key'], 'POST', $resource, $message->getTranslation());
129
        } catch (HttpException $e) {
130
            if ($e->getCode() === 404) {
131
                //Asset does not exist
132
                if ($this->createAsset($message)) {
133
                    //Try again
134
                    return $this->updateTranslation($message);
135
                }
136
137
                return false;
138
            }
139
            throw $e;
140
        }
141
142
        $this->filesystemService->updateMessageCatalog([$message]);
143
144
        return true;
145
    }
146
147
    /**
148
     * If there is something wrong with the translation, please flag it.
149
     *
150
     * @param Message $message
151
     * @param int $type 0: Fuzzy, 1: Incorrect, 2: Provisional, 3: Unapproved, 4: Incomplete
152
     *
153
     * @return bool
154
     */
155
    public function flagTranslation(Message $message, $type = 0)
156
    {
157
        $project = $this->getProject($message);
158
        $flags = ['fuzzy', 'incorrect', 'provisional', 'unapproved', 'incomplete'];
159
160
        try {
161
            $resource = sprintf('translations/%s/%s/flag', $message->getId(), $message->getLocale());
162
            $this->makeApiRequest($project['api_key'], 'POST', $resource, ['flag' => $flags[$type]]);
163
        } catch (HttpException $e) {
164
            if ($e->getCode() === 404) {
165
                //Message does not exist
166
                return false;
167
            }
168
            throw $e;
169
        }
170
171
        return true;
172
    }
173
174
    /**
175
     * Create a new asset in Loco.
176
     *
177
     * @param Message $message
178
     *
179
     * @return bool
180
     */
181
    public function createAsset(Message $message)
182
    {
183
        $project = $this->getProject($message);
184
185
        try {
186
            $response = $this->makeApiRequest($project['api_key'], 'POST', 'assets', [
187
                'id' => $message->getId(),
188
                'name' => $message->getId(),
189
                'type' => 'text',
190
                // Tell Loco not to translate the asset
191
                'default' => 'untranslated',
192
            ]);
193
194
            if ($message->hasParameters()) {
195
                // Send those parameter as a note to Loco
196
                $notes = '';
197
                foreach ($message->getParameters() as $key => $value) {
198
                    if (!is_array($value)) {
199
                        $notes .= 'Parameter: ' . $key . ' (i.e. : ' . $value . ")\n";
200
                    } else {
201
                        foreach ($value as $k => $v) {
202
                            $notes .= 'Parameter: ' . $k . ' (i.e. : ' . $v . ")\n";
203
                        }
204
                    }
205
                }
206
207
                $resource = sprintf('assets/%s.json', $message->getId());
208
                $this->makeApiRequest($project['api_key'], 'PATCH', $resource, ['notes' => $notes], 'json');
209
            }
210
        } catch (HttpException $e) {
211
            if ($e->getCode() === 409) {
212
                //conflict.. ignore
213
                return false;
214
            }
215
            throw $e;
216
        }
217
218
        // if this project has multiple domains. Make sure to tag it
219
        if (!empty($project['domains'])) {
220
            $this->addTagToAsset($project, $response['id'], $message->getDomain());
221
        }
222
223
        return true;
224
    }
225
226
    /**
227
     * @param Message $message
228
     *
229
     * @return array
230
     */
231
    protected function getProject(Message $message)
232
    {
233
        if (isset($this->projects[$message->getDomain()])) {
234
            return $this->projects[$message->getDomain()];
235
        }
236
237
        // Return the first project that has the correct domain and locale
238
        foreach ($this->projects as $project) {
239
            if (in_array($message->getDomain(), $project['domains'])) {
240
                if (in_array($message->getLocale(), $project['locales'])) {
241
                    return $project;
242
                }
243
            }
244
        }
245
    }
246
247
    /**
248
     * @param $project
249
     * @param $messageId
250
     * @param $domain
251
     */
252
    protected function addTagToAsset($project, $messageId, $domain)
253
    {
254
        $resource = sprintf('assets/%s/tags', $messageId);
255
        $this->makeApiRequest($project['api_key'], 'POST', $resource, ['name' => $domain]);
256
    }
257
258
    /**
259
     * Download all the translations from Loco. This will replace all the local files.
260
     * This is a quick method of getting all the latest translations and assets.
261
     */
262
    public function downloadAllTranslations()
263
    {
264
        $data = [];
265
        foreach ($this->projects as $name => $config) {
266
            if (empty($config['domains'])) {
267
                $this->getUrls($data, $config, $name, false);
268
            } else {
269
                foreach ($config['domains'] as $domain) {
270
                    $this->getUrls($data, $config, $domain, true);
271
                }
272
            }
273
        }
274
        $this->requestManager->downloadFiles($this->filesystemService, $data);
275
    }
276
277
    /**
278
     * Upload all the translations from the symfony project into Loco. This will override
279
     * every changed strings in loco
280
     */
281 View Code Duplication
    public function uploadAllTranslations()
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...
282
    {
283
        foreach ($this->projects as $name => $config) {
284
            if (empty($config['domains'])) {
285
                $this->doUploadDomains($config, $name, false);
286
            } else {
287
                foreach ($config['domains'] as $domain) {
288
                    $this->doUploadDomains($config, $domain, true);
289
                }
290
            }
291
        }
292
    }
293
294
    /**
295
     * @param array $config
296
     * @param       $domain
297
     * @param       $useDomainAsFilter
298
     */
299
    protected function doUploadDomains(array &$config, $domain, $useDomainAsFilter)
300
    {
301
        $query = $this->getExportQueryParams($config['api_key']);
302
303
        if ($useDomainAsFilter) {
304
            $query['filter'] = $domain;
305
        }
306
307
        foreach ($config['locales'] as $locale) {
308
            $extension = $this->filesystemService->getFileExtension();
309
            $file = $this->filesystemService->getTargetDir();
310
            $file .= sprintf('/%s.%s.%s', $domain, $locale, $extension);
311
312
            if (is_file($file)) {
313
                $query = [
314
                    'index' => 'id',
315
                    'tag' => $domain,
316
                    'locale'=> $locale
317
                ];
318
319
                $resource = sprintf('import/%s', $extension);
320
                $response = $this->makeApiRequest($config['api_key'], 'POST', $resource, file_get_contents($file), 'form', $query);
321
                $this->flatten($response);
322
            } else {
323
                throw new FileNotFoundException(sprintf("Can't find %s file, perhaps you should generate the translations file ?", $file));
324
            }
325
        }
326
    }
327
328
    /**
329
     * Synchronize all the translations with Loco. This will keep placeholders. This function is slower
330
     * than just to download the translations.
331
     */
332 View Code Duplication
    public function synchronizeAllTranslations()
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...
333
    {
334
        foreach ($this->projects as $name => $config) {
335
            if (empty($config['domains'])) {
336
                $this->doSynchronizeDomain($config, $name, false);
337
            } else {
338
                foreach ($config['domains'] as $domain) {
339
                    $this->doSynchronizeDomain($config, $domain, true);
340
                }
341
            }
342
        }
343
    }
344
345
    /**
346
     * @param array $config
347
     * @param       $domain
348
     * @param       $useDomainAsFilter
349
     */
350
    protected function doSynchronizeDomain(array &$config, $domain, $useDomainAsFilter)
351
    {
352
        $query = $this->getExportQueryParams($config['api_key']);
353
354
        if ($useDomainAsFilter) {
355
            $query['filter'] = $domain;
356
        }
357
358
        foreach ($config['locales'] as $locale) {
359
            $resource = sprintf('export/locale/%s.%s', $locale, 'json');
360
            $response = $this->makeApiRequest($config['api_key'], 'GET', $resource, ['query' => $query]);
361
362
            $this->flatten($response);
363
364
            $messages = array();
365
            foreach ($response as $id => $translation) {
366
                $messages[] = new Message([
367
                    'count' => 1,
368
                    'domain' => $domain,
369
                    'id' => $id,
370
                    'locale' => $locale,
371
                    'state' => 1,
372
                    'translation' => $translation,
373
                ]);
374
            }
375
376
            $this->filesystemService->updateMessageCatalog($messages);
377
        }
378
    }
379
380
    /**
381
     * Flattens an nested array of translations.
382
     *
383
     * The scheme used is:
384
     *   'key' => array('key2' => array('key3' => 'value'))
385
     * Becomes:
386
     *   'key.key2.key3' => 'value'
387
     *
388
     * This function takes an array by reference and will modify it
389
     *
390
     * @param array &$messages The array that will be flattened
391
     * @param array $subnode Current subnode being parsed, used internally for recursive calls
392
     * @param string $path Current path being parsed, used internally for recursive calls
393
     */
394
    private function flatten(array &$messages, array $subnode = null, $path = null)
395
    {
396
        if (null === $subnode) {
397
            $subnode = &$messages;
398
        }
399
        foreach ($subnode as $key => $value) {
400
            if (is_array($value)) {
401
                $nodePath = $path ? $path . '.' . $key : $key;
402
                $this->flatten($messages, $value, $nodePath);
403
                if (null === $path) {
404
                    unset($messages[$key]);
405
                }
406
            } elseif (null !== $path) {
407
                $messages[$path . '.' . $key] = $value;
408
            }
409
        }
410
    }
411
412
    /**
413
     * @param array $data
414
     * @param array $config
415
     * @param string $domain
416
     * @param bool $useDomainAsFilter
417
     */
418
    protected function getUrls(array &$data, array $config, $domain, $useDomainAsFilter)
419
    {
420
        $query = $this->getExportQueryParams($config['api_key']);
421
422
        if ($useDomainAsFilter) {
423
            $query['filter'] = $domain;
424
        }
425
426
        foreach ($config['locales'] as $locale) {
427
            // Build url
428
            $url = sprintf('%sexport/locale/%s.%s?%s', self::BASE_URL, $locale, $this->filesystemService->getFileExtension(), http_build_query($query));
429
            $fileName = sprintf('%s.%s.%s', $domain, $locale, $this->filesystemService->getFileExtension());
430
431
            $data[$url] = $fileName;
432
        }
433
    }
434
435
    /**
436
     * @param array $config
0 ignored issues
show
There is no parameter named $config. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
437
     *
438
     * @return array
439
     */
440
    private function getExportQueryParams($key)
441
    {
442
        $data = array(
443
            'index' => 'id',
444
            'status' => 'translated',
445
            'key' => $key,
446
        );
447
        switch ($this->filesystemService->getFileExtension()) {
448
            case 'php':
449
                $data['format'] = 'zend'; // 'Zend' will give us a flat array
450
                break;
451
            case 'xlf':
452
            default:
453
                $data['format'] = 'symfony';
454
        }
455
456
        return $data;
457
    }
458
}
459