Passed
Push — master ( 629f1b...acb001 )
by Sebastian
02:33
created

Mailcode_Parser_Safeguard   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 542
Duplicated Lines 0 %

Importance

Changes 17
Bugs 0 Features 1
Metric Value
eloc 155
c 17
b 0
f 1
dl 0
loc 542
rs 7.44
wmc 52

25 Methods

Rating   Name   Duplication   Size   Complexity  
A hasPlaceholders() 0 5 1
A isValid() 0 3 1
A getPlaceholderByID() 0 19 3
A getCollection() 0 10 2
A getPlaceholderByString() 0 19 3
A makeHighlightedPartial() 0 6 1
A getPlaceholderStrings() 0 17 3
A isPlaceholder() 0 5 1
A createFormatting() 0 8 2
A getPlaceholders() 0 23 3
A protectContents() 0 17 3
A getOriginalString() 0 3 1
A getDelimiter() 0 3 1
A makePlaceholderSafe() 0 20 2
A makeSafe() 0 5 1
A makeSafePartial() 0 15 2
A setDelimiter() 0 14 2
A __construct() 0 4 1
A restoreContents() 0 15 3
B analyzeURLs() 0 27 7
A requireValidCollection() 0 15 2
A makeWholePartial() 0 6 1
A restore() 0 24 4
A makeWhole() 0 6 1
A makeHighlighted() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Mailcode_Parser_Safeguard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Mailcode_Parser_Safeguard, and based on these observations, apply Extract Interface, too.

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
    public function __construct(Mailcode_Parser $parser, string $subject)
100
    {
101
        $this->parser = $parser;
102
        $this->originalString = $subject;
103
    }
104
    
105
   /**
106
    * Retrieves the string the safeguard was created for.
107
    * 
108
    * @return string
109
    */
110
    public function getOriginalString() : string
111
    {
112
        return $this->originalString;
113
    }
114
    
115
   /**
116
    * Sets the delimiter character sequence used to prepend
117
    * and append to the placeholders.
118
    * 
119
    * The delimiter's default is "__" (two underscores).
120
    * 
121
    * @param string $delimiter
122
    * @return Mailcode_Parser_Safeguard
123
    */
124
    public function setDelimiter(string $delimiter) : Mailcode_Parser_Safeguard
125
    {
126
        if(empty($delimiter))
127
        {
128
            throw new Mailcode_Exception(
129
                'Empty delimiter',
130
                'Delimiters may not be empty.',
131
                self::ERROR_EMPTY_DELIMITER
132
            );
133
        }
134
        
135
        $this->delimiter = $delimiter;
136
        
137
        return $this;
138
    }
139
    
140
    public function getDelimiter() : string
141
    {
142
        return $this->delimiter;
143
    }
144
    
145
   /**
146
    * Retrieves the safe string in which all commands have been replaced
147
    * by placeholder strings.
148
    *
149
    * @return string
150
    * @throws Mailcode_Exception 
151
    *
152
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
153
    */
154
    public function makeSafe() : string
155
    {
156
        $this->requireValidCollection();
157
        
158
        return $this->makeSafePartial();
159
    }
160
    
161
   /**
162
    * Like makeSafe(), but allows partial (invalid) commands: use this
163
    * if the subject string may contain only part of the whole set of
164
    * commands. 
165
    * 
166
    * Example: parsing a text with an opening if statement, without the 
167
    * matching end statement.
168
    * 
169
    * @return string
170
    */
171
    public function makeSafePartial() : string
172
    {
173
        $placeholders = $this->getPlaceholders();
174
        $string = $this->originalString;
175
        
176
        foreach($placeholders as $placeholder)
177
        {
178
            $string = $this->makePlaceholderSafe($string, $placeholder);
179
        }
180
181
        $string = $this->protectContents($string);
182
183
        $this->analyzeURLs($string);
184
185
        return $string;
186
    }
187
188
    /**
189
     * Goes through all placeholders in the specified string, and
190
     * checks if there are any commands whose content must be protected,
191
     * like the `code` command.
192
     *
193
     * It automatically calls the protectContent method of the command,
194
     * which replaces the command with a separate placeholder text.
195
     *
196
     * @param string $string
197
     * @return string
198
     * @see Mailcode_Interfaces_Commands_ProtectedContent
199
     * @see Mailcode_Traits_Commands_ProtectedContent
200
     */
201
    private function protectContents(string $string) : string
