@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
at recaptime-dev/main 123 lines 3.2 kB view raw
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}