1 | <?php |
||||
2 | |||||
3 | namespace Sunnysideup\ExternalURLField; |
||||
4 | |||||
5 | use SilverStripe\Forms\TextField; |
||||
6 | use SilverStripe\ORM\DataObject; |
||||
7 | use SilverStripe\ORM\FieldType\DBHTMLText; |
||||
8 | |||||
9 | /** |
||||
10 | * ExternalURLField. |
||||
11 | * |
||||
12 | * Form field for entering, saving, validating external urls. |
||||
13 | */ |
||||
14 | class ExternalURLField extends TextField |
||||
15 | { |
||||
16 | /** |
||||
17 | * @var array |
||||
18 | */ |
||||
19 | protected $config = []; |
||||
20 | |||||
21 | /** |
||||
22 | * Default configuration. |
||||
23 | * |
||||
24 | * URL validation regular expression was sourced from |
||||
25 | * |
||||
26 | * @see https://gist.github.com/dperini/729294 |
||||
27 | * |
||||
28 | * @var array |
||||
29 | */ |
||||
30 | private static $default_config = [ |
||||
31 | 'defaultparts' => [ |
||||
32 | 'scheme' => 'https', |
||||
33 | ], |
||||
34 | 'removeparts' => [ |
||||
35 | 'scheme' => false, |
||||
36 | 'user' => true, |
||||
37 | 'pass' => true, |
||||
38 | 'host' => false, |
||||
39 | 'port' => false, |
||||
40 | 'path' => false, |
||||
41 | 'query' => false, |
||||
42 | 'fragment' => false, |
||||
43 | ], |
||||
44 | 'html5validation' => true, |
||||
45 | 'validregex' => '%^(?:(?:https?|ftp)://)(?:\S+(?::\S*)' |
||||
46 | . '?@|\d{1,3}(?:\.\d{1,3}){3}|(?:(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)' |
||||
47 | . '(?:\.(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)*(?:\.[a-z\x{00a1}-\x{ffff}]{2,6}))' |
||||
48 | . '(?::\d+)?(?:[^\s]*)?$%iu', |
||||
49 | ]; |
||||
50 | |||||
51 | public function __construct($name, $title = null, $value = null) |
||||
52 | { |
||||
53 | $this->config = $this->config()->default_config; |
||||
54 | |||||
55 | parent::__construct($name, $title, $value); |
||||
56 | } |
||||
57 | |||||
58 | public function Type() |
||||
59 | { |
||||
60 | return 'url text'; |
||||
61 | } |
||||
62 | |||||
63 | /** |
||||
64 | * @param string $name |
||||
65 | * @param mixed $val |
||||
66 | */ |
||||
67 | public function setConfig($name, $val = null) |
||||
68 | { |
||||
69 | if (is_array($name) && null === $val) { |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
70 | foreach ($name as $n => $value) { |
||||
71 | $this->setConfig($n, $value); |
||||
72 | } |
||||
73 | |||||
74 | return $this; |
||||
75 | } |
||||
76 | |||||
77 | if (is_array($this->config[$name])) { |
||||
78 | if (! is_array($val)) { |
||||
79 | user_error("The value for {$name} must be an array"); |
||||
80 | } |
||||
81 | |||||
82 | $this->config[$name] = array_merge($this->config[$name], $val); |
||||
0 ignored issues
–
show
It seems like
$val can also be of type null ; however, parameter $arrays of array_merge() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
83 | } elseif (isset($this->config[$name])) { |
||||
84 | $this->config[$name] = $val; |
||||
85 | } |
||||
86 | |||||
87 | return $this; |
||||
88 | } |
||||
89 | |||||
90 | /** |
||||
91 | * @param string $name Optional, returns the whole configuration array if empty |
||||
92 | * |
||||
93 | * @return array|mixed |
||||
94 | */ |
||||
95 | public function getConfig($name = null) |
||||
96 | { |
||||
97 | if ($name) { |
||||
98 | return isset($this->config[$name]) ? $this->config[$name] : null; |
||||
99 | } |
||||
100 | |||||
101 | return $this->config; |
||||
102 | } |
||||
103 | |||||
104 | /** |
||||
105 | * Set additional attributes. |
||||
106 | * |
||||
107 | * @return array Attributes |
||||
108 | */ |
||||
109 | public function getAttributes() |
||||
110 | { |
||||
111 | $parentAttributes = parent::getAttributes(); |
||||
112 | $attributes = []; |
||||
113 | |||||
114 | if (! isset($parentAttributes['placeholder'])) { |
||||
115 | $attributes['placeholder'] = $this->config['defaultparts']['scheme'] . '://example.com'; //example url |
||||
116 | } |
||||
117 | |||||
118 | if ($this->config['html5validation']) { |
||||
119 | $attributes += [ |
||||
120 | 'type' => 'url', //html5 field type |
||||
121 | 'pattern' => 'https?://.+', //valid urls only |
||||
122 | ]; |
||||
123 | } |
||||
124 | |||||
125 | return array_merge( |
||||
126 | $parentAttributes, |
||||
127 | $attributes |
||||
128 | ); |
||||
129 | } |
||||
130 | |||||
131 | /** |
||||
132 | * Rebuild url on save. |
||||
133 | * |
||||
134 | * @param string $url |
||||
135 | * @param array|DataObject $data {@see Form::loadDataFrom} |
||||
136 | * |
||||
137 | * @return $this |
||||
138 | */ |
||||
139 | public function setValue($url, $data = null) |
||||
140 | { |
||||
141 | if ($url) { |
||||
142 | $url = $this->rebuildURL($url); |
||||
143 | } |
||||
144 | |||||
145 | return parent::setValue($url, $data); |
||||
146 | } |
||||
147 | |||||
148 | /** |
||||
149 | * Server side validation, using a regular expression. |
||||
150 | * |
||||
151 | * @param mixed $validator |
||||
152 | */ |
||||
153 | public function validate($validator) |
||||
154 | { |
||||
155 | $this->value = trim(string: (string) $this->value); |
||||
156 | $regex = $this->config['validregex']; |
||||
157 | if ($this->value && $regex && ! preg_match($regex, $this->value)) { |
||||
158 | $validator->validationError( |
||||
159 | $this->name, |
||||
160 | _t('ExternalURLField.VALIDATION', 'Please enter a valid URL'), |
||||
161 | 'validation' |
||||
162 | ); |
||||
163 | |||||
164 | return false; |
||||
165 | } |
||||
166 | |||||
167 | return true; |
||||
168 | } |
||||
169 | |||||
170 | public function RightTitle() |
||||
171 | { |
||||
172 | if ($this->value) { |
||||
173 | return DBHTMLText::create_field(DBHTMLText::class, parent::RightTitle() . '<a href="' . $this->value . '" target="_blank" onclick="event.stopPropagation();"rel="noreferrer noopener">open ↗</a>'); |
||||
174 | } |
||||
175 | |||||
176 | return parent::RightTitle(); |
||||
177 | } |
||||
178 | |||||
179 | /** |
||||
180 | * Add config scheme, if missing. |
||||
181 | * Remove the parts of the url we don't want. |
||||
182 | * Set any defaults, if missing. |
||||
183 | * Remove any trailing slash, and rebuild. |
||||
184 | * |
||||
185 | * @param mixed $url |
||||
186 | * |
||||
187 | * @return string |
||||
188 | */ |
||||
189 | protected function rebuildURL($url) |
||||
190 | { |
||||
191 | $defaults = $this->config['defaultparts']; |
||||
192 | if (! preg_match('#^[a-zA-Z]+://#', $url)) { |
||||
193 | $url = $defaults['scheme'] . '://' . $url; |
||||
194 | } |
||||
195 | |||||
196 | $parts = parse_url($url); |
||||
197 | if (! $parts) { |
||||
0 ignored issues
–
show
The expression
$parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||||
198 | //can't parse url, abort |
||||
199 | return ''; |
||||
200 | } |
||||
201 | |||||
202 | foreach (array_keys($parts) as $part) { |
||||
203 | if (true === $this->config['removeparts'][$part]) { |
||||
204 | unset($parts[$part]); |
||||
205 | } |
||||
206 | } |
||||
207 | |||||
208 | // this causes errors! |
||||
209 | // $parts = array_filter($defaults, fn ($default) => ! isset($parts[$part])); |
||||
210 | |||||
211 | return rtrim(http_build_url($defaults, $parts), '/'); |
||||
212 | } |
||||
213 | } |
||||
214 |