Passed
Push — master ( c28222...9253d7 )
by Sebastian
02:41
created

Mailcode_Parser_Safeguard::protectContents()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 17
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
use AppUtils\ConvertHelper;
15
16
/**
17
 * Command safeguarder: used to replace the mailcode commands
18
 * in a string with placeholders, to allow safe text transformation
19
 * and filtering operations on strings, without risking to break 
20
 * any of the contained commands (if any).
21
 * 
22
 * Usage:
23
 * 
24
 * <pre>
25
 * $safeguard = Mailcode::create()->createSafeguard($sourceString);
26
 * 
27
 * // replace all commands with placeholders
28
 * $workString = $safeguard->makeSafe();
29
 * 
30
 * // dome something with the work string - filtering, parsing...
31
 * 
32
 * // restore all command placeholders
33
 * $resultString = $safeguard->makeWhole($workString);
34
 * </pre>
35
 * 
36
 * Note that by default, the placeholders are delimited with
37
 * two underscores, e.g. <code>__PCH0001__</code>. If the text
38
 * transformations include replacing or modifying underscores,
39
 * you should use a different delimiter:
40
 * 
41
 * <pre>
42
 * $safeguard = Mailcode::create()->createSafeguard($sourceString);
43
 * 
44
 * // change the delimiter to %%. Can be any arbitrary string.
45
 * $safeguard->setDelimiter('%%');
46
 * </pre>
47
 *
48
 * @package Mailcode
49
 * @subpackage Parser
50
 * @author Sebastian Mordziol <[email protected]>
51
 */
