Test Failed
Push — master ( 36c5b6...af11e6 )
by Sebastian
04:51
created

getPlaceholdersCollection()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 24
rs 9.8666
1
<?php
2
/**
3
 * File containing the {@see Mailcode_Parser_Safeguard} class.
4
 *
5
 * @package Mailcode
6
 * @subpackage Parser
7
 * @see Mailcode_Parser_Safeguard
8
 */
9
10
declare(strict_types=1);
11
12
namespace Mailcode;
13
14
/**
15
 * Command safeguarder: used to replace the mailcode commands
16
 * in a string with placeholders, to allow safe text transformation
17
 * and filtering operations on strings, without risking to break 
18
 * any of the contained commands (if any).
19
 * 
20
 * Usage:
21
 * 
22
 * <pre>
23
 * $safeguard = Mailcode::create()->createSafeguard($sourceString);
24
 * 
25
 * // replace all commands with placeholders
26
 * $workString = $safeguard->makeSafe();
27
 * 
28
 * // dome something with the work string - filtering, parsing...
29
 * 
30
 * // restore all command placeholders
31
 * $resultString = $safeguard->makeWhole($workString);
32
 * </pre>
33
 * 
34
 * Note that by default, the placeholders are delimited with
35
 * two underscores, e.g. <code>__PCH0001__</code>. If the text
36
 * transformations include replacing or modifying underscores,
37
 * you should use a different delimiter:
38
 * 
39
 * <pre>
40
 * $safeguard = Mailcode::create()->createSafeguard($sourceString);
41
 * 
42
 * // change the delimiter to %%. Can be any arbitrary string.
43
 * $safeguard->setDelimiter('%%');
44
 * </pre>
45
 *
46
 * @package Mailcode
47
 * @subpackage Parser
48
 * @author Sebastian Mordziol <[email protected]>
49
 */
