Passed
Push — master ( 272fc4...138b71 )
by Sebastian
04:04
created

Mailcode_Parser_Safeguard::makeSafePartial()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 13
rs 10
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
    const ERROR_INVALID_COMMANDS = 47801;
53
    
54
    const ERROR_COMMAND_PLACEHOLDER_MISSING = 47802;
55
    
56
    const ERROR_EMPTY_DELIMITER = 47803;
57
    
58
    const ERROR_PLACEHOLDER_NOT_FOUND = 47804;
59
    
60
    const ERROR_UNKNOWN_FORMATTER = 47805;
61
    
62
    const ERROR_NOT_A_SINGLE_LINES_FORMATTER = 47806;
63
    
64
   /**
65
    * @var Mailcode_Parser
66
    */
67
    protected $parser;
68
    
69
   /**
70
    * @var Mailcode_Collection
71
    */
72
    protected $commands;
73
    
74
   /**
75
    * @var string
76
    */
77
    protected $originalString;
78
    
79
   /**
80
    * @var Mailcode_Collection
81
    */
82
    protected $collection;
83
    
84
   /**
85
    * Counter for the placeholders, global for all placeholders.
86
    * @var integer
87
    */
88
    private static $counter = 0;
89
    
90
   /**
91
    * @var Mailcode_Parser_Safeguard_Placeholder[]
92
    */
93
    protected $placeholders;
94
    
95
   /**
96
    * @var string
97
    */
98
    protected $delimiter = '__';
99
    
100
   /**
101
    * @var string[]|NULL
102
    */
103
    protected $placeholderStrings;
104
    
105
   /**
106
    * @var Mailcode_Parser_Safeguard_Formatter
107
    */
108
    protected $formatter;
109
    
110
    public function __construct(Mailcode_Parser $parser, string $subject)
111
    {
112
        $this->parser = $parser;
113
        $this->originalString = $subject;
114
    }
115
    
116
   /**
117
    * Retrieves the string the safeguard was created for.
118
    * 
119
    * @return string
120
    */
121
    public function getOriginalString() : string
122
    {
123
        return $this->originalString;
124
    }
125
    
126
   /**
127
    * Sets the delimiter character sequence used to prepend
128
    * and append to the placeholders.
129
    * 
130
    * The delimiter's default is "__" (two underscores).
131
    * 
132
    * @param string $delimiter
133
    * @return Mailcode_Parser_Safeguard
134
    */
135
    public function setDelimiter(string $delimiter) : Mailcode_Parser_Safeguard
136
    {
137
        if(empty($delimiter))
138
        {
139
            throw new Mailcode_Exception(
140
                'Empty delimiter',
141
                'Delimiters may not be empty.',
142
                self::ERROR_EMPTY_DELIMITER
143
            );
144
        }
145
        
146
        $this->delimiter = $delimiter;
147
        
148
        return $this;
149
    }
150
    
151
    public function getDelimiter() : string
152
    {
153
        return $this->delimiter;
154
    }
155
    
156
   /**
157
    * Retrieves the safe string in which all commands have been replaced
158
    * by placeholder strings.
159
    *
160
    * @return string
161
    * @throws Mailcode_Exception 
162
    *
163
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
164
    */
165
    public function makeSafe() : string
166
    {
167
        $this->requireValidCollection();
168
        
169
        return $this->makeSafePartial();
170
    }
171
    
172
   /**
173
    * Like makeSafe(), but allows partial (invalid) commands: use this
174
    * if the subject string may contain only part of the whole set of
175
    * commands. 
176
    * 
177
    * Example: parsing a text with an opening if statement, without the 
178
    * matching end statement.
179
    * 
180
    * @return string
181
    */
182
    public function makeSafePartial() : string
183
    {
184
        $replaces = $this->getReplaces();
185
        
186
        $safe = str_replace(array_values($replaces), array_keys($replaces), $this->originalString);
187
188
        // If a formatter has been selected, let it modify the string.
189
        if(isset($this->formatter))
190
        {
191
            $safe = $this->formatter->format($safe);
192
        }
193
        
194
        return $safe;
195
    }
196
    
197
    public function selectFormatter(string $formatterID) : Mailcode_Parser_Safeguard_Formatter
198
    {
199
        $class = 'Mailcode\Mailcode_Parser_Safeguard_Formatter_'.$formatterID;
200
        
201
        if(class_exists($class))
202
        {
203
            $this->formatter = new $class($this);
204
            
205
            return $this->formatter;
206
        }
207
        
208
        throw new Mailcode_Exception(
209
            'Unknown safeguard formatter.',
210
            sprintf(
211
                'The formatter [%s] does not exist, could not find class [%s].',
212
                $formatterID,
213
                $class
214
            ),
215
            self::ERROR_UNKNOWN_FORMATTER
216
        );
217
    }
218
    
