jii
Version:
Jii - Full-Stack JavaScript Framework
316 lines (278 loc) • 10.9 kB
JavaScript
/**
* @author <a href="http://www.affka.ru">Vladimir Kozhin</a>
* @license MIT
*/
;
const Jii = require('../index');
const UrlRule = require('./UrlRule');
const Url = require('../helpers/Url');
const HttpRequest = require('../base/HttpRequest');
const _trimStart = require('lodash/trimStart');
const _isObject = require('lodash/isObject');
const _isEmpty = require('lodash/isEmpty');
const _isString = require('lodash/isString');
const _each = require('lodash/each');
const _has = require('lodash/has');
const _cloneDeep = require('lodash/cloneDeep');
const Component = require('../base/Component');
class UrlManager extends Component {
preInit() {
this._hostInfo = null;
this._baseUrl = null;
/**
* The default configuration of URL rules. Individual rule configurations
* specified via [[rules]] will take precedence when the same property of the rule is configured.
* @type {object}
*/
this.ruleConfig = {
className: UrlRule
};
/**
* The cache object or the application component ID of the cache object.
* Compiled URL rules will be cached through this cache object, if it is available.
*
* After the UrlManager object is created, if you want to change this property,
* you should only assign it with a cache object.
* Set this property to null if you do not want to cache the URL rules.
* @type {Cache|string}
*/
this.cache = 'cache';
/**
* The URL suffix used when in 'path' format.
* For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
* @type {string}
*/
this.suffix = null;
/**
* Each element in the array
* is the configuration array for creating a single URL rule. The configuration will
* be merged with [[ruleConfig]] first before it is used for creating the rule object.
*
* A special shortcut format can be used if a rule only specifies [[UrlRule::pattern|pattern]]
* and [[UrlRule::route|route]]: `'pattern': 'route'`. That is, instead of using a configuration
* array, one can use the key to represent the pattern and the value the corresponding route.
* For example, `'post/<id:\d+>': 'post/view'`.
*
* For RESTful routing the mentioned shortcut format also allows you to specify the
* [[UrlRule::verb|HTTP verb]] that the rule should apply for.
* You can do that by prepending it to the pattern, separated by space.
* For example, `'PUT post/<id:\d+>': 'post/update'`.
* You may specify multiple verbs by separating them with comma
* like this: `'POST,PUT post/index': 'post/create'`.
* The supported verbs in the shortcut format are: GET, HEAD, POST, PUT, PATCH and DELETE.
* Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way
* so you normally would not specify a verb for normal GET request.
*
* Here is an example configuration for RESTful CRUD controller:
*
* ~~~
* {
* 'dashboard': 'site/index',
*
* 'POST <controller:\w+>s': '<controller>/create',
* <controller:\w+>s': '<controller>/index',
*
* 'PUT <controller:\w+>/<id:\d+>' : '<controller>/update',
* 'DELETE <controller:\w+>/<id:\d+>': '<controller>/delete',
* '<controller:\w+>/<id:\d+>' : '<controller>/view'
* }
* ~~~
*
* Note that if you modify this property after the UrlManager object is created, make sure
* you populate the array with rule objects instead of rule configurations.
* @type {object}
*/
this.rules = {};
/**
* Whether to enable strict parsing. If strict parsing is enabled, the incoming
* requested URL must match at least one of the [[rules]] in order to be treated as a valid request.
* Otherwise, the path info part of the request will be treated as the requested route.
* @type {boolean}
*/
this.enableStrictParsing = false;
/**
* Instance with request data
* @type {BaseRequest}
*/
this.request = null;
super.preInit(...arguments);
}
static buildQuery(obj, tempKey) {
var outputs = [];
var cleanRegexp = /[!'()*]/g;
_each(obj, (value, key) => {
key = encodeURIComponent(key.replace(cleanRegexp, encodeURIComponent));
if (tempKey) {
key = tempKey + '[' + key + ']';
}
if (_isObject(value)) {
outputs.push(this.constructor.buildQuery(value, key));
} else {
value = encodeURIComponent(value.toString().replace(cleanRegexp, encodeURIComponent));
outputs.push(key + '=' + value);
}
});
return outputs.join('&');
}
/**
* Parses the URL rules.
*/
_compileRules() {
if (_isEmpty(this.rules)) {
return;
}
if (_isString(this.cache)) {
}
// @todo Cache
/*if (this.cache instanceof Cache) {
key = __CLASS__;
hash = md5(json_encode(this.rules));
if ((data = this.cache.get(key)) !== false && isset(data[1]) && data[1] === hash) {
this.rules = data[0];
return;
}
}*/
var rules = [];
var verbRegexp = /^((?:(GET|HEAD|POST|PUT|PATCH|DELETE),)*(GET|HEAD|POST|PUT|PATCH|DELETE))\s+(.*)/;
_each(this.rules, (rule, key) => {
if (!_isObject(rule)) {
rule = {
route: rule
};
var matches = verbRegexp.exec(key);
if (matches !== null) {
// TODO: Here something happening. Need document.
rule.verb = matches[1].split(',');
rule.mode = UrlRule.PARSING_ONLY;
key = matches[4];
}
rule.pattern = key;
}
var ruleConfig = Jii.mergeConfigs(this.ruleConfig, rule);
rules.push(Jii.createObject(ruleConfig));
});
this.rules = rules;
// @todo Cache
/*if (isset(key, hash)) {
this.cache.set(key, [this.rules, hash]);
}*/
}
/**
* Parses the user request.
* @param {BaseRequest} request the request component
* @return {array|boolean} the route and the associated parameters. The latter is always empty
*/
parseRequest(request) {
var result = false;
/**
* @type {UrlRule} rule
*/
_each(this.rules, rule => {
if (result === false) {
result = rule.parseRequest(this, request);
}
});
if (result !== false) {
return result;
}
if (this.enableStrictParsing) {
return false;
}
//Yii::trace('No matching URL rules. Using default URL parsing logic.', __METHOD__);
var pathInfo = request.getPathInfo();
if (this.suffix !== null && pathInfo !== '') {
var n = this.suffix.length;
if (pathInfo.substr(-1 * n) === this.suffix) {
pathInfo = pathInfo.substr(0, pathInfo.length - n);
if (pathInfo === '') {
// suffix alone is not allowed
return false;
}
} else {
// suffix doesn't match
return false;
}
}
return [
pathInfo,
[]
];
}
/**
* Creates a URL using the given route and parameters.
* The URL created is a relative one. Use [[createAbsoluteUrl()]] to create an absolute URL.
* @param {string|array|object} route the route
* @param {Context} [context]
* @return {string} the created URL
*/
createUrl(route, context) {
route = _cloneDeep(route);
var parts = Url.parseRoute(route, context);
var params = parts.params;
route = parts.route;
var anchor = _has(params, '#') ? '#'.params['#'] : '';
delete params['#'];
var baseUrl = context && context.request instanceof HttpRequest ? context.request.getBaseUrl() : '';
var url = false;
/** @type {UrlRule} rule */
_each(this.rules, rule => {
if (url !== false) {
return;
}
url = rule.createUrl(this, route, params);
if (url === false) {
return;
}
if (rule.host !== null) {
var index = url.indexOf('/', 8);
if (baseUrl !== '' && index !== -1) {
url = url.substr(0, index) + baseUrl + url.substr(index);
} else {
url = url + baseUrl + anchor;
}
} else {
url = baseUrl + url + anchor;
}
return false;
});
if (url !== false) {
return url;
}
if (this.suffix !== null) {
route += this.suffix;
}
if (!_isEmpty(params)) {
route += '?' + this.constructor.buildQuery(params);
}
return baseUrl + route + anchor;
}
/**
* Creates an absolute URL using the given route and parameters.
* This method prepends the URL created by [[createUrl()]] with the [[hostInfo]].
* @param {string|array|object} route the route
* @param {Context} [context]
* @param {boolean|string} [scheme]
* @return {string} the created URL
* @see createUrl()
*/
createAbsoluteUrl(route, context, scheme) {
var url = this.createUrl(route, context);
if (url.indexOf('://') === -1 && context && context.request instanceof HttpRequest) {
url = context.request.getHostInfo() + url;
} else {
url = '/' + _trimStart(url, '/');
}
if (_isString(scheme)) {
}
return url;
}
/**
* set rules
* @param {array} rules
*/
setRules(rules){
this.rules = rules;
this._compileRules();
}
}
module.exports = UrlManager;