Passed
Push — master ( 75ee28...758f58 )
by Sebastian
04:00
created

Mailcode_Parser_Safeguard   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 481
Duplicated Lines 0 %

Importance

Changes 15
Bugs 0 Features 1
Metric Value
eloc 137
c 15
b 0
f 1
dl 0
loc 481
rs 8.8798
wmc 44

23 Methods

Rating   Name   Duplication   Size   Complexity  
A getDelimiter() 0 3 1
A getOriginalString() 0 3 1
A makePlaceholderSafe() 0 20 2
A analyzeURLs() 0 19 5
A hasPlaceholders() 0 5 1
A isValid() 0 3 1
A getPlaceholderByID() 0 19 3
A getCollection() 0 10 2
A requireValidCollection() 0 15 2
A getPlaceholderByString() 0 19 3
A createFormatting() 0 8 2
A getPlaceholders() 0 23 3
A makeWholePartial() 0 6 1
A makeHighlightedPartial() 0 6 1
A restore() 0 24 4
A getPlaceholderStrings() 0 17 3
A makeSafe() 0 5 1
A makeWhole() 0 6 1
A makeHighlighted() 0 6 1
A isPlaceholder() 0 5 1
A makeSafePartial() 0 13 2
A setDelimiter() 0 14 2
A __construct() 0 4 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
   /**
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
        $this->analyzeURLs($string);
187
188
        return $string;
189
    }
190
191
    private function makePlaceholderSafe(string $string, Mailcode_Parser_Safeguard_Placeholder $placeholder) : string
192
    {
193
        $pos = mb_strpos($string, $placeholder->getOriginalText());
194
195
        if($pos === false)
196
        {
197
            throw new Mailcode_Exception(
198
                'Placeholder original text not found',
199
                sprintf(
200
                    'Tried finding the command string [%s], but it has disappeared.',
201
                    $placeholder->getOriginalText()
202
                ),
203
                self::ERROR_PLACEHOLDER_NOT_FOUND
204
            );
205
        }
206
207
        $before = mb_substr($string, 0, $pos);
208
        $after = mb_substr($string, $pos + mb_strlen($placeholder->getOriginalText()));
209
210
        return $before.$placeholder->getReplacementText().$after;
211
    }
212
213
    /**
214
     * Detects all URLs in the subject string, and tells all placeholders
215
     * that are contained in URLs, that they are in an URL.
216
     *
217
     * @param string $string
218
     */
219
    private function analyzeURLs(string $string) : void
220
    {
221
        $urls = ConvertHelper::createURLFinder($string)->getURLs();
222
        $placeholders = $this->getPlaceholders();
223
224
        foreach($urls as $url)
225
        {
226
            foreach($placeholders as $placeholder)
227
            {
228
                $command = $placeholder->getCommand();
229
230
                if(!$command->supportsURLEncoding())
231
                {
232
                    continue;
233
                }
234
235
                if(strstr($url, $placeholder->getReplacementText()))
236
                {
237
                    $command->setURLEncoding(true);
238
                }
239
            }
240
        }
241
    }
242
    
243
   /**
244
    * Creates a formatting handler, which can be used to specify
245
    * which formattings to use for the commands in the subject string.
246
    * 
247
    * @param Mailcode_StringContainer|string $subject
248
    * @return Mailcode_Parser_Safeguard_Formatting
249
    */
250
    public function createFormatting($subject) : Mailcode_Parser_Safeguard_Formatting
251
    {
252
        if(is_string($subject))
253
        {
254
            $subject = Mailcode::create()->createString($subject);
255
        }
256
        
257
        return new Mailcode_Parser_Safeguard_Formatting($this, $subject);
258
    }
259
    
260
   /**
261
    * Retrieves all placeholders that have to be added to
262
    * the subject text.
263
    * 
264
    * @return \Mailcode\Mailcode_Parser_Safeguard_Placeholder[]
265
    */
266
    public function getPlaceholders()
267
    {
268
        if(isset($this->placeholders))
269
        {
270
            return $this->placeholders;
271
        }
272
        
273
        $this->placeholders = array();
274
        
275
        $cmds = $this->getCollection()->getCommands();
276
        
277
        foreach($cmds as $command)
278
        {
279
            self::$counter++;
280
            
281
            $this->placeholders[] = new Mailcode_Parser_Safeguard_Placeholder(
282
                self::$counter,
283
                $command,
284
                $this
285
            );
286
        }
287
288
        return $this->placeholders;
289
    }
290
    
291
    protected function restore(string $string, bool $partial=false, bool $highlighted=false) : string
292
    {
293
        if(!$partial)
294
        {
295
            $this->requireValidCollection();
296
        }
297
        
298
        $formatting = $this->createFormatting($string);
299
300
        if($partial)
301
        {
302
            $formatting->makePartial();
303
        }
304
        
305
        if($highlighted)
306
        {
307
            $formatting->replaceWithHTMLHighlighting();
308
        }
309
        else 
310
        {
311
            $formatting->replaceWithNormalized();
312
        }
313
        
314
        return $formatting->toString();
315
    }
316
    
317
   /**
318
    * Makes the string whole again after transforming or filtering it,
319
    * by replacing the command placeholders with the original commands.
320
    *
321
    * @param string $string
322
    * @return string
323
    * @throws Mailcode_Exception
324
    *
325
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
326
    */
327
    public function makeWhole(string $string) : string
328
    {
329
        return $this->restore(
330
            $string, 
331
            false, // partial? 
332
            false // highlight?
333
        );
334
    }
335
    