219
   /**
220
    * Enables the formatter that ensures that all commands that
221
    * @return Mailcode_Parser_Safeguard_Formatter_SingleLines
222
    */
223
    public function selectSingleLinesFormatter() : Mailcode_Parser_Safeguard_Formatter_SingleLines
224
    {
225
        $formatter = $this->selectFormatter('SingleLines');
226
        
227
        if($formatter instanceof Mailcode_Parser_Safeguard_Formatter_SingleLines)
0 ignored issues
show
introduced by
$formatter is always a sub-type of Mailcode\Mailcode_Parser...d_Formatter_SingleLines.
Loading history...
228
        {
229
            return $formatter;
230
        }
231
        
232
        throw new Mailcode_Exception(
233
            'Not the expected formatter',
234
            sprintf(
235
                'The formatter is not an instance of [%s].',
236
                Mailcode_Parser_Safeguard_Formatter_SingleLines::class
237
            ),
238
            self::ERROR_NOT_A_SINGLE_LINES_FORMATTER
239
        );
240
    }
241
    
242
   /**
243
    * Retrieves an associative array with pairs of
244
    * [placeholder string => replacement text]. 
245
    * 
246
    * @param bool $highlighted
247
    * @return string[]string
248
    */
249
    protected function getReplaces(bool $highlighted=false) : array
250
    {
251
        $placeholders = $this->getPlaceholders();
252
        
253
        $replaces = array();
254
        
255
        foreach($placeholders as $placeholder)
256
        {
257
            $replace = '';
258
            
259
            if($highlighted)
260
            {
261
                $replace = $placeholder->getHighlightedText();
262
            }
263
            else 
264
            {
265
                $replace = $placeholder->getOriginalText();
266
            }
267
            
268
            $replaces[$placeholder->getReplacementText()] = $replace;
269
        }
270
        
271
        return $replaces;
272
    }
273
    
274
    
275
   /**
276
    * Retrieves all placeholders that have to be added to
277
    * the subject text.
278
    * 
279
    * @return \Mailcode\Mailcode_Parser_Safeguard_Placeholder[]
280
    */
281
    public function getPlaceholders()
282
    {
283
        if(isset($this->placeholders))
284
        {
285
            return $this->placeholders;
286
        }
287
        
288
        $this->placeholders = array();
289
        
290
        $cmds = $this->getCollection()->getGroupedByHash();
291
        
292
        foreach($cmds as $command)
293
        {
294
            self::$counter++;
295
            
296
            $this->placeholders[] = new Mailcode_Parser_Safeguard_Placeholder(
297
                self::$counter,
298
                $command,
299
                $this
300
            );
301
        }
302
303
        return $this->placeholders;
304
    }
305
    
306
    protected function restore(string $string, bool $partial=false, bool $highlighted=false) : string
307
    {
308
        $this->requireValidCollection();
309
        
310
        $replaces = $this->getReplaces($highlighted);
311
        
312
        $placeholderStrings = array_keys($replaces);
313
        
314
        foreach($placeholderStrings as $search)
315
        {
316
            if(!$partial && !strstr($string, $search))
317
            {
318
                throw new Mailcode_Exception(
319
                    'Command placeholder not found',
320
                    sprintf(
321
                        'A placeholder for a command could not be found in the string to restore: [%s].',
322
                        $search
323
                    ),
324
                    self::ERROR_COMMAND_PLACEHOLDER_MISSING
325
                );
326
            }
327
        }
328
        
329
        return str_replace($placeholderStrings, array_values($replaces), $string);
330
    }
331
    
332
   /**
333
    * Makes the string whole again after transforming or filtering it,
334
    * by replacing the command placeholders with the original commands.
335
    *
336
    * @param string $string
337
    * @return string
338
    * @throws Mailcode_Exception
339
    *
340
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
341
    * @see Mailcode_Parser_Safeguard::ERROR_COMMAND_PLACEHOLDER_MISSING
342
    */
343
    public function makeWhole(string $string) : string
344
    {
345
        return $this->restore(
346
            $string, 
347
            false, // partial? 
348
            false // highlight?
349
        );
350
    }
351
    
352
   /**
353
    * Like `makeWhole()`, but ignores missing command placeholders.
354
    *
355
    * @param string $string
356
    * @return string
357
    * @throws Mailcode_Exception
358
    *
359
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
360
    */
361
    public function makeWholePartial(string $string) : string
362
    {
363
        return $this->restore(
364
            $string,
365
            true, // partial?
366
            false // highlight?
367
        );
368
    }
369
370
   /**
371
    * Like `makeWhole()`, but replaces the commands with a syntax
372
    * highlighted version, meant for human readable texts only.
373
    * 
374
    * Note: the commands lose their functionality (They cannot be 
375
    * parsed from that string again).
376
    *
377
    * @param string $string
378
    * @return string
379
    * @throws Mailcode_Exception
380
    *
381
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
382
    * @see Mailcode_Parser_Safeguard::ERROR_COMMAND_PLACEHOLDER_MISSING
383
    */
