1+ <?php
2+
3+ namespace AtlassianConnectCore \Http \Auth ;
4+
5+ /**
6+ * Class QSH creates a Query String Hash
7+ *
8+ * Documentation:
9+ * https://docs.atlassian.com/DAC/bitbucket/concepts/qsh.html
10+ *
11+ * @package App\Http\Auth
12+ *
13+ * @author Artem Brezhnev <brezzhnev@gmail.com>
14+ */
15+ class QSH
16+ {
17+ /**
18+ * The request URL.
19+ *
20+ * @var string
21+ */
22+ protected $ url ;
23+
24+ /**
25+ * The request HTTP method.
26+ *
27+ * @var string
28+ */
29+ protected $ method ;
30+
31+ /**
32+ * The URL parts (host, port, path...)
33+ *
34+ * @var array
35+ */
36+ protected $ parts = [];
37+
38+ /**
39+ * The list of prefixes which should be removed.
40+ *
41+ * @var array
42+ */
43+ protected $ prefixes = [
44+ '/wiki '
45+ ];
46+
47+ /**
48+ * QSH constructor.
49+ *
50+ * @param string $url
51+ * @param string $method
52+ */
53+ public function __construct (string $ url , string $ method )
54+ {
55+ $ this ->url = $ url ;
56+ $ this ->parts = parse_url ($ url );
57+
58+ $ this ->method = strtoupper ($ method );
59+ }
60+
61+ /**
62+ * Create a QSH string.
63+ *
64+ * More details:
65+ * https://docs.atlassian.com/DAC/bitbucket/concepts/qsh.html
66+ *
67+ * @return string
68+ */
69+ public function create (): string
70+ {
71+ $ parts = [
72+ $ this ->method ,
73+ $ this ->canonicalUri (),
74+ $ this ->canonicalQuery ()
75+ ];
76+
77+ return hash ('sha256 ' , implode ('& ' , $ parts ));
78+ }
79+
80+ /**
81+ * Make a canonical URI.
82+ *
83+ * @return string|null
84+ */
85+ public function canonicalUri ()
86+ {
87+ if (!$ path = array_get ($ this ->parts , 'path ' )) {
88+ return '/ ' ;
89+ }
90+
91+ // Remove a prefix of instance from the path
92+ // Eg. remove `/wiki` part which means Confluence instance.
93+ $ uri = $ this ->removePrefix ($ path );
94+
95+ // The canonical URI should not contain & characters.
96+ // Therefore, any & characters should be URL-encoded to %26.
97+ $ uri = str_replace ('& ' , '%26 ' , $ uri );
98+
99+ // The canonical URI only ends with a / character if it is the only character.
100+ $ uri = $ uri === '/ '
101+ ? $ uri
102+ : rtrim ($ uri , '/ ' );
103+
104+ return $ uri ;
105+ }
106+
107+ /**
108+ * Make a canonical query string.
109+ *
110+ * @return string|null
111+ */
112+ public function canonicalQuery ()
113+ {
114+ if (!$ query = array_get ($ this ->parts , 'query ' )) {
115+ return null ;
116+ }
117+
118+ $ params = $ this ->parseQuery ($ query );
119+
120+ // We should ignore the "JWT" parameter.
121+ $ params = array_filter ($ params , function (string $ key ) {
122+ return strtolower ($ key ) !== 'jwt ' ;
123+ }, ARRAY_FILTER_USE_KEY );
124+
125+ ksort ($ params );
126+
127+ $ query = $ this ->buildQuery ($ params );
128+
129+ // Encode underscores.
130+ $ query = str_replace ('_ ' , '%20 ' , $ query );
131+
132+ return $ query ;
133+ }
134+
135+ /**
136+ * Remove a prefix from the URL path.
137+ *
138+ * @param string $path
139+ *
140+ * @return string
141+ */
142+ protected function removePrefix (string $ path ): string
143+ {
144+ foreach ($ this ->prefixes as $ prefix ) {
145+ $ pattern = '/^ ' . preg_quote ($ prefix , '/ ' ) . '/ ' ;
146+
147+ if (preg_match ($ pattern , $ path )) {
148+ $ path = preg_replace ($ pattern , '' , $ path );
149+
150+ break ;
151+ }
152+ }
153+
154+ return $ path ;
155+ }
156+
157+ /**
158+ * Parse a query to array of parameters.
159+ *
160+ * @param string $query
161+ *
162+ * @return array
163+ */
164+ protected function parseQuery (string $ query ): array
165+ {
166+ $ output = [];
167+
168+ $ query = ltrim ($ query , '? ' );
169+
170+ $ parameters = explode ('& ' , $ query );
171+
172+ foreach ($ parameters as $ parameter ) {
173+ list ($ key , $ value ) = array_pad (explode ('= ' , $ parameter ), 2 , null );
174+
175+ $ output = array_merge_recursive ($ output , [$ key => $ value ]);
176+ }
177+
178+ return $ output ;
179+ }
180+
181+ /**
182+ * Build a query accordingly to RFC3986
183+ *
184+ * @param array $params
185+ *
186+ * @return string
187+ */
188+ protected function buildQuery (array $ params ): string
189+ {
190+ $ pieces = [];
191+
192+ foreach ($ this ->encodeQueryParams ($ params ) as $ param => $ values ) {
193+ $ value = implode (', ' , $ values );
194+
195+ $ pieces [] = implode ('= ' , !$ value
196+ ? [$ param ]
197+ : [$ param , $ value ]
198+ );
199+ }
200+
201+ return implode ('& ' , array_filter ($ pieces ));
202+ }
203+
204+ /**
205+ * Encode query parameters.
206+ *
207+ * @param array $params
208+ *
209+ * @return array
210+ */
211+ protected function encodeQueryParams (array $ params ): array
212+ {
213+ $ encoded = [];
214+
215+ array_walk ($ params , function ($ value , string $ param ) use (&$ encoded ) {
216+ $ key = str_replace ('+ ' , ' ' , $ param );
217+ $ key = rawurlencode (rawurldecode ($ key ));
218+
219+ $ values = array_wrap ($ value );
220+ $ values = array_map (function ($ value ) {
221+ $ value = str_replace ('+ ' , ' ' , $ value );
222+ return rawurlencode (rawurldecode ($ value ));
223+ }, $ values );
224+
225+ $ encoded [$ key ] = $ values ;
226+ });
227+
228+ return $ encoded ;
229+ }
230+
231+ /**
232+ * Convert an object to a string representation.
233+ *
234+ * @return string
235+ */
236+ public function __toString ()
237+ {
238+ return $ this ->create ();
239+ }
240+ }
0 commit comments