UNPKG

@jymfony/routing

Version:

Jymfony Routing component

392 lines (339 loc) 14.1 kB
import { stringify as qsStringify } from 'querystring'; const InvalidParameterException = Jymfony.Component.Routing.Exception.InvalidParameterException; const MissingMandatoryParametersException = Jymfony.Component.Routing.Exception.MissingMandatoryParametersException; const RouteNotFoundException = Jymfony.Component.Routing.Exception.RouteNotFoundException; const UrlGeneratorInterface = Jymfony.Component.Routing.Generator.UrlGeneratorInterface; const RequestContext = Jymfony.Component.Routing.RequestContext; const decodedChars = { '%2F': '/', '%40': '@', '%3A': ':', '%3B': ';', '%2C': ',', '%3D': '=', '%2B': '+', '%21': '!', '%2A': '*', '%7C': '|', }; /** * UrlGenerator can generate a URL or a path for any route in the RouteCollection * based on the passed parameters. * * @memberOf Jymfony.Component.Routing.Generator */ export default class UrlGenerator extends implementationOf(UrlGeneratorInterface) { /** * Constructor. * * @param {Jymfony.Component.Routing.RouteCollection} routeCollection * @param {Jymfony.Component.Routing.RequestContext} [context = new RequestContext()] * @param {string} [defaultLocale] */ __construct(routeCollection, context = new RequestContext(), defaultLocale = undefined) { /** * @type {Jymfony.Component.Routing.RouteCollection} * * @private */ this._routeCollection = routeCollection; /** * @type {Jymfony.Component.Routing.RequestContext} * * @private */ this._context = context; /** * @type {string} * * @private */ this._defaultLocale = defaultLocale; } /** * @inheritdoc */ withContext(request) { const isSecure = request.isSecure; const context = new RequestContext( request.method, request.host, request.scheme, isSecure ? 80 : (request.port || 80), isSecure ? (request.port || 443) : 443, request.pathInfo, qsStringify(request.query.all), ); return new __self(this._routeCollection, context, this._defaultLocale); } /** * @inheritdoc */ generate(name, parameters = {}, referenceType = UrlGeneratorInterface.ABSOLUTE_PATH) { let route = null; let locale = parameters._locale || this._context.getParameter('_locale') || this._defaultLocale; if (locale) { do { if ((route = this._routeCollection.get(name + '.' + locale)) && route.getDefault('_canonical_route') === name) { delete parameters._locale; break; } const idx = locale.indexOf('_'); if (-1 === idx) { break; } locale = locale.substr(idx); } while (locale); } if (undefined === (route = route || this._routeCollection.get(name))) { throw new RouteNotFoundException(__jymfony.sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', name)); } const compiledRoute = route.compile(); return this._doGenerate( compiledRoute.variables, route.defaults, compiledRoute.tokens, parameters, name, referenceType, compiledRoute.hostTokens, route.schemes ); } /** * Do generate an address from route components. * * @param {string[]} variables * @param {Object<string, string>} defaults * @param {string[][]} tokens * @param {Object<string, *>} parameters * @param {string} name * @param {int} referenceType * @param {string[][]} hostTokens * @param {string[]} requiredSchemes * * @returns {string} * * @private */ _doGenerate(variables, defaults, tokens, parameters, name, referenceType, hostTokens, requiredSchemes = []) { const mergedParams = Object.assign({}, defaults, parameters); const diff = variables.filter(name => !mergedParams.hasOwnProperty(name)); if (diff.length) { throw new MissingMandatoryParametersException(__jymfony.sprintf( 'Some mandatory parameters are missing ("%s") to generate a URL for route "%s".', diff.join('", "'), name )); } let url = ''; let optional = true; const message = 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.'; for (const token of tokens) { if ('variable' === token[0]) { let varName = token[3]; const important = '!' === varName[0]; if (important) { varName = varName.substr(1); } if (!optional || important || !defaults.hasOwnProperty(token[3]) || undefined !== mergedParams[varName] && String(mergedParams[varName]) !== String(defaults[varName])) { const regex = new RegExp('^' + token[2] + '$', !!token[4] ? 'u' : ''); if (! regex.test(mergedParams[varName])) { throw new InvalidParameterException( __jymfony.strtr(message, { '{parameter}': varName, '{route}': name, '{expected}': token[2], '{given}': mergedParams[varName], }) ); } url = token[1] + mergedParams[varName] + url; optional = false; } } else { url = token[1] + url; optional = false; } } if ('' === url) { url = '/'; } url = __jymfony.strtr( encodeURIComponent(url) .replace(/'/g, '%27') .replace(/\(/g, '%28') .replace(/\)/g, '%29'), decodedChars ); // The path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3 // So we need to encode them as they are not used for this purpose here // Otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route url = __jymfony.strtr(url, {'/../': '/%2E%2E/', '/./': '/%2E/'}); if ('/..' === url.substr(-3)) { url = url.substr(0, -2) + '%2E%2E'; } else if ('/.' === url.substr(-2)) { url = url.substr(0, -1) + '%2E'; } let schemeAuthority = ''; let host = this._context.host; if (!! host) { let scheme = this._context.scheme; if (requiredSchemes.length) { if (-1 === requiredSchemes.indexOf(scheme)) { referenceType = UrlGenerator.ABSOLUTE_URL; scheme = requiredSchemes[0]; } } if (hostTokens.length) { let routeHost = ''; for (const token of hostTokens) { if ('variable' === token[0]) { const regex = new RegExp('^' + token[2] + '$', !!token[4] ? 'u' : ''); if (! regex.test(mergedParams[token[3]])) { throw new InvalidParameterException( __jymfony.strtr(message, { '{parameter}': token[3], '{route}': name, '{expected}': token[2], '{given}': mergedParams[token[3]], }) ); } routeHost = token[1] + mergedParams[token[3]] + routeHost; } else { routeHost = token[1] + routeHost; } } if (routeHost !== host) { host = routeHost; if (UrlGenerator.ABSOLUTE_URL !== referenceType) { referenceType = UrlGenerator.NETWORK_PATH; } } } if (UrlGenerator.ABSOLUTE_URL === referenceType || UrlGenerator.NETWORK_PATH === referenceType) { let port = ''; if ('http' === scheme && 80 !== this._context.httpPort) { port += ':' + this._context.httpPort; } else if ('https' === scheme && 443 !== this._context.httpsPort) { port += ':' + this._context.httpsPort; } schemeAuthority = UrlGenerator.NETWORK_PATH === referenceType ? '//' : `${scheme}://`; schemeAuthority += host + port; } } if (UrlGenerator.RELATIVE_PATH === referenceType) { url = UrlGenerator.getRelativePath(this._context.pathinfo, url); } else { url = schemeAuthority + url; } // Add a query string if needed const extras = Object.keys(parameters) .filter(name => { if (-1 !== variables.indexOf(name)) { return false; } if (Object.prototype.hasOwnProperty.call(defaults, name)) { return __jymfony.equal(defaults[name], parameters[name], false); } return true; }); // Extract fragment let fragment = ''; if (defaults._fragment) { fragment = defaults._fragment; } let idx; if (-1 !== (idx = extras.indexOf('_fragment'))) { fragment = parameters._fragment; delete extras[idx]; } if (extras.length) { const toHashTable = (obj) => { if (! isObjectLiteral(obj) && ! isArray(obj)) { return obj; } const table = new HashTable(); for (const [ k, v ] of __jymfony.getEntries(obj)) { table.put(k, toHashTable(v)); } return table; }; const toQuery = (key, value, base = '') => { if (value instanceof HashTable) { return [ ...value ] .map(el => toQuery(el[0], el[1], base ? base + '[' + key + ']' : key)) .join('&'); } return encodeURIComponent(base ? base + '[' + key + ']' : key) + '=' + encodeURIComponent(value); }; const ht = toHashTable(Object.keys( parameters ) .filter( key => -1 !== extras.indexOf(key) ) .reduce( (res, key) => (res[key] = parameters[key], res), {} )); const query = Array.from(ht) .map(el => toQuery(el[0], el[1])) .join('&'); url += '?' + __jymfony.strtr(query, {'%2F': '/'}); } if ('' !== fragment) { fragment = encodeURIComponent(fragment) .replace(/!/g, '%21') .replace(/'/g, '%27') .replace(/\(/g, '%28') .replace(/\)/g, '%29') .replace(/\*/g, '%2A'); url += '#' + __jymfony.strtr(fragment, {'%2F': '/', '%3F': '?'}); } return url; } /** * Returns the target path as relative reference from the base path. * * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash. * Both paths must be absolute and not contain relative parts. * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives. * Furthermore, they can be used to reduce the link size in documents. * * Example target paths, given a base path of "/a/b/c/d": * - "/a/b/c/d" -> "" * - "/a/b/c/" -> "./" * - "/a/b/" -> "../" * - "/a/b/c/other" -> "other" * - "/a/x/y" -> "../../x/y" * * @param {string} basePath The base path * @param {string} targetPath The target path * * @returns {string} The relative target path */ static getRelativePath(basePath, targetPath) { if (basePath === targetPath) { return ''; } const sourceDirs = ('/' === basePath.charAt(0) ? basePath.substr(1) : basePath).split('/'); const targetDirs = ('/' === targetPath.charAt(0) ? targetPath.substr(1) : targetPath).split('/'); sourceDirs.pop(); const targetFile = targetDirs.pop(); for (const [ i, dir ] of __jymfony.getEntries(sourceDirs)) { if (targetDirs[i] && dir === targetDirs[i]) { delete sourceDirs[i]; delete targetDirs[i]; } else { break; } } targetDirs.push(targetFile); const path = '../'.repeat(sourceDirs.length) + targetDirs.join('/'); // A reference to the same base directory or an empty subdirectory must be prefixed with "./". // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used // As the first segment of a relative-path reference, as it would be mistaken for a scheme name // (see http://tools.ietf.org/html/rfc3986#section-4.2). let colonPos, slashPos; return '' === path || '/' === path.charAt(0) || -1 !== (colonPos = path.indexOf(':')) && (colonPos < (slashPos = path.indexOf('/')) || -1 === slashPos) ? `./${path}` : path; } }