Passed
Push — master ( 8c805d...5e8702 )
by Sebastian
02:45
created

Mailcode_Parser_Safeguard::analyzeURLs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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