50
class Mailcode_Parser_Safeguard
51
{
52
    public const ERROR_INVALID_COMMANDS = 47801;
53
    public const ERROR_PLACEHOLDER_NOT_FOUND = 47804;
54
    public const ERROR_NO_PLACEHOLDER_FOR_COMMAND = 47805;
55
    public const ERROR_NO_FIRST_PLACEHOLDER = 47806;
56
57
   /**
58
    * @var Mailcode_Parser
59
    */
60
    protected Mailcode_Parser $parser;
61
    
62
   /**
63
    * @var Mailcode_Collection
64
    */
65
    protected Mailcode_Collection $commands;
66
    
67
   /**
68
    * @var string
69
    */
70
    protected string $originalString;
71
    
72
   /**
73
    * @var Mailcode_Collection
74
    */
75
    protected Mailcode_Collection $collection;
76
    
77
   /**
78
    * Counter for the placeholders, global for all placeholders.
79
    * @var integer
80
    */
81
    private static int $counter = 0;
82
    
83
   /**
84
    * @var Mailcode_Parser_Safeguard_PlaceholderCollection|NULL
85
    */
86
    protected ?Mailcode_Parser_Safeguard_PlaceholderCollection $placeholders = null;
87
    
88
   /**
89
    * @var string
90
    */
91
    protected string $delimiter = '999';
92
    
93
    public function __construct(Mailcode_Parser $parser, string $subject)
94
    {
95
        $this->parser = $parser;
96
        $this->originalString = $subject;
97
    }
98
99
    /**
100
     * Resets the internal placeholders counter, which is
101
     * used to number the placeholder strings. Mainly used
102
     * in the tests suite.
103
     */
104
    public static function resetCounter() : void
105
    {
106
        self::$counter = 0;
107
    }
108
    
109
   /**
110
    * Retrieves the string the safeguard was created for.
111
    * 
112
    * @return string
113
    */
114
    public function getOriginalString() : string
115
    {
116
        return $this->originalString;
117
    }
118
    
119
   /**
120
    * Sets the delimiter character sequence used to prepend
121
    * and append to the placeholders.
122
    * 
123
    * The delimiter's default is "999".
124
    *
125
    * Minimum characters: 2
126
    * Invalid characters: Any characters that get URL encoded
127
    *
128
    * @param string $delimiter
129
    * @return Mailcode_Parser_Safeguard
130
    */
131
    public function setDelimiter(string $delimiter) : Mailcode_Parser_Safeguard
132
    {
133
        $validator = new Mailcode_Parser_Safeguard_DelimiterValidator($delimiter);
134
        $validator->throwExceptionIfInvalid();
135
136
        $this->delimiter = $delimiter;
137
        
138
        return $this;
139
    }
140
    
141
    public function getDelimiter() : string
142
    {
143
        return $this->delimiter;
144
    }
145
    
146
   /**
147
    * Retrieves the safe string in which all commands have been replaced
148
    * by placeholder strings.
149
    *
150
    * @return string
151
    * @throws Mailcode_Exception 
152
    *
153
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
154
    */
155
    public function makeSafe() : string
156
    {
157
        $this->requireValidCollection();
158
        
159
        return $this->makeSafePartial();
160
    }
161
    
162
   /**
163
    * Like makeSafe(), but allows partial (invalid) commands: use this
164
    * if the subject string may contain only part of the whole set of
165
    * commands. 
166
    * 
167
    * Example: parsing a text with an opening if statement, without the 
168
    * matching end statement.
169
    * 
170
    * @return string
171
    */
172
    public function makeSafePartial() : string
173
    {
174
        $placeholders = $this->getPlaceholdersCollection()->getAll();
175
        $string = $this->originalString;
176
        
177
        foreach($placeholders as $placeholder)
178
        {
179
            $string = $this->makePlaceholderSafe($string, $placeholder);
180
        }
181
182
        $this->analyzeURLs($string);
183
184
        return $string;
185
    }
186
187
    private function makePlaceholderSafe(string $string, Mailcode_Parser_Safeguard_Placeholder $placeholder) : string
188
    {
189
        $pos = mb_strpos($string, $placeholder->getOriginalText());
190
191
        if($pos === false)
192
        {
193
            throw new Mailcode_Exception(
194
                'Placeholder original text not found',
195
                sprintf(
196
                    'Tried finding the command string [%s], but it has disappeared.',
197
                    $placeholder->getOriginalText()
198
                ),
199
                self::ERROR_PLACEHOLDER_NOT_FOUND
200
            );
201
        }
202
203
        $before = mb_substr($string, 0, $pos);
204
        $after = mb_substr($string, $pos + mb_strlen($placeholder->getOriginalText()));
205
206
        return $before.$placeholder->getReplacementText().$after;
207
    }
208
209
    /**
210
     * Detects all URLs in the subject string, and tells all placeholders
211
     * that are contained in URLs, that they are in an URL.
212
     *
213
     * @param string $string
214
     */
215
    private function analyzeURLs(string $string) : void
216
    {
217
        $analyzer = new Mailcode_Parser_Safeguard_URLAnalyzer($string, $this);
218
        $analyzer->analyze();
219
    }
220
    
221
   /**
222
    * Creates a formatting handler, which can be used to specify
223
    * which formatting to use for the commands in the subject string.
224
    * 
225
    * @param Mailcode_StringContainer|string $subject
226
    * @return Mailcode_Parser_Safeguard_Formatting
227
    */
228
    public function createFormatting($subject) : Mailcode_Parser_Safeguard_Formatting
229
    {
230
        if(is_string($subject))
231
        {
232
            $subject = Mailcode::create()->createString($subject);
233
        }
234
        
235
        return new Mailcode_Parser_Safeguard_Formatting($this, $subject);
236
    }
237
238
    /**
239
     * Retrieves all placeholders that have to be added to
240
     * the subject text.
241
     *
242
     * @return Mailcode_Parser_Safeguard_Placeholder[]
243
     *
244
     * @deprecated Use the placeholder collection instead {@see Mailcode_Parser_Safeguard::getPlaceholdersCollection()}.
245
     */
246
    public function getPlaceholders() : array
247
    {
248
        return $this->getPlaceholdersCollection()->getAll();
249
    }
250
251
    /**
252
    * Retrieves all placeholders that have to be added to
253
    * the subject text.
254
    * 
255
    * @return Mailcode_Parser_Safeguard_PlaceholderCollection
256
    */
257
    public function getPlaceholdersCollection() : Mailcode_Parser_Safeguard_PlaceholderCollection
258
    {
259
        if(isset($this->placeholders))
260
        {
261
            return $this->placeholders;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->placeholders could return the type null which is incompatible with the type-hinted return Mailcode\Mailcode_Parser...d_PlaceholderCollection. Consider adding an additional type-check to rule them out.
Loading history...
262
        }
263
        
264
        $placeholders = array();
265
        $commands = $this->getCollection()->getCommands();
266
        
267
        foreach($commands as $command)
268
        {
269
            self::$counter++;
270
            
271
            $placeholders[] = new Mailcode_Parser_Safeguard_Placeholder(
272
                self::$counter,
273
                $command,
274
                $this
275
            );
276
        }
277
278
        $this->placeholders = new Mailcode_Parser_Safeguard_PlaceholderCollection($placeholders);
279
280
        return $this->placeholders;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->placeholders returns the type null which is incompatible with the type-hinted return Mailcode\Mailcode_Parser...d_PlaceholderCollection.
Loading history...
281
    }
282
283
    /**
284
     * @param string $string
285
     * @param bool $partial
286
     * @param bool $highlighted
287
     * @return string
288
     * @throws Mailcode_Exception
289
     */
290
    protected function restore(string $string, bool $partial=false, bool $highlighted=false) : string
291
    {
292
        if(!$partial)
293
        {
294
            $this->requireValidCollection();
295
        }
296
        
297
        $formatting = $this->createFormatting($string);
298
299
        if($partial)
300
        {
301
            $formatting->makePartial();
302
        }
303
        
304
        if($highlighted)
305
        {
306
            $formatting->replaceWithHTMLHighlighting();
307
        }
308
        else 
309
        {
310
            $formatting->replaceWithNormalized();
311
        }
312
        
313
        return $formatting->toString();
314
    }
315
316
   /**
317
    * Makes the string whole again after transforming or filtering it,
318
    * by replacing the command placeholders with the original commands.
319
    *
320
    * @param string $string
321
    * @return string
322
    * @throws Mailcode_Exception
323
    *
324
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
325
    */
326
    public function makeWhole(string $string) : string
327
    {
328
        return $this->restore(
329
            $string, 
330
            false, // partial? 
331
            false // highlight?
332
        );
333
    }
334
    
335
   /**
336
    * Like `makeWhole()`, but ignores missing command placeholders.
337
    *
338
    * @param string $string
339
    * @return string
340
    * @throws Mailcode_Exception
341
    *
342
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
343
    */
344
    public function makeWholePartial(string $string) : string
345
    {
346
        return $this->restore(
347
            $string,
348
            true, // partial?
349
            false // highlight?
350
        );
351
    }
352
353
   /**
354
    * Like `makeWhole()`, but replaces the commands with a syntax
355
    * highlighted version, meant for human readable texts only.
356
    * 
357
    * Note: the commands lose their functionality (They cannot be 
358
    * parsed from that string again).
359
    *
360
    * @param string $string
361
    * @return string
362
    * @throws Mailcode_Exception
363
    *
364
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
365
    */
366
    public function makeHighlighted(string $string) : string
367
    {
368
        return $this->restore(
369
            $string, 
370
            false, // partial? 
371
            true // highlighted?
372
        );
373
    }
374
    
375
   /**
376
    * Like `makeHighlighted()`, but ignores missing command placeholders.
377
    * 
378
    * @param string $string
379
    * @return string
380
    * @throws Mailcode_Exception
381
    *
382
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
383
    */
384
    public function makeHighlightedPartial(string $string) : string
385
    {
386
        return $this->restore(
387
            $string, 
388
            true, // partial? 
389
            true // highlight?
390
        );
391
    }
392
    
393
   /**
394
    * Retrieves the command collection contained in the string.
395
    * 
396
    * @return Mailcode_Collection
397
    */
398
    public function getCollection() : Mailcode_Collection
399
    {
400
        if(isset($this->collection))
401
        {
402
            return $this->collection;
403
        }
404
405
        $result = $this->parser->parseString($this->originalString);
406
407
        $this->collection = $result->getCollection();
408
409
        // Overwrite the string, since the pre-parser may
410
        // have modified it.
411
        $this->originalString = $result->getPreParser()->getString();
412
        
413
        return $this->collection;
414
    }
415
    
416
    public function isValid() : bool
417
    {
418
        return $this->getCollection()->isValid();
419
    }
420
    
421
   /**
422
    * @throws Mailcode_Exception
423
    * 
424
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
425
    */
426
    protected function requireValidCollection() : void
427
    {
428
        if($this->getCollection()->isValid())
429
        {
430
            return;
431
        }
432
        
433
        $exception = new Mailcode_Exception(
434
            'Cannot safeguard invalid commands',
435
            sprintf(
436
                'The collection contains invalid commands. Safeguarding is only allowed with valid commands.'.
437
                'Source string: [%s]',
438
                $this->originalString
439
            ),
440
            self::ERROR_INVALID_COMMANDS
441
        );
442
443
        $exception->setCollection($this->getCollection());
444
445
        throw $exception;
446
    }
447
    
448
   /**
449
    * Retrieves a list of all placeholder IDs used in the text.
450
    * 
451
    * @return string[]
452
    *
453
    * @deprecated Use the placeholder collection instead {@see Mailcode_Parser_Safeguard::getPlaceholdersCollection()}.
454
    */
455
    public function getPlaceholderStrings() : array
456
    {
457
        return $this->getPlaceholdersCollection()->getStrings();
458
    }
459
    
460
    public function isPlaceholder(string $subject) : bool
461
    {
462
        return $this->getPlaceholdersCollection()->isStringPlaceholder($subject);
463
    }
464
    
465
   /**
466
    * Retrieves a placeholder instance by its ID.
467
    * 
468
    * @param int $id
469
    * @throws Mailcode_Exception If the placeholder was not found.
470
    * @return Mailcode_Parser_Safeguard_Placeholder
471
    *
472
    * @deprecated Use the placeholder collection instead {@see Mailcode_Parser_Safeguard::getPlaceholdersCollection()}.
473
    */
474
    public function getPlaceholderByID(int $id) : Mailcode_Parser_Safeguard_Placeholder
475
    {
476
        return $this->getPlaceholdersCollection()->getByID($id);
477
    }
478
    
479
   /**
480
    * Retrieves a placeholder instance by its replacement text.
481
    * 
482
    * @param string $string
483
    * @throws Mailcode_Exception
484
    * @return Mailcode_Parser_Safeguard_Placeholder
485
    *
486
    * @deprecated Use the placeholder collection instead {@see Mailcode_Parser_Safeguard::getPlaceholdersCollection()}.
487
    */
488
    public function getPlaceholderByString(string $string) : Mailcode_Parser_Safeguard_Placeholder
489
    {
490
        return $this->getPlaceholdersCollection()->getByString($string);
491
    }
492
    
493
    public function hasPlaceholders() : bool
494
    {
495
        return $this->getPlaceholdersCollection()->hasPlaceholders();
496
    }
497
}
498