Passed
Push — master ( acb001...8c805d )
by Sebastian
03:51
created

getPlaceholderByString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 10
c 1
b 0
f 1
nc 3
nop 1
dl 0
loc 19
rs 9.9332
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
    const ERROR_INVALID_DELIMITER = 47805;
58
    
59
   /**
60
    * @var Mailcode_Parser
61
    */
62
    protected $parser;
63
    
64
   /**
65
    * @var Mailcode_Collection
66
    */
67
    protected $commands;
68
    
69
   /**
70
    * @var string
71
    */
72
    protected $originalString;
73
    
74
   /**
75
    * @var Mailcode_Collection
76
    */
77
    protected $collection;
78
    
79
   /**
80
    * Counter for the placeholders, global for all placeholders.
81
    * @var integer
82
    */
83
    private static $counter = 0;
84
    
85
   /**
86
    * @var Mailcode_Parser_Safeguard_Placeholder[]
87
    */
88
    protected $placeholders;
89
    
90
   /**
91
    * @var string
92
    */
93
    protected $delimiter = '__';
94
    
95
   /**
96
    * @var string[]|NULL
97
    */
98
    protected $placeholderStrings;
99
100
    public function __construct(Mailcode_Parser $parser, string $subject)
101
    {
102
        $this->parser = $parser;
103
        $this->originalString = $subject;
104
    }
105
106
    /**
107
     * Resets the internal placeholders counter, which is
108
     * used to number the placeholder strings. Mainly used
109
     * in the tests suite.
110
     */
111
    public static function resetCounter() : void
