Skip to content

Commit da73fc8

Browse files
OscarVanLEnricoMi
andauthored
Fix url encoding of strings with slashes in URLs (#3263)
Fixes #3262. This changes the URL encoding function used for strings in URLs. Currently, this uses `urllib.parse.quote`, which makes an exclusion of URL-encoding the `/` character: > urllib.parse.quote(string, safe='/', encoding=None, errors=None) Replace special characters in string using the %xx escape. Letters, digits, and the characters '_.-~' are never quoted. By default, this function is intended for quoting the path section of a URL. The optional safe parameter specifies additional ASCII characters that should not be quoted — its default value is '/'. The GitHub APIs expect that `/` _should_ be URL encoded. For example, if my environment name contains `/` I get a 404 from the GitHub API. I have modified the call to remove this exclusion for the `/` character, which causes the environment name to be correctly encoded. I have updated the test cases to provide coverage for this change. --------- Co-authored-by: Enrico Minack <[email protected]>
1 parent f51a3f4 commit da73fc8

File tree

5 files changed

+38
-36
lines changed

5 files changed

+38
-36
lines changed

github/Repository.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,7 +1268,7 @@ def add_to_collaborators(self, collaborator: str | NamedUser, permission: Opt[st
12681268
if isinstance(collaborator, github.NamedUser.NamedUser):
12691269
collaborator = collaborator._identity
12701270
else:
1271-
collaborator = urllib.parse.quote(collaborator)
1271+
collaborator = urllib.parse.quote(collaborator, safe="")
12721272

12731273
if is_defined(permission):
12741274
put_parameters = {"permission": permission}
@@ -1294,7 +1294,7 @@ def get_collaborator_permission(self, collaborator: str | NamedUser) -> str:
12941294
if isinstance(collaborator, github.NamedUser.NamedUser):
12951295
collaborator = collaborator._identity
12961296
else:
1297-
collaborator = urllib.parse.quote(collaborator)
1297+
collaborator = urllib.parse.quote(collaborator, safe="")
12981298
headers, data = self._requester.requestJsonAndCheck(
12991299
"GET",
13001300
f"{self.url}/collaborators/{collaborator}/permission",
@@ -1949,7 +1949,7 @@ def create_secret(
19491949
assert isinstance(unencrypted_value, str), unencrypted_value
19501950
assert secret_type in ["actions", "dependabot"], "secret_type should be actions or dependabot"
19511951

1952-
secret_name = urllib.parse.quote(secret_name)
1952+
secret_name = urllib.parse.quote(secret_name, safe="")
19531953
public_key = self.get_public_key(secret_type=secret_type)
19541954
payload = public_key.encrypt(unencrypted_value)
19551955
put_parameters = {
@@ -1997,7 +1997,7 @@ def get_secret(self, secret_name: str, secret_type: str = "actions") -> github.S
19971997
assert isinstance(secret_name, str), secret_name
19981998
assert secret_type in ["actions", "dependabot"], "secret_type should be actions or dependabot"
19991999

2000-
secret_name = urllib.parse.quote(secret_name)
2000+
secret_name = urllib.parse.quote(secret_name, safe="")
20012001
return github.Secret.Secret(
20022002
requester=self._requester,
20032003
headers={},
@@ -2047,7 +2047,7 @@ def get_variable(self, variable_name: str) -> github.Variable.Variable:
20472047
:rtype: github.Variable.Variable
20482048
"""
20492049
assert isinstance(variable_name, str), variable_name
2050-
variable_name = urllib.parse.quote(variable_name)
2050+
variable_name = urllib.parse.quote(variable_name, safe="")
20512051
return github.Variable.Variable(
20522052
requester=self._requester,
20532053
headers={},
@@ -2064,7 +2064,7 @@ def delete_secret(self, secret_name: str, secret_type: str = "actions") -> bool:
20642064
"""
20652065
assert isinstance(secret_name, str), secret_name
20662066
assert secret_type in ["actions", "dependabot"], "secret_type should be actions or dependabot"
2067-
secret_name = urllib.parse.quote(secret_name)
2067+
secret_name = urllib.parse.quote(secret_name, safe="")
20682068
status, headers, data = self._requester.requestJson("DELETE", f"{self.url}/{secret_type}/secrets/{secret_name}")
20692069
return status == 204
20702070

@@ -2075,7 +2075,7 @@ def delete_variable(self, variable_name: str) -> bool:
20752075
:rtype: bool
20762076
"""
20772077
assert isinstance(variable_name, str), variable_name
2078-
variable_name = urllib.parse.quote(variable_name)
2078+
variable_name = urllib.parse.quote(variable_name, safe="")
20792079
status, headers, data = self._requester.requestJson("DELETE", f"{self.url}/actions/variables/{variable_name}")
20802080
return status == 204
20812081

@@ -2226,11 +2226,11 @@ def get_archive_link(self, archive_format: str, ref: Opt[str] = NotSet) -> str:
22262226
:rtype: string
22272227
"""
22282228
assert isinstance(archive_format, str), archive_format
2229-
archive_format = urllib.parse.quote(archive_format)
2229+
archive_format = urllib.parse.quote(archive_format, safe="")
22302230
assert is_optional(ref, str), ref
22312231
url = f"{self.url}/{archive_format}"
22322232
if is_defined(ref):
2233-
ref = urllib.parse.quote(ref)
2233+
ref = urllib.parse.quote(ref, safe="")
22342234
url += f"/{ref}"
22352235
headers, data = self._requester.requestJsonAndCheck("GET", url)
22362236
return headers["location"]
@@ -3010,7 +3010,7 @@ def get_git_matching_refs(self, ref: str) -> PaginatedList[GitRef]:
30103010
:rtype: :class:`PaginatedList` of :class:`github.GitRef.GitRef`
30113011
"""
30123012
assert isinstance(ref, str), ref
3013-
ref = urllib.parse.quote(ref)
3013+
ref = urllib.parse.quote(ref, safe="")
30143014
return PaginatedList(
30153015
github.GitRef.GitRef,
30163016
self._requester,
@@ -3025,7 +3025,7 @@ def get_git_tag(self, sha: str) -> GitTag:
30253025
:rtype: :class:`github.GitTag.GitTag`
30263026
"""
30273027
assert isinstance(sha, str), sha
3028-
sha = urllib.parse.quote(sha)
3028+
sha = urllib.parse.quote(sha, safe="")
30293029
headers, data = self._requester.requestJsonAndCheck("GET", f"{self.url}/git/tags/{sha}")
30303030
return github.GitTag.GitTag(self._requester, headers, data, completed=True)
30313031

@@ -3256,7 +3256,9 @@ def get_label(self, name: str) -> Label:
32563256
:rtype: :class:`github.Label.Label`
32573257
"""
32583258
assert isinstance(name, str), name
3259-
headers, data = self._requester.requestJsonAndCheck("GET", f"{self.url}/labels/{urllib.parse.quote(name)}")
3259+
headers, data = self._requester.requestJsonAndCheck(
3260+
"GET", f"{self.url}/labels/{urllib.parse.quote(name, safe='')}"
3261+
)
32603262
return github.Label.Label(self._requester, headers, data, completed=True)
32613263

32623264
def get_labels(self) -> PaginatedList[Label]:
@@ -3607,7 +3609,7 @@ def get_release(self, id: int | str) -> GitRelease:
36073609
headers, data = self._requester.requestJsonAndCheck("GET", f"{self.url}/releases/{id}")
36083610
return github.GitRelease.GitRelease(self._requester, headers, data, completed=True)
36093611
elif isinstance(id, str):
3610-
id = urllib.parse.quote(id)
3612+
id = urllib.parse.quote(id, safe="")
36113613
headers, data = self._requester.requestJsonAndCheck("GET", f"{self.url}/releases/tags/{id}")
36123614
return github.GitRelease.GitRelease(self._requester, headers, data, completed=True)
36133615

@@ -3665,7 +3667,7 @@ def get_workflow(self, id_or_file_name: str | int) -> Workflow:
36653667
:rtype: :class:`github.Workflow.Workflow`
36663668
"""
36673669
assert isinstance(id_or_file_name, (int, str)), id_or_file_name
3668-
id_or_file_name = urllib.parse.quote(str(id_or_file_name))
3670+
id_or_file_name = urllib.parse.quote(str(id_or_file_name), safe="")
36693671
headers, data = self._requester.requestJsonAndCheck("GET", f"{self.url}/actions/workflows/{id_or_file_name}")
36703672
return github.Workflow.Workflow(self._requester, headers, data, completed=True)
36713673

@@ -3755,7 +3757,7 @@ def has_in_assignees(self, assignee: str | NamedUser) -> bool:
37553757
if isinstance(assignee, github.NamedUser.NamedUser):
37563758
assignee = assignee._identity
37573759
else:
3758-
assignee = urllib.parse.quote(assignee)
3760+
assignee = urllib.parse.quote(assignee, safe="")
37593761

37603762
status, headers, data = self._requester.requestJson("GET", f"{self.url}/assignees/{assignee}")
37613763
return status == 204
@@ -3771,7 +3773,7 @@ def has_in_collaborators(self, collaborator: str | NamedUser) -> bool:
37713773
if isinstance(collaborator, github.NamedUser.NamedUser):
37723774
collaborator = collaborator._identity
37733775
else:
3774-
collaborator = urllib.parse.quote(collaborator)
3776+
collaborator = urllib.parse.quote(collaborator, safe="")
37753777

37763778
status, headers, data = self._requester.requestJson("GET", f"{self.url}/collaborators/{collaborator}")
37773779
return status == 204
@@ -3803,7 +3805,7 @@ def legacy_search_issues(self, state: str, keyword: str) -> list[Issue]:
38033805
assert isinstance(keyword, str), keyword
38043806
headers, data = self._requester.requestJsonAndCheck(
38053807
"GET",
3806-
f"/legacy/issues/search/{self.owner.login}/{self.name}/{state}/{urllib.parse.quote(keyword)}",
3808+
f"/legacy/issues/search/{self.owner.login}/{self.name}/{state}/{urllib.parse.quote(keyword, safe='')}",
38073809
)
38083810
return [
38093811
github.Issue.Issue(
@@ -3979,7 +3981,7 @@ def remove_from_collaborators(self, collaborator: str | NamedUser) -> None:
39793981
if isinstance(collaborator, github.NamedUser.NamedUser):
39803982
collaborator = collaborator._identity
39813983
else:
3982-
collaborator = urllib.parse.quote(collaborator)
3984+
collaborator = urllib.parse.quote(collaborator, safe="")
39833985

39843986
headers, data = self._requester.requestJsonAndCheck("DELETE", f"{self.url}/collaborators/{collaborator}")
39853987

@@ -4083,7 +4085,7 @@ def _hub(self, mode: str, event: str, callback: str, secret: Opt[str]) -> None:
40834085
assert isinstance(event, str), event
40844086
assert isinstance(callback, str), callback
40854087
assert is_optional(secret, str), secret
4086-
event = urllib.parse.quote(event)
4088+
event = urllib.parse.quote(event, safe="")
40874089

40884090
post_parameters = collections.OrderedDict()
40894091
post_parameters["hub.callback"] = callback
@@ -4235,7 +4237,7 @@ def get_environment(self, environment_name: str) -> Environment:
42354237
:rtype: :class:`github.Environment.Environment`
42364238
"""
42374239
assert isinstance(environment_name, str), environment_name
4238-
environment_name = urllib.parse.quote(environment_name)
4240+
environment_name = urllib.parse.quote(environment_name, safe="")
42394241
headers, data = self._requester.requestJsonAndCheck("GET", f"{self.url}/environments/{environment_name}")
42404242
data["environments_url"] = f"/repositories/{self.id}/environments"
42414243
return Environment(self._requester, headers, data, completed=True)
@@ -4271,7 +4273,7 @@ def create_environment(
42714273
)
42724274
or deployment_branch_policy is None
42734275
)
4274-
environment_name = urllib.parse.quote(environment_name)
4276+
environment_name = urllib.parse.quote(environment_name, safe="")
42754277

42764278
put_parameters = {
42774279
"wait_timer": wait_timer,
@@ -4293,7 +4295,7 @@ def delete_environment(self, environment_name: str) -> None:
42934295
:rtype: None
42944296
"""
42954297
assert isinstance(environment_name, str), environment_name
4296-
environment_name = urllib.parse.quote(environment_name)
4298+
environment_name = urllib.parse.quote(environment_name, safe="")
42974299

42984300
headers, data = self._requester.requestJsonAndCheck("DELETE", f"{self.url}/environments/{environment_name}")
42994301

github/Team.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def get_team_membership(self, member: str | NamedUser) -> Membership:
283283
if isinstance(member, github.NamedUser.NamedUser):
284284
member = member._identity
285285
else:
286-
member = urllib.parse.quote(member)
286+
member = urllib.parse.quote(member, safe="")
287287
headers, data = self._requester.requestJsonAndCheck("GET", f"{self.url}/memberships/{member}")
288288
return github.Membership.Membership(self._requester, headers, data, completed=True)
289289

@@ -302,7 +302,7 @@ def get_repo_permission(self, repo: Repository) -> Permissions | None:
302302
if isinstance(repo, github.Repository.Repository):
303303
repo = repo._identity # type: ignore
304304
else:
305-
repo = urllib.parse.quote(repo)
305+
repo = urllib.parse.quote(repo, safe="")
306306
try:
307307
headers, data = self._requester.requestJsonAndCheck(
308308
"GET",
@@ -343,7 +343,7 @@ def update_team_repository(self, repo: Repository, permission: str) -> bool:
343343
if isinstance(repo, github.Repository.Repository):
344344
repo_url_param = repo._identity
345345
else:
346-
repo_url_param = urllib.parse.quote(repo)
346+
repo_url_param = urllib.parse.quote(repo, safe="")
347347
put_parameters = {
348348
"permission": permission,
349349
}

tests/Environment.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,17 @@ def testGetEnvironments(self):
114114
self.assertEqual(environments[0].name, "dev")
115115

116116
def testCreateEnvironment(self):
117-
environment = self.repo.create_environment("test")
118-
self.assertEqual(environment.name, "test")
117+
environment = self.repo.create_environment("test/env")
118+
self.assertEqual(environment.name, "test/env")
119119
self.assertEqual(environment.id, 470015651)
120120
self.assertEqual(environment.node_id, "EN_kwDOHKhL9c4cA96j")
121121
self.assertEqual(
122122
environment.url,
123-
"https://api.github.com/repos/alson/PyGithub/environments/test",
123+
"https://api.github.com/repos/alson/PyGithub/environments/test/env",
124124
)
125125
self.assertEqual(
126126
environment.html_url,
127-
"https://github.com/alson/PyGithub/deployments/activity_log?environments_filter=test",
127+
"https://github.com/alson/PyGithub/deployments/activity_log?environments_filter=test%2Fenv",
128128
)
129129
self.assertEqual(
130130
environment.created_at,
@@ -139,7 +139,7 @@ def testCreateEnvironment(self):
139139

140140
def testUpdateEnvironment(self):
141141
environment = self.repo.create_environment(
142-
"test",
142+
"test/env",
143143
wait_timer=42,
144144
reviewers=[github.EnvironmentProtectionRuleReviewer.ReviewerParams(type_="User", id_=19245)],
145145
prevent_self_review=True,
@@ -152,11 +152,11 @@ def testUpdateEnvironment(self):
152152
self.assertEqual(environment.node_id, "EN_kwDOHKhL9c4cA96j")
153153
self.assertEqual(
154154
environment.url,
155-
"https://api.github.com/repos/alson/PyGithub/environments/test",
155+
"https://api.github.com/repos/alson/PyGithub/environments/test/env",
156156
)
157157
self.assertEqual(
158158
environment.html_url,
159-
"https://github.com/alson/PyGithub/deployments/activity_log?environments_filter=test",
159+
"https://github.com/alson/PyGithub/deployments/activity_log?environments_filter=test%2Fenv",
160160
)
161161
self.assertEqual(
162162
environment.created_at,

tests/ReplayData/Environment.testCreateEnvironment.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ https
22
PUT
33
api.github.com
44
None
5-
/repos/alson/PyGithub/environments/test
5+
/repos/alson/PyGithub/environments/test%2Fenv
66
{'Authorization': 'token private_token_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'}
77
{"wait_timer": 0, "reviewers": [], "deployment_branch_policy": null, "prevent_self_review": false}
88
200
99
[('Server', 'GitHub.com'), ('Date', 'Tue, 19 Apr 2022 14:04:32 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding, Accept, X-Requested-With'), ('ETag', 'W/"551b8efc5f64e50c7aea535a16d55561640ea5649a4e1d349ed09f953cae96da"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('github-authentication-token-expiration', '2022-05-13 15:01:13 UTC'), ('X-GitHub-Media-Type', 'github.v3; format=json'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4993'), ('X-RateLimit-Reset', '1650380634'), ('X-RateLimit-Used', '7'), ('X-RateLimit-Resource', 'core'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '0'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'C759:D172:956268:97BB8A:625EC170')]
10-
{"id":470015651,"node_id":"EN_kwDOHKhL9c4cA96j","name":"test","url":"https://api.github.com/repos/alson/PyGithub/environments/test","html_url":"https://github.com/alson/PyGithub/deployments/activity_log?environments_filter=test","created_at":"2022-04-19T14:04:32Z","updated_at":"2022-04-19T14:04:32Z","protection_rules":[],"deployment_branch_policy":null}
10+
{"id":470015651,"node_id":"EN_kwDOHKhL9c4cA96j","name":"test/env","url":"https://api.github.com/repos/alson/PyGithub/environments/test/env","html_url":"https://github.com/alson/PyGithub/deployments/activity_log?environments_filter=test%2Fenv","created_at":"2022-04-19T14:04:32Z","updated_at":"2022-04-19T14:04:32Z","can_admins_bypass":true,"protection_rules":[],"deployment_branch_policy":null}

0 commit comments

Comments
 (0)