202
    {
203
        $placeholders = $this->getPlaceholders();
204
        $total = count($placeholders);
205
206
        for($i=0; $i < $total; $i++)
207
        {
208
            $placeholder = $placeholders[$i];
209
            $command = $placeholder->getCommand();
210
211
            if($command instanceof Mailcode_Interfaces_Commands_ProtectedContent)
212
            {
213
                $string = $command->protectContent($string, $placeholder, $placeholders[$i+1]);
214
            }
215
        }
216
217
        return $string;
218
    }
219
220
    private function makePlaceholderSafe(string $string, Mailcode_Parser_Safeguard_Placeholder $placeholder) : string
221
    {
222
        $pos = mb_strpos($string, $placeholder->getOriginalText());
223
224
        if($pos === false)
225
        {
226
            throw new Mailcode_Exception(
227
                'Placeholder original text not found',
228
                sprintf(
229
                    'Tried finding the command string [%s], but it has disappeared.',
230
                    $placeholder->getOriginalText()
231
                ),
232
                self::ERROR_PLACEHOLDER_NOT_FOUND
233
            );
234
        }
235
236
        $before = mb_substr($string, 0, $pos);
237
        $after = mb_substr($string, $pos + mb_strlen($placeholder->getOriginalText()));
238
239
        return $before.$placeholder->getReplacementText().$after;
240
    }
241
242
    /**
243
     * Detects all URLs in the subject string, and tells all placeholders
244
     * that are contained in URLs, that they are in an URL.
245
     *
246
     * @param string $string
247
     */
248
    private function analyzeURLs(string $string) : void
249
    {
250
        $urls = ConvertHelper::createURLFinder($string)
251
            ->includeEmails(false)
252
            ->getURLs();
253
254
        $placeholders = $this->getPlaceholders();
255
256
        foreach($urls as $url)
257
        {
258
            if(stristr($url, 'tel:'))
259
            {
260
                continue;
261
            }
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()) && !$command->isURLDecoded())
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
    /**
329
     * @param string $string
330
     * @param bool $partial
331
     * @param bool $highlighted
332
     * @return string
333
     * @throws Mailcode_Exception
334
     */
335
    protected function restore(string $string, bool $partial=false, bool $highlighted=false) : string
336
    {
337
        if(!$partial)
338
        {
339
            $this->requireValidCollection();
340
        }
341
        
342
        $formatting = $this->createFormatting($string);
343
344
        if($partial)
345
        {
346
            $formatting->makePartial();
347
        }
348
        
349
        if($highlighted)
350
        {
351
            $formatting->replaceWithHTMLHighlighting();
352
        }
353
        else 
354
        {
355
            $formatting->replaceWithNormalized();
356
        }
357
        
358
        return $this->restoreContents($formatting->toString());
359
    }
360
361
    private function restoreContents(string $string) : string
362
    {
363
        $placeholders = $this->getPlaceholders();
364
365
        foreach ($placeholders as $placeholder)
366
        {
367
            $command = $placeholder->getCommand();
368
369
            if($command instanceof Mailcode_Interfaces_Commands_ProtectedContent)
370
            {
371
                $string = $command->restoreContent($string);
372
            }
373
        }
374
375
        return $string;
376
    }
377
    
378
   /**
379
    * Makes the string whole again after transforming or filtering it,
380
    * by replacing the command placeholders with the original commands.
381
    *
382
    * @param string $string
383
    * @return string
384
    * @throws Mailcode_Exception
385
    *
386
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
387
    */
388
    public function makeWhole(string $string) : string
389
    {
390
        return $this->restore(
391
            $string, 
392
            false, // partial? 
393
            false // highlight?
394
        );
395
    }
396
    
397
   /**
398
    * Like `makeWhole()`, but ignores missing command placeholders.
399
    *
400
    * @param string $string
401
    * @return string
402
    * @throws Mailcode_Exception
403
    *
404
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
405
    */
406
    public function makeWholePartial(string $string) : string
407
    {
408
        return $this->restore(
409
            $string,
410
            true, // partial?
411
            false // highlight?
412
        );
413
    }
414
415
   /**
416
    * Like `makeWhole()`, but replaces the commands with a syntax
417
    * highlighted version, meant for human readable texts only.
418
    * 
419
    * Note: the commands lose their functionality (They cannot be 
420
    * parsed from that string again).
421
    *
422
    * @param string $string
423
    * @return string
424
    * @throws Mailcode_Exception
425
    *
426
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
427
    */
428
    public function makeHighlighted(string $string) : string
429
    {
430
        return $this->restore(
431
            $string, 
432
            false, // partial? 
433
            true // highlighted?
434
        );
435
    }
