Skip to content

Commit e5652c1

Browse files
authored
Fix bug with symlink creation (#3028)
* Fix bug with symlink creation Previously, this was executing a command using a shell, but without properly escaping the input. This instead passes a list in to Popen, to ensure escape is working properly. * More refactoring * Fix argument missed in refactor
1 parent 64b4ec3 commit e5652c1

File tree

3 files changed

+82
-16
lines changed

3 files changed

+82
-16
lines changed

readthedocs/core/symlink.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,11 @@ def symlink_cnames(self, domain=None):
157157

158158
# CNAME to doc root
159159
symlink = os.path.join(self.CNAME_ROOT, dom.domain)
160-
run('ln -nsf {0} {1}'.format(self.project_root, symlink))
160+
run(['ln', '-nsf', self.project_root, symlink])
161161

162162
# Project symlink
163163
project_cname_symlink = os.path.join(self.PROJECT_CNAME_ROOT, dom.domain)
164-
run('ln -nsf %s %s' % (self.project.doc_path, project_cname_symlink))
164+
run(['ln', '-nsf', self.project.doc_path, project_cname_symlink])
165165

166166
def remove_symlink_cname(self, domain):
167167
"""Remove CNAME symlink"""
@@ -198,7 +198,13 @@ def symlink_subprojects(self):
198198
symlink_dir = os.sep.join(symlink.split(os.path.sep)[:-1])
199199
if not os.path.lexists(symlink_dir):
200200
safe_makedirs(symlink_dir)
201-
run('ln -nsf %s %s' % (docs_dir, symlink))
201+
# TODO this should use os.symlink, not a call to shell. For now,
202+
# this passes command as a list to be explicit about escaping
203+
# characters like spaces.
204+
status, _, stderr = run(['ln', '-nsf', docs_dir, symlink])
205+
if status > 0:
206+
log.error('Could not symlink path: status=%d error=%s',
207+
status, stderr)
202208

203209
# Remove old symlinks
204210
if os.path.exists(self.subproject_root):
@@ -228,7 +234,7 @@ def symlink_translations(self):
228234
self._log(u"Symlinking translation: {0}->{1}".format(language, slug))
229235
symlink = os.path.join(self.project_root, language)
230236
docs_dir = os.path.join(self.WEB_ROOT, slug, language)
231-
run('ln -nsf {0} {1}'.format(docs_dir, symlink))
237+
run(['ln', '-nsf', docs_dir, symlink])
232238

233239
# Remove old symlinks
234240
for lang in os.listdir(self.project_root):
@@ -259,7 +265,7 @@ def symlink_single_version(self):
259265
if version is not None:
260266
docs_dir = os.path.join(settings.DOCROOT, self.project.slug,
261267
'rtd-builds', version.slug)
262-
run('ln -nsf %s/ %s' % (docs_dir, symlink))
268+
run(['ln', '-nsf', docs_dir, symlink])
263269

264270
def symlink_versions(self):
265271
"""Symlink project's versions
@@ -279,7 +285,7 @@ def symlink_versions(self):
279285
self._log(u"Symlinking Version: %s" % version)
280286
symlink = os.path.join(version_dir, version.slug)
281287
docs_dir = os.path.join(settings.DOCROOT, self.project.slug, 'rtd-builds', version.slug)
282-
run('ln -nsf {0} {1}'.format(docs_dir, symlink))
288+
run(['ln', '-nsf', docs_dir, symlink])
283289
versions.add(version.slug)
284290

285291
# Remove old symlinks

readthedocs/projects/utils.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from httplib2 import Http
1111

1212
import redis
13+
import six
1314
from django.conf import settings
1415
from django.core.cache import cache
1516

@@ -42,9 +43,13 @@ def find_file(filename):
4243
return matches
4344

4445

45-
def run(*commands, **kwargs):
46+
def run(*commands):
4647
"""Run one or more commands
4748
49+
Each argument in `commands` can be passed as a string or as a list. Passing
50+
as a list is the preferred method, as space escaping is more explicit and it
51+
avoids the need for executing anything in a shell.
52+
4853
If more than one command is given, then this is equivalent to
4954
chaining them together with ``&&``; if all commands succeed, then
5055
``(status, out, err)`` will represent the last successful command.
@@ -66,19 +71,27 @@ def run(*commands, **kwargs):
6671
cwd = os.getcwd()
6772
if not commands:
6873
raise ValueError("run() requires one or more command-line strings")
69-
shell = kwargs.get('shell', False)
7074

7175
for command in commands:
72-
if shell:
73-
log.info("Running commands in a shell")
74-
run_command = command
75-
else:
76+
# If command is a string, split it up by spaces to pass into Popen.
77+
# Otherwise treat the command as an iterable.
78+
if isinstance(command, six.string_types):
7679
run_command = command.split()
77-
log.info("Running: '%s' [%s]", command, cwd)
80+
else:
81+
try:
82+
run_command = list(command)
83+
command = ' '.join(command)
84+
except TypeError:
85+
run_command = command
86+
log.info('Running command: cwd=%s command=%s', cwd, command)
7887
try:
79-
p = subprocess.Popen(run_command, shell=shell, cwd=cwd,
80-
stdout=subprocess.PIPE,
81-
stderr=subprocess.PIPE, env=environment)
88+
p = subprocess.Popen(
89+
run_command,
90+
cwd=cwd,
91+
stdout=subprocess.PIPE,
92+
stderr=subprocess.PIPE,
93+
env=environment
94+
)
8295

8396
out, err = p.communicate()
8497
ret = p.returncode

readthedocs/rtd_tests/tests/test_project_symlinks.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,53 @@ def test_subproject_alias(self):
322322
filesystem['private_web_root'] = public_root
323323
self.assertFilesystem(filesystem)
324324

325+
def test_subproject_alias_with_spaces(self):
326+
"""Symlink pass adds symlink for subproject alias"""
327+
self.project.add_subproject(self.subproject, alias='Sweet Alias')
328+
self.symlink.symlink_subprojects()
329+
filesystem = {
330+
'private_cname_project': {},
331+
'private_cname_root': {},
332+
'private_web_root': {
333+
'kong': {'en': {}},
334+
'sub': {'en': {}},
335+
},
336+
'public_cname_project': {},
337+
'public_cname_root': {},
338+
'public_web_root': {
339+
'kong': {
340+
'en': {'latest': {
341+
'type': 'link',
342+
'target': 'user_builds/kong/rtd-builds/latest',
343+
}},
344+
'projects': {
345+
'sub': {
346+
'type': 'link',
347+
'target': 'public_web_root/sub',
348+
},
349+
'Sweet Alias': {
350+
'type': 'link',
351+
'target': 'public_web_root/sub',
352+
},
353+
}
354+
},
355+
'sub': {
356+
'en': {'latest': {
357+
'type': 'link',
358+
'target': 'user_builds/sub/rtd-builds/latest',
359+
}}
360+
}
361+
}
362+
}
363+
if self.privacy == 'private':
364+
public_root = filesystem['public_web_root'].copy()
365+
private_root = filesystem['private_web_root'].copy()
366+
public_root['kong']['projects']['sub']['target'] = 'private_web_root/sub'
367+
public_root['kong']['projects']['Sweet Alias']['target'] = 'private_web_root/sub'
368+
filesystem['public_web_root'] = private_root
369+
filesystem['private_web_root'] = public_root
370+
self.assertFilesystem(filesystem)
371+
325372
def test_remove_subprojects(self):
326373
"""Nonexistant subprojects are unlinked"""
327374
self.project.add_subproject(self.subproject)

0 commit comments

Comments
 (0)