52
class Mailcode_Parser_Safeguard
53
{
54
    const ERROR_INVALID_COMMANDS = 47801;
55
    const ERROR_EMPTY_DELIMITER = 47803;
56
    const ERROR_PLACEHOLDER_NOT_FOUND = 47804;
57
    
58
   /**
59
    * @var Mailcode_Parser
60
    */
61
    protected $parser;
62
    
63
   /**
64
    * @var Mailcode_Collection
65
    */
66
    protected $commands;
67
    
68
   /**
69
    * @var string
70
    */
71
    protected $originalString;
72
    
73
   /**
74
    * @var Mailcode_Collection
75
    */
76
    protected $collection;
77
    
78
   /**
79
    * Counter for the placeholders, global for all placeholders.
80
    * @var integer
81
    */
82
    private static $counter = 0;
83
    
84
   /**
85
    * @var Mailcode_Parser_Safeguard_Placeholder[]
86
    */
87
    protected $placeholders;
88
    
89
   /**
90
    * @var string
91
    */
92
    protected $delimiter = '__';
93
    
94
   /**
95
    * @var string[]|NULL
96
    */
97
    protected $placeholderStrings;
98
    
99
   /**
100
    * @var Mailcode_Parser_Safeguard_Formatting|NULL
101
    */
102
    private $formatting = null;
0 ignored issues
show
introduced by
The private property $formatting is not used, and could be removed.
Loading history...
103
    
104
    public function __construct(Mailcode_Parser $parser, string $subject)
105
    {
106
        $this->parser = $parser;
107
        $this->originalString = $subject;
108
    }
109
    
110
   /**
111
    * Retrieves the string the safeguard was created for.
112
    * 
113
    * @return string
114
    */
115
    public function getOriginalString() : string
116
    {
117
        return $this->originalString;
118
    }
119
    
120
   /**
121
    * Sets the delimiter character sequence used to prepend
122
    * and append to the placeholders.
123
    * 
124
    * The delimiter's default is "__" (two underscores).
125
    * 
126
    * @param string $delimiter
127
    * @return Mailcode_Parser_Safeguard
128
    */
129
    public function setDelimiter(string $delimiter) : Mailcode_Parser_Safeguard
130
    {
131
        if(empty($delimiter))
132
        {
133
            throw new Mailcode_Exception(
134
                'Empty delimiter',
135
                'Delimiters may not be empty.',
136
                self::ERROR_EMPTY_DELIMITER
137
            );
138
        }
139
        
140
        $this->delimiter = $delimiter;
141
        
142
        return $this;
143
    }
144
    
145
    public function getDelimiter() : string
146
    {
147
        return $this->delimiter;
148
    }
149
    
150
   /**
151
    * Retrieves the safe string in which all commands have been replaced
152
    * by placeholder strings.
153
    *
154
    * @return string
155
    * @throws Mailcode_Exception 
156
    *
157
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
158
    */
159
    public function makeSafe() : string
160
    {
161
        $this->requireValidCollection();
162
        
163
        return $this->makeSafePartial();
164
    }
165
    
166
   /**
167
    * Like makeSafe(), but allows partial (invalid) commands: use this
168
    * if the subject string may contain only part of the whole set of
169
    * commands. 
170
    * 
171
    * Example: parsing a text with an opening if statement, without the 
172
    * matching end statement.
173
    * 
174
    * @return string
175
    */
176
    public function makeSafePartial() : string
177
    {
178
        $placeholders = $this->getPlaceholders();
179
        $string = $this->originalString;
180
        
181
        foreach($placeholders as $placeholder)
182
        {
183
            $string = $this->makePlaceholderSafe($string, $placeholder);
184
        }
185
186
        $string = $this->protectContents($string);
187
188
        $this->analyzeURLs($string);
189
190
        return $string;
191
    }
192
193
    /**
194
     * Goes through all placeholders in the specified string, and
195
     * checks if there are any commands whose content must be protected,
196
     * like the `code` command.
197
     *
198
     * It automatically calls the protectContent method of the command,
199
     * which replaces the command with a separate placeholder text.
200
     *
201
     * @param string $string
202
     * @return string
203
     * @see Mailcode_Interfaces_Commands_ProtectedContent
204
     * @see Mailcode_Traits_Commands_ProtectedContent
205
     */
206
    private function protectContents(string $string) : string
207
    {
208
        $placeholders = $this->getPlaceholders();
209
        $total = count($placeholders);
210
211
        for($i=0; $i < $total; $i++)
212
        {
213
            $placeholder = $placeholders[$i];
214
            $command = $placeholder->getCommand();
215
216
            if($command instanceof Mailcode_Interfaces_Commands_ProtectedContent)
217
            {
218
                $string = $command->protectContent($string, $placeholder, $placeholders[$i+1]);
219
            }
220
        }
221
222
        return $string;
223
    }
224
225
    private function makePlaceholderSafe(string $string, Mailcode_Parser_Safeguard_Placeholder $placeholder) : string
226
    {
227
        $pos = mb_strpos($string, $placeholder->getOriginalText());
228
229
        if($pos === false)
230
        {
231
            throw new Mailcode_Exception(
232
                'Placeholder original text not found',
233
                sprintf(
234
                    'Tried finding the command string [%s], but it has disappeared.',
235
                    $placeholder->getOriginalText()
236
                ),
237
                self::ERROR_PLACEHOLDER_NOT_FOUND
238
            );
239
        }
240
241
        $before = mb_substr($string, 0, $pos);
242
        $after = mb_substr($string, $pos + mb_strlen($placeholder->getOriginalText()));
243
244
        return $before.$placeholder->getReplacementText().$after;
245
    }
246
247
    /**
248
     * Detects all URLs in the subject string, and tells all placeholders
249
     * that are contained in URLs, that they are in an URL.
250
     *
251
     * @param string $string
252
     */
253
    private function analyzeURLs(string $string) : void
254
    {
255
        $urls = ConvertHelper::createURLFinder($string)
256
            ->includeEmails(false)
257
            ->getURLs();
258
259
        $placeholders = $this->getPlaceholders();
260
261
        foreach($urls as $url)
262
        {
263
            foreach($placeholders as $placeholder)
264
            {
265
                $command = $placeholder->getCommand();
266
267
                if(!$command->supportsURLEncoding())
268
                {
269
                    continue;
270
                }
271
272
                if(strstr($url, $placeholder->getReplacementText()))
273
                {
274
                    $command->setURLEncoding(true);
275
                }
276
            }
277
        }
278
    }
279
    
280
   /**
281
    * Creates a formatting handler, which can be used to specify
282
    * which formattings to use for the commands in the subject string.
283
    * 
284
    * @param Mailcode_StringContainer|string $subject
285
    * @return Mailcode_Parser_Safeguard_Formatting
286
    */
287
    public function createFormatting($subject) : Mailcode_Parser_Safeguard_Formatting
288
    {
289
        if(is_string($subject))
290
        {
291
            $subject = Mailcode::create()->createString($subject);
292
        }
293
        
294
        return new Mailcode_Parser_Safeguard_Formatting($this, $subject);
295
    }
296
    
297
   /**
298
    * Retrieves all placeholders that have to be added to
299
    * the subject text.
300
    * 
301
    * @return \Mailcode\Mailcode_Parser_Safeguard_Placeholder[]
302
    */
303
    public function getPlaceholders()
304
    {
305
        if(isset($this->placeholders))
306
        {
307
            return $this->placeholders;
308
        }
309
        
310
        $this->placeholders = array();
311
        
312
        $cmds = $this->getCollection()->getCommands();
313
        
314
        foreach($cmds as $command)
315
        {
316
            self::$counter++;
317
            
318
            $this->placeholders[] = new Mailcode_Parser_Safeguard_Placeholder(
319
                self::$counter,
320
                $command,
321
                $this
322
            );
323
        }
324
325
        return $this->placeholders;
326
    }
327
    
328
    protected function restore(string $string, bool $partial=false, bool $highlighted=false) : string
329
    {
330
        if(!$partial)
331
        {
332
            $this->requireValidCollection();
333
        }
334
        
335
        $formatting = $this->createFormatting($string);
336
337
        if($partial)
338
        {
339
            $formatting->makePartial();
340
        }
341
        
342
        if($highlighted)
343
        {
344
            $formatting->replaceWithHTMLHighlighting();
345
        }
346
        else 
347
        {
348
            $formatting->replaceWithNormalized();
349
        }
350
        
351
        return $this->restoreContents($formatting->toString());
352
    }
353
354
    private function restoreContents(string $string) : string
355
    {
356
        $placeholders = $this->getPlaceholders();
357
358
        foreach ($placeholders as $placeholder)
359
        {
360
            $command = $placeholder->getCommand();
361
362
            if($command instanceof Mailcode_Interfaces_Commands_ProtectedContent)
363
            {
364
                $string = $command->restoreContent($string);
365
            }
366
        }
367
368
        return $string;
369
    }
370
    
371
   /**
372
    * Makes the string whole again after transforming or filtering it,
373
    * by replacing the command placeholders with the original commands.
374
    *
375
    * @param string $string
376
    * @return string
377
    * @throws Mailcode_Exception
378
    *
379
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
380
    */
381
    public function makeWhole(string $string) : string
382
    {
383
        return $this->restore(
384
            $string, 
385
            false, // partial? 
386
            false // highlight?
387
        );
388
    }
389
    
390
   /**
391
    * Like `makeWhole()`, but ignores missing command placeholders.
392
    *
393
    * @param string $string
394
    * @return string
395
    * @throws Mailcode_Exception
396
    *
397
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
398
    */
399
    public function makeWholePartial(string $string) : string
400
    {
401
        return $this->restore(
402
            $string,
403
            true, // partial?
404
            false // highlight?
405
        );
406
    }
407
408
   /**
409
    * Like `makeWhole()`, but replaces the commands with a syntax
410
    * highlighted version, meant for human readable texts only.
411
    * 
412
    * Note: the commands lose their functionality (They cannot be 
413
    * parsed from that string again).
414
    *
415
    * @param string $string
416
    * @return string
417
    * @throws Mailcode_Exception
418
    *
419
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
420
    */
421
    public function makeHighlighted(string $string) : string
422
    {
423
        return $this->restore(
424
            $string, 
425
            false, // partial? 
426
            true // highlighted?
427
        );
428
    }
429
    
430
   /**
431
    * Like `makeHighlighted()`, but ignores missing command placeholders.
432
    * 
433
    * @param string $string
434
    * @return string
435
    * @throws Mailcode_Exception
436
    *
437
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
438
    */
439
    public function makeHighlightedPartial(string $string) : string
440
    {
441
        return $this->restore(
442
            $string, 
443
            true, // partial? 
444
            true // highlight?
445
        );
446
    }
447
    
448
   /**
449
    * Retrieves the commands collection contained in the string.
450
    * 
451
    * @return Mailcode_Collection
452
    */
453
    public function getCollection() : Mailcode_Collection
454
    {
455
        if(isset($this->collection))
456
        {
457
            return $this->collection;
458
        }
459
        
460
        $this->collection = $this->parser->parseString($this->originalString);
461
        
462
        return $this->collection;
463
    }
464
    
465
    public function isValid() : bool
466
    {
467
        return $this->getCollection()->isValid();
468
    }
469
    
470
   /**
471
    * @throws Mailcode_Exception
472
    * 
473
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
474
    */
475
    protected function requireValidCollection() : void
476
    {
477
        if($this->getCollection()->isValid())
478
        {
479
            return;
480
        }
481
        
482
        throw new Mailcode_Exception(
483
            'Cannot safeguard invalid commands',
484
            sprintf(
485
                'The collection contains invalid commands. Safeguarding is only allowed with valid commands.'.
486
                'Source string: [%s]',
487
                $this->originalString
488
            ),
489
            self::ERROR_INVALID_COMMANDS
490
        );
491
    }
492
    
493
   /**
494
    * Retrieves a list of all placeholder IDs used in the text.
495
    * 
496
    * @return string[]
497
    */
498
    public function getPlaceholderStrings() : array
499
    {
500
        if(is_array($this->placeholderStrings))
501
        {
502
            return $this->placeholderStrings;
503
        }
504
        
505
        $placeholders = $this->getPlaceholders();
506
        
507
        $this->placeholderStrings = array();
508
        
509
        foreach($placeholders as $placeholder)
510
        {
511
            $this->placeholderStrings[] = $placeholder->getReplacementText();
512
        }
513
        
514
        return $this->placeholderStrings;
515
    }
516
    
517
    public function isPlaceholder(string $subject) : bool
518
    {
519
        $ids = $this->getPlaceholderStrings();
520
        
521
        return in_array($subject, $ids);
522
    }
523
    
524
   /**
525
    * Retrieves a placeholder instance by its ID.
526
    * 
527
    * @param int $id
528
    * @throws Mailcode_Exception If the placeholder was not found.
529
    * @return Mailcode_Parser_Safeguard_Placeholder
530
    */
531
    public function getPlaceholderByID(int $id) : Mailcode_Parser_Safeguard_Placeholder
532
    {
533
        $placeholders = $this->getPlaceholders();
534
        
535
        foreach($placeholders as $placeholder)
536
        {
537
            if($placeholder->getID() === $id)
538
            {
539
                return $placeholder;
540
            }
541
        }
542
        
543
        throw new Mailcode_Exception(
544
            'No such safeguard placeholder.',
545
            sprintf(
546
                'The placeholder ID [%s] is not present in the safeguard instance.',
547
                $id
548
            ),
549
            self::ERROR_PLACEHOLDER_NOT_FOUND
550
        );
551
    }
552
    
553
   /**
554
    * Retrieves a placeholder instance by its replacement text.
555
    * 
556
    * @param string $string
557
    * @throws Mailcode_Exception
558
    * @return Mailcode_Parser_Safeguard_Placeholder
559
    */
560
    public function getPlaceholderByString(string $string) : Mailcode_Parser_Safeguard_Placeholder
561
    {
562
        $placeholders = $this->getPlaceholders();
563
        
564
        foreach($placeholders as $placeholder)
565
        {
566
            if($placeholder->getReplacementText() === $string)
567
            {
568
                return $placeholder;
569
            }
570
        }
571
        
572
        throw new Mailcode_Exception(
573
            'No such safeguard placeholder.',
574
            sprintf(
575
                'The placeholder replacement string [%s] is not present in the safeguard instance.',
576
                $string
577
            ),
578
            self::ERROR_PLACEHOLDER_NOT_FOUND
579
        );
580
    }
581
    
582
    public function hasPlaceholders() : bool
583
    {
584
        $placeholders = $this->getPlaceholders();
585
        
586
        return !empty($placeholders);
587
    }
588
}
589