336
   /**
337
    * Like `makeWhole()`, but ignores missing command placeholders.
338
    *
339
    * @param string $string
340
    * @return string
341
    * @throws Mailcode_Exception
342
    *
343
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
344
    */
345
    public function makeWholePartial(string $string) : string
346
    {
347
        return $this->restore(
348
            $string,
349
            true, // partial?
350
            false // highlight?
351
        );
352
    }
353
354
   /**
355
    * Like `makeWhole()`, but replaces the commands with a syntax
356
    * highlighted version, meant for human readable texts only.
357
    * 
358
    * Note: the commands lose their functionality (They cannot be 
359
    * parsed from that string again).
360
    *
361
    * @param string $string
362
    * @return string
363
    * @throws Mailcode_Exception
364
    *
365
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
366
    */
367
    public function makeHighlighted(string $string) : string
368
    {
369
        return $this->restore(
370
            $string, 
371
            false, // partial? 
372
            true // highlighted?
373
        );
374
    }
375
    
376
   /**
377
    * Like `makeHighlighted()`, but ignores missing command placeholders.
378
    * 
379
    * @param string $string
380
    * @return string
381
    * @throws Mailcode_Exception
382
    *
383
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
384
    */
385
    public function makeHighlightedPartial(string $string) : string
386
    {
387
        return $this->restore(
388
            $string, 
389
            true, // partial? 
390
            true // highlight?
391
        );
392
    }
393
    
394
   /**
395
    * Retrieves the commands collection contained in the string.
396
    * 
397
    * @return Mailcode_Collection
398
    */
399
    public function getCollection() : Mailcode_Collection
400
    {
401
        if(isset($this->collection))
402
        {
403
            return $this->collection;
404
        }
405
        
406
        $this->collection = $this->parser->parseString($this->originalString);
407
        
408
        return $this->collection;
409
    }
410
    
411
    public function isValid() : bool
412
    {
413
        return $this->getCollection()->isValid();
414
    }
415
    
416
   /**
417
    * @throws Mailcode_Exception
418
    * 
419
    * @see Mailcode_Parser_Safeguard::ERROR_INVALID_COMMANDS
420
    */
421
    protected function requireValidCollection() : void
422
    {
423
        if($this->getCollection()->isValid())
424
        {
425
            return;
426
        }
427
        
428
        throw new Mailcode_Exception(
429
            'Cannot safeguard invalid commands',
430
            sprintf(
431
                'The collection contains invalid commands. Safeguarding is only allowed with valid commands.'.
432
                'Source string: [%s]',
433
                $this->originalString
434
            ),
435
            self::ERROR_INVALID_COMMANDS
436
        );
437
    }
438
    
439
   /**
440
    * Retrieves a list of all placeholder IDs used in the text.
441
    * 
442
    * @return string[]
443
    */
444
    public function getPlaceholderStrings() : array
445
    {
446
        if(is_array($this->placeholderStrings))
447
        {
448
            return $this->placeholderStrings;
449
        }
450
        
451
        $placeholders = $this->getPlaceholders();
452
        
453
        $this->placeholderStrings = array();
454
        
455
        foreach($placeholders as $placeholder)
456
        {
457
            $this->placeholderStrings[] = $placeholder->getReplacementText();
458
        }
459
        
460
        return $this->placeholderStrings;
461
    }
462
    
463
    public function isPlaceholder(string $subject) : bool
464
    {
465
        $ids = $this->getPlaceholderStrings();
466
        
467
        return in_array($subject, $ids);
468
    }
469
    
470
   /**
471
    * Retrieves a placeholder instance by its ID.
472
    * 
473
    * @param int $id
474
    * @throws Mailcode_Exception If the placeholder was not found.
475
    * @return Mailcode_Parser_Safeguard_Placeholder
476
    */
477
    public function getPlaceholderByID(int $id) : Mailcode_Parser_Safeguard_Placeholder
478
    {
479
        $placeholders = $this->getPlaceholders();
480
        
481
        foreach($placeholders as $placeholder)
482
        {
483
            if($placeholder->getID() === $id)
484
            {
485
                return $placeholder;
486
            }
487
        }
488
        
489
        throw new Mailcode_Exception(
490
            'No such safeguard placeholder.',
491
            sprintf(
492
                'The placeholder ID [%s] is not present in the safeguard instance.',
493
                $id
494
            ),
495
            self::ERROR_PLACEHOLDER_NOT_FOUND
496
        );
497
    }
498
    
499
   /**
500
    * Retrieves a placeholder instance by its replacement text.
501
    * 
502
    * @param string $string
503
    * @throws Mailcode_Exception
504
    * @return Mailcode_Parser_Safeguard_Placeholder
505
    */
506
    public function getPlaceholderByString(string $string) : Mailcode_Parser_Safeguard_Placeholder
507
    {
508
        $placeholders = $this->getPlaceholders();
509
        
510
        foreach($placeholders as $placeholder)
511
        {
512
            if($placeholder->getReplacementText() === $string)
513
            {
514
                return $placeholder;
515
            }
516
        }
517
        
518
        throw new Mailcode_Exception(
519
            'No such safeguard placeholder.',
520
            sprintf(
521
                'The placeholder replacement string [%s] is not present in the safeguard instance.',
522
                $string
523
            ),
524
            self::ERROR_PLACEHOLDER_NOT_FOUND
525
        );
526
    }
527
    
528
    public function hasPlaceholders() : bool
529
    {
530
        $placeholders = $this->getPlaceholders();
531
        
532
        return !empty($placeholders);
533
    }
534
}
535