diff --git a/core/js/js.js b/core/js/js.js
index e0adc3591ac1f72e5a828404d4bd60f4900e770f..ff52c8f1f3903674d6a82e4817c57154648ed212 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -325,6 +325,48 @@ var OC={
 		return path.replace(/\\/g,'/').replace(/\/[^\/]*$/, '');
 	},
 
+	/**
+	 * Join path sections
+	 *
+	 * @param {...String} path sections
+	 *
+	 * @return {String} joined path, any leading or trailing slash
+	 * will be kept
+	 *
+	 * @since 8.2
+	 */
+	joinPaths: function() {
+		if (arguments.length < 1) {
+			return '';
+		}
+		var path = '';
+		var lastArg = arguments[arguments.length - 1];
+		var leadingSlash = arguments[0].charAt(0) === '/';
+		var trailingSlash = lastArg.charAt(lastArg.length - 1) === '/';
+		var sections = [];
+		var i;
+		for (i = 0; i < arguments.length; i++) {
+			sections = sections.concat(arguments[i].split('/'));
+		}
+		var first = !leadingSlash;
+		for (i = 0; i < sections.length; i++) {
+			if (sections[i] !== '') {
+				if (first) {
+					first = false;
+				} else {
+					path += '/';
+				}
+				path += sections[i];
+			}
+		}
+
+		if (trailingSlash) {
+			// add it back
+			path += '/';
+		}
+		return path;
+	},
+
 	/**
 	 * Do a search query and display the results
 	 * @param {string} query the search query
diff --git a/core/js/tests/specs/coreSpec.js b/core/js/tests/specs/coreSpec.js
index 6f7a34d21c8e87caad1712e96fe871700d4288e2..5caaf109bf11f9cb8398e44439bbdf20dcf41b27 100644
--- a/core/js/tests/specs/coreSpec.js
+++ b/core/js/tests/specs/coreSpec.js
@@ -134,6 +134,44 @@ describe('Core base tests', function() {
 			expect(escapeHTML('This is a good string without HTML.')).toEqual('This is a good string without HTML.');
 		});
 	});
+	describe('joinPaths', function() {
+		it('returns empty string with no or empty arguments', function() {
+			expect(OC.joinPaths()).toEqual('');
+			expect(OC.joinPaths('')).toEqual('');
+			expect(OC.joinPaths('', '')).toEqual('');
+		});
+		it('returns joined path sections', function() {
+			expect(OC.joinPaths('abc')).toEqual('abc');
+			expect(OC.joinPaths('abc', 'def')).toEqual('abc/def');
+			expect(OC.joinPaths('abc', 'def', 'ghi')).toEqual('abc/def/ghi');
+		});
+		it('keeps leading slashes', function() {
+			expect(OC.joinPaths('/abc')).toEqual('/abc');
+			expect(OC.joinPaths('/abc', 'def')).toEqual('/abc/def');
+			expect(OC.joinPaths('/abc', 'def', 'ghi')).toEqual('/abc/def/ghi');
+		});
+		it('keeps trailing slashes', function() {
+			expect(OC.joinPaths('abc/')).toEqual('abc/');
+			expect(OC.joinPaths('abc', 'def/')).toEqual('abc/def/');
+			expect(OC.joinPaths('abc', 'def', 'ghi/')).toEqual('abc/def/ghi/');
+		});
+		it('splits paths in specified strings and discards extra slashes', function() {
+			expect(OC.joinPaths('//abc//')).toEqual('/abc/');
+			expect(OC.joinPaths('//abc//def//')).toEqual('/abc/def/');
+			expect(OC.joinPaths('//abc//', '//def//')).toEqual('/abc/def/');
+			expect(OC.joinPaths('//abc//', '//def//', '//ghi//')).toEqual('/abc/def/ghi/');
+			expect(OC.joinPaths('//abc//def//', '//ghi//jkl/mno/', '//pqr//'))
+				.toEqual('/abc/def/ghi/jkl/mno/pqr/');
+		});
+		it('discards empty sections', function() {
+			expect(OC.joinPaths('abc', '', 'def')).toEqual('abc/def');
+		});
+		it('returns root if only slashes', function() {
+			expect(OC.joinPaths('//')).toEqual('/');
+			expect(OC.joinPaths('/', '/')).toEqual('/');
+			expect(OC.joinPaths('/', '//', '/')).toEqual('/');
+		});
+	});
 	describe('filePath', function() {
 		beforeEach(function() {
 			OC.webroot = 'http://localhost';