1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
|
<?php
/**
* Markup.class.php - Handling of Stud.IP- and HTML-markup.
**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* @category Stud.IP
* @copyright (c) 2014 Stud.IP e.V.
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @since File available since Release 3.0
* @author Robert Costa <rcosta@uos.de>
*/
namespace Studip;
require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_ClassifyLinks.php';
require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_ClassifyTables.php';
require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_LinkifyEmail.php';
require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_TransformLinks.php';
require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_Unlinkify.php';
class Markup
{
/**
* Apply markup rules and clean the text up.
*
* @param TextFormat $markup Markup rules applied on marked-up text.
* @param string $text Marked-up text on which rules are applied.
* @param boolean $trim Trim text before applying markup rules, if TRUE.
*
* @return string HTML code computed from marked-up text.
*/
public static function apply($markup, $text, $trim)
{
return $markup->format(self::markupToHtml($text, $trim, false));
}
// signature for HTML entries
const HTML_MARKER = '<!--HTML-->';
// signature for HTML fallback entries
const HTML_MARKER_FALLBACK = '<!-- HTML: Insert text after this line only. -->';
// regular expression for detecting HTML signature
const HTML_MARKER_REGEXP = '/^\s*<!--\s*HTML.*?-->/i';
/**
* Return `true` if the WYSIWYG editor is enabled for this user.
* @deprecated since Stud.IP 5.5
*
* @return boolean always returns `true`.
*/
public static function editorEnabled()
{
return true;
}
/**
* Return `true` for HTML code and `false` for plain text.
*
* HTML code must either match `HTML_MARKER_REGEXP` or begin
* with '<' and end with '>' (leading and trailing whitespace
* is ignored). Everything else is considered to be plain
* text.
*
* @param string $text HTML code or plain text.
*
* @return boolean `true` for HTML code, `false` for plain text.
*/
public static function isHtml($text)
{
return self::hasHtmlMarker($text);
}
/**
* Return `true` for Stud.IP-HTML and `false` otherwise.
*
* Stud.IP-HTML is HTML that can contain Stud.IP Markup.
*
* Stud.IP-HTML must match Stud.IP 3.2's HTML marker.
* Leading and trailing whitespace is ignored.
*
* Everything else is considered not Stud.IP-HTML. In other
* words, if it's not Stud.IP-HTML it might be everything
* from plain text to binary code. But usually it's either
* Stud.IP markup or plain HTML code, then.
*
* @param string $text Text that is or isn't Stud.IP-HTML.
*
* @return boolean `true` for Stud.IP-HTML
*/
public static function isHtmlFallback($text)
{
$text = trim($text);
// it's not fallback if the new HTML marker is detected
if (MarkupPrivate\Text\startsWith($text, self::HTML_MARKER)) {
return false;
}
// it's Stud.IP-HTML if Stud.IP 3.2's HTML marker is detected
if (MarkupPrivate\Text\startsWith($text, self::HTML_MARKER_FALLBACK)) {
return true;
}
return false;
}
/**
* Return `true` for HTML code and `false` for plain text.
*
* HTML code must start with a match for `HTML_MARKER_REGEXP`.
*
* @param string $text HTML code or plain text.
*
* @return boolean `true` for HTML code, `false` for plain text.
*/
public static function hasHtmlMarker($text)
{
return preg_match(self::HTML_MARKER_REGEXP, $text);
}
/**
* Mark a given text as HTML code.
*
* No sanity-checking is done on the given text. It is simply
* marked up so to be identified by Markup::isHtml as HTML
* code.
*
* @param string $text The text to be marked up as HTML code.
*
* @return string The text marked up as HTML code.
*/
public static function markAsHtml($text)
{
// NOTE keep this function in sync with the JavaScript
// function markAsHtml in WyswygHtmlHead.php
if (self::hasHtmlMarker($text) || trim($text) === '') {
return $text; // marker already set, don't set twice
}
return self::HTML_MARKER . $text;
}
/**
* Apply markup rules after running text through HTML ready.
*
* @param TextFormat $markup Markup rules applied on marked-up text.
* @param string $text Marked-up text on which rules are applied.
* @param boolean $trim Trim text before applying markup rules, if TRUE.
*
* @return string HTML code computed from marked-up text.
*/
private static function markupHtmlReady($markup, $text, $trim)
{
return str_replace("\n", '<br>', self::markupText(
$markup, self::htmlReady(self::unixEOL($text), $trim)));
}
/**
* Convert line break to Unix format.
*
* @param string $text Text with possibly mixed line breaks (Win, Mac, Unix).
*
* @return string Text with Unix line breaks only.
*/
private static function unixEOL($text)
{
return preg_replace("/\r\n?/", "\n", $text);
}
/**
* Apply markup rules on plain text.
*
* @param TextFormat $markup Markup rules applied on marked-up text.
* @param string $text Marked-up text on which rules are applied.
*
* @return string HTML code computed from marked-up text.
*/
private static function markupText($markup, $text)
{
return symbol($markup->format($text));
}
/**
* Call HTMLPurifier to create safe HTML.
*
* @param string $dirty_html Unsafe or 'uncleaned' HTML code.
* @param boolean $autoformat Apply the AutoFormat rules
* @return string Clean and safe HTML code.
*/
private static function purify($dirty_html, $autoformat = true)
{
$purifier = self::createPurifier($autoformat);
return $purifier->purify($dirty_html);
}
/**
* Call HTMLPurifier to filter the HTML code (if the source is detected
* to contain HTML, returns the argument unchanged otherwise). The HTML
* marker is restored afterwards, if it was present.
*
* @param string $dirty_html Unsafe or 'uncleaned' HTML code.
* @return string Clean and safe HTML code.
*/
public static function purifyHtml($html)
{
if ($html instanceof \I18NString) {
$base = self::purifyHtml($html->original());
$lang = $html->toArray();
foreach ($lang as &$value) {
$value = self::purifyHtml($value);
}
return new \I18NString($base, $lang);
}
if (self::isHtml($html)) {
$html = self::markAsHtml(self::purify($html));
}
return $html;
}
/**
* Create HTML purifier instance with Stud.IP-specific configuration.
*
* @param boolean $autoformat Apply the AutoFormat rules
* @return \HTMLPurifier A new instance of the HTML purifier.
*/
private static function createPurifier($autoformat)
{
$config = \HTMLPurifier_Config::createDefault();
$config->set('Cache.SerializerPath', $GLOBALS['TMP_PATH']);
$config->set('Core.RemoveInvalidImg', true);
// restrict allowed HTML tags and attributes
//
// note that changes here should also be reflected in CKEditor's
// settings!!
//
// NOTE The list could be restricted even further by allowing only
// specific values for some attributes and CSS styles, but that is not
// directly supported by HTMLPurifier and would need to be implemented
// with a filter similar to ClassifyLinks.
//
// This is a list of further restrictions that can/should be introduced
// at a later time point maybe, if possible:
//
// - always open external links in a new tab or window
// a[class="link-extern" href="..." target="_blank"]
// - only allow left margin and horizontal text alignment to be set in
// divs (NOTE maybe remove these two features completely?):
// div[style="margin-left:(40|80|...)px; text-align:(center|right|justify)"]
// - img[style] should only allow float:left or float:right
// - only allow text color and background color to be set in a span's
// style attribute (NOTE 'wiki-links' are currently set here due to
// implementation difficulties, but probably this should be
// changed...):
// span[style="color:(#000000|#800000|...);
// background-color:(#000000|#800000|...)"
// class="wiki-link"]
// - tables should always have the class "content" (it should not be
// optional and no other class should be set):
// table[class="content"]
// - table headings should have a column and/or a row scope or no scope
// at all, but nothing else:
// th[scope="(col | row)"]
// - fonts: only Stud.IP-specific fonts should be allowed
//
$config->set('HTML.Allowed', '
a[class|href|target|rel|name|id]
audio[controls|src|height|width|style]
big
blockquote
br
caption
code[class]
div[class|style]
em
figure[class|style]
figcaption
h1
h2
h3
h4
h5
h6
hr
i
img[alt|src|height|width|class|style]
li
ol[reversed|start|style]
p[style]
pre[class]
span[style|class]
strong
u
ul[style]
s
small
sub
sup
table[class|style]
tbody
td[colspan|rowspan|style]
thead
th[colspan|rowspan|style|scope]
tr
tt
video[controls|src|height|width|style]
');
$config->set('Attr.AllowedFrameTargets', ['_blank']);
$config->set('Attr.AllowedRel', ['nofollow']);
$config->set('Attr.DefaultImageAlt', '');
$config->set('Attr.EnableID', true);
$config->set('Attr.AllowedClasses', [
'author',
'content',
'image',
'image-style-side',
'image_resized',
'language-cpp',
'language-css',
'language-diff',
'language-java',
'language-javascript',
'language-json',
'language-php',
'language-python',
'language-ruby',
'language-scss',
'language-sql',
'language-xml',
'link-extern',
'link-intern',
'math-tex',
'table',
'usercode',
'wiki-link'
]);
$config->set('CSS.AllowedFonts', [
'serif',
'sans-serif',
'monospace',
'cursive'
]);
$config->set('CSS.AllowedProperties', [
'margin-left',
'text-align',
'width',
'height',
'color',
'background-color', // needed by span, td
'border-color',
'border-style',
'float',
'border',
'vertical-align'
]);
$config->set('CSS.MaxImgLength', null);
if ($autoformat) {
$config->set('AutoFormat.Linkify', true);
$config->set('AutoFormat.Custom', [
'ClassifyLinks',
'ClassifyTables',
'LinkifyEmail'
]);
$config->set('AutoFormat.RemoveSpansWithoutAttributes', true);
} else {
$config->set('AutoFormat.Custom', ['TransformLinks']);
}
// avoid <img src="evil_CSRF_stuff">
$def = $config->getHTMLDefinition(true);
$img = $def->addBlankElement('img');
$img->attr_transform_post[] = new MarkupPrivate\Purifier\AttrTransform_Image_Source();
$def->addElement('audio', 'Inline', 'Flow', 'Common', [
'src*' => 'URI',
'width' => 'Length',
'height' => 'Length',
'controls' => 'Text', // Bool triggers bug in HTMLPurifier
]);
$def->addElement('video', 'Inline', 'Flow', 'Common', [
'src*' => 'URI',
'width' => 'Length',
'height' => 'Length',
'controls' => 'Text', // Bool triggers bug in HTMLPurifier
]);
$def->addElement('figcaption', 'Inline', 'Flow', 'Common');
$def->addElement('figure', 'Block', 'Optional: (figcaption, Flow) | (Flow, figcaption) | Flow', 'Common');
$def->addAttribute('ol', 'reversed', 'Bool');
$def->addAttribute('ol', 'style', 'Text');
$def->addAttribute('ul', 'style', 'Text');
return new \HTMLPurifier($config);
}
/**
* Convert special characters to HTML entities, and clean up.
*
* @param string $text This text's special chars will be converted.
* @param boolean $trim Trim text before applying markup rules, if TRUE.
* @param boolean $br Replace newlines by <br>, if TRUE.
* @param boolean $double_encode Encode existing HTML entities, if TRUE.
* @return string The converted string.
*/
public static function htmlReady(
$text, $trim = true, $br = false, $double_encode = true
) {
$text = htmlspecialchars($text, ENT_QUOTES, 'utf-8', $double_encode);
if ($trim) {
$text = trim($text);
}
if ($br) { // fix newlines
$text = nl2br($text, false);
}
return $text;
}
/**
* Prepare text for wysiwyg (if enabled), otherwise convert special
* characters using htmlReady.
*
* @param string $text The text.
* @param boolean $trim Trim text before applying markup rules, if TRUE.
* @param boolean $br Replace newlines by <br>, if TRUE and wysiwyg editor disabled.
* @param boolean $double_encode Encode existing HTML entities, if TRUE and wysiwyg editor disabled.
* @return string The converted string.
*/
public static function wysiwygReady(
$text, $trim = true, $br = false, $double_encode = true
) {
if (self::editorEnabled()) {
$text = self::markupToHtml($text, $trim);
}
return self::htmlReady($text, $trim, $br, $double_encode);
}
/**
* Convert Stud.IP markup (possibly mixed with HTML if fallback mode is
* enabled) to editable HTML. Pure HTML will only run through the purifier.
*
* @param string $text The text.
* @param boolean $trim Trim text before applying markup rules, if TRUE.
* @param boolean $mark Mark result text as HTML, if TRUE.
* @return string The converted string.
*/
public static function markupToHtml($text, $trim = true, $mark = true)
{
if (!trim($text)) {
return $text;
}
if (self::isHtml($text)) {
$is_fallback = self::isHtmlFallback($text);
$text = self::purify($text, false);
if ($is_fallback) {
$text = self::markupText(new \StudipCoreFormat(), $text);
}
} else {
$text = self::markupHtmlReady(new \StudipCoreFormat(), $text, $trim);
}
return $mark ? self::markAsHtml($text) : $text;
}
/**
* Call HTMLPurifier to remove all HTML tags from the string (if the source
* is detected to contain HTML, returns the argument unchanged otherwise).
*
* @param string $html HTML code to filter
* @return string The converted string.
*/
public static function removeHtml($html)
{
if (self::isHtml($html)) {
$config = \HTMLPurifier_Config::createDefault();
$config->set('Cache.SerializerPath', $GLOBALS['TMP_PATH']);
$config->set('HTML.Allowed', 'a[href],img[alt|src],br');
$config->set('AutoFormat.Custom', ['Unlinkify']);
$html = str_replace('</li>', '</li><br>', $html);
$html = str_replace('</ol>', '</ol><br>', $html);
$html = str_replace('</ul>', '</ul><br>', $html);
$html = str_replace('</tr>', '</tr><br>', $html);
$html = str_replace('</p>', '</p><br><br>', $html);
$html = str_replace('</div>', '</div><br><br>', $html);
$purifier = new \HTMLPurifier($config);
$html = $purifier->purify($html);
// Replace new lines with simple line break; twice because we don't
// want to create unneccessary white space if a <br /> is followed
// by a new line
$html = str_replace('<br />' . PHP_EOL, PHP_EOL, $html);
$html = str_replace('<br />', PHP_EOL, $html);
$html = \decodeHTML(trim($html));
}
return $html;
}
}
/**
* Members of Studip\MarkupPrivate must not be used outside of this file!!
*/
namespace Studip\MarkupPrivate\Purifier;
use Studip\MarkupPrivate\MediaProxy;
/**
* Remove invalid <img src> attributes.
*/
class AttrTransform_Image_Source extends \HTMLPurifier_AttrTransform
{
/**
* Implements abstract method of base class.
*/
function transform($attr, $config, $context)
{
if (!isset($attr['src'])) {
return $attr;
}
try {
$attr['src'] = MediaProxy\getMediaUrl($attr['src']);
} catch (MediaProxy\InvalidInternalLinkException $e) {
// invalid internal link ==> remove <img src> attribute
$GLOBALS['msg'][] = _('Ungültige interne Medienverknüpfung entfernt: ')
. \htmlentities($e->getUrl());
$attr['src'] = NULL; // remove <img src> attribute
} catch (MediaProxy\ExternalMediaDeniedException $e) {
$GLOBALS['msg'][] = _('Verbotene externe Medienverknüpfung entfernt: ')
. \htmlentities($e->getUrl());
$attr['src'] = NULL; // remove <img src> attribute
}
return $attr;
}
}
//// media proxy //////////////////////////////////////////////////////////////
namespace Studip\MarkupPrivate\MediaProxy;
use Studip\MarkupPrivate\Text;
/**
* Check if media proxy should be used and if so return the respective URL.
*
* @param string $url URL to media file.
* @return mixed URL string to media file (possibly 'proxied')
* or NULL if URL is invalid.
*/
function getMediaUrl($url) {
// even though proxied URLs shouldn't be stored in the database, the
// next line will handle those cases where they're accidentally there
$url = decodeMediaProxyUrl($url);
// handle internal media links
if (isStudipMediaUrl($url)) {
return transformInternalIdnaLink($url);
}
if (isInternalLink($url)) {
// link is studip-internal, but not to a valid media location
throw new InvalidInternalLinkException($url);
}
// handle external media links
$external_media = \Config::get()->LOAD_EXTERNAL_MEDIA;
if ($external_media === 'proxy' &&
\Seminar_Session::is_current_session_authenticated()
) {
// media proxy must be accessed by an internal link
return encodeMediaProxyUrl($url);
}
if ($external_media === 'allow') {
return $url;
}
throw new ExternalMediaDeniedException($url);
}
/**
* Return media proxy URL for an unproxied URL.
*
* @params string $url Unproxied media URL.
* @return string Media proxy URL for accessing the same resource.
*/
function encodeMediaProxyUrl($url) {
return transformInternalIdnaLink(
getMediaProxyUrl() .'?url=' . \urlencode(\idna_link($url)));
}
/**
* Extract the original URL from a media proxy URL.
*
* @param string $url The media proxy URL.
* return string The original URL. If $url does not point to the media
* proxy then this is the exact same value given by $url.
*/
function decodeMediaProxyUrl($url) {
# TODO make it work for 'url=' at any position in query
$urlpath = removeStudipDomain($url);
$proxypath = removeStudipDomain(getMediaProxyUrl()) . '?url=';
if (Text\startsWith($urlpath, $proxypath)) {
return \urldecode(Text\removePrefix($urlpath, $proxypath));
}
return $url;
}
/**
* Return Stud.IP's absolute media proxy URL.
*/
function getMediaProxyUrl() {
return $GLOBALS['ABSOLUTE_URI_STUDIP'] . 'dispatch.php/media_proxy';
}
/**
* Test if an URL points to a valid internal Stud.IP media path.
*
* @param string $url Internal Stud.IP URL.
* @returns boolean TRUE for internal media link URLs, FALSE otherwise.
*/
function isStudipMediaUrl($url) {
return isInternalLink($url) &&
isStudipMediaUrlPath(getStudipRelativePath($url));
}
function isInternalLink($url) {
return is_internal_url(transformInternalIdnaLink($url));
}
//// url utilities ////////////////////////////////////////////////////////////
/**
* Remove domain name from internal URLs.
*
* Remove scheme, domain and authentication information from internal
* Stud.IP URLs. Leave external URLs untouched.
*
* @param string $url URL from which to remove internal domain.
* @returns string URL without internal domain or the exact same
* value as $url for external URLs.
*/
function removeStudipDomain($url) {
if (!isInternalLink($url)) {
return $url;
}
$parsed_url = \parse_url(transformInternalIdnaLink($url));
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return $path . $query . $fragment;
}
/**
* Return a URL's path component with the absolute Stud.IP path removed.
*
* NOTE: If the URL is not an internal Stud.IP URL, the path component will
* nevertheless be returned without issuing an error message.
*
* Example:
* >>> getStudipRelativePath('http://localhost:8080'
* . '/studip/sendfile.php?type=0&file_id=ABC123&file_name=nice.jpg')
* 'sendfile.php'
*
* @param string $url The URL from which to return the Stud.IP-relative
* path component.
* returns string Stud.IP-relative path component of $url.
*/
function getStudipRelativePath($url) {
$parsed_url = \parse_url(transformInternalIdnaLink($url));
$parsed_studip_url = getParsedStudipUrl();
return Text\removePrefix($parsed_url['path'], $parsed_studip_url['path']);
}
/**
* Return an associative array containing the Stud.IP URL elements.
*
* see also: http://php.net/manual/en/function.parse-url.php
*
* @returns mixed Same values that PHP's parse_url() returns.
*/
function getParsedStudipUrl() {
return \parse_url($GLOBALS['ABSOLUTE_URI_STUDIP']);
}
/**
* Test if path is valid for internal Stud.IP media URLs.
*
* @params string $path The path component of an URL.
* return boolean TRUE for valid media paths, FALSE otherwise.
*/
function isStudipMediaUrlPath($path) {
list($path_head) = \explode('/', $path);
$valid_paths = ['sendfile.php', 'download', 'assets', 'pictures'];
return \mb_strpos(\urldecode($path), '../') === false && \in_array($path_head, $valid_paths);
}
/**
* Return a normalized, internal URL.
*
* @params string $url An internal URL.
* @returns string Normalized internal URL.
*/
function transformInternalIdnaLink($url) {
return \idna_link(\TransformInternalLinks($url));
}
//// url exceptions ///////////////////////////////////////////////////////////
class UrlException extends \Exception
{
private $url;
public function __construct($url) {
parent::__construct();
$this->url = $url;
}
public function getUrl()
{
return $this->url;
}
}
class InvalidInternalLinkException extends UrlException
{
}
class ExternalMediaDeniedException extends UrlException
{
}
//// string utilities /////////////////////////////////////////////////////////
namespace Studip\MarkupPrivate\Text;
/**
* Test if string starts with prefix.
*
* @param string $string Tested string.
* @param string $prefix Prefix of tested string.
*
* @return boolean TRUE if string starts with prefix.
*/
function startsWith($string, $prefix) {
return \mb_substr($string, 0, \mb_strlen($prefix)) === $prefix;
}
/**
* Test if string ends with suffix.
*
* @param string $string Tested string.
* @param string $suffix Suffix of tested string.
*
* @return boolean TRUE if string ends with suffix.
*/
function endsWith($string, $suffix) {
return \mb_substr($string, - \mb_strlen($suffix)) === $suffix;
}
/**
* Remove prefix from string.
*
* Does not change the string if it has a different prefix.
*
* @param string $string The string that must start with the prefix.
* @param string $prefix The prefix of the string.
*
* @return string String without prefix.
*/
function removePrefix($string, $prefix) {
return startsWith($string, $prefix) ? \mb_substr($string, \mb_strlen($prefix)) : $string;
}
|