112
    {
113
        self::$counter = 0;
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
    * Minimum characters: 2
133
    * Invalid characters: *
134
    * Rules: May not end or begin with a number
135
    * 
136
    * @param string $delimiter
137
    * @return Mailcode_Parser_Safeguard
138
    */
139
    public function setDelimiter(string $delimiter) : Mailcode_Parser_Safeguard
140
    {
141
        if(empty($delimiter))
142
        {
143
            throw new Mailcode_Exception(
144
                'Empty delimiter',
145
                'Delimiters may not be empty.',
146
                self::ERROR_EMPTY_DELIMITER
147
            );
148
        }
149
150
        if(!preg_match('/\A[^0-9*].*[^0-9*]\z/x', $delimiter))
151
        {
152
            throw new Mailcode_Exception(
153
                'The delimiter may not begin or end with a number.',
154
                '',
155
                self::ERROR_INVALID_DELIMITER
156
            );
157
        }
158
159
        if(strstr($delimiter, '*') !== false)
160
        {
161
            throw new Mailcode_Exception(
162
                'The delimiter may not contain the * character.',
163
                'The * character is used in placeholders as ID separator character.',
164
                self::ERROR_INVALID_DELIMITER
165
            );
166
        }
167
        
168
        $this->delimiter = $delimiter;
169
        
170
        return $this;
171
    }
172
    
173
    public function getDelimiter() : string
174
    {
175
        return $this->delimiter;
176
    }
177
    
178
   /**
179
    * Retrieves the safe string in which all commands have been replaced
180
    * by placeholder strings.
181
    *
182
    * @return string
183
    * @throws Mailcode_Exception 
184
    *
185
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
186
    */
187
    public function makeSafe() : string
188
    {
189
        $this->requireValidCollection();
190
        
191
        return $this->makeSafePartial();
192
    }
193
    
194
   /**
195
    * Like makeSafe(), but allows partial (invalid) commands: use this
196
    * if the subject string may contain only part of the whole set of
197
    * commands. 
198
    * 
199
    * Example: parsing a text with an opening if statement, without the 
200
    * matching end statement.
201
    * 
202
    * @return string
203
    */
204
    public function makeSafePartial() : string
205
    {
206
        $placeholders = $this->getPlaceholders();
207
        $string = $this->originalString;
208
        
209
        foreach($placeholders as $placeholder)
210
        {
211
            $string = $this->makePlaceholderSafe($string, $placeholder);
212
        }
213
214
        $string = $this->protectContents($string);
215
216
        $this->analyzeURLs($string);
217
218
        return $string;
219
    }
220
221
    /**
222
     * Goes through all placeholders in the specified string, and
223
     * checks if there are any commands whose content must be protected,
224
     * like the `code` command.
225
     *
226
     * It automatically calls the protectContent method of the command,
227
     * which replaces the command with a separate placeholder text.
228
     *
229
     * @param string $string
230
     * @return string
231
     * @see Mailcode_Interfaces_Commands_ProtectedContent
232
     * @see Mailcode_Traits_Commands_ProtectedContent
233
     */
234
    private function protectContents(string $string) : string
235
    {
236
        $placeholders = $this->getPlaceholders();
237
        $total = count($placeholders);
238
239
        for($i=0; $i < $total; $i++)
240
        {
241
            $placeholder = $placeholders[$i];
242
            $command = $placeholder->getCommand();
243
244
            if($command instanceof Mailcode_Interfaces_Commands_ProtectedContent)
245
            {
246
                $string = $command->protectContent($string, $placeholder, $placeholders[$i+1]);
247
            }
248
        }
249
250
        return $string;
251
    }
252
253
    private function makePlaceholderSafe(string $string, Mailcode_Parser_Safeguard_Placeholder $placeholder) : string
254
    {
255
        $pos = mb_strpos($string, $placeholder->getOriginalText());
256
257
        if($pos === false)
258
        {
259
            throw new Mailcode_Exception(
260
                'Placeholder original text not found',
261
                sprintf(
262
                    'Tried finding the command string [%s], but it has disappeared.',
263
                    $placeholder->getOriginalText()
264
                ),
265
                self::ERROR_PLACEHOLDER_NOT_FOUND
266
            );
267
        }
268
269
        $before = mb_substr($string, 0, $pos);
270
        $after = mb_substr($string, $pos + mb_strlen($placeholder->getOriginalText()));
271
272
        return $before.$placeholder->getReplacementText().$after;
273
    }
274
275
    /**
276
     * Detects all URLs in the subject string, and tells all placeholders
277
     * that are contained in URLs, that they are in an URL.
278
     *
279
     * @param string $string
280
     */
281
    private function analyzeURLs(string $string) : void
282
    {
283
        $urls = ConvertHelper::createURLFinder($string)
284
            ->includeEmails(false)
285
            ->getURLs();
286
287
        $placeholders = $this->getPlaceholders();
288
289
        foreach($urls as $url)
290
        {
291
            if(stristr($url, 'tel:'))
292
            {
293
                continue;
294
            }
295
296
            foreach($placeholders as $placeholder)
297
            {
298
                $command = $placeholder->getCommand();
299
300
                if(!$command->supportsURLEncoding())
301
                {
302
                    continue;
303
                }
304
305
                if(strstr($url, $placeholder->getReplacementText()) && !$command->isURLDecoded())
306
                {
307
                    $command->setURLEncoding(true);
308
                }
309
            }
310
        }
311
    }
312
    
313
   /**
314
    * Creates a formatting handler, which can be used to specify
315
    * which formattings to use for the commands in the subject string.
316
    * 
317
    * @param Mailcode_StringContainer|string $subject
318
    * @return Mailcode_Parser_Safeguard_Formatting
319
    */
320
    public function createFormatting($subject) : Mailcode_Parser_Safeguard_Formatting
321
    {
322
        if(is_string($subject))
323
        {
324
            $subject = Mailcode::create()->createString($subject);
325
        }
326
        
327
        return new Mailcode_Parser_Safeguard_Formatting($this, $subject);
328
    }
329
    
330
   /**
331
    * Retrieves all placeholders that have to be added to
332
    * the subject text.
333
    * 
334
    * @return \Mailcode\Mailcode_Parser_Safeguard_Placeholder[]
335
    */
336
    public function getPlaceholders()
337
    {
338
        if(isset($this->placeholders))
339
        {
340
            return $this->placeholders;
341
        }
342
        
343
        $this->placeholders = array();
344
        
345
        $cmds = $this->getCollection()->getCommands();
346
        
347
        foreach($cmds as $command)
348
        {
349
            self::$counter++;
350
            
351
            $this->placeholders[] = new Mailcode_Parser_Safeguard_Placeholder(
352
                self::$counter,
353
                $command,
354
                $this
355
            );
356
        }
357
358
        return $this->placeholders;
359
    }
360
361
    /**
362
     * @param string $string
363
     * @param bool $partial
364
     * @param bool $highlighted
365
     * @return string
366
     * @throws Mailcode_Exception
367
     */
368
    protected function restore(string $string, bool $partial=false, bool $highlighted=false) : string
369
    {
370
        if(!$partial)
371
        {
372
            $this->requireValidCollection();
373
        }
374
        
375
        $formatting = $this->createFormatting($string);
376
377
        if($partial)
378
        {
379
            $formatting->makePartial();
380
        }
381
        
382
        if($highlighted)
383
        {
384
            $formatting->replaceWithHTMLHighlighting();
385
        }
386
        else 
387
        {
388
            $formatting->replaceWithNormalized();
389
        }
390
        
391
        return $this->restoreContents($formatting->toString());
392
    }
393
394
    private function restoreContents(string $string) : string
395
    {
396
        $placeholders = $this->getPlaceholders();
397
398
        foreach ($placeholders as $placeholder)
399
        {
400
            $command = $placeholder->getCommand();
401
402
            if($command instanceof Mailcode_Interfaces_Commands_ProtectedContent)
403
            {
404
                $string = $command->restoreContent($string);
405
            }
406
        }
407
408
        return $string;
409
    }
410
    
411
   /**
412
    * Makes the string whole again after transforming or filtering it,
413
    * by replacing the command placeholders with the original commands.
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 makeWhole(string $string) : string
422
    {
423
        return $this->restore(
424
            $string, 
425
            false, // partial? 
426
            false // highlight?
427
        );
428
    }
429
    
430
   /**
431
    * Like `makeWhole()`, 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 makeWholePartial(string $string) : string
440
    {
441
        return $this->restore(
442
            $string,
443
            true, // partial?
444
            false // highlight?
445
        );
446
    }
447
448
   /**
449
    * Like `makeWhole()`, but replaces the commands with a syntax
450
    * highlighted version, meant for human readable texts only.
451
    * 
452
    * Note: the commands lose their functionality (They cannot be 
453
    * parsed from that string again).
454
    *
455
    * @param string $string
456
    * @return string
457
    * @throws Mailcode_Exception
458
    *
459
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
460
    */
461
    public function makeHighlighted(string $string) : string
462
    {
463
        return $this->restore(
464
            $string, 
465
            false, // partial? 
466
            true // highlighted?
467
        );
468
    }
469
    
470
   /**
471
    * Like `makeHighlighted()`, but ignores missing command placeholders.
472
    * 
473
    * @param string $string
474
    * @return string
475
    * @throws Mailcode_Exception
476
    *
477
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
478
    */
479
    public function makeHighlightedPartial(string $string) : string
480
    {
481
        return $this->restore(
482
            $string, 
483
            true, // partial? 
484
            true // highlight?
485
        );
486
    }
487
    
488
   /**
489
    * Retrieves the commands collection contained in the string.
490
    * 
491
    * @return Mailcode_Collection
492
    */
493
    public function getCollection() : Mailcode_Collection
494
    {
495
        if(isset($this->collection))
496
        {
497
            return $this->collection;
498
        }
499
        
500
        $this->collection = $this->parser->parseString($this->originalString);
501
        
502
        return $this->collection;
503
    }
504
    
505
    public function isValid() : bool
506
    {
507
        return $this->getCollection()->isValid();
508
    }
509
    
510
   /**
511
    * @throws Mailcode_Exception
512
    * 
513
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
514
    */
515
    protected function requireValidCollection() : void
516
    {
517
        if($this->getCollection()->isValid())
518
        {
519
            return;
520
        }
521
        
522
        throw new Mailcode_Exception(
523
            'Cannot safeguard invalid commands',
524
            sprintf(
525
                'The collection contains invalid commands. Safeguarding is only allowed with valid commands.'.
526
                'Source string: [%s]',
527
                $this->originalString
528
            ),
529
            self::ERROR_INVALID_COMMANDS
530
        );
531
    }
532
    
533
   /**
534
    * Retrieves a list of all placeholder IDs used in the text.
535
    * 
536
    * @return string[]
537
    */
538
    public function getPlaceholderStrings() : array
539
    {
540
        if(is_array($this->placeholderStrings))
541
        {
542
            return $this->placeholderStrings;
543
        }
544
        
545
        $placeholders = $this->getPlaceholders();
546
        
547
        $this->placeholderStrings = array();
548
        
549
        foreach($placeholders as $placeholder)
550
        {
551
            $this->placeholderStrings[] = $placeholder->getReplacementText();
552
        }
553
        
554
        return $this->placeholderStrings;
555
    }
556
    
557
    public function isPlaceholder(string $subject) : bool
558
    {
559
        $ids = $this->getPlaceholderStrings();
560
        
561
        return in_array($subject, $ids);
562
    }
563
    
564
   /**
565
    * Retrieves a placeholder instance by its ID.
566
    * 
567
    * @param int $id
568
    * @throws Mailcode_Exception If the placeholder was not found.
569
    * @return Mailcode_Parser_Safeguard_Placeholder
570
    */
571
    public function getPlaceholderByID(int $id) : Mailcode_Parser_Safeguard_Placeholder
572
    {
573
        $placeholders = $this->getPlaceholders();
574
        
575
        foreach($placeholders as $placeholder)
576
        {
577
            if($placeholder->getID() === $id)
578
            {
579
                return $placeholder;
580
            }
581
        }
582
        
583
        throw new Mailcode_Exception(
584
            'No such safeguard placeholder.',
585
            sprintf(
586
                'The placeholder ID [%s] is not present in the safeguard instance.',
587
                $id
588
            ),
589
            self::ERROR_PLACEHOLDER_NOT_FOUND
590
        );
591
    }
592
    
593
   /**
594
    * Retrieves a placeholder instance by its replacement text.
595
    * 
596
    * @param string $string
597
    * @throws Mailcode_Exception
598
    * @return Mailcode_Parser_Safeguard_Placeholder
599
    */
600
    public function getPlaceholderByString(string $string) : Mailcode_Parser_Safeguard_Placeholder
601
    {
602
        $placeholders = $this->getPlaceholders();
603
        
604
        foreach($placeholders as $placeholder)
605
        {
606
            if($placeholder->getReplacementText() === $string)
607
            {
608
                return $placeholder;
609
            }
610
        }
611
        
612
        throw new Mailcode_Exception(
613
            'No such safeguard placeholder.',
614
            sprintf(
615
                'The placeholder replacement string [%s] is not present in the safeguard instance.',
616
                $string
617
            ),
618
            self::ERROR_PLACEHOLDER_NOT_FOUND
619
        );
620
    }
621
    
622
    public function hasPlaceholders() : bool
623
    {
624
        $placeholders = $this->getPlaceholders();
625
        
626
        return !empty($placeholders);
627
    }
628
}
629