384
    public function makeHighlighted(string $string) : string
385
    {
386
        return $this->restore(
387
            $string, 
388
            false, // partial? 
389
            true // highlighted?
390
        );
391
    }
392
    
393
   /**
394
    * Like `makeHighlighted()`, but ignores missing command placeholders.
395
    * 
396
    * @param string $string
397
    * @return string
398
    * @throws Mailcode_Exception
399
    *
400
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
401
    */
402
    public function makeHighlightedPartial(string $string) : string
403
    {
404
        return $this->restore(
405
            $string, 
406
            true, // partial? 
407
            true // highlight?
408
        );
409
    }
410
    
411
   /**
412
    * Retrieves the commands collection contained in the string.
413
    * 
414
    * @return Mailcode_Collection
415
    */
416
    public function getCollection() : Mailcode_Collection
417
    {
418
        if(isset($this->collection))
419
        {
420
            return $this->collection;
421
        }
422
        
423
        $this->collection = $this->parser->parseString($this->originalString);
424
        
425
        return $this->collection;
426
    }
427
    
428
    public function isValid() : bool
429
    {
430
        return $this->getCollection()->isValid();
431
    }
432
    
433
   /**
434
    * @throws Mailcode_Exception
435
    * 
436
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
437
    */
438
    protected function requireValidCollection() : void
439
    {
440
        if($this->getCollection()->isValid())
441
        {
442
            return;
443
        }
444
        
445
        throw new Mailcode_Exception(
446
            'Cannot safeguard invalid commands',
447
            sprintf(
448
                'The collection contains invalid commands. Safeguarding is only allowed with valid commands.'.
449
                'Source string: [%s]',
450
                $this->originalString
451
            ),
452
            self::ERROR_INVALID_COMMANDS
453
        );
454
    }
455
    
456
   /**
457
    * Retrieves a list of all placeholder IDs used in the text.
458
    * 
459
    * @return string[]
460
    */
461
    public function getPlaceholderStrings() : array
462
    {
463
        if(is_array($this->placeholderStrings))
464
        {
465
            return $this->placeholderStrings;
466
        }
467
        
468
        $placeholders = $this->getPlaceholders();
469
        
470
        $this->placeholderStrings = array();
471
        
472
        foreach($placeholders as $placeholder)
473
        {
474
            $this->placeholderStrings[] = $placeholder->getReplacementText();
475
        }
476
        
477
        return $this->placeholderStrings;
478
    }
479
    
480
    public function isPlaceholder(string $subject) : bool
481
    {
482
        $ids = $this->getPlaceholderStrings();
483
        
484
        return in_array($subject, $ids);
485
    }
486
    
487
   /**
488
    * Retrieves a placeholder instance by its ID.
489
    * 
490
    * @param int $id
491
    * @throws Mailcode_Exception If the placeholder was not found.
492
    * @return Mailcode_Parser_Safeguard_Placeholder
493
    */
494
    public function getPlaceholderByID(int $id) : Mailcode_Parser_Safeguard_Placeholder
495
    {
496
        $placeholders = $this->getPlaceholders();
497
        
498
        foreach($placeholders as $placeholder)
499
        {
500
            if($placeholder->getID() === $id)
501
            {
502
                return $placeholder;
503
            }
504
        }
505
        
506
        throw new Mailcode_Exception(
507
            'No such safeguard placeholder.',
508
            sprintf(
509
                'The placeholder ID [%s] is not present in the safeguard instance.',
510
                $id
511
            ),
512
            self::ERROR_PLACEHOLDER_NOT_FOUND
513
        );
514
    }
515
    
516
   /**
517
    * Retrieves a placeholder instance by its replacement text.
518
    * 
519
    * @param string $string
520
    * @throws Mailcode_Exception
521
    * @return Mailcode_Parser_Safeguard_Placeholder
522
    */
523
    public function getPlaceholderByString(string $string) : Mailcode_Parser_Safeguard_Placeholder
524
    {
525
        $placeholders = $this->getPlaceholders();
526
        
527
        foreach($placeholders as $placeholder)
528
        {
529
            if($placeholder->getReplacementText() === $string)
530
            {
531
                return $placeholder;
532
            }
533
        }
534
        
535
        throw new Mailcode_Exception(
536
            'No such safeguard placeholder.',
537
            sprintf(
538
                'The placeholder replacement string [%s] is not present in the safeguard instance.',
539
                $string
540
            ),
541
            self::ERROR_PLACEHOLDER_NOT_FOUND
542
        );
543
    }
544
    
545
    public function hasPlaceholders() : bool
546
    {
547
        $placeholders = $this->getPlaceholders();
548
        
549
        return !empty($placeholders);
550
    }
551
}
552