مدیاویکی:Gadget-SettingsManager.js

    از ویکی پاسخ
    نسخهٔ تاریخ ‏۲۰ سپتامبر ۲۰۲۰، ساعت ۱۲:۳۴ توسط Nazarzadeh (بحث | مشارکت‌ها) (صفحه‌ای تازه حاوی «/** * commons:MediaWiki:Gadget-SettingsManager.js * Managing user preferences of scripts * Managing gadgets and gadget preferences...» ایجاد کرد)
    (تفاوت) → نسخهٔ قدیمی‌تر | نمایش نسخهٔ فعلی (تفاوت) | نسخهٔ جدیدتر ← (تفاوت)

    نکته: پس از انتشار ممکن است برای دیدن تغییرات نیاز باشد که حافظهٔ نهانی مرورگر خود را پاک کنید.

    • فایرفاکس / سافاری: کلید Shift را نگه دارید و روی دکمهٔ Reload کلیک کنید، یا کلید‌های Ctrl-F5 یا Ctrl-R را با هم فشار دهید (در رایانه‌های اپل مکینتاش کلید‌های ⌘-R)
    • گوگل کروم: کلیدهای Ctrl+Shift+R را با هم فشار دهید (در رایانه‌های اپل مکینتاش کلید‌های ⌘-Shift-R)
    • اینترنت اکسپلورر/ Edge: کلید Ctrl را نگه‌دارید و روی دکمهٔ Refresh کلیک کنید، یا کلید‌های Ctrl-F5 را با هم فشار دهید
    • اپرا: Ctrl-F5 را بفشارید.
    /**
     * [[:commons:MediaWiki:Gadget-SettingsManager.js]]
     * Managing user preferences of scripts
     * Managing gadgets and gadget preferences
     *
     * Use it for good, not for evil.
     *
     * @author Rillke, 2012
     * @license GPL v.3
     * <nowiki>
     */
     
    // List the global variables for jsHint-Validation.
    // Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting]
    /*global jQuery:false, mediaWiki:false*/
    
    // Set jsHint-options.
    /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:false, browser:true*/
    
    ( function ( $, mw ) {
    "use strict";
    
    // Different tokes exist only to confuse the user (at least in 2012)
    // All of them carry the same value except the watchlist token
    if (!mw.user.tokens.exists('preferencesToken')) mw.user.tokens.set('preferencesToken', mw.user.tokens.get('editToken'));
    
    /**
    * Refresh preferences-token
    *
    * @example
    *      refreshToken( function() { doGoodStuff.retry(); } );
    *
    * @param cb {Function} Callback function. The first argument supplied is whether the operation succeeded.
    * @context {closure} private function
    * @return {Object} a jQuery deferred-object-queue
    */
    var refreshToken = function(cb) {
    	var mwa = new mw.Api(),
    		apiDef = mwa.get( {
    			meta: 'userinfo',
    			uiprop: 'preferencestoken'
    		} );
    		
    	apiDef.done(function(result) {
    		if (!result.query || !result.query.userinfo) return cb( false, 'wrong-response' );
    		mw.user.tokens.set( 'preferencesToken', result.query.userinfo.preferencestoken );
    		cb( true );
    	});
    	apiDef.fail(function(code, result) {
    		if (!result.query || !result.query.userinfo) return cb( false, code );
    	});
    	return apiDef;
    };
    
    var firstItem = function(o) { 
    	for (var i in o) {
    		if (o.hasOwnProperty( i )) { 
    			return o[i]; 
    		} 
    	} 
    };
    
    var valByString = function(identifier) {
    	var arr = identifier.split( '.' ),
    		lenArr = arr.length,
    		i,
    		elemArr,
    		objCurrent = window;
    		
    	for (i = 0; i < lenArr; i++) {
    		elemArr = arr[i];
    		objCurrent = objCurrent[elemArr];
    	}
    	return objCurrent;
    };
    
    var mwPrefPrefix = 'userjs-sm-';
    
    var sm = {
    	version: '0.1.0.1',
    	errorPrefix: "SettingsWizard encountered a problem. We regret the inconvenience. ",
    	/**
    	* Constructor-method. Returns an option-object you should perform the actions on.
    	*
    	* @example
    	*      var opt = mw.libs.settingsManager.option( { optionName: 'CatALotOptions', value: { watchCopy: false, watchRemove: false } } );
    	*      // Save the options we've set before and make the script triggering events on the document, you can listen to
    	*      opt.save( $(document), 'CatALotSaveProgress' );
    	*      // or, use the deferred-object returned:
    	*      opt.save().done(function(msg, status, jsFile) { 
    	*          alert( msg );
    	*      }).status(function(msg, status, jsFile) { 
    	*          console.log( 'settings progress>' + msg );
    	*      });
    	*
    	* @param specsIn {Object} specifications passed in. List of defaults (cf. specs) follows.
    	* @context {mw.libs.settingsManager}
    	* @return {Object} option-object you can use for performing actions on.
    	*/
    	option: function(specsIn) {
    		// List of defaults:
    		var specs = {
    			// The global name the option will be saved under
    			// This can be also something like "mw.settingsOfToolX"
    			optionName: '',
    			// The position where to save them. By default options are
    			// saved at the user's common.js or <skin>.js (e.g. vector.js)
    			// specify other locations like "settingsOfToolX" --> "User:Example/prefs/settingsOfToolX.js"
    			saveAt: false,
    			// By default the option is not enclosed in a comment-block
    			// Comment-blocks are recommended for larger configurations
    			// specifies the signature that will be used for the enclosing comments
    			// blockSettingsOfToolX --> //blockSettingsOfToolX///////////////////////
    			// (ignored when saveAt is set)
    			encloseSignature: false,
    			// Specify additional block comments added below the signature
    			// Should be something that explains what the following JSON does or is good for
    			// Recommended line-length for consistent alignment: 48 chars
    			encloseBlock: false,
    			// If no own location for saving the option is used (options that must be
    			// available while loading the script should not be saved to a separate file while 
    			// complex options should), if the RegExp will have a match on either the common.js
    			// or the <skin>.js, and this option is not saved yet to another .js, the option
    			// will be saved to this js-file. In case the option is not saved yet and there is
    			// no RegExp supplied or it did not match, the option will be saved to the larger
    			// JavaScript
    			triggerSaveAt: false,
    			// Should the new content be insered in front of the match by triggerSaveAt
    			// If none of the following options is specified, the new content 
    			// will be appended to the script
    			insertBeforeTrigger: false,
    			insertAfterTrigger: false,
    			replaceTrigger: false,
    			// Finally the option's value. Objects are possible. They will be automatically
    			// transformed into a JSON-string
    			value: undefined,
    			// Edit summary to use while saving the JavaScript
    			editSummary: ""
    		};
    		if (!specsIn) throw new Error(sm.errorPrefix + "Data to save or retrieve was not supplied by the script using SettingsWizard.");
    		if (!specsIn.optionName && !specsIn.saveAt) throw new Error(sm.errorPrefix + "The options\'s name was not supplied by the script using SettingsWizard.");
    		$.extend( true, specs, specsIn );
    		
    		// Prepare variables we need later
    		var nsUser   = mw.config.get('wgFormattedNamespaces')[2],
    			skin     = mw.config.get('skin'),
    			user     = mw.config.get('wgUserName'),
    			skinJS   = [nsUser, ':', user, '/', skin, '.js'].join(''),
    			commonJS = [nsUser, ':', user, '/','common', '.js'].join('');
    		
    		// Event-handler system
    		var $el, evt, jsFiles, process, $progress = new $.Deferred(), customJS;
    		var triggerEvt = function(any) {
    			return (evt && $el && $el instanceof jQuery && $el.triggerHandler( evt, Array.prototype.slice.call( arguments, 0 ) ));
    		};
    		
    		process = {
    			updateVars: function() {
    				// Reset variables that could be polluted
    				jsFiles = [];
    				$progress = new $.Deferred();
    				customJS = [nsUser, ':', user, '/prefs/', specs.saveAt, '.js'].join('');
    			},
    			start: function() {
    				this.updateVars();
    				
    				// Subscribe to any event: We want to know everything :-)
    				$progress.then( triggerEvt, triggerEvt, triggerEvt );
    				
    				// Always async
    				setTimeout( $.proxy( this.getScripts, this ), 1 );
    				
    				return $progress;
    			},
    			getScripts: function() {
    				var i, len;
    				
    				$progress.notify( "Preparing", 1 );
    				
    				// First, we need something to work on/ edit token, etc. - request the JavaScript(s)
    				if (specs.saveAt) {
    					jsFiles.push( sm.script( customJS ) );
    				} else {
    					jsFiles.push( sm.script( skinJS ) );
    					jsFiles.push( sm.script( commonJS ) );
    				}
    				len = jsFiles.length;
    				for (i = 0; i < len; i++) {
    					var jsFile = jsFiles[i];
    					jsFile.fetchText( process.gotJS, process.gotJSErr );
    					$progress.notify( "Requesting " + jsFile.getSource(), Math.round( (i+1)*(9/len) ) + 1, jsFile );
    				}
    				return $progress;
    			},
    			gotJS: function(jsFile, r){
    				jsFile.gotContent = true;
    				
    				var i, len = jsFiles.length, pendings = 0;
    				for (i = 0; i < len; i++) {
    					if (!jsFiles[i].gotContent) {
    						pendings++;
    					}
    				}
    				$progress.notify( "Got " + jsFile.getSource() + '. File length: ' + jsFile.get().length + ' characters.' , Math.round( (len - pendings)*(9/len) ) + 10, jsFile );
    				
    				if (pendings) return;
    				process.process();
    			},
    			gotJSErr: function(jsFile) {
    				$progress.reject( "Failed. Could not retrieve " + jsFile.getSource(), -1, jsFile );
    			},
    			getStartBlock: function(sig) {
    				// String concat is sloooow
    				return '//' + sig + new Array(48 - 2 - sig.length + 1).join('/');
    			},
    			getEndBlock: function(sig) {
    				return new Array(48 - 2 - 3 - sig.length + 1).join('/') + sig + 'End' + '//';
    			},
    			getBlockRegExp: function(sig) {
    				var escSig = process.escapeRE(sig);
    				return new RegExp('\\n?\\n?\\/\\/' + escSig + '(?:.|\\n)*' + escSig + 'End\\/\\/', 'g');
    			},
    			escapeRE: function(string) {
    				string = mw.RegExp.escape(string);
    				
    				var specials = ['t', 'n', 'v', '0', 'f'];
    				$.each(specials, function(i, s) {
    					var rx = new RegExp('\\'+s, 'g');
    					string = string.replace(rx, '\\'+s);
    				});
    				return string;
    			},
    			getVariableRegExp: function(varName) {
    				var escVar = process.escapeRE(varName);
    				return {
    					varRE: new RegExp('\\s*(?:var\\s+|window\\.)?' + escVar + '\\s*=.+', 'g'),
    					// Throw a warning if the last char of the line is a "+" , "{", "(" or ","
    					varWarnRE: new RegExp('\\s*(?:var\\s+|window\\.)?' + escVar + '\\s*=.+(?:\\n?\\s*[\\,\\+\\{\\(])\\s*\\n')
    				};
    			},
    			process: function() {
    				var JSONVal = JSON.stringify( specs.value ),
    					sig = specs.encloseSignature,
    					tsa = specs.triggerSaveAt,
    					opn = specs.optionName,
    					jsFile, i, len = jsFiles.length,
    					plainJSON = !opn && !!jsFile,
    					oldText, newText, hadMatch;
    				
    				if (opn) {
    					// No semicolon for valid JSON!
    					JSONVal = 'window.' + opn + ' = ' + JSONVal + ';';
    				}
    				
    				if (!plainJSON) JSONVal = ((specs.encloseBlock && ('\n' + specs.encloseBlock)) || '') + JSONVal;
    				
    				if (sig && !plainJSON) JSONVal = process.getStartBlock( sig ) + JSONVal + '\n' + process.getEndBlock( sig );
    				
    				JSONVal = '\n\n' + JSONVal;
    				
    				// Fine, we've constructed everything we'll need. Now look up where to insert.
    				// Looking for signature
    				if (sig) {
    					var reBl = process.getBlockRegExp( sig );
    						
    					for (i = 0; i < len; i++) {
    						jsFile = jsFiles[i];
    						oldText = jsFile.get();
    						newText = oldText.replace( reBl, JSONVal );
    						if (reBl.test( oldText )) {
    							$progress.notify( "Replacing text enclosed by signature " + jsFile.getSource(), 25, jsFile );
    							process.save( jsFile.set( newText ) );
    							hadMatch = true;
    						}
    					}
    				}
    				if (hadMatch) return;
    				
    				// Looking for variable-name
    				if (opn) {
    					var vre = process.getVariableRegExp( opn ),
    						warnFile;
    					
    					for (i = 0; i < len; i++) {
    						jsFile = jsFiles[i];
    						oldText = jsFile.get();
    						
    						if (vre.varWarnRE.test(oldText)) {
    							// WARNING!!!
    							$progress.notify( "Unable to remove config from " + jsFile.getSource(), -2, jsFile );
    							warnFile = jsFile;
    						} else {
    							newText = oldText.replace( vre.varRE, JSONVal );
    							if (vre.varRE.test( oldText )) {
    								$progress.notify( "Replacing variable " + jsFile.getSource(), 25, jsFile );
    								process.save( jsFile.set( newText ) );
    								hadMatch = true;
    							}
    						}
    						// Only append in case of warning if it was not added to another file
    						if (warnFile && !hadMatch) {
    							$progress.notify( "Appending variable after warning to " + jsFile.getSource(), 25, jsFile );
    							process.save( warnFile.set( oldText + JSONVal ) );
    							hadMatch = true;
    						}
    					}
    				}
    				if (hadMatch) return;
    				
    				// If it's just JSON, replace the whole thingy
    				if (!opn && specs.saveAt) {
    					jsFile = jsFiles[0];
    					$progress.notify( "Replacing whole content of " + jsFile.getSource(), 25, jsFile );
    					process.save( jsFile.set( JSONVal ) );
    					hadMatch = true;
    				}
    				if (hadMatch) return;
    				
    				// Looking whether supplied RegExp can find something
    				if (tsa) {
    					var searchMatch,
    						triggerLen = 0;
    					
    					for (i = 0; i < len; i++) {
    						jsFile = jsFiles[i];
    						oldText = jsFile.get();
    						
    						searchMatch = oldText.search( tsa );
    						if (-1 !== searchMatch) {
    							if (specs.insertBeforeTrigger) {
    								$progress.notify( "Inserting before pattern in " + jsFile.getSource(), 25, jsFile );
    								jsFile.set( oldText.slice( 0, searchMatch ) + JSONVal + oldText.slice( searchMatch ) );
    							} else if (specs.insertAfterTrigger) {
    								triggerLen = oldText.match( tsa )[0].length;
    								$progress.notify( "Inserting after pattern in " + jsFile.getSource(), 25, jsFile );
    								jsFile.set( oldText.slice( 0, searchMatch + triggerLen ) + JSONVal + oldText.slice( searchMatch + triggerLen ) );
    							} else if (specs.replaceTrigger) {
    								$progress.notify( "Replacing pattern with new content in " + jsFile.getSource(), 25, jsFile );
    								jsFile.set( oldText.replace( tsa, JSONVal ) );
    							} else {
    								$progress.notify( "Found pattern, appending to " + jsFile.getSource(), 25, jsFile );
    								jsFile.set( oldText + '\n//<nowiki>' + JSONVal + '\n//<\/nowiki>' );
    							}
    							process.save( jsFile );
    							hadMatch = true;
    							break;
    						}
    					}
    				}
    				if (hadMatch) return;
    				
    				// Finally compare file size
    				var biggest = { size: 0, jsFile: null };
    					
    				for (i = 0; i < len; i++) {
    					jsFile = jsFiles[i];
    					oldText = jsFile.get();
    					var oldTextLen = oldText.length;
    					
    					if (oldTextLen >= biggest.size) biggest = {
    						size: oldTextLen,
    						jsFile: jsFile
    					};
    				}
    				$progress.notify( "Appending to bigger file: " + biggest.jsFile.getSource(), 25, biggest.jsFile );
    				biggest.jsFile.set( biggest.jsFile.get() + '\n//<nowiki>' + JSONVal + '\n//<\/nowiki>' );
    				process.save( biggest.jsFile );
    			},
    			save: function(jsFile) {
    				jsFile.saving = true;
    				$progress.notify( "Saving " + jsFile.getSource(), 30, jsFile );
    				jsFile.save( process.saved, process.savedErr, "[[MediaWiki:Gadget-SettingsManager.js|SettingsManager]]: " + specs.editSummary );
    			},
    			saved: function(jsFile) {
    				var i, len = jsFiles.length, jsf, waitingFor = [];
    				
    				jsFile.saving = false;
    				
    				for (i = 0; i < len; i++) {
    					jsf = jsFiles[i];
    					if (jsf.saving) {
    						waitingFor.push(jsf.getSource());
    					}
    				}
    				$progress.notify( "Saved " + jsFile.getSource() + ". Waiting for " + (waitingFor.join(', ') || '-'), Math.round( (len - waitingFor.length)*(20/len) ) + 50,  jsFile );
    				
    				if (waitingFor.length) return;
    				$progress.resolve( "Success!", 100, jsFile );
    			},
    			savedErr: function(jsFile, code, errObj) {
    				$progress.reject( "Error saving " + jsFile.getSource() + ". Code is " + code + ".\n", -1, errObj );
    			}
    		};
    		
    		return {
    			getSpecs: function() {
    				return specs;
    			},
    			setSpecs: function(specsIn) {
    				specs = specsIn;
    				return this;
    			},
    			// Warning: If you specified a different save-position ("saveAt")
    			// and also an optionName, the script has to be fetched and evaluated
    			// We recommend omitting setting "optionName" when using "saveAt"
    			fetchValue: function(cb, errCb) {
    				process.updateVars();
    				
    				if (specs.saveAt) {
    					var s = sm.script( customJS );
    					if (specs.optionName) {
    						s.fetchText(function() {
    							s.doEval();
    							cb( valByString( specs.optionName ) );
    						}, errCb);
    					} else {
    						s.fetchJSON(function(scriptObj, JSON) {
    							cb( JSON );
    						}, errCb);
    					}
    					return this;
    				}
    				cb( valByString( specs.optionName ) );
    				return this;
    			},
    			getValue: function() {
    				return specs.value;
    			},
    			setValue: function(val) {
    				specs.value = val;
    				return this;
    			},
    			save: function($elem, event) {
    				// We won't check whether the value is undefined. This is your task.
    				$el = $elem;
    				evt = event;
    				return process.start();
    			},
    			getProgress: function() {
    				return $progress;
    			}
    		};
    	},
    	/**
    	* Constructor-method. Returns a script-object you should perform the actions on.
    	*
    	* @example
    	*      var commonJS = mw.libs.settingsManager.script( 'User:Example/common.js' );
    	*      commonJS.set( '// empty!' ).setSummary( 'Removing Content' ).save( function() { console.log( 'Successfully removed content from ' + commonJS.getSource() ) } )
    	*
    	* @param source {String} The name of the JavaScript file with namespace.
    	* @context {mw.libs.settingsManager}
    	* @return {Object} script-object you can use for performing actions on.
    	*/
    	script: function(source) {
    		var content,
    			page,
    			summary = "Changing configuration using [[:commons:MediaWiki:Gadget-SettingsManager.js]]",
    			minor = 1,
    			exists,
    			fetch,
    			save;
    
    		fetch = function() {
    			var mwa = new mw.Api();
    			return mwa.get( {
    				prop: 'info|revisions',
    				titles: source,
    				rvprop: 'timestamp|content',
    				intoken: 'edit'
    			} );
    		};
    		
    		save = function() {
    			var mwa = new mw.Api(),
    				edit = {
    					action: 'edit',
    					title: source,
    					text: 'object' === typeof content ? JSON.stringify(content) : content,
    					summary: summary,
    					watchlist: 'nochange',
    					recreate: 1
    				};
    				
    			if (minor) edit.minor = 1;
    			if (exists) {
    				edit.basetimestamp = page.revisions[0].timestamp;
    			} else {
    				edit.starttimestamp = page.starttimestamp;
    			}
    			
    			edit.token = page.edittoken;
    			return mwa.post( edit );
    		};
    			
    		return {
    			get: function() {
    				return content;
    			},
    			getSource: function() {
    				return source;
    			},
    			doEval: function() {
    				/*jshint evil:true */
    				return eval(content);
    			},
    			parseJSON: function() {
    				return ('string' === typeof content && content !== '') ? JSON.parse( content ) : '';
    			},
    			// Supplied callback called with a string as second argument
    			fetchText: function(cb, errCb) {
    				var pgs, pg, scriptObj = this;
    				
    				fetch().done( function(result) {
    					pgs = result.query.pages;
    					page = firstItem( pgs );
    					exists = !!(page.revisions && page.revisions[0]);
    					content = (exists && page.revisions[0]['*']) || '';
    					cb( scriptObj, content );
    				} ).fail( function( status, errObj ) {
    					errCb( scriptObj, status, errObj );
    				} );
    				return this;
    			},
    			// Supplied callback called with parsed JSON-data as second argument
    			fetchJSON: function(cb, errCb) {
    				this.fetchText( function(scriptObj, content) {
    					cb( scriptObj, scriptObj.parseJSON() );
    				}, function(scriptObj, status, errObj) {
    					errCb( scriptObj, status, errObj );
    				} );
    				return this;
    			},
    			set: function(newContent) {
    				content = newContent;
    				return this;
    			},
    			setMinor: function(newMinor) {
    				minor = !!newMinor;
    			},
    			setSummary: function(newSummary) {
    				summary = newSummary;
    			},
    			save: function(cb, errCb, newSummary, newContent, newMinor) {
    				var scriptObj = this;
    				if (newContent !== undefined) content = newContent;
    				if (newSummary !== undefined) summary = newSummary;
    				if (newMinor !== undefined) minor = !!newMinor;
    				save().done( function(result) {
    					cb( scriptObj, result );
    				} ).fail( function(status, errObj) {
    					errCb( scriptObj, status, errObj );
    				} );
    				return this;
    			}
    		};
    	},
    	/**
    	* Switch a user preference using Ajax!
    	*
    	* @example
    	*      mw.libs.settingsManager.switchPref( 'myOption', 'new value' );
    	*
    	* @param prefName {String} The name of the preference.
    	* @param prefVal {String} The new value the preference should set to.
    	* @param cb {Function} Callback in case of success.
    	* @param cb {Function} Callback in case of an error.
    	* @context {mw.libs.settingsManager}
    	* @return {Object} a jQuery deferred-object-queue. Don't use it for error-handling - Done by this method.
    	*/
    	switchPref: function(prefName, prefVal, cb, errCb) {
    		var mwa = new mw.Api(),
    			args = arguments,
    			prefString = (typeof prefVal === 'object') ? JSON.stringify(prefVal) : prefVal,
    			apiDef = mwa.post( {
    				action: 'options',
    				token: mw.user.tokens.get('preferencesToken'),
    				optionname: prefName,
    				optionvalue: prefString || 0
    			} );
    
    		// If we changed a preference successfully, update user.options reflecting the change
    		apiDef.done( function() {
    			mw.user.options.set( prefName, prefString );
    		} );
    		if (cb) apiDef.done( cb );
    		// Catch badtoken and some other common errors
    		apiDef.fail( function(code, result) {
    			switch (code) {
    				case 'badtoken':
    					refreshToken(function (gotANewToken) {
    						if (gotANewToken) return sm.switchPref.apply( sm, Array.prototype.slice.call( args, 0 ) );
    					} );
    					// Stop the propagation of 
    					return false;
    				case 'http':
    				case 'ok-but-empty':
    					setTimeout( function() {
    						return sm.switchPref.apply( sm, Array.prototype.slice.call(args, 0) );
    					}, 2500 );
    					return false;
    				default:
    					return (errCb && errCb(code, result) && false);
    			}
    		} );
    		return apiDef;
    	},
    	/**
    	* Switch a gadget preference using Ajax!
    	*
    	* @example
    	*      mw.libs.settingsManager.switchGadgetPref( 'myOption', 'new value' ).done(function() { console.log("DONE!") });
    	*
    	* @param prefName {String} The name of the preference.
    	* @param prefVal {String} The new value the preference should set to.
    	* @context {mw.libs.settingsManager}
    	* @return {Object} $.Deferred; a jQuery deferred-object-queue.
    	*/
    	switchGadgetPref: function(prefName, prefVal) {
    		var $def = $.Deferred();
    
    		sm.switchPref( mwPrefPrefix + prefName, prefVal, $.proxy( $def.resolve, $def ), $.proxy( $def.reject, $def ) );
    		return $def;
    	},
    	
    	/**
    	* Fetch a Gadget preference from various sources!
    	*
    	* @example
    	*      mw.libs.settingsManager.fetchGadgetSetting( 'mySetting', ['storage', 'option'] ).done(function(prefName, settingValue) { console.log("DONE!") });
    	*
    	* @param prefName {String} The name of the preference.
    	* @param prefSources {Array} One or more of the following values 'storage', 'cookie', 'option', 'window'. Default (if not passed): 
    	*                             All in the order listed there. Note that they are processed in the order you pass them in and as soon as one is found,
    	*                             Script will return.
    	*                             IMPORTANT: The Array is changed while processing. So make a copy if you need it again before passing it.
    	*
    	* @context {mw.libs.settingsManager}
    	* @return {Object} $.Deferred; a jQuery deferred-object-queue.
    	*/
    	fetchGadgetSetting: function(prefName, prefSources) {
    		var $def = $.Deferred(), requires = [], options = {
    			'storage': {
    				requires: ['jquery.jStorage'],
    				fetch: function() {
    					var v = $.jStorage.get( prefName );
    					return (null === v || undefined === v) ? undefined : v;
    				}
    			},
    			'cookie': {
    				requires: ['jquery.cookie'],
    				fetch: function() {
    					var v = $.cookie( prefName );
    					try {
    						v = JSON.parse( v );
    					} catch(invalidJSON) {}
    					return (null === v || undefined === v) ? undefined : v;
    				}
    			},
    			'option': {
    				requires: ['mediawiki.user', 'user.options'],
    				fetch: function() {
    					var v = mw.user.options.get( mwPrefPrefix + prefName );
    					try {
    						v = JSON.parse( v );
    					} catch(invalidJSON) {}
    					return (null === v || undefined === v) ? undefined : v;
    				}
    			},
    			'window': {
    				requires: [],
    				fetch: function() {
    					return window[prefName];
    				}
    			}
    		};
    		if (!prefSources) prefSources = [];
    		if (!prefSources.length) prefSources = ['storage', 'cookie', 'option', 'window'];
    		
    		var _fetch = function(s) {
    				var so = options[s];
    				if (so) {
    					mw.loader.using( so.requires, function() {
    						var v = so.fetch();
    						if (undefined === v) {
    							_fetched();
    						} else {
    							$def.resolve( prefName, v );
    						}
    					} );
    				} else {
    					// Security guard: Don't load settings from unprotected pages
    					if (!/^(?:User\:|MediaWiki\:).+\.js$/.test( s )) _fetched();
    					sm.script( s ).fetchJSON( function(me, jsonData) {
    						if (jsonData) {
    							$def.resolve( prefName, jsonData );
    						} else {
    							_fetched();
    						}
    					}, $.proxy( $def.reject, $def ) );
    				}
    			},
    			_fetched = function() {
    				prefSources.shift();
    				if (prefSources.length) {
    					_fetch( prefSources[0] );
    				} else {
    					$def.resolve( prefName /* no pref found */ );
    				}
    			};
    			
    		$.each( prefSources, function(i, s) {
    			var so = options[s];
    			if (so) requires = requires.concat( options[s].requires );
    		} );
    		mw.loader.load( requires );
    			
    		// ensure async
    		setTimeout( function() {
    			_fetch(prefSources[0]);
    		}, 10 );
    		
    		return $def;
    	},
    	
    	/**
    	* Constructor-method. Returns an option-object you should perform the actions on.
    	*
    	* @example
    	*      var slideshowGadget = mw.libs.settingsManager.gadget( 'Slideshow' );
    	*      if (slideshowGadget.isEnabled()) { slideshowGadget.disable( myCallback ) }
    	*
    	*      // Enable a gadget and load it:
    	*      mw.libs.settingsManager.gadget( 'Slideshow' ).load().enable();
    	*
    	* @param gadgetName {Object} The name of the gadget. (Not the script file; without Gadget- prefix or other decoration)
    	* @context {mw.libs.settingsManager}
    	* @return {Object} gadget-object you can use for performing actions on.
    	*/
    	gadget: function(gadgetName) {
    		var optGadget = 'gadget-' + gadgetName,
    			rlGadget = 'ext.gadget.' + gadgetName;
    			
    		return {
    			getName: function() {
    				return gadgetName;
    			},
    			isDefault: function() {
    				var opt = mw.user.options.get( optGadget );
    				return ('number' === typeof opt || '' === opt);
    			},
    			isEnabled: function() {
    				var opt = mw.user.options.get( optGadget );
    				return !!opt;
    			},
    			getState: function() {
    				return mw.loader.getState( rlGadget );
    			},
    			isLoaded: function() {
    				return ('ready' === this.getState());
    			},
    			load: function(cb, errCb) {
    				// Always async
    				if (this.isLoaded && cb) return setTimeout( function() {
    					cb( gadgetName, true );
    				}, 1 );
    				mw.loader.using( rlGadget, 
    					cb ? function() { 
    						cb( gadgetName ); 
    					} : undefined, 
    					errCb ? function() { 
    						errCb( gadgetName ); 
    					} : undefined 
    				);
    				return this;
    			},
    			enable: function(cb, errCb) {
    				// Type wouldn't matter due to URL-encoding but we also want to update
    				// the user.options object
    				sm.switchPref( optGadget, this.isDefault() ? 1 : '1', cb, errCb );
    				return this;
    			},
    			disable: function(cb, errCb) {
    				sm.switchPref( optGadget, '', cb, errCb );
    				return this;
    			}
    		};
    	}
    };
    
    
    mw.libs.settingsManager = sm;
    
    // TODO add to gadget-def
    // mw.loader.load(['json', 'mediawiki.user', 'user.options', 'user.tokens']);
    }( jQuery, mediaWiki ));