( function ( mw, $ ) { /* eslint no-underscore-dangle: "off" */ /** * URI Processor for RCFilters * * @class * * @constructor * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model * @param {Object} [config] Configuration object * @cfg {boolean} [normalizeTarget] Dictates whether or not to go through the * title normalization to separate title subpage/parts into the target= url * parameter */ mw.rcfilters.UriProcessor = function MwRcfiltersController( filtersModel, config ) { config = config || {}; this.filtersModel = filtersModel; this.normalizeTarget = !!config.normalizeTarget; }; /* Initialization */ OO.initClass( mw.rcfilters.UriProcessor ); /* Static methods */ /** * Replace the url history through replaceState * * @param {mw.Uri} newUri New URI to replace */ mw.rcfilters.UriProcessor.static.replaceState = function ( newUri ) { window.history.replaceState( { tag: 'rcfilters' }, document.title, newUri.toString() ); }; /** * Push the url to history through pushState * * @param {mw.Uri} newUri New URI to push */ mw.rcfilters.UriProcessor.static.pushState = function ( newUri ) { window.history.pushState( { tag: 'rcfilters' }, document.title, newUri.toString() ); }; /* Methods */ /** * Get the version that this URL query is tagged with. * * @param {Object} [uriQuery] URI query * @return {number} URL version */ mw.rcfilters.UriProcessor.prototype.getVersion = function ( uriQuery ) { uriQuery = uriQuery || new mw.Uri().query; return Number( uriQuery.urlversion || 1 ); }; /** * Get an updated mw.Uri object based on the model state * * @param {mw.Uri} [uri] An external URI to build the new uri * with. This is mainly for tests, to be able to supply external query * parameters and make sure they are retained. * @return {mw.Uri} Updated Uri */ mw.rcfilters.UriProcessor.prototype.getUpdatedUri = function ( uri ) { var normalizedUri = this._normalizeTargetInUri( uri || new mw.Uri() ), unrecognizedParams = this.getUnrecognizedParams( normalizedUri.query ); normalizedUri.query = this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {}, normalizedUri.query, // The representation must be expanded so it can // override the uri query params but we then output // a minimized version for the entire URI representation // for the method this.filtersModel.getExpandedParamRepresentation() ) ); // Reapply unrecognized params and url version normalizedUri.query = $.extend( true, {}, normalizedUri.query, unrecognizedParams, { urlversion: '2' } ); return normalizedUri; }; /** * Move the subpage to the target parameter * * @param {mw.Uri} uri * @return {mw.Uri} * @private */ mw.rcfilters.UriProcessor.prototype._normalizeTargetInUri = function ( uri ) { var parts, // matches [/wiki/]SpecialNS:RCL/[Namespace:]Title/Subpage/Subsubpage/etc re = /^((?:\/.+?\/)?.*?:.*?)\/(.*)$/; if ( !this.normalizeTarget ) { return uri; } // target in title param if ( uri.query.title ) { parts = uri.query.title.match( re ); if ( parts ) { uri.query.title = parts[ 1 ]; uri.query.target = parts[ 2 ]; } } // target in path parts = mw.Uri.decode( uri.path ).match( re ); if ( parts ) { uri.path = parts[ 1 ]; uri.query.target = parts[ 2 ]; } return uri; }; /** * Get an object representing given parameters that are unrecognized by the model * * @param {Object} params Full params object * @return {Object} Unrecognized params */ mw.rcfilters.UriProcessor.prototype.getUnrecognizedParams = function ( params ) { // Start with full representation var givenParamNames = Object.keys( params ), unrecognizedParams = $.extend( true, {}, params ); // Extract unrecognized parameters Object.keys( this.filtersModel.getEmptyParameterState() ).forEach( function ( paramName ) { // Remove recognized params if ( givenParamNames.indexOf( paramName ) > -1 ) { delete unrecognizedParams[ paramName ]; } } ); return unrecognizedParams; }; /** * Update the URL of the page to reflect current filters * * This should not be called directly from outside the controller. * If an action requires changing the URL, it should either use the * highlighting actions below, or call #updateChangesList which does * the uri corrections already. * * @param {Object} [params] Extra parameters to add to the API call */ mw.rcfilters.UriProcessor.prototype.updateURL = function ( params ) { var currentUri = new mw.Uri(), updatedUri = this.getUpdatedUri(); updatedUri.extend( params || {} ); if ( this.getVersion( currentUri.query ) !== 2 || this.isNewState( currentUri.query, updatedUri.query ) ) { this.constructor.static.replaceState( updatedUri ); } }; /** * Update the filters model based on the URI query * This happens on initialization, and from this moment on, * we consider the system synchronized, and the model serves * as the source of truth for the URL. * * This methods should only be called once on initialization. * After initialization, the model updates the URL, not the * other way around. * * @param {Object} [uriQuery] URI query */ mw.rcfilters.UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) { uriQuery = uriQuery || this._normalizeTargetInUri( new mw.Uri() ).query; this.filtersModel.updateStateFromParams( this._getNormalizedQueryParams( uriQuery ) ); }; /** * Compare two URI queries to decide whether they are different * enough to represent a new state. * * @param {Object} currentUriQuery Current Uri query * @param {Object} updatedUriQuery Updated Uri query * @return {boolean} This is a new state */ mw.rcfilters.UriProcessor.prototype.isNewState = function ( currentUriQuery, updatedUriQuery ) { var currentParamState, updatedParamState, notEquivalent = function ( obj1, obj2 ) { var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) ); return keys.some( function ( key ) { return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq } ); }; // Compare states instead of parameters // This will allow us to always have a proper check of whether // the requested new url is one to change or not, regardless of // actual parameter visibility/representation in the URL currentParamState = $.extend( true, {}, this.filtersModel.getMinimizedParamRepresentation( currentUriQuery ), this.getUnrecognizedParams( currentUriQuery ) ); updatedParamState = $.extend( true, {}, this.filtersModel.getMinimizedParamRepresentation( updatedUriQuery ), this.getUnrecognizedParams( updatedUriQuery ) ); return notEquivalent( currentParamState, updatedParamState ); }; /** * Check whether the given query has parameters that are * recognized as parameters we should load the system with * * @param {mw.Uri} [uriQuery] Given URI query * @return {boolean} Query contains valid recognized parameters */ mw.rcfilters.UriProcessor.prototype.doesQueryContainRecognizedParams = function ( uriQuery ) { var anyValidInUrl, validParameterNames = Object.keys( this.filtersModel.getEmptyParameterState() ); uriQuery = uriQuery || new mw.Uri().query; anyValidInUrl = Object.keys( uriQuery ).some( function ( parameter ) { return validParameterNames.indexOf( parameter ) > -1; } ); // URL version 2 is allowed to be empty or within nonrecognized params return anyValidInUrl || this.getVersion( uriQuery ) === 2; }; /** * Get the adjusted URI params based on the url version * If the urlversion is not 2, the parameters are merged with * the model's defaults. * Always merge in the hidden parameter defaults. * * @private * @param {Object} uriQuery Current URI query * @return {Object} Normalized parameters */ mw.rcfilters.UriProcessor.prototype._getNormalizedQueryParams = function ( uriQuery ) { // Check whether we are dealing with urlversion=2 // If we are, we do not merge the initial request with // defaults. Not having urlversion=2 means we need to // reproduce the server-side request and merge the // requested parameters (or starting state) with the // wiki default. // Any subsequent change of the URL through the RCFilters // system will receive 'urlversion=2' var base = this.getVersion( uriQuery ) === 2 ? {} : this.filtersModel.getDefaultParams(); return $.extend( true, {}, this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {}, base, uriQuery ) ), { urlversion: '2' } ); }; }( mediaWiki, jQuery ) );