/********************************************************************************
 * Copyright (C) 2018 Red Hat, Inc. and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/ import { Emitter , Event } from '@theia/core/lib/common/event' ; import * as theia from '@theia/plugin' ; import { PLUGIN_RPC_CONTEXT , PreferenceRegistryExt , PreferenceRegistryMain , PreferenceData , PreferenceChangeExt } from '../common/plugin-api-rpc' ; import { RPCProtocol } from '../common/rpc-protocol' ; import { isObject , mixin } from '../common/types' ; import { Configuration , ConfigurationModel } from './preferences/configuration' ; import { WorkspaceExtImpl } from './workspace' ; import cloneDeep = require ( 'lodash.clonedeep' ) ; const injectionRe = /\b__proto__\b|\bconstructor\.prototype\b/ ; enum ConfigurationTarget { Global = 1 , Workspace = 2 , WorkspaceFolder = 3 } enum PreferenceScope { Default , User , Workspace , Folder } interface ConfigurationInspect < T > { key : string ; defaultValue ? : T ; globalValue ? : T ; workspaceValue ? : T ; workspaceFolderValue ? : T ; } function lookUp ( tree , key ) { if ( ! key ) { return ; } const parts = key . split ( '.' ) ; let node = tree ; for ( let i = 0 ; node && i < parts . length ; i ++ ) { node = node [ parts [ i ] ] ; } return node ; } export class PreferenceRegistryExtImpl implements PreferenceRegistryExt { private proxy : PreferenceRegistryMain ; private _preferences : Configuration ; private readonly _onDidChangeConfiguration = new Emitter < theia . ConfigurationChangeEvent > ( ) ; readonly onDidChangeConfiguration : Event < theia . ConfigurationChangeEvent > = this . _onDidChangeConfiguration . event ; constructor ( rpc , private readonly workspace ) { this . proxy = rpc . getProxy ( PLUGIN_RPC_CONTEXT . PREFERENCE_REGISTRY_MAIN ) ; } init ( data ) { this . _preferences = this . parse ( data ) ; } $acceptConfigurationChanged ( data , eventData ) { this . init ( data ) ; this . _onDidChangeConfiguration . fire ( this . toConfigurationChangeEvent ( eventData ) ) ; } getConfiguration ( section ? , resource ? , extensionId ? ) { resource = resource === null ? undefined : resource ; const preferences = this . toReadonlyValue ( section ? lookUp ( this . _preferences . getValue ( undefined , this . workspace , resource ) , section ) : this . _preferences . getValue ( undefined , this . workspace , resource ) ) ; const configuration = { has ( key ) { return typeof lookUp ( preferences , key ) !== 'undefined' ; } , get : < T > ( key , defaultValue ? ) => { const result = lookUp ( preferences , key ) ; if ( typeof result === 'undefined' ) { return defaultValue ; } else { let clonedConfig = undefined ; const cloneOnWriteProxy = ( target , accessor ) : any => { let clonedTarget = undefined ; const cloneTarget = ( ) => { clonedConfig = clonedConfig ? clonedConfig : cloneDeep ( preferences ) ; clonedTarget = clonedTarget ? clonedTarget : lookUp ( clonedConfig , accessor ) ; } ; if ( ! isObject ( target ) ) { return target ; } return new Proxy ( target , { get : ( targ , prop ) => { if ( typeof prop === 'string' && prop . toLowerCase ( ) === 'tojson' ) { cloneTarget ( ) ; return ( ) => clonedTarget ; } if ( clonedConfig ) { clonedTarget = clonedTarget ? clonedTarget : lookUp ( clonedConfig , accessor ) ; return clonedTarget [ prop ] ; } const res = targ [ prop ] ; if ( typeof prop === 'string' ) { return cloneOnWriteProxy ( res , `${ accessor }.${ prop }` ) ; } return res ; } , set : ( targ , prop , val ) => { cloneTarget ( ) ; clonedTarget [ prop ] = val ; return true ; } , deleteProperty : ( targ , prop ) => { cloneTarget ( ) ; delete clonedTarget [ prop ] ; return true ; } , defineProperty : ( targ , prop , descr ) => { cloneTarget ( ) ; Object . defineProperty ( clonedTarget , prop , descr ) ; return true ; } } ) ; } ; return cloneOnWriteProxy ( result , key ) ; } } , update : ( key , value , arg ? ) : PromiseLike < void > => { key = section ? `${ section }.${ key }` : key ; const resourceStr = resource ? resource . toString ( ) : undefined ; if ( typeof value !== 'undefined' ) { return this . proxy . $updateConfigurationOption ( arg , key , value , resourceStr ) ; } else { return this . proxy . $removeConfigurationOption ( arg , key , resourceStr ) ; } } , inspect : < T > ( key ) : ConfigurationInspect < T > | undefined => { key = section ? `${ section }.${ key }` : key ; resource = resource === null ? undefined : resource ; const result = cloneDeep ( this . _preferences . inspect < T > ( key , this . workspace , resource ) ) ; if ( ! result ) { return undefined ; } const configInspect = { key } ; if ( typeof result . default !== 'undefined' ) { configInspect . defaultValue = result . default ; } if ( typeof result . user !== 'undefined' ) { configInspect . globalValue = result . user ; } if ( typeof result . workspace !== 'undefined' ) { configInspect . workspaceValue = result . workspace ; } if ( typeof result . workspaceFolder !== 'undefined' ) { configInspect . workspaceFolderValue = result . workspaceFolder ; } return configInspect ; } } ; if ( typeof preferences === 'object' ) { mixin ( configuration , preferences , false ) ; } return Object . freeze ( configuration ) ; } private toReadonlyValue ( data ) { const readonlyProxy = ( target ) : any => isObject ( target ) ? new Proxy ( target , { get : ( targ , prop ) => readonlyProxy ( targ [ prop ] ) , set : ( targ , prop , val ) => { throw new Error ( `TypeError: Cannot assign to read only property '${ prop }' of object` ) ; } , deleteProperty : ( targ , prop ) => { throw new Error ( `TypeError: Cannot delete read only property '${ prop }' of object` ) ; } , defineProperty : ( targ , prop ) => { throw new Error ( `TypeError: Cannot define property '${ prop }' of a readonly object` ) ; } , setPrototypeOf : ( targ ) => { throw new Error ( 'TypeError: Cannot set prototype for a readonly object' ) ; } , isExtensible : ( ) => false , preventExtensions : ( ) => true } ) : target ; return readonlyProxy ( data ) ; } private parse ( data ) { const defaultConfiguration = this . getConfigurationModel ( data [ PreferenceScope . Default ] ) ; const userConfiguration = this . getConfigurationModel ( data [ PreferenceScope . User ] ) ; const workspaceConfiguration = this . getConfigurationModel ( data [ PreferenceScope . Workspace ] ) ; const folderConfigurations = { } as { [ resource ] : ConfigurationModel ; } ; Object . keys ( data [ PreferenceScope . Folder ] ) . forEach ( resource => { folderConfigurations [ resource ] = this . getConfigurationModel ( data [ PreferenceScope . Folder ] [ resource ] ) ; } ) ; return new Configuration ( defaultConfiguration , userConfiguration , workspaceConfiguration , folderConfigurations ) ; } private getConfigurationModel ( data ) { if ( ! data ) { return new ConfigurationModel ( ) ; } return new ConfigurationModel ( this . parseConfigurationData ( data ) , Object . keys ( data ) ) ; } private readonly OVERRIDE_PROPERTY = '\\[(.*)\\]$' ; private readonly OVERRIDE_PROPERTY_PATTERN = new RegExp ( this . OVERRIDE_PROPERTY ) ; private parseConfigurationData ( data ) { return Object . keys ( data ) . reduce ( ( result , key ) => { if ( injectionRe . test ( key ) ) { return result ; } const parts = key . split ( '.' ) ; let branch = result ; for ( let i = 0 ; i < parts . length ; i ++ ) { if ( i === parts . length - 1 ) { branch [ parts [ i ] ] = data [ key ] ; continue ; } if ( ! branch [ parts [ i ] ] ) { branch [ parts [ i ] ] = Object . create ( null ) ; } branch = branch [ parts [ i ] ] ; if ( i === 0 && this . OVERRIDE_PROPERTY_PATTERN . test ( parts [ i ] ) ) { branch [ key . substring ( parts [ 0 ] . length + 1 ) ] = data [ key ] ; break ; } } return result ; } , Object . create ( null ) ) ; } private toConfigurationChangeEvent ( eventData ) { return Object . freeze ( { affectsConfiguration : ( section , uri ? ) : boolean => { for ( const change of eventData ) { const tree = change . preferenceName . split ( '.' ) . reverse ( ) . reduce ( ( prevValue , curValue ) => ( { [ curValue ] : prevValue } ) , change . newValue ) ; return typeof lookUp ( tree , section ) !== 'undefined' ; } return false ; } } ) ; } }