@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorSlug extends Phobject {
4
5 public static function normalizeProjectSlug($slug) {
6 $slug = str_replace('/', ' ', $slug);
7 $slug = self::normalize($slug, $hashtag = true);
8 return rtrim($slug, '/');
9 }
10
11 public static function isValidProjectSlug($slug) {
12 $slug = self::normalizeProjectSlug($slug);
13 return ($slug != '_');
14 }
15
16 public static function normalize($slug, $hashtag = false) {
17 $slug = preg_replace('@/+@', '/', $slug);
18 $slug = trim($slug, '/');
19 $slug = phutil_utf8_strtolower($slug);
20
21 $ban =
22 // Ban control characters since users can't type them and they create
23 // various other problems with parsing and rendering.
24 "\\x00-\\x19".
25
26 // Ban characters with special meanings in URIs (and spaces), since we
27 // want slugs to produce nice URIs.
28 "#%&+=?".
29 " ".
30
31 // Ban backslashes and various brackets for parsing and URI quality.
32 "\\\\".
33 "<>{}\\[\\]".
34
35 // Ban single and double quotes since they can mess up URIs.
36 "'".
37 '"';
38
39 // In hashtag mode (used for Project hashtags), ban additional characters
40 // which cause parsing problems.
41 if ($hashtag) {
42 $ban .= '`~!@$^*,:;(|)';
43 }
44
45 $slug = preg_replace('(['.$ban.']+)', '_', $slug);
46 $slug = preg_replace('@_+@', '_', $slug);
47
48 $parts = explode('/', $slug);
49
50 // Remove leading and trailing underscores from each component, if the
51 // component has not been reduced to a single underscore. For example, "a?"
52 // converts to "a", but "??" converts to "_".
53 foreach ($parts as $key => $part) {
54 if ($part != '_') {
55 $parts[$key] = trim($part, '_');
56 }
57 }
58 $slug = implode('/', $parts);
59
60 // Specifically rewrite these slugs. It's OK to have a slug like "a..b",
61 // but not a slug which is only "..".
62
63 // NOTE: These are explicitly not pht()'d, because they should be stable
64 // across languages.
65
66 $replace = array(
67 '.' => 'dot',
68 '..' => 'dotdot',
69 );
70
71 foreach ($replace as $pattern => $replacement) {
72 $pattern = preg_quote($pattern, '@');
73 $slug = preg_replace(
74 '@(^|/)'.$pattern.'(\z|/)@',
75 '\1'.$replacement.'\2', $slug);
76 }
77
78 return $slug.'/';
79 }
80
81 public static function getDefaultTitle($slug) {
82 $parts = explode('/', trim($slug, '/'));
83 $default_title = end($parts);
84 $default_title = str_replace('_', ' ', $default_title);
85 $default_title = phutil_utf8_ucwords($default_title);
86 $default_title = nonempty($default_title, pht('Untitled Document'));
87 return $default_title;
88 }
89
90 public static function getAncestry($slug) {
91 $slug = self::normalize($slug);
92
93 if ($slug == '/') {
94 return array();
95 }
96
97 $ancestors = array(
98 '/',
99 );
100
101 $slug = explode('/', $slug);
102 array_pop($slug);
103 array_pop($slug);
104
105 $accumulate = '';
106 foreach ($slug as $part) {
107 $accumulate .= $part.'/';
108 $ancestors[] = $accumulate;
109 }
110
111 return $ancestors;
112 }
113
114 public static function getDepth($slug) {
115 $slug = self::normalize($slug);
116 if ($slug == '/') {
117 return 0;
118 } else {
119 return substr_count($slug, '/');
120 }
121 }
122
123}