436
    
437
   /**
438
    * Like `makeHighlighted()`, but ignores missing command placeholders.
439
    * 
440
    * @param string $string
441
    * @return string
442
    * @throws Mailcode_Exception
443
    *
444
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
445
    */
446
    public function makeHighlightedPartial(string $string) : string
447
    {
448
        return $this->restore(
449
            $string, 
450
            true, // partial? 
451
            true // highlight?
452
        );
453
    }
454
    
455
   /**
456
    * Retrieves the commands collection contained in the string.
457
    * 
458
    * @return Mailcode_Collection
459
    */
460
    public function getCollection() : Mailcode_Collection
461
    {
462
        if(isset($this->collection))
463
        {
464
            return $this->collection;
465
        }
466
        
467
        $this->collection = $this->parser->parseString($this->originalString);
468
        
469
        return $this->collection;
470
    }
471
    
472
    public function isValid() : bool
473
    {
474
        return $this->getCollection()->isValid();
475
    }
476
    
477
   /**
478
    * @throws Mailcode_Exception
479
    * 
480
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
481
    */
482
    protected function requireValidCollection() : void
483
    {
484
        if($this->getCollection()->isValid())
485
        {
486
            return;
487
        }
488
        
489
        throw new Mailcode_Exception(
490
            'Cannot safeguard invalid commands',
491
            sprintf(
492
                'The collection contains invalid commands. Safeguarding is only allowed with valid commands.'.
493
                'Source string: [%s]',
494
                $this->originalString
495
            ),
496
            self::ERROR_INVALID_COMMANDS
497
        );
498
    }
499
    
500
   /**
501
    * Retrieves a list of all placeholder IDs used in the text.
502
    * 
503
    * @return string[]
504
    */
505
    public function getPlaceholderStrings() : array
506
    {
507
        if(is_array($this->placeholderStrings))
508
        {
509
            return $this->placeholderStrings;
510
        }
511
        
512
        $placeholders = $this->getPlaceholders();
513
        
514
        $this->placeholderStrings = array();
515
        
516
        foreach($placeholders as $placeholder)
517
        {
518
            $this->placeholderStrings[] = $placeholder->getReplacementText();
519
        }
520
        
521
        return $this->placeholderStrings;
522
    }
523
    
524
    public function isPlaceholder(string $subject) : bool
525
    {
526
        $ids = $this->getPlaceholderStrings();
527
        
528
        return in_array($subject, $ids);
529
    }
530
    
531
   /**
532
    * Retrieves a placeholder instance by its ID.
533
    * 
534
    * @param int $id
535
    * @throws Mailcode_Exception If the placeholder was not found.
536
    * @return Mailcode_Parser_Safeguard_Placeholder
537
    */
538
    public function getPlaceholderByID(int $id) : Mailcode_Parser_Safeguard_Placeholder
539
    {
540
        $placeholders = $this->getPlaceholders();
541
        
542
        foreach($placeholders as $placeholder)
543
        {
544
            if($placeholder->getID() === $id)
545
            {
546
                return $placeholder;
547
            }
548
        }
549
        
550
        throw new Mailcode_Exception(
551
            'No such safeguard placeholder.',
552
            sprintf(
553
                'The placeholder ID [%s] is not present in the safeguard instance.',
554
                $id
555
            ),
556
            self::ERROR_PLACEHOLDER_NOT_FOUND
557
        );
558
    }
559
    
560
   /**
561
    * Retrieves a placeholder instance by its replacement text.
562
    * 
563
    * @param string $string
564
    * @throws Mailcode_Exception
565
    * @return Mailcode_Parser_Safeguard_Placeholder
566
    */
567
    public function getPlaceholderByString(string $string) : Mailcode_Parser_Safeguard_Placeholder
568
    {
569
        $placeholders = $this->getPlaceholders();
570
        
571
        foreach($placeholders as $placeholder)
572
        {
573
            if($placeholder->getReplacementText() === $string)
574
            {
575
                return $placeholder;
576
            }
577
        }
578
        
579
        throw new Mailcode_Exception(
580
            'No such safeguard placeholder.',
581
            sprintf(
582
                'The placeholder replacement string [%s] is not present in the safeguard instance.',
583
                $string
584
            ),
585
            self::ERROR_PLACEHOLDER_NOT_FOUND
586
        );
587
    }
588
    
589
    public function hasPlaceholders() : bool
590
    {
591
        $placeholders = $this->getPlaceholders();
592
        
593
        return !empty($placeholders);
594
    }
595
}
596