Passed
Push — master ( 7dcb2c...27a2d0 )
by Daniel
11:00 queued 01:58
created

Convert::slashes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 3
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core;
4
5
use SilverStripe\ORM\DB;
6
use SilverStripe\View\Parsers\URLSegmentFilter;
7
use InvalidArgumentException;
8
use SimpleXMLElement;
9
use Exception;
10
11
/**
12
 * Library of conversion functions, implemented as static methods.
13
 *
14
 * The methods are all of the form (format)2(format), where the format is one of
15
 *
16
 *  raw: A UTF8 string
17
 *  attr: A UTF8 string suitable for inclusion in an HTML attribute
18
 *  js: A UTF8 string suitable for inclusion in a double-quoted javascript string.
19
 *
20
 *  array: A PHP associative array
21
 *  json: JavaScript object notation
22
 *
23
 *  html: HTML source suitable for use in a page or email
24
 *  text: Plain-text content, suitable for display to a user as-is, or insertion in a plaintext email.
25
 *
26
 * Objects of type {@link ViewableData} can have an "escaping type",
27
 * which determines if they are automatically escaped before output by {@link SSViewer}.
28
 */
29
class Convert
30
{
31
32
    /**
33
     * Convert a value to be suitable for an XML attribute.
34
     *
35
     * @param array|string $val String to escape, or array of strings
36
     * @return array|string
37
     */
38
    public static function raw2att($val)
39
    {
40
        return self::raw2xml($val);
41
    }
42
43
    /**
44
     * Convert a value to be suitable for an HTML attribute.
45
     *
46
     * @param string|array $val String to escape, or array of strings
47
     * @return array|string
48
     */
49
    public static function raw2htmlatt($val)
50
    {
51
        return self::raw2att($val);
52
    }
53
54
    /**
55
     * Convert a value to be suitable for an HTML ID attribute. Replaces non
56
     * supported characters with a space.
57
     *
58
     * @see http://www.w3.org/TR/REC-html40/types.html#type-cdata
59
     *
60
     * @param array|string $val String to escape, or array of strings
61
     *
62
     * @return array|string
63
     */
64
    public static function raw2htmlname($val)
65
    {
66
        if (is_array($val)) {
67
            foreach ($val as $k => $v) {
68
                $val[$k] = self::raw2htmlname($v);
69
            }
70
71
            return $val;
72
        } else {
73
            return self::raw2att($val);
74
        }
75
    }
76
77
    /**
78
     * Convert a value to be suitable for an HTML ID attribute. Replaces non
79
     * supported characters with an underscore.
80
     *
81
     * @see http://www.w3.org/TR/REC-html40/types.html#type-cdata
82
     *
83
     * @param array|string $val String to escape, or array of strings
84
     *
85
     * @return array|string
86
     */
87
    public static function raw2htmlid($val)
88
    {
89
        if (is_array($val)) {
90
            foreach ($val as $k => $v) {
91
                $val[$k] = self::raw2htmlid($v);
92
            }
93
94
            return $val;
95
        } else {
96
            return trim(
97
                preg_replace(
98
                    '/_+/',
99
                    '_',
100
                    preg_replace('/[^a-zA-Z0-9\-_:.]+/', '_', $val)
101
                ),
102
                '_'
103
            );
104
        }
105
    }
106
107
    /**
108
     * Ensure that text is properly escaped for XML.
109
     *
110
     * @see http://www.w3.org/TR/REC-xml/#dt-escape
111
     * @param array|string $val String to escape, or array of strings
112
     * @return array|string
113
     */
114
    public static function raw2xml($val)
115
    {
116
        if (is_array($val)) {
117
            foreach ($val as $k => $v) {
118
                $val[$k] = self::raw2xml($v);
119
            }
120
            return $val;
121
        } else {
122
            return htmlspecialchars($val, ENT_QUOTES, 'UTF-8');
123
        }
124
    }
125
126
    /**
127
     * Ensure that text is properly escaped for Javascript.
128
     *
129
     * @param array|string $val String to escape, or array of strings
130
     * @return array|string
131
     */
132
    public static function raw2js($val)
133
    {
134
        if (is_array($val)) {
135
            foreach ($val as $k => $v) {
136
                $val[$k] = self::raw2js($v);
137
            }
138
            return $val;
139
        } else {
140
            return str_replace(
141
                // Intercepts some characters such as <, >, and & which can interfere
142
                array("\\", '"', "\n", "\r", "'", "<", ">", "&"),
143
                array("\\\\", '\"', '\n', '\r', "\\'", "\\x3c", "\\x3e", "\\x26"),
144
                $val
145
            );
146
        }
147
    }
148
149
    /**
150
     * Encode a value as a JSON encoded string. You can optionally pass a bitmask of
151
     * JSON constants as options through to the encode function.
152
     *
153
     * @param  mixed $val     Value to be encoded
154
     * @param  int   $options Optional bitmask of JSON constants
155
     * @return string           JSON encoded string
156
     */
157
    public static function raw2json($val, $options = 0)
158
    {
159
        return json_encode($val, $options);
160
    }
161
162
    /**
163
     * Encode an array as a JSON encoded string.
164
     * This is an alias to {@link raw2json()}
165
     *
166
     * @param  array  $val     Array to convert
167
     * @param  int    $options Optional bitmask of JSON constants
168
     * @return string          JSON encoded string
169
     */
170
    public static function array2json($val, $options = 0)
171
    {
172
        return self::raw2json($val, $options);
173
    }
174
175
    /**
176
     * Safely encodes a value (or list of values) using the current database's
177
     * safe string encoding method
178
     *
179
     * @param mixed|array $val Input value, or list of values as an array
180
     * @param boolean $quoted Flag indicating whether the value should be safely
181
     * quoted, instead of only being escaped. By default this function will
182
     * only escape the string (false).
183
     * @return string|array Safely encoded value in the same format as the input
184
     */
185
    public static function raw2sql($val, $quoted = false)
186
    {
187
        if (is_array($val)) {
188
            foreach ($val as $k => $v) {
189
                $val[$k] = self::raw2sql($v, $quoted);
190
            }
191
            return $val;
192
        } else {
193
            if ($quoted) {
194
                return DB::get_conn()->quoteString($val);
195
            } else {
196
                return DB::get_conn()->escapeString($val);
197
            }
198
        }
199
    }
200
201
    /**
202
     * Safely encodes a SQL symbolic identifier (or list of identifiers), such as a database,
203
     * table, or column name. Supports encoding of multi identfiers separated by
204
     * a delimiter (e.g. ".")
205
     *
206
     * @param string|array $identifier The identifier to escape. E.g. 'SiteTree.Title' or list of identifiers
207
     * to be joined via the separator.
208
     * @param string $separator The string that delimits subsequent identifiers
209
     * @return string The escaped identifier. E.g. '"SiteTree"."Title"'
210
     */
211
    public static function symbol2sql($identifier, $separator = '.')
212
    {
213
        return DB::get_conn()->escapeIdentifier($identifier, $separator);
214
    }
215
216
    /**
217
     * Convert XML to raw text.
218
     * @uses html2raw()
219
     * @todo Currently &#xxx; entries are stripped; they should be converted
220
     * @param mixed $val
221
     * @return array|string
222
     */
223
    public static function xml2raw($val)
224
    {
225
        if (is_array($val)) {
226
            foreach ($val as $k => $v) {
227
                $val[$k] = self::xml2raw($v);
228
            }
229
            return $val;
230
        } else {
231
            // More complex text needs to use html2raw instead
232
            if (strpos($val, '<') !== false) {
233
                return self::html2raw($val);
234
            } else {
235
                return html_entity_decode($val, ENT_QUOTES, 'UTF-8');
236
            }
237
        }
238
    }
239
240
    /**
241
     * Convert a JSON encoded string into an object.
242
     *
243
     * @param string $val
244
     * @return object|boolean
245
     */
246
    public static function json2obj($val)
247
    {
248
        return json_decode($val);
249
    }
250
251
    /**
252
     * Convert a JSON string into an array.
253
     *
254
     * @uses json2obj
255
     * @param string $val JSON string to convert
256
     * @return array|boolean
257
     */
258
    public static function json2array($val)
259
    {
260
        return json_decode($val, true);
261
    }
262
263
    /**
264
     * Converts an XML string to a PHP array
265
     * See http://phpsecurity.readthedocs.org/en/latest/Injection-Attacks.html#xml-external-entity-injection
266
     *
267
     * @uses recursiveXMLToArray()
268
     * @param string $val
269
     * @param boolean $disableDoctypes Disables the use of DOCTYPE, and will trigger an error if encountered.
270
     * false by default.
271
     * @param boolean $disableExternals Disables the loading of external entities. false by default.
272
     * @return array
273
     * @throws Exception
274
     */
275
    public static function xml2array($val, $disableDoctypes = false, $disableExternals = false)
276
    {
277
        // Check doctype
278
        if ($disableDoctypes && preg_match('/\<\!DOCTYPE.+]\>/', $val)) {
279
            throw new InvalidArgumentException('XML Doctype parsing disabled');
280
        }
281
282
        // Disable external entity loading
283
        $oldVal = null;
284
        if ($disableExternals) {
285
            $oldVal = libxml_disable_entity_loader($disableExternals);
286
        }
287
        try {
288
            $xml = new SimpleXMLElement($val);
289
            $result = self::recursiveXMLToArray($xml);
290
        } catch (Exception $ex) {
291
            if ($disableExternals) {
292
                libxml_disable_entity_loader($oldVal);
293
            }
294
            throw $ex;
295
        }
296
        if ($disableExternals) {
297
            libxml_disable_entity_loader($oldVal);
298
        }
299
        return $result;
300
    }
301
302
    /**
303
     * Convert a XML string to a PHP array recursively. Do not
304
     * call this function directly, Please use {@link Convert::xml2array()}
305
     *
306
     * @param SimpleXMLElement
307
     *
308
     * @return mixed
309
     */
310
    protected static function recursiveXMLToArray($xml)
311
    {
312
        $x = null;
313
        if ($xml instanceof SimpleXMLElement) {
314
            $attributes = $xml->attributes();
315
            foreach ($attributes as $k => $v) {
316
                if ($v) {
317
                    $a[$k] = (string) $v;
318
                }
319
            }
320
            $x = $xml;
321
            $xml = get_object_vars($xml);
322
        }
323
        if (is_array($xml)) {
324
            if (count($xml) == 0) {
325
                return (string)$x;
326
            } // for CDATA
327
            $r = [];
328
            foreach ($xml as $key => $value) {
329
                $r[$key] = self::recursiveXMLToArray($value);
330
            }
331
            // Attributes
332
            if (isset($a)) {
333
                $r['@'] = $a;
334
            }
335
            return $r;
336
        }
337
338
        return (string) $xml;
339
    }
340
341
    /**
342
     * Create a link if the string is a valid URL
343
     *
344
     * @param string $string The string to linkify
345
     * @return string A link to the URL if string is a URL
346
     */
347
    public static function linkIfMatch($string)
348
    {
349
        if (preg_match('/^[a-z+]+\:\/\/[a-zA-Z0-9$-_.+?&=!*\'()%]+$/', $string)) {
350
            return "<a style=\"white-space: nowrap\" href=\"$string\">$string</a>";
351
        } else {
352
            return $string;
353
        }
354
    }
355
356
    /**
357
     * Simple conversion of HTML to plaintext.
358
     *
359
     * @param string $data Input data
360
     * @param bool $preserveLinks
361
     * @param int $wordWrap
362
     * @param array $config
363
     * @return string
364
     */
365
    public static function html2raw($data, $preserveLinks = false, $wordWrap = 0, $config = null)
366
    {
367
        $defaultConfig = array(
368
            'PreserveLinks' => false,
369
            'ReplaceBoldAsterisk' => true,
370
            'CompressWhitespace' => true,
371
            'ReplaceImagesWithAlt' => true,
372
        );
373
        if (isset($config)) {
374
            $config = array_merge($defaultConfig, $config);
375
        } else {
376
            $config = $defaultConfig;
377
        }
378
379
        $data = preg_replace("/<style([^A-Za-z0-9>][^>]*)?>.*?<\/style[^>]*>/is", "", $data);
380
        $data = preg_replace("/<script([^A-Za-z0-9>][^>]*)?>.*?<\/script[^>]*>/is", "", $data);
381
382
        if ($config['ReplaceBoldAsterisk']) {
383
            $data = preg_replace('%<(strong|b)( [^>]*)?>|</(strong|b)>%i', '*', $data);
384
        }
385
386
        // Expand hyperlinks
387
        if (!$preserveLinks && !$config['PreserveLinks']) {
388
            $data = preg_replace_callback('/<a[^>]*href\s*=\s*"([^"]*)">(.*?)<\/a>/ui', function ($matches) {
389
                return Convert::html2raw($matches[2]) . "[$matches[1]]";
390
            }, $data);
391
            $data = preg_replace_callback('/<a[^>]*href\s*=\s*([^ ]*)>(.*?)<\/a>/ui', function ($matches) {
392
                return Convert::html2raw($matches[2]) . "[$matches[1]]";
393
            }, $data);
394
        }
395
396
        // Replace images with their alt tags
397
        if ($config['ReplaceImagesWithAlt']) {
398
            $data = preg_replace('/<img[^>]*alt *= *"([^"]*)"[^>]*>/i', ' \\1 ', $data);
399
            $data = preg_replace('/<img[^>]*alt *= *([^ ]*)[^>]*>/i', ' \\1 ', $data);
400
        }
401
402
        // Compress whitespace
403
        if ($config['CompressWhitespace']) {
404
            $data = preg_replace("/\s+/u", " ", $data);
405
        }
406
407
        // Parse newline tags
408
        $data = preg_replace("/\s*<[Hh][1-6]([^A-Za-z0-9>][^>]*)?> */u", "\n\n", $data);
409
        $data = preg_replace("/\s*<[Pp]([^A-Za-z0-9>][^>]*)?> */u", "\n\n", $data);
410
        $data = preg_replace("/\s*<[Dd][Ii][Vv]([^A-Za-z0-9>][^>]*)?> */u", "\n\n", $data);
411
        $data = preg_replace("/\n\n\n+/", "\n\n", $data);
412
413
        $data = preg_replace("/<[Bb][Rr]([^A-Za-z0-9>][^>]*)?> */", "\n", $data);
414
        $data = preg_replace("/<[Tt][Rr]([^A-Za-z0-9>][^>]*)?> */", "\n", $data);
415
        $data = preg_replace("/<\/[Tt][Dd]([^A-Za-z0-9>][^>]*)?> */", "    ", $data);
416
        $data = preg_replace('/<\/p>/i', "\n\n", $data);
417
418
        // Replace HTML entities
419
        $data = html_entity_decode($data, ENT_QUOTES, 'UTF-8');
420
        // Remove all tags (but optionally keep links)
421
422
        // strip_tags seemed to be restricting the length of the output
423
        // arbitrarily. This essentially does the same thing.
424
        if (!$preserveLinks && !$config['PreserveLinks']) {
425
            $data = preg_replace('/<\/?[^>]*>/', '', $data);
426
        } else {
427
            $data = strip_tags($data, '<a>');
428
        }
429
430
        // Wrap
431
        if ($wordWrap) {
432
            $data = wordwrap(trim($data), $wordWrap);
433
        }
434
        return trim($data);
435
    }
436
437
    /**
438
     * There are no real specifications on correctly encoding mailto-links,
439
     * but this seems to be compatible with most of the user-agents.
440
     * Does nearly the same as rawurlencode().
441
     * Please only encode the values, not the whole url, e.g.
442
     * "mailto:[email protected]?subject=" . Convert::raw2mailto($subject)
443
     *
444
     * @param $data string
445
     * @return string
446
     * @see http://www.ietf.org/rfc/rfc1738.txt
447
     */
448
    public static function raw2mailto($data)
449
    {
450
        return str_ireplace(
451
            array("\n",'?','=',' ','(',')','&','@','"','\'',';'),
452
            array('%0A','%3F','%3D','%20','%28','%29','%26','%40','%22','%27','%3B'),
453
            $data
454
        );
455
    }
456
457
    /**
458
     * Convert a string (normally a title) to a string suitable for using in
459
     * urls and other html attributes. Uses {@link URLSegmentFilter}.
460
     *
461
     * @param string
462
     * @return string
463
     */
464
    public static function raw2url($title)
465
    {
466
        $f = URLSegmentFilter::create();
467
        return $f->filter($title);
468
    }
469
470
    /**
471
     * Normalises newline sequences to conform to (an) OS specific format.
472
     *
473
     * @param string $data Text containing potentially mixed formats of newline
474
     * sequences including \r, \r\n, \n, or unicode newline characters
475
     * @param string $nl The newline sequence to normalise to. Defaults to that
476
     * specified by the current OS
477
     * @return string
478
     */
479
    public static function nl2os($data, $nl = PHP_EOL)
480
    {
481
        return preg_replace('~\R~u', $nl, $data);
482
    }
483
484
    /**
485
     * Encode a value into a string that can be used as part of a filename.
486
     * All string data must be UTF-8 encoded.
487
     *
488
     * @param mixed $val Value to be encoded
489
     * @return string
490
     */
491
    public static function base64url_encode($val)
492
    {
493
        return rtrim(strtr(base64_encode(json_encode($val)), '+/', '~_'), '=');
494
    }
495
496
    /**
497
     * Decode a value that was encoded with Convert::base64url_encode.
498
     *
499
     * @param string $val Value to be decoded
500
     * @return mixed Original value
501
     */
502
    public static function base64url_decode($val)
503
    {
504
        return json_decode(
505
            base64_decode(str_pad(strtr($val, '~_', '+/'), strlen($val) % 4, '=', STR_PAD_RIGHT)),
506
            true
507
        );
508
    }
509
510
    /**
511
     * Converts upper camel case names to lower camel case,
512
     * with leading upper case characters replaced with lower case.
513
     * Tries to retain word case.
514
     *
515
     * Examples:
516
     * - ID => id
517
     * - IDField => idField
518
     * - iDField => iDField
519
     *
520
     * @param $str
521
     * @return string
522
     */
523
    public static function upperCamelToLowerCamel($str)
524
    {
525
        $return = null;
526
        $matches = null;
527
        if (preg_match('/(^[A-Z]{1,})([A-Z]{1})([a-z]+.*)/', $str, $matches)) {
528
            // If string has trailing lowercase after more than one leading uppercase characters,
529
            // match everything but the last leading uppercase character.
530
            $return = implode('', [
531
                strtolower($matches[1]),
532
                $matches[2],
533
                $matches[3]
534
            ]);
535
        } elseif (preg_match('/(^[A-Z]{1})([a-z]+.*)/', $str, $matches)) {
536
            // If string has trailing lowercase after exactly one leading uppercase characters,
537
            // match everything but the last leading uppercase character.
538
            $return = implode('', [
539
                strtolower($matches[1]),
540
                $matches[2]
541
            ]);
542
        } elseif (preg_match('/^[A-Z]+$/', $str)) {
543
            // If string has leading uppercase without trailing lowercase,
544
            // just lowerase the whole thing.
545
            $return = strtolower($str);
546
        } else {
547
            // If string has no leading uppercase, just return.
548
            $return = $str;
549
        }
550
551
        return $return;
552
    }
553
554
    /**
555
     * Turn a memory string, such as 512M into an actual number of bytes.
556
     *
557
     * @param string $memString A memory limit string, such as "64M"
558
     * @return float
559
     */
560
    public static function memstring2bytes($memString)
561
    {
562
        // Remove  non-unit characters from the size
563
        $unit = preg_replace('/[^bkmgtpezy]/i', '', $memString);
564
        // Remove non-numeric characters from the size
565
        $size = preg_replace('/[^0-9\.]/', '', $memString);
566
567
        if ($unit) {
568
            // Find the position of the unit in the ordered string which is the power
569
            // of magnitude to multiply a kilobyte by
570
            return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
571
        }
572
573
        return round($size);
0 ignored issues
show
Bug introduced by
$size of type string is incompatible with the type double expected by parameter $val of round(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

573
        return round(/** @scrutinizer ignore-type */ $size);
Loading history...
574
    }
575
576
    /**
577
     * @param float $bytes
578
     * @param int $decimal decimal precision
579
     * @return string
580
     */
581
    public static function bytes2memstring($bytes, $decimal = 0)
582
    {
583
        $scales = ['B','K','M','G','T','P','E','Z','Y'];
584
        // Get scale
585
        $scale = (int)floor(log($bytes, 1024));
586
        if (!isset($scales[$scale])) {
587
            $scale = 2;
588
        }
589
590
        // Size
591
        $num = round($bytes / pow(1024, $scale), $decimal);
592
        return $num . $scales[$scale];
593
    }
594
595
    /**
596
     * Convert slashes in relative or asolute filesystem path. Defaults to DIRECTORY_SEPARATOR
597
     *
598
     * @param string $path
599
     * @param string $separator
600
     * @param bool $multiple Collapses multiple slashes or not
601
     * @return string
602
     */
603
    public static function slashes($path, $separator = DIRECTORY_SEPARATOR, $multiple = true)
604
    {
605
        if ($multiple) {
606
            return preg_replace('#[/\\\\]+#', $separator, $path);
607
        }
608
        return str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
609
    }
610
}
611