From 6be9a25d99736da416d06a317cedb691972663af Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Sat, 7 Jan 2017 01:24:32 +0100 Subject: [PATCH 01/64] new package.json to support gulp --- src/Umbraco.Web.UI.Client/package.json | 51 ++++++++++---------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index d2af0e2b83..30060c3b7e 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -14,38 +14,27 @@ "engines": { "node": ">= 0.8.4" }, + "scripts": { + "install": "bower-installer", + "test": "karma start test/config/karma.conf.js --singlerun" + }, "dependencies": {}, "devDependencies": { - "autoprefixer-core": "~5.2.1", - "bower": "^1.4.1", - "eslint": "^0.23.0", - "eslint-plugin-angular": "0.0.13", - "grunt": "~0.4.0", - "grunt-bower": "^0.19.0", - "grunt-bower-install-simple": "^1.1.3", - "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-concat": "~0.1.3", - "grunt-contrib-connect": "~0.3.0", - "grunt-contrib-copy": "~0.7.0", - "grunt-contrib-jshint": "~0.2.0", - "grunt-contrib-uglify": "~0.1.1", - "grunt-contrib-watch": "~0.3.1", - "grunt-eslint": "^15.0.0", - "grunt-html2js": "~0.1.0", - "grunt-hustler": "^4.0.6", - "grunt-karma": "~0.5", - "grunt-ngdocs": "~0.1.2", - "grunt-open": "~0.2.0", - "grunt-postcss": "~0.6.0", - "grunt-recess": "~0.3", - "karma": "~0.9", - "karma-chrome-launcher": "0.0.2", - "karma-coffee-preprocessor": "0.0.1", - "karma-firefox-launcher": "0.0.2", - "karma-jasmine": "0.0.1", - "karma-phantomjs-launcher": "0.0.2", - "karma-requirejs": "0.0.1", - "karma-script-launcher": "0.0.1", - "phantomjs": "~1.9.1-0" + "autoprefixer": "^6.5.0", + "bower-installer": "^1.2.0", + "cssnano": "^3.7.6", + "gulp": "^3.9.1", + "gulp-concat": "^2.6.0", + "gulp-less": "^3.1.0", + "gulp-postcss": "^6.2.0", + "gulp-rename": "^1.2.2", + "gulp-sourcemaps": "^1.6.0", + "gulp-watch": "^4.3.10", + "gulp-wrap": "^0.13.0", + "jasmine-core": "^2.5.2", + "karma": "^1.3.0", + "karma-jasmine": "^0.1.0", + "karma-phantomjs-launcher": "^1.0.2", + "lodash": "^4.16.3" } } From 4a191f0711e1b680b8bf36b82566e1fd2353a483 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Sat, 7 Jan 2017 01:25:43 +0100 Subject: [PATCH 02/64] Rebuild bower.json to support bower-installer removes most of the custom bower lib installing we had in gruntfile --- src/Umbraco.Web.UI.Client/bower.json | 49 ++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 0f3eea3291..0e18f99b46 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -25,9 +25,52 @@ "jquery-migrate": "1.4.0", "angular-dynamic-locale": "0.1.28", "ng-file-upload": "~7.3.8", - "tinymce": "~4.1.10", - "codemirror": "~5.3.0", "angular-local-storage": "~0.2.3", - "moment": "~2.10.3" + "moment": "~2.10.3", + "ace-builds": "^1.2.3", + "tinymce": "tinymce-dist#^4.5.2" + }, + + "install": { + + "path": "lib-bower", + + "sources": { + "moment": "bower_components/moment/min/moment-with-locales.js", + "underscore": [ + "bower_components/underscore/underscore-min.js", + "bower_components/underscore/underscore-min.map" + ], + + "jquery": [ + "bower_components/jquery/dist/jquery.min.js", + "bower_components/jquery/dist/jquery.min.map" + ], + + "ace-builds": "bower_components/ace-builds/src-min-noconflict/**", + + "angular-dynamic-locale": [ + "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js", + "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js.map" + ], + + "angular-local-storage": [ + "bower_components/angular-local-storage/dist/angular-local-storage.min.js", + "bower_components/angular-local-storage/dist/angular-local-storage.min.js.map" + ], + + "tinymce": [ + "bower_components/tinymce/tinymce.min.js" + ], + + "typeahead.js": "bower_components/typeahead.js/dist/typeahead.bundle.min.js", + + "rgrove-lazyload":"bower_components/rgrove-lazyload/lazyload.js", + + "ng-file-upload":"bower_components/ng-file-upload/ng-file-upload.min.js", + + "jquery-ui":"bower_components/jquery-ui/jquery-ui.min.js", + "jquery-migrate":"bower_components/jquery-migrate/jquery-migrate.min.js" + } } } From 904265cdf5b3796fb7c6dd290fccc707b520551e Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Sat, 7 Jan 2017 01:26:13 +0100 Subject: [PATCH 03/64] replaces gruntfile with gulpfile Meaner, faster, better --- src/Umbraco.Web.UI.Client/gulpfile.js | 196 ++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/gulpfile.js diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js new file mode 100644 index 0000000000..6c7e581530 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -0,0 +1,196 @@ +var gulp = require('gulp'); +var watch = require("gulp-watch"); +var concat = require("gulp-concat"); +var rename = require("gulp-rename"); +var wrap = require("gulp-wrap"); + +var _ = require("lodash"); + +//Less + css +var postcss = require('gulp-postcss'); +var less = require('gulp-less'); +var autoprefixer = require('autoprefixer'); +var cssnano = require('cssnano'); + + +/*************************************************************** +Helper functions +***************************************************************/ +function processJs(files, out) { + console.log("Compiling JS:" + files + " to " + out); + + return gulp.src(files) + .pipe(concat(out)) + .pipe(wrap('(function(){\n"use strict";\n<%= contents %>\n})();')) + .pipe(gulp.dest(root + targets.js)); +} + +function processLess(files, out) { + console.log("Compiling LESS:" + files + " to " + out); + + var processors = [ + autoprefixer, + cssnano + ]; + + return gulp.src(files) + .pipe(less()) + .pipe(postcss(processors)) + .pipe(rename(out)) + .pipe(gulp.dest(root + targets.css)); +} + +/*************************************************************** +Paths and destinations +Each group is iterated automatically in the setup tasks below +***************************************************************/ +var sources = { + + //less files used by backoffice and preview + //processed in the less task + less: { + installer: { files: ["src/less/installer.less"], out: "installer.css" }, + preview: { files: ['src/less/canvas-designer.less', 'src/less/helveticons.less'], out: "canvasdesigner.css" }, + umbraco: { files: ["src/less/belle.less"], out: "umbraco.css" } + }, + + //js files for backoffie + //processed in the js task + js: { + preview: { files: ["src/canvasdesigner/**/*.js"], out: "umbraco.canvasdesigner.js" }, + installer: { files: ["src/installer/**/*.js"], out: "umbraco.installer.js" }, + + controllers: { files: ["src/views/**/*.controller.js"], out: "umbraco.controllers.js" }, + directives: { files: ["src/common/directives/**/*.js"], out: "umbraco.directives.js" }, + filters: { files: ["src/common/filters/**/*.js"], out: "umbraco.filters.js" }, + resources: { files: ["src/common/resources/**/*.js"], out: "umbraco.resources.js" }, + services: { files: ["src/common/services/**/*.js"], out: "umbraco.services.js" }, + security: { files: ["src/common/security/**/*.js"], out: "umbraco.security.js" } + }, + + //selectors for copying all views into the build + //processed in the views task + views:{ + umbraco: {files: ["src/views/**/*html"], folder: "/"}, + preview: { files: ["src/canvasdesigner/**/*.html"], folder: "../preview"}, + installer: {files: ["src/installer/steps/*.html"], folder: "/install"} + }, + + //globs for file-watching + globs:{ + views: "src/views/**/*html", + less: "src/less*.less", + js: "src/*.js", + lib: "lib/**/*.*, lib-bower/**/*.*", + assets: "src/assets/**" + } +}; + +var root = "../__BUILD/umbraco/"; +var targets = { + js: "js", + lib: "lib", + views: "views", + css: "assets/css", + assets: "assets" +}; + + +/************************** + * Runs the default tasks for the project to prepare backoffice files + **************************/ +var defaultTasks = ["dependencies","js", "less", "views"]; +gulp.task("default", defaultTasks); + + +/************************** + * Task processes and copies all dependencies, either installed by bower, npm or stored locally in the project + **************************/ +gulp.task('dependencies', function () { + + //bower component specific copy rules + //this is to patch the sometimes wonky rules these libs are distrbuted under + + //Tinymce + gulp.src("bower_components/tinymce/plugins/**") + .pipe(gulp.dest(root + targets.lib + "/tinymce/plugins/")); + + //font-awesome + gulp.src("bower_components/font-awesome/{fonts,css}/*") + .pipe(gulp.dest(root + targets.lib + "/font-awesome")); + + //copy over libs which are not on bower (/lib) and + //libraries that have been managed by bower-installer (/lib-bower) + gulp.src(sources.globs.lib) + .pipe(gulp.dest(root + targets.lib)); + + //Copies all static assets into /root / assets folder + //css, fonts and image files + gulp.src(sources.globs.assets) + .pipe(gulp.dest(root + targets.assets)); + + // Copies all the less files related to the preview into their folder + //these are not pre-processed as preview has its own less combiler client side + gulp.src("src/canvasdesigner/editors/*.less") + .pipe(gulp.dest(root + targets.assets + "/less")); +}); + + +/************************** + * Copies all angular JS files into their seperate umbraco.*.js file + **************************/ +gulp.task('js', function () { + + gulp.src(sources.globs.js) + .pipe(gulp.dest(root + targets.js)); + + return _.forEach(sources.js, function (group) { + processJs(group.files, group.out); + }); +}); + +gulp.task('less', function () { + + gulp.src(sources.globs.assets) + .pipe(gulp.dest(targets.assets)); + + return _.forEach(sources.less, function (group) { + processLess(group.files, group.out); + }); +}); + + +gulp.task('views', function () { + + return _.forEach(sources.views, function (group) { + gulp.src(group.files) + .pipe(gulp.dest(root + targets.views + group.folder)); + }); + +}); + + +gulp.task('watch', defaultTasks, function () { + + //Setup a watcher for all groups of javascript files + _.forEach(sources.js, function (group) { + if(group.watch !== false){ + watch(group.files, { ignoreInitial: true }, function () { + processJs(group.files, group.out); + }); + } + }); + + //watch all less files and trigger the less task + watch(sources.globs.less, { ignoreInitial: true }, function () { + gulp.run(['less']); + }); + + //watch all views - copy single file changes + watch(sources.globs.views) + .pipe(gulp.dest(root + targets.views)); + + //watch all app js files that will not be merged - copy single file changes + watch(sources.globs.js) + .pipe(gulp.dest(root + targets.js)); +}); \ No newline at end of file From 4e9a8623f05e9160ee0a1a18c84a4a23b00342c5 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Sat, 7 Jan 2017 01:26:35 +0100 Subject: [PATCH 04/64] syntax issue in eslintrc --- src/Umbraco.Web.UI.Client/.eslintrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/.eslintrc b/src/Umbraco.Web.UI.Client/.eslintrc index 811fabdabf..205d8dcf50 100644 --- a/src/Umbraco.Web.UI.Client/.eslintrc +++ b/src/Umbraco.Web.UI.Client/.eslintrc @@ -39,7 +39,7 @@ "globals": { "angular": false, "_": false, - "$", false, + "$": false, "tinymce": false, "tinyMCE": false, "FileReader": false, @@ -47,7 +47,7 @@ "window": false, "LazyLoad": false, "ActiveXObject": false, - "Bloodhound", false + "Bloodhound": false } } \ No newline at end of file From ee9deacf92d3263f2f87b9c0e2d31dac9e3a01cd Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Sat, 7 Jan 2017 01:26:47 +0100 Subject: [PATCH 05/64] ignore lib-bower temp folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 16994e0ab1..7465b3c452 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ src/Umbraco.Web.UI/[Ww]eb.config webpihash.txt node_modules +lib-bower src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ib/* src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/umbraco.* From cc3355b1f984b390f481c4e4584da183e6dfdb4f Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Sat, 7 Jan 2017 01:27:26 +0100 Subject: [PATCH 06/64] fixes issues with bootstrap2 navbar and less2 syntax --- src/Umbraco.Web.UI.Client/lib/bootstrap/less/navbar.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/navbar.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/navbar.less index a448255731..b4e1edf65f 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/navbar.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/navbar.less @@ -193,10 +193,10 @@ // Reset container width // Required here as we reset the width earlier on and the grid mixins don't override early enough -.navbar-static-top .container, +.navbar-static-top .container, .navbar-fixed-top .container, -.navbar-fixed-bottom .container { - #grid > .core > .span(@gridColumns); +.navbar-fixed-bottom .container { +width: (@gridColumnWidth * @gridColumns) + (@gridGutterWidth * (@gridColumns - 1)); } // Fixed to top From 7e930330e2acd932dc410a35f8748937147d68c2 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Sat, 7 Jan 2017 01:27:59 +0100 Subject: [PATCH 07/64] fixes karma testing config - now runs with "npm test" command instead --- .../test/config/karma.conf.js | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index ede7c20538..e5198e9caa 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -1,5 +1,7 @@ -module.exports = function(karma) { - karma.configure({ +module.exports = function(config) { + + config.set({ + // base path, that will be used to resolve files and exclude basePath: '../..', @@ -7,40 +9,36 @@ module.exports = function(karma) { // list of files / patterns to load in the browser files: [ - 'lib/../build/belle/lib/jquery/jquery.min.js', + + //libraries + 'lib-bower/jquery/jquery.min.js', 'lib/angular/1.1.5/angular.js', 'lib/angular/1.1.5/angular-cookies.min.js', 'lib/angular/1.1.5/angular-mocks.js', 'lib/angular/angular-ui-sortable.js', - - /* - For angular 1.2: - 'lib/angular/1.2/angular.js', - 'lib/angular/1.2/angular-route.min.js', - 'lib/angular/1.2/angular-touch.min.js', - 'lib/angular/1.2/angular-cookies.min.js', - 'lib/angular/1.2/angular-animate.min.js', - 'lib/angular/1.2/angular-mocks.js',*/ - - - 'lib/../build/belle/lib/underscore/underscore-min.js', - 'lib/../build/belle/lib/moment/moment-with-locales.js', + 'lib-bower/underscore/underscore-min.js', + 'lib-bower/moment/moment-with-locales.js', 'lib/umbraco/Extensions.js', - 'lib/../build/belle/lib/rgrove-lazyload/lazyload.js', - 'lib/../build/belle/lib/angular-local-storage/angular-local-storage.min.js', + 'lib-bower/rgrove-lazyload/lazyload.js', + 'lib-bower//angular-local-storage/angular-local-storage.min.js', + //app bootstrap and loader 'test/config/app.unit.js', - 'src/common/mocks/umbraco.servervariables.js', + //application files 'src/common/directives/*.js', 'src/common/filters/*.js', 'src/common/services/*.js', 'src/common/security/*.js', 'src/common/resources/*.js', - 'src/common/mocks/**/*.js', 'src/views/**/*.controller.js', - 'test/unit/**/*.spec.js', - {pattern: 'lib/**/*.js', watched: true, served: true, included: false} + + //mocked data and routing + 'src/common/mocks/umbraco.servervariables.js', + 'src/common/mocks/**/*.js', + + //tests + 'test/unit/**/*.spec.js' ], // list of files to exclude @@ -66,7 +64,7 @@ module.exports = function(karma) { // level of logging // possible values: karma.LOG_DISABLE || karma.LOG_ERROR || karma.LOG_WARN || karma.LOG_INFO || karma.LOG_DEBUG // CLI --log-level debug - logLevel: karma.LOG_INFO, + logLevel: config.LOG_WARN, // enable / disable watching file and executing tests whenever any file changes // CLI --auto-watch --no-auto-watch From 1e34570a1f1d14ed00cc452e4b0a8c1c2a76bde6 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Sat, 7 Jan 2017 01:28:16 +0100 Subject: [PATCH 08/64] consistent naming for canvasdesigner js file --- src/Umbraco.Web.UI.Client/src/canvasdesigner.loader.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner.loader.js b/src/Umbraco.Web.UI.Client/src/canvasdesigner.loader.js index 59d5a4bef5..eefb5beed6 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner.loader.js +++ b/src/Umbraco.Web.UI.Client/src/canvasdesigner.loader.js @@ -11,8 +11,7 @@ LazyLoad.js([ '../js/umbraco.security.js', '../ServerVariables', '../lib/spectrum/spectrum.js', - - '../js/canvasdesigner.panel.js', + '../js/umbraco.canvasdesigner.js', ], function () { jQuery(document).ready(function () { angular.bootstrap(document, ['Umbraco.canvasdesigner']); From 5cee5a27c95b2ab974c4eec73bd62d2bab40be10 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Mon, 9 Jan 2017 15:26:05 +0100 Subject: [PATCH 09/64] Wraps tasks in a merge-stream This ensures proper timing and feedback of tasks --- src/Umbraco.Web.UI.Client/gulpfile.js | 143 +++++++++++++++++-------- src/Umbraco.Web.UI.Client/package.json | 3 +- 2 files changed, 102 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 6c7e581530..a2b6983c6a 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -1,10 +1,12 @@ var gulp = require('gulp'); -var watch = require("gulp-watch"); -var concat = require("gulp-concat"); -var rename = require("gulp-rename"); -var wrap = require("gulp-wrap"); +var watch = require('gulp-watch'); +var concat = require('gulp-concat'); +var rename = require('gulp-rename'); +var wrap = require('gulp-wrap'); + +var _ = require('lodash'); +var MergeStream = require('merge-stream'); -var _ = require("lodash"); //Less + css var postcss = require('gulp-postcss'); @@ -17,17 +19,17 @@ var cssnano = require('cssnano'); Helper functions ***************************************************************/ function processJs(files, out) { - console.log("Compiling JS:" + files + " to " + out); - + return gulp.src(files) .pipe(concat(out)) .pipe(wrap('(function(){\n"use strict";\n<%= contents %>\n})();')) .pipe(gulp.dest(root + targets.js)); + + console.log(out + " compiled"); } function processLess(files, out) { - console.log("Compiling LESS:" + files + " to " + out); - + var processors = [ autoprefixer, cssnano @@ -38,6 +40,8 @@ function processLess(files, out) { .pipe(postcss(processors)) .pipe(rename(out)) .pipe(gulp.dest(root + targets.css)); + + console.log(out + " compiled"); } /*************************************************************** @@ -60,7 +64,7 @@ var sources = { preview: { files: ["src/canvasdesigner/**/*.js"], out: "umbraco.canvasdesigner.js" }, installer: { files: ["src/installer/**/*.js"], out: "umbraco.installer.js" }, - controllers: { files: ["src/views/**/*.controller.js"], out: "umbraco.controllers.js" }, + controllers: { files: ["src/{views,controllers}/**/*.controller.js"], out: "umbraco.controllers.js" }, directives: { files: ["src/common/directives/**/*.js"], out: "umbraco.directives.js" }, filters: { files: ["src/common/filters/**/*.js"], out: "umbraco.filters.js" }, resources: { files: ["src/common/resources/**/*.js"], out: "umbraco.resources.js" }, @@ -111,28 +115,43 @@ gulp.task('dependencies', function () { //bower component specific copy rules //this is to patch the sometimes wonky rules these libs are distrbuted under + //as we do multiple things in this task, we merge the multiple streams + var stream = new MergeStream(); + //Tinymce - gulp.src("bower_components/tinymce/plugins/**") - .pipe(gulp.dest(root + targets.lib + "/tinymce/plugins/")); + stream.add( + gulp.src("bower_components/tinymce/plugins/**") + .pipe(gulp.dest(root + targets.lib + "/tinymce/plugins/")) + ); //font-awesome - gulp.src("bower_components/font-awesome/{fonts,css}/*") - .pipe(gulp.dest(root + targets.lib + "/font-awesome")); + stream.add( + gulp.src("bower_components/font-awesome/{fonts,css}/*") + .pipe(gulp.dest(root + targets.lib + "/font-awesome")) + ); //copy over libs which are not on bower (/lib) and //libraries that have been managed by bower-installer (/lib-bower) - gulp.src(sources.globs.lib) - .pipe(gulp.dest(root + targets.lib)); + stream.add( + gulp.src(sources.globs.lib) + .pipe(gulp.dest(root + targets.lib)) + ); //Copies all static assets into /root / assets folder //css, fonts and image files - gulp.src(sources.globs.assets) - .pipe(gulp.dest(root + targets.assets)); + stream.add( + gulp.src(sources.globs.assets) + .pipe(gulp.dest(root + targets.assets)) + ); // Copies all the less files related to the preview into their folder //these are not pre-processed as preview has its own less combiler client side - gulp.src("src/canvasdesigner/editors/*.less") - .pipe(gulp.dest(root + targets.assets + "/less")); + stream.add( + gulp.src("src/canvasdesigner/editors/*.less") + .pipe(gulp.dest(root + targets.assets + "/less")) + ); + + return stream; }); @@ -140,57 +159,95 @@ gulp.task('dependencies', function () { * Copies all angular JS files into their seperate umbraco.*.js file **************************/ gulp.task('js', function () { + + //we run multiple streams, so merge them all together + var stream = new MergeStream(); - gulp.src(sources.globs.js) - .pipe(gulp.dest(root + targets.js)); + stream.add( + gulp.src(sources.globs.js) + .pipe(gulp.dest(root + targets.js)) + ); - return _.forEach(sources.js, function (group) { - processJs(group.files, group.out); - }); + _.forEach(sources.js, function (group) { + stream.add (processJs(group.files, group.out) ); + }); + + return stream; }); gulp.task('less', function () { - gulp.src(sources.globs.assets) - .pipe(gulp.dest(targets.assets)); + var stream = new MergeStream(); - return _.forEach(sources.less, function (group) { - processLess(group.files, group.out); + _.forEach(sources.less, function (group) { + stream.add( processLess(group.files, group.out) ); }); + + return stream; }); gulp.task('views', function () { - return _.forEach(sources.views, function (group) { - gulp.src(group.files) - .pipe(gulp.dest(root + targets.views + group.folder)); + var stream = new MergeStream(); + + _.forEach(sources.views, function (group) { + + stream.add ( + gulp.src(group.files) + .pipe(gulp.dest(root + targets.views + group.folder)) + ); + }); + return stream; }); gulp.task('watch', defaultTasks, function () { + var stream = new MergeStream(); + //Setup a watcher for all groups of javascript files _.forEach(sources.js, function (group) { + if(group.watch !== false){ - watch(group.files, { ignoreInitial: true }, function () { - processJs(group.files, group.out); - }); + + stream.add( + + watch(group.files, { ignoreInitial: true }, function (file) { + + console.info(file.path + " has changed, added to: " + group.out); + processJs(group.files, group.out); + + }) + + ); + } + }); - //watch all less files and trigger the less task - watch(sources.globs.less, { ignoreInitial: true }, function () { - gulp.run(['less']); - }); + stream.add( + //watch all less files and trigger the less task + watch(sources.globs.less, { ignoreInitial: true }, function () { + gulp.run(['less']); + }) + + ); + //watch all views - copy single file changes - watch(sources.globs.views) - .pipe(gulp.dest(root + targets.views)); + stream.add( + watch(sources.globs.views) + .pipe(gulp.dest(root + targets.views)) + ); //watch all app js files that will not be merged - copy single file changes - watch(sources.globs.js) - .pipe(gulp.dest(root + targets.js)); + stream.add( + watch(sources.globs.js) + .pipe(gulp.dest(root + targets.js)) + ); + + return stream; }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 30060c3b7e..b9b88090ee 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -35,6 +35,7 @@ "karma": "^1.3.0", "karma-jasmine": "^0.1.0", "karma-phantomjs-launcher": "^1.0.2", - "lodash": "^4.16.3" + "lodash": "^4.16.3", + "merge-stream": "^1.0.1" } } From 7c9476ca41de36b5e20d32af48dde91f36aa0af0 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Mon, 9 Jan 2017 15:27:05 +0100 Subject: [PATCH 10/64] Sets the correct root dir for compiling files --- src/Umbraco.Web.UI.Client/gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index a2b6983c6a..0be14c1b08 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -90,7 +90,7 @@ var sources = { } }; -var root = "../__BUILD/umbraco/"; +var root = "../Umbraco.Web.UI/umbraco/"; var targets = { js: "js", lib: "lib", From a68eb91c145e6fbab0fa3e1b921ccf09a91fa242 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Tue, 10 Jan 2017 14:58:46 +0100 Subject: [PATCH 11/64] update buildbell.bat file to use gulp --- build/BuildBelle.bat | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index 8a07ee380a..7fb1725cb2 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -25,9 +25,8 @@ CD %CD%\..\src\Umbraco.Web.UI.Client\ ECHO Do npm install and the grunt build of Belle call npm cache clean --quiet call npm install --quiet -call npm install -g grunt-cli --quiet -call npm install -g bower --quiet -call grunt build --buildversion=%release% +call npm install -g gulp-cli --quiet +call gulp --buildversion=%release% ECHO Move back to the build folder CD %buildFolder% \ No newline at end of file From 1757e7b4c86103cdd6ea569e8e4d1b6389bd8ede Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Mon, 16 Jan 2017 14:53:07 +0100 Subject: [PATCH 12/64] Update gulpfile and buildbelle file Should now have the right paths for running it from VS --- build/BuildBelle.bat | 2 +- src/Umbraco.Web.UI.Client/gulpfile.js | 42 +++++++++++++++------------ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index 7fb1725cb2..fe06f81587 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -26,7 +26,7 @@ ECHO Do npm install and the grunt build of Belle call npm cache clean --quiet call npm install --quiet call npm install -g gulp-cli --quiet -call gulp --buildversion=%release% +call gulp ECHO Move back to the build folder CD %buildFolder% \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 0be14c1b08..644adfc718 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -75,28 +75,29 @@ var sources = { //selectors for copying all views into the build //processed in the views task views:{ - umbraco: {files: ["src/views/**/*html"], folder: "/"}, + umbraco: {files: ["src/views/**/*html"], folder: ""}, preview: { files: ["src/canvasdesigner/**/*.html"], folder: "../preview"}, - installer: {files: ["src/installer/steps/*.html"], folder: "/install"} + installer: {files: ["src/installer/steps/*.html"], folder: "install"} }, //globs for file-watching globs:{ - views: "src/views/**/*html", - less: "src/less*.less", - js: "src/*.js", - lib: "lib/**/*.*, lib-bower/**/*.*", - assets: "src/assets/**" + views: "./src/views/**/*.html", + less: "./src/less/**/*.less", + js: "./src/*.js", + lib: "./lib/**/*", + bower: "./lib-bower/**/*", + assets: "./src/assets/**" } }; -var root = "../Umbraco.Web.UI/umbraco/"; +var root = "../Umbraco.Web.UI/Umbraco/"; var targets = { - js: "js", - lib: "lib", - views: "views", - css: "assets/css", - assets: "assets" + js: "js/", + lib: "lib/", + views: "views/", + css: "assets/css/", + assets: "assets/" }; @@ -120,13 +121,13 @@ gulp.task('dependencies', function () { //Tinymce stream.add( - gulp.src("bower_components/tinymce/plugins/**") + gulp.src("./bower_components/tinymce/plugins/**") .pipe(gulp.dest(root + targets.lib + "/tinymce/plugins/")) ); //font-awesome stream.add( - gulp.src("bower_components/font-awesome/{fonts,css}/*") + gulp.src("./bower_components/font-awesome/{fonts,css}/*") .pipe(gulp.dest(root + targets.lib + "/font-awesome")) ); @@ -137,6 +138,11 @@ gulp.task('dependencies', function () { .pipe(gulp.dest(root + targets.lib)) ); + stream.add( + gulp.src(sources.globs.bower) + .pipe(gulp.dest(root + targets.lib)) + ); + //Copies all static assets into /root / assets folder //css, fonts and image files stream.add( @@ -193,9 +199,11 @@ gulp.task('views', function () { _.forEach(sources.views, function (group) { + console.log("copying " + group.files + " to " + root + targets.views + group.folder) + stream.add ( gulp.src(group.files) - .pipe(gulp.dest(root + targets.views + group.folder)) + .pipe( gulp.dest(root + targets.views + group.folder) ) ); }); @@ -233,10 +241,8 @@ gulp.task('watch', defaultTasks, function () { watch(sources.globs.less, { ignoreInitial: true }, function () { gulp.run(['less']); }) - ); - //watch all views - copy single file changes stream.add( watch(sources.globs.views) From 21f003e705586dc2aea4f981cc939a61c7ee04ca Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 May 2017 12:26:04 +0200 Subject: [PATCH 13/64] clean up ace builds --- src/Umbraco.Web.UI.Client/bower.json | 2 -- src/Umbraco.Web.UI.Client/gulpfile.js | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 0e18f99b46..ba76c249b2 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -47,8 +47,6 @@ "bower_components/jquery/dist/jquery.min.map" ], - "ace-builds": "bower_components/ace-builds/src-min-noconflict/**", - "angular-dynamic-locale": [ "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js", "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js.map" diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 644adfc718..96b24a7cec 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -130,6 +130,22 @@ gulp.task('dependencies', function () { gulp.src("./bower_components/font-awesome/{fonts,css}/*") .pipe(gulp.dest(root + targets.lib + "/font-awesome")) ); + + // ace Editor + stream.add( + gulp.src(["bower_components/ace-builds/src-min-noconflict/ace.js", + "bower_components/ace-builds/src-min-noconflict/ext-language_tools.js", + "bower_components/ace-builds/src-min-noconflict/ext-searchbox.js", + "bower_components/ace-builds/src-min-noconflict/ext-settings_menu.js", + "bower_components/ace-builds/src-min-noconflict/snippets/text.js", + "bower_components/ace-builds/src-min-noconflict/snippets/javascript.js", + "bower_components/ace-builds/src-min-noconflict/theme-chrome.js", + "bower_components/ace-builds/src-min-noconflict/mode-razor.js", + "bower_components/ace-builds/src-min-noconflict/mode-javascript.js", + "bower_components/ace-builds/src-min-noconflict/worker-javascript.js"], + {base: "./bower_components/ace-builds/"}) + .pipe(gulp.dest(root + targets.lib + "/ace-builds")) + ); //copy over libs which are not on bower (/lib) and //libraries that have been managed by bower-installer (/lib-bower) From 229a15be4fec037cb0bc5909f7c1b5e0fbe0a0ee Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 May 2017 13:10:40 +0200 Subject: [PATCH 14/64] add nonodes to gulp --- src/Umbraco.Web.UI.Client/gulpfile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 96b24a7cec..e3433d99ad 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -54,10 +54,11 @@ var sources = { //processed in the less task less: { installer: { files: ["src/less/installer.less"], out: "installer.css" }, + nonodes: { files: ["src/less/pages/nonodes.less"], out: "nonodes.style.min.css"}, preview: { files: ['src/less/canvas-designer.less', 'src/less/helveticons.less'], out: "canvasdesigner.css" }, umbraco: { files: ["src/less/belle.less"], out: "umbraco.css" } }, - + //js files for backoffie //processed in the js task js: { From 631a9f92eaa1281a96942bca960cde481d1817bd Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 May 2017 14:19:37 +0200 Subject: [PATCH 15/64] fix merge bug + add ignore list --- src/Umbraco.Web.UI.Client/bower.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 15311b645f..b539217338 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -30,13 +30,19 @@ "angular-local-storage": "~0.2.3", "moment": "~2.10.3", "ace-builds": "^1.2.3", - "tinymce": "tinymce-dist#^4.5.2" + "font-awesome": "~4.2" }, "install": { - + "path": "lib-bower", + "ignore": [ + "font-awesome", + "angular", + "bootstrap" + ], + "sources": { "moment": "bower_components/moment/min/moment-with-locales.js", "underscore": [ From e35a748c92f4ff76324c99076e50fa2c666b2122 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Tue, 27 Jun 2017 17:00:00 +0200 Subject: [PATCH 16/64] Fixes issues with missing angular modules and adds sourcemap support --- src/Umbraco.Web.UI.Client/gulpfile.js | 11 +++++++++-- src/Umbraco.Web.UI.Client/package.json | 14 +++++++++----- src/Umbraco.Web.UI.Client/src/installer/_module.js | 1 + 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/installer/_module.js diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index e3433d99ad..cb1883d3ed 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -2,7 +2,10 @@ var watch = require('gulp-watch'); var concat = require('gulp-concat'); var rename = require('gulp-rename'); -var wrap = require('gulp-wrap'); +var sourcemaps = require('gulp-sourcemaps'); +var wrap = require("gulp-wrap-js"); +var sort = require('gulp-sort'); +var uglify = require('gulp-uglify'); var _ = require('lodash'); var MergeStream = require('merge-stream'); @@ -21,8 +24,12 @@ Helper functions function processJs(files, out) { return gulp.src(files) + .pipe(sort()) + .pipe(sourcemaps.init()) .pipe(concat(out)) - .pipe(wrap('(function(){\n"use strict";\n<%= contents %>\n})();')) + .pipe(wrap('(function(){\n%= body %\n})();')) + .pipe(uglify({ mangle: false })) + .pipe(sourcemaps.write()) .pipe(gulp.dest(root + targets.js)); console.log(out + " compiled"); diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index b9b88090ee..4ca32e5f41 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -16,7 +16,8 @@ }, "scripts": { "install": "bower-installer", - "test": "karma start test/config/karma.conf.js --singlerun" + "test": "karma start test/config/karma.conf.js --singlerun", + "build": "gulp" }, "dependencies": {}, "devDependencies": { @@ -28,13 +29,16 @@ "gulp-less": "^3.1.0", "gulp-postcss": "^6.2.0", "gulp-rename": "^1.2.2", - "gulp-sourcemaps": "^1.6.0", + "gulp-sort": "^2.0.0", + "gulp-sourcemaps": "^2.6.0", + "gulp-uglify": "^3.0.0", "gulp-watch": "^4.3.10", "gulp-wrap": "^0.13.0", + "gulp-wrap-js": "^0.4.1", "jasmine-core": "^2.5.2", - "karma": "^1.3.0", - "karma-jasmine": "^0.1.0", - "karma-phantomjs-launcher": "^1.0.2", + "karma": "^1.7.0", + "karma-jasmine": "^1.1.0", + "karma-phantomjs-launcher": "^1.0.4", "lodash": "^4.16.3", "merge-stream": "^1.0.1" } diff --git a/src/Umbraco.Web.UI.Client/src/installer/_module.js b/src/Umbraco.Web.UI.Client/src/installer/_module.js new file mode 100644 index 0000000000..d031c60679 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/installer/_module.js @@ -0,0 +1 @@ +angular.module('umbraco.install', []); \ No newline at end of file From d4a22441905333a2499df08fe8d31e8e818169fa Mon Sep 17 00:00:00 2001 From: Stephan Lonntorp Date: Tue, 25 Jul 2017 13:28:46 +0200 Subject: [PATCH 17/64] Added missing translations --- src/Umbraco.Web.UI/umbraco/config/lang/sv.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index 51c780f57e..17bc3ee5d0 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -638,8 +638,12 @@ Återställ + Definiera beskräning + Ge beskärningen ett alias och dess standardbredd och -höjd + spara beskärning + Lägg till ny beskärning - + Nuvarande version Röd text kommer inte att synas i den valda versionen. , Grön betyder att den har tillkommit]]> Dokumentet har återgått till en tidigare version From babcbd2f7aff1a2f522a066c8a5969eed4713088 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Mon, 7 Aug 2017 15:29:34 +0200 Subject: [PATCH 18/64] Updates from dev-v7 --- src/Umbraco.Web.UI.Client/package.json | 2 +- .../Views/Partials/Grid/Bootstrap2.cshtml | 32 ++--- .../Views/Partials/Grid/Bootstrap3.cshtml | 32 ++--- .../Views/Partials/Grid/Editors/Base.cshtml | 1 + .../Views/Partials/Grid/Editors/Embed.cshtml | 7 +- .../Views/Partials/Grid/Editors/Macro.cshtml | 2 + .../Views/Partials/Grid/Editors/Media.cshtml | 3 +- .../Partials/Grid/Editors/TextString.cshtml | 5 +- .../config/grid.editors.config.js | 109 +++++++++++++++++- 9 files changed, 141 insertions(+), 52 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 4ca32e5f41..89dcd3a0db 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -6,7 +6,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git@github.com:umbraco/umbraco-cms.git" + "url": "https://github.com/umbraco/Umbraco-CMS.git" }, "bugs": { "url": "https://issues.umbraco.org" diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml index c8f9ab7cd1..8b189ae1a0 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml @@ -2,10 +2,6 @@ @using Umbraco.Web.Templates @using Newtonsoft.Json.Linq -@* - Razor helpers located at the bottom of this file -*@ - @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; @@ -64,29 +60,21 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) - { - var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); - attrs.Add(property.Name + "=\"" + propertyValue + "\""); + foreach (JProperty property in cfg.Properties()) { + attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - { - var propertyValue = property.Value.ToString(); - if (string.IsNullOrWhiteSpace(propertyValue) == false) - { - cssVals.Add(property.Name + ":" + propertyValue + ";"); - } - } + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); - if (cssVals.Any()) - attrs.Add("style=\"" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "\""); + if (cssVals.Any()) + attrs.Add("style='" + string.Join(" ", cssVals) + "'"); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml index 6ab5c1355a..e672aa2a11 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml @@ -2,10 +2,6 @@ @using Umbraco.Web.Templates @using Newtonsoft.Json.Linq -@* - Razor helpers located at the bottom of this file -*@ - @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; @@ -64,29 +60,21 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) - { - var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); - attrs.Add(property.Name + "=\"" + propertyValue + "\""); + foreach (JProperty property in cfg.Properties()) { + attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - { - var propertyValue = property.Value.ToString(); - if (string.IsNullOrWhiteSpace(propertyValue) == false) - { - cssVals.Add(property.Name + ":" + propertyValue + ";"); - } - } + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); - if (cssVals.Any()) - attrs.Add("style=\"" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "\""); + if (cssVals.Any()) + attrs.Add("style='" + string.Join(" ", cssVals) + "'"); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml index ffb7603048..a86c04819a 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml @@ -1,4 +1,5 @@ @model dynamic +@using Umbraco.Web.Templates @functions { public static string EditorView(dynamic contentItem) diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml index c27be6bcdf..393157bcf8 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml @@ -1,2 +1,7 @@ @model dynamic -@Html.Raw(Model.value) +@using Umbraco.Web.Templates + + +
+ @Html.Raw(Model.value) +
diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml index ed08bb2484..e0822808d8 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml @@ -1,4 +1,6 @@ @inherits UmbracoViewPage +@using Umbraco.Web.Templates + @if (Model.value != null) { diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml index 5b5adbdc7d..09d04219f2 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml @@ -1,4 +1,5 @@ @model dynamic +@using Umbraco.Web.Templates @if (Model.value != null) { @@ -13,7 +14,7 @@ } } - @Model.value.altText + @Model.value.caption if (Model.value.caption != null) { diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml index 8c92ca0d83..0cac4eb1ff 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml @@ -4,9 +4,8 @@ @if (Model.editor.config.markup != null) { string markup = Model.editor.config.markup.ToString(); - var umbracoHelper = new UmbracoHelper(UmbracoContext.Current); - - markup = markup.Replace("#value#", umbracoHelper.ReplaceLineBreaksForHtml(HttpUtility.HtmlEncode(Model.value.ToString()))); + + markup = markup.Replace("#value#", Model.value.ToString()); markup = markup.Replace("#style#", Model.editor.config.style.ToString()); diff --git a/src/Umbraco.Web.UI/config/grid.editors.config.js b/src/Umbraco.Web.UI/config/grid.editors.config.js index b904920566..1adb6da2dc 100644 --- a/src/Umbraco.Web.UI/config/grid.editors.config.js +++ b/src/Umbraco.Web.UI/config/grid.editors.config.js @@ -1,4 +1,4 @@ -[ +[ { "name": "Rich text editor", "alias": "rte", @@ -11,6 +11,40 @@ "view": "media", "icon": "icon-picture" }, + { + "name": "Image wide", + "alias": "media_wide", + "view": "media", + "render": "/App_Plugins/Grid/Editors/Render/media_wide.cshtml", + "icon": "icon-picture" + }, + { + "name": "Image wide cropped", + "alias": "media_wide_cropped", + "view": "media", + "render": "media", + "icon": "icon-picture", + "config": { + "size": { + "width": 1920, + "height": 700 + } + } + }, + { + "name": "Image rounded", + "alias": "media_round", + "view": "media", + "render": "/App_Plugins/Grid/Editors/Render/media_round.cshtml", + "icon": "icon-picture" + }, + { + "name": "Image w/ text right", + "alias": "media_text_right", + "view": "/App_Plugins/Grid/Editors/Views/media_with_description.html", + "render": "/App_Plugins/Grid/Editors/Render/media_text_right.cshtml", + "icon": "icon-picture" + }, { "name": "Macro", "alias": "macro", @@ -21,8 +55,29 @@ "name": "Embed", "alias": "embed", "view": "embed", + "render": "/App_Plugins/Grid/Editors/Render/embed_videowrapper.cshtml", "icon": "icon-movie-alt" }, + { + "name": "Banner Headline", + "alias": "banner_headline", + "view": "textstring", + "icon": "icon-coin", + "config": { + "style": "font-size: 36px; line-height: 45px; font-weight: bold; text-align:center", + "markup": "

#value#

" + } + }, + { + "name": "Banner Tagline", + "alias": "banner_tagline", + "view": "textstring", + "icon": "icon-coin", + "config": { + "style": "font-size: 25px; line-height: 35px; font-weight: normal; text-align:center", + "markup": "

#value#

" + } + }, { "name": "Headline", "alias": "headline", @@ -32,6 +87,36 @@ "style": "font-size: 36px; line-height: 45px; font-weight: bold", "markup": "

#value#

" } + }, + { + "name": "Headline centered", + "alias": "headline_centered", + "view": "textstring", + "icon": "icon-coin", + "config": { + "style": "font-size: 30px; line-height: 45px; font-weight: bold; text-align:center;", + "markup": "

#value#

" + } + }, + { + "name": "Abstract", + "alias": "abstract", + "view": "textstring", + "icon": "icon-coin", + "config": { + "style": "font-size: 16px; line-height: 20px; font-weight: bold;", + "markup": "

#value#

" + } + }, + { + "name": "Paragraph", + "alias": "paragraph", + "view": "textstring", + "icon": "icon-font", + "config": { + "style": "font-size: 16px; line-height: 20px; font-weight: light;", + "markup": "

#value#

" + } }, { "name": "Quote", @@ -39,8 +124,28 @@ "view": "textstring", "icon": "icon-quote", "config": { - "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-style: italic; font-size: 18px", + "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-variant: italic; font-size: 18px", "markup": "
#value#
" } + }, + { + "name": "Quote with description", + "alias": "quote_D", + "view": "/App_Plugins/Grid/Editors/Views/quote_with_description.html", + "render": "/App_Plugins/Grid/Editors/Render/quote_with_description.cshtml", + "icon": "icon-quote", + "config": { + "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-variant: italic; font-size: 18px" + } + }, + { + "name": "Code", + "alias": "code", + "view": "textstring", + "icon": "icon-code", + "config": { + "style": "overflow: auto;padding: 6px 10px;border: 1px solid #ddd;border-radius: 3px;background-color: #f8f8f8;font-size: .9rem;font-family: 'Courier 10 Pitch', Courier, monospace;line-height: 19px;", + "markup": "
#value#
" + } } ] \ No newline at end of file From 2d089033d0558f93d00079b83ea21fb933212226 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 21 Aug 2017 17:01:18 +1000 Subject: [PATCH 19/64] U4-10268 7.7 Beta - Changing password should be safe by default --- .../Providers/UsersMembershipProvider.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs index 7034101a83..fdebf78480 100644 --- a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs @@ -42,10 +42,36 @@ namespace Umbraco.Web.Security.Providers return entity.AsConcreteMembershipUser(Name, true); } + private bool _allowManuallyChangingPassword = false; + private bool _enablePasswordReset = false; + + /// + /// Indicates whether the membership provider is configured to allow users to reset their passwords. + /// + /// + /// true if the membership provider supports password reset; otherwise, false. The default is FALSE for users. + public override bool EnablePasswordReset + { + get { return _enablePasswordReset; } + } + + /// + /// For backwards compatibility, this provider supports this option by default it is FALSE for users + /// + public override bool AllowManuallyChangingPassword + { + get { return _allowManuallyChangingPassword; } + } + public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { base.Initialize(name, config); + if (config == null) { throw new ArgumentNullException("config"); } + + _allowManuallyChangingPassword = config.GetValue("allowManuallyChangingPassword", false); + _enablePasswordReset = config.GetValue("enablePasswordReset", false); + // test for membertype (if not specified, choose the first member type available) // We'll support both names for legacy reasons: defaultUserTypeAlias & defaultUserGroupAlias From c468e4736e2753d410b860cfe0c5a3f51a687a7d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 25 Aug 2017 14:19:25 +0200 Subject: [PATCH 20/64] bower name should be lowercase --- src/Umbraco.Web.UI.Client/bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index b539217338..dabd110ea9 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -1,5 +1,5 @@ { - "name": "Umbraco", + "name": "umbraco", "version": "7", "homepage": "https://github.com/umbraco/Umbraco-CMS", "authors": [ From 0089f79bd7d04ac17c787e1f0aa7c801f4cebe77 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 25 Aug 2017 14:22:42 +0200 Subject: [PATCH 21/64] add tasks for ngdocs --- src/Umbraco.Web.UI.Client/gulpfile.js | 57 +++++++++++++++++++++++++- src/Umbraco.Web.UI.Client/package.json | 6 ++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index cb1883d3ed..9e32bb85de 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -6,17 +6,21 @@ var sourcemaps = require('gulp-sourcemaps'); var wrap = require("gulp-wrap-js"); var sort = require('gulp-sort'); var uglify = require('gulp-uglify'); +var connect = require('gulp-connect'); +var open = require('gulp-open'); +var runSequence = require('run-sequence'); var _ = require('lodash'); var MergeStream = require('merge-stream'); - //Less + css var postcss = require('gulp-postcss'); var less = require('gulp-less'); var autoprefixer = require('autoprefixer'); var cssnano = require('cssnano'); +// Documentation +var gulpDocs = require('gulp-ngdocs'); /*************************************************************** Helper functions @@ -280,4 +284,55 @@ gulp.task('watch', defaultTasks, function () { ); return stream; +}); + +/************************** + * Build Backoffice UI API documentation + **************************/ +gulp.task('docs', [], function (cb) { + + var options = { + html5Mode: false, + startPage: '/api', + title: "Umbraco Backoffice UI API Documentation", + dest: 'docs/api', + styles: ['docs/umb-docs.css'], + image: "https://our.umbraco.org/assets/images/logo.svg" + } + + return gulpDocs.sections({ + api: { + glob: ['src/common/**/*.js', 'docs/src/api/**/*.ngdoc'], + api: true, + title: 'API Documentation' + } + }) + .pipe(gulpDocs.process(options)) + .pipe(gulp.dest('docs/api')); + cb(); +}); + +gulp.task('connect:docs', function (cb) { + connect.server({ + root: 'docs/api', + livereload: true, + fallback: 'docs/api/index.html', + port: 8880 + }); + cb(); +}); + +gulp.task('open:docs', function (cb) { + + var options = { + uri: 'http://localhost:8880/index.html' + }; + + gulp.src(__filename) + .pipe(open(options)); + cb(); +}); + +gulp.task('docserve', function(cb) { + runSequence('docs', 'connect:docs', 'open:docs', cb); }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 89dcd3a0db..19b124cd07 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -26,7 +26,10 @@ "cssnano": "^3.7.6", "gulp": "^3.9.1", "gulp-concat": "^2.6.0", + "gulp-connect": "^5.0.0", "gulp-less": "^3.1.0", + "gulp-ngdocs": "^0.3.0", + "gulp-open": "^2.0.0", "gulp-postcss": "^6.2.0", "gulp-rename": "^1.2.2", "gulp-sort": "^2.0.0", @@ -40,6 +43,7 @@ "karma-jasmine": "^1.1.0", "karma-phantomjs-launcher": "^1.0.4", "lodash": "^4.16.3", - "merge-stream": "^1.0.1" + "merge-stream": "^1.0.1", + "run-sequence": "^2.1.0" } } From 5cbe2b6ce09116f50237a5cd845d47b2aff9716a Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 28 Aug 2017 14:41:54 +0200 Subject: [PATCH 22/64] Add tasks for karma testing --- src/Umbraco.Web.UI.Client/gulpfile.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 9e32bb85de..bae17d3da1 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -22,6 +22,9 @@ var cssnano = require('cssnano'); // Documentation var gulpDocs = require('gulp-ngdocs'); +// Testing +var karmaServer = require('karma').Server; + /*************************************************************** Helper functions ***************************************************************/ @@ -335,4 +338,25 @@ gulp.task('open:docs', function (cb) { gulp.task('docserve', function(cb) { runSequence('docs', 'connect:docs', 'open:docs', cb); +}); + +/************************** + * Build tests + **************************/ + + // Karma test +gulp.task('test:unit', function() { + new karmaServer({ + configFile: __dirname + "/test/config/karma.conf.js", + keepalive: true + }) + .start(); +}); + +gulp.task('test:e2e', function() { + new karmaServer({ + configFile: __dirname + "/test/config/e2e.js", + keepalive: true + }) + .start(); }); \ No newline at end of file From aa5d7fc9fc66318b28831d1c05f26214e57c7042 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 28 Aug 2017 15:42:31 +0200 Subject: [PATCH 23/64] only use source maps when developing --- src/Umbraco.Web.UI.Client/gulpfile.js | 58 +++++++++++++++++++------- src/Umbraco.Web.UI.Client/package.json | 1 + 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index bae17d3da1..28c45317ca 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -9,6 +9,7 @@ var uglify = require('gulp-uglify'); var connect = require('gulp-connect'); var open = require('gulp-open'); var runSequence = require('run-sequence'); +var gulpif = require('gulp-if'); var _ = require('lodash'); var MergeStream = require('merge-stream'); @@ -28,15 +29,15 @@ var karmaServer = require('karma').Server; /*************************************************************** Helper functions ***************************************************************/ -function processJs(files, out) { +function processJs(files, out, makeSourcemaps) { return gulp.src(files) .pipe(sort()) - .pipe(sourcemaps.init()) + .pipe(gulpif(makeSourcemaps, sourcemaps.init())) .pipe(concat(out)) .pipe(wrap('(function(){\n%= body %\n})();')) .pipe(uglify({ mangle: false })) - .pipe(sourcemaps.write()) + .pipe(gulpif(makeSourcemaps, sourcemaps.write())) .pipe(gulp.dest(root + targets.js)); console.log(out + " compiled"); @@ -117,11 +118,23 @@ var targets = { /************************** - * Runs the default tasks for the project to prepare backoffice files + * Main tasks for the project to prepare backoffice files **************************/ -var defaultTasks = ["dependencies","js", "less", "views"]; -gulp.task("default", defaultTasks); + // Build - build the files ready for production +gulp.task('build', function(cb) { + runSequence(["dependencies", "js:production", "less", "views"], "test:unit", cb); +}); + +// Dev - build the files ready for development and start watchers +gulp.task('dev', function(cb) { + runSequence(["dependencies", "js:development", "less", "views"], "watch", cb); +}); + +// Docserve - build and open the back office documentation +gulp.task('docserve', function(cb) { + runSequence('docs', 'connect:docs', 'open:docs', cb); +}); /************************** * Task processes and copies all dependencies, either installed by bower, npm or stored locally in the project @@ -195,7 +208,7 @@ gulp.task('dependencies', function () { /************************** * Copies all angular JS files into their seperate umbraco.*.js file **************************/ -gulp.task('js', function () { +gulp.task('js:production', function () { //we run multiple streams, so merge them all together var stream = new MergeStream(); @@ -206,12 +219,29 @@ gulp.task('js', function () { ); _.forEach(sources.js, function (group) { - stream.add (processJs(group.files, group.out) ); + stream.add (processJs(group.files, group.out, false) ); }); return stream; }); +gulp.task('js:development', function () { + + //we run multiple streams, so merge them all together + var stream = new MergeStream(); + + stream.add( + gulp.src(sources.globs.js) + .pipe(gulp.dest(root + targets.js)) + ); + + _.forEach(sources.js, function (group) { + stream.add (processJs(group.files, group.out, true) ); + }); + + return stream; +}); + gulp.task('less', function () { var stream = new MergeStream(); @@ -243,7 +273,7 @@ gulp.task('views', function () { }); -gulp.task('watch', defaultTasks, function () { +gulp.task('watch', function () { var stream = new MergeStream(); @@ -286,7 +316,7 @@ gulp.task('watch', defaultTasks, function () { .pipe(gulp.dest(root + targets.js)) ); - return stream; + return stream; }); /************************** @@ -336,10 +366,6 @@ gulp.task('open:docs', function (cb) { cb(); }); -gulp.task('docserve', function(cb) { - runSequence('docs', 'connect:docs', 'open:docs', cb); -}); - /************************** * Build tests **************************/ @@ -348,7 +374,7 @@ gulp.task('docserve', function(cb) { gulp.task('test:unit', function() { new karmaServer({ configFile: __dirname + "/test/config/karma.conf.js", - keepalive: true + keepalive: true }) .start(); }); @@ -356,7 +382,7 @@ gulp.task('test:unit', function() { gulp.task('test:e2e', function() { new karmaServer({ configFile: __dirname + "/test/config/e2e.js", - keepalive: true + keepalive: true }) .start(); }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 19b124cd07..d2a5b6ac41 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -27,6 +27,7 @@ "gulp": "^3.9.1", "gulp-concat": "^2.6.0", "gulp-connect": "^5.0.0", + "gulp-if": "^2.0.2", "gulp-less": "^3.1.0", "gulp-ngdocs": "^0.3.0", "gulp-open": "^2.0.0", From 70231fcee238ee2e1d06e472537a2a29bd2d92a1 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 29 Aug 2017 10:26:11 +0200 Subject: [PATCH 24/64] fix codemirror folder structure --- src/Umbraco.Web.UI.Client/bower.json | 3 +- src/Umbraco.Web.UI.Client/gulpfile.js | 55 ++++++++++++++++++--------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index dabd110ea9..11196da4dd 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -40,7 +40,8 @@ "ignore": [ "font-awesome", "angular", - "bootstrap" + "bootstrap", + "codemirror" ], "sources": { diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 28c45317ca..89b686116a 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -149,31 +149,50 @@ gulp.task('dependencies', function () { //Tinymce stream.add( - gulp.src("./bower_components/tinymce/plugins/**") - .pipe(gulp.dest(root + targets.lib + "/tinymce/plugins/")) - ); + gulp.src("./bower_components/tinymce/plugins/**") + .pipe(gulp.dest(root + targets.lib + "/tinymce/plugins/")) + ); //font-awesome stream.add( - gulp.src("./bower_components/font-awesome/{fonts,css}/*") - .pipe(gulp.dest(root + targets.lib + "/font-awesome")) - ); + gulp.src("./bower_components/font-awesome/{fonts,css}/*") + .pipe(gulp.dest(root + targets.lib + "/font-awesome")) + ); // ace Editor stream.add( - gulp.src(["bower_components/ace-builds/src-min-noconflict/ace.js", - "bower_components/ace-builds/src-min-noconflict/ext-language_tools.js", - "bower_components/ace-builds/src-min-noconflict/ext-searchbox.js", - "bower_components/ace-builds/src-min-noconflict/ext-settings_menu.js", - "bower_components/ace-builds/src-min-noconflict/snippets/text.js", - "bower_components/ace-builds/src-min-noconflict/snippets/javascript.js", - "bower_components/ace-builds/src-min-noconflict/theme-chrome.js", - "bower_components/ace-builds/src-min-noconflict/mode-razor.js", - "bower_components/ace-builds/src-min-noconflict/mode-javascript.js", - "bower_components/ace-builds/src-min-noconflict/worker-javascript.js"], - {base: "./bower_components/ace-builds/"}) + gulp.src(["bower_components/ace-builds/src-min-noconflict/ace.js", + "bower_components/ace-builds/src-min-noconflict/ext-language_tools.js", + "bower_components/ace-builds/src-min-noconflict/ext-searchbox.js", + "bower_components/ace-builds/src-min-noconflict/ext-settings_menu.js", + "bower_components/ace-builds/src-min-noconflict/snippets/text.js", + "bower_components/ace-builds/src-min-noconflict/snippets/javascript.js", + "bower_components/ace-builds/src-min-noconflict/theme-chrome.js", + "bower_components/ace-builds/src-min-noconflict/mode-razor.js", + "bower_components/ace-builds/src-min-noconflict/mode-javascript.js", + "bower_components/ace-builds/src-min-noconflict/worker-javascript.js"], + { base: "./bower_components/ace-builds/" }) .pipe(gulp.dest(root + targets.lib + "/ace-builds")) - ); + ); + + // code mirror + stream.add( + gulp.src([ + "bower_components/codemirror/lib/codemirror.js", + "bower_components/codemirror/lib/codemirror.css", + + "bower_components/codemirror/mode/css/*", + "bower_components/codemirror/mode/javascript/*", + "bower_components/codemirror/mode/xml/*", + "bower_components/codemirror/mode/htmlmixed/*", + + "bower_components/codemirror/addon/search/*", + "bower_components/codemirror/addon/edit/*", + "bower_components/codemirror/addon/selection/*", + "bower_components/codemirror/addon/dialog/*"], + { base: "./bower_components/codemirror/" }) + .pipe(gulp.dest(root + targets.lib + "/codemirror")) + ); //copy over libs which are not on bower (/lib) and //libraries that have been managed by bower-installer (/lib-bower) From 1307cb117337f55979f953bb1b9ee7e8323fe1af Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 29 Aug 2017 11:39:16 +0200 Subject: [PATCH 25/64] add themes for tinymce - they are needed for loading it --- src/Umbraco.Web.UI.Client/gulpfile.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 89b686116a..e9acb1d10a 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -149,8 +149,10 @@ gulp.task('dependencies', function () { //Tinymce stream.add( - gulp.src("./bower_components/tinymce/plugins/**") - .pipe(gulp.dest(root + targets.lib + "/tinymce/plugins/")) + gulp.src(["./bower_components/tinymce/plugins/**", + "./bower_components/tinymce/themes/**"], + { base: "./bower_components/tinymce/" }) + .pipe(gulp.dest(root + targets.lib + "/tinymce")) ); //font-awesome From 7ccfc79080a0383912f87484c9e100b3c56d1572 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 29 Aug 2017 12:40:57 +0200 Subject: [PATCH 26/64] update BuildBelle.bat to use gulp build task --- build/BuildBelle.bat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index a6fafd669e..7f13d548dc 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -57,13 +57,13 @@ IF NOT [%npmPath%] == [] GOTO :build ECHO Change directory to %CD%\..\src\Umbraco.Web.UI.Client\ CD %CD%\..\src\Umbraco.Web.UI.Client\ - ECHO Do npm install and the grunt build of Belle + ECHO Do npm install and the gulp build of Belle call npm cache clean --quiet call npm install -g gulp-cli --quiet call npm install -g bower --quiet call npm install --quiet - call gulp --buildversion=%release% + call gulp build --buildversion=%release% ECHO Move back to the build folder CD "%buildFolder%" \ No newline at end of file From ce8390768e01c52c31835934111037d01dc2981b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 29 Aug 2017 14:17:41 +0200 Subject: [PATCH 27/64] Source maps gets cached really hard in the browser so I will remove them for now until we find a solution to clear them --- src/Umbraco.Web.UI.Client/gulpfile.js | 33 ++++---------------------- src/Umbraco.Web.UI.Client/package.json | 3 --- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index e9acb1d10a..9bce49b145 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -2,14 +2,11 @@ var watch = require('gulp-watch'); var concat = require('gulp-concat'); var rename = require('gulp-rename'); -var sourcemaps = require('gulp-sourcemaps'); var wrap = require("gulp-wrap-js"); var sort = require('gulp-sort'); -var uglify = require('gulp-uglify'); var connect = require('gulp-connect'); var open = require('gulp-open'); var runSequence = require('run-sequence'); -var gulpif = require('gulp-if'); var _ = require('lodash'); var MergeStream = require('merge-stream'); @@ -29,15 +26,12 @@ var karmaServer = require('karma').Server; /*************************************************************** Helper functions ***************************************************************/ -function processJs(files, out, makeSourcemaps) { +function processJs(files, out) { return gulp.src(files) .pipe(sort()) - .pipe(gulpif(makeSourcemaps, sourcemaps.init())) .pipe(concat(out)) .pipe(wrap('(function(){\n%= body %\n})();')) - .pipe(uglify({ mangle: false })) - .pipe(gulpif(makeSourcemaps, sourcemaps.write())) .pipe(gulp.dest(root + targets.js)); console.log(out + " compiled"); @@ -123,12 +117,12 @@ var targets = { // Build - build the files ready for production gulp.task('build', function(cb) { - runSequence(["dependencies", "js:production", "less", "views"], "test:unit", cb); + runSequence(["dependencies", "js", "less", "views"], "test:unit", cb); }); // Dev - build the files ready for development and start watchers gulp.task('dev', function(cb) { - runSequence(["dependencies", "js:development", "less", "views"], "watch", cb); + runSequence(["dependencies", "js", "less", "views"], "watch", cb); }); // Docserve - build and open the back office documentation @@ -229,7 +223,7 @@ gulp.task('dependencies', function () { /************************** * Copies all angular JS files into their seperate umbraco.*.js file **************************/ -gulp.task('js:production', function () { +gulp.task('js', function () { //we run multiple streams, so merge them all together var stream = new MergeStream(); @@ -240,29 +234,12 @@ gulp.task('js:production', function () { ); _.forEach(sources.js, function (group) { - stream.add (processJs(group.files, group.out, false) ); + stream.add (processJs(group.files, group.out) ); }); return stream; }); -gulp.task('js:development', function () { - - //we run multiple streams, so merge them all together - var stream = new MergeStream(); - - stream.add( - gulp.src(sources.globs.js) - .pipe(gulp.dest(root + targets.js)) - ); - - _.forEach(sources.js, function (group) { - stream.add (processJs(group.files, group.out, true) ); - }); - - return stream; -}); - gulp.task('less', function () { var stream = new MergeStream(); diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index d2a5b6ac41..d95652bba2 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -27,15 +27,12 @@ "gulp": "^3.9.1", "gulp-concat": "^2.6.0", "gulp-connect": "^5.0.0", - "gulp-if": "^2.0.2", "gulp-less": "^3.1.0", "gulp-ngdocs": "^0.3.0", "gulp-open": "^2.0.0", "gulp-postcss": "^6.2.0", "gulp-rename": "^1.2.2", "gulp-sort": "^2.0.0", - "gulp-sourcemaps": "^2.6.0", - "gulp-uglify": "^3.0.0", "gulp-watch": "^4.3.10", "gulp-wrap": "^0.13.0", "gulp-wrap-js": "^0.4.1", From 1bda9f32c189c471322e6458f183b31f94acd42f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 29 Aug 2017 14:27:43 +0200 Subject: [PATCH 28/64] only copy minified version of font-awesome --- src/Umbraco.Web.UI.Client/gulpfile.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 9bce49b145..b1839d1e6f 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -151,7 +151,9 @@ gulp.task('dependencies', function () { //font-awesome stream.add( - gulp.src("./bower_components/font-awesome/{fonts,css}/*") + gulp.src(["./bower_components/font-awesome/fonts/*", + "./bower_components/font-awesome/css/font-awesome.min.css"], + { base: "./bower_components/font-awesome/" }) .pipe(gulp.dest(root + targets.lib + "/font-awesome")) ); From 110a3b2a12e058b6277fecfb558e523096cbadd9 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 29 Aug 2017 14:35:54 +0200 Subject: [PATCH 29/64] add small interval to file watcher to keep windows calm --- src/Umbraco.Web.UI.Client/gulpfile.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index b1839d1e6f..be72090661 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -276,6 +276,7 @@ gulp.task('views', function () { gulp.task('watch', function () { var stream = new MergeStream(); + var watchInterval = 500; //Setup a watcher for all groups of javascript files _.forEach(sources.js, function (group) { @@ -284,7 +285,7 @@ gulp.task('watch', function () { stream.add( - watch(group.files, { ignoreInitial: true }, function (file) { + watch(group.files, { ignoreInitial: true, interval: watchInterval }, function (file) { console.info(file.path + " has changed, added to: " + group.out); processJs(group.files, group.out); @@ -299,20 +300,20 @@ gulp.task('watch', function () { stream.add( //watch all less files and trigger the less task - watch(sources.globs.less, { ignoreInitial: true }, function () { + watch(sources.globs.less, { ignoreInitial: true, interval: watchInterval }, function () { gulp.run(['less']); }) ); //watch all views - copy single file changes stream.add( - watch(sources.globs.views) + watch(sources.globs.views, { interval: watchInterval }) .pipe(gulp.dest(root + targets.views)) ); //watch all app js files that will not be merged - copy single file changes stream.add( - watch(sources.globs.js) + watch(sources.globs.js, { interval: watchInterval }) .pipe(gulp.dest(root + targets.js)) ); From b10e646adeafb78c8f71ab18a2c91550c12ff0fa Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 29 Aug 2017 14:53:44 +0200 Subject: [PATCH 30/64] remove grunt file --- src/Umbraco.Web.UI.Client/gruntFile.js | 597 ------------------------- 1 file changed, 597 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/gruntFile.js diff --git a/src/Umbraco.Web.UI.Client/gruntFile.js b/src/Umbraco.Web.UI.Client/gruntFile.js deleted file mode 100644 index 83cb15df64..0000000000 --- a/src/Umbraco.Web.UI.Client/gruntFile.js +++ /dev/null @@ -1,597 +0,0 @@ -module.exports = function (grunt) { - - - - // Default task. - grunt.registerTask('default', ['jshint:dev', 'build', 'karma:unit']); - grunt.registerTask('dev', ['jshint:dev', 'build-dev', 'webserver', 'open:dev', 'watch']); - grunt.registerTask('docserve', ['docs:api', 'connect:docserver', 'open:docs', 'watch:docs']); - grunt.registerTask('vs', ['jshint:dev', 'build-dev', 'watch']); - - //TODO: Too much watching, this brings windows to it's knees when in dev mode - //run by the watch task - grunt.registerTask('watch-js', ['jshint:dev', 'concat', 'copy:app', 'copy:mocks', 'copy:canvasdesigner', 'copy:vs', 'karma:unit']); - grunt.registerTask('watch-less', ['recess:build', 'recess:installer', 'recess:nonodes', 'recess:canvasdesigner', 'postcss', 'copy:canvasdesigner', 'copy:assets', 'copy:vs']); - grunt.registerTask('watch-html', ['copy:views', 'copy:vs']); - grunt.registerTask('watch-installer', ['concat:install', 'concat:installJs', 'copy:installer', 'copy:vs']); - grunt.registerTask('watch-canvasdesigner', ['copy:canvasdesigner', 'concat:canvasdesignerJs', 'copy:vs']); - grunt.registerTask('watch-test', ['jshint:dev', 'karma:unit']); - - //triggered from grunt - grunt.registerTask('build', ['concat', 'recess:build', 'recess:installer', 'recess:nonodes', 'recess:canvasdesigner', 'postcss', 'bower-install-simple', 'bower', 'copy', 'clean:post']); - - //triggered from grunt dev vs or grunt vs - grunt.registerTask('build-dev', ['clean:pre', 'concat', 'recess:build', 'recess:installer', 'recess:nonodes', 'postcss', 'bower-install-simple', 'bower', 'copy']); - - //utillity tasks - grunt.registerTask('docs', ['ngdocs']); - grunt.registerTask('webserver', ['connect:devserver']); - - - // Print a timestamp (useful for when watching) - grunt.registerTask('timestamp', function () { - grunt.log.subhead(Date()); - }); - - // Project configuration. - grunt.initConfig({ - buildVersion: grunt.option('buildversion') || '7', - connect: { - devserver: { - options: { - port: 9990, - hostname: '0.0.0.0', - base: './build', - middleware: function(connect, options) { - return [ - //uncomment to enable CSP - // util.csp(), - //util.rewrite(), - connect.favicon('images/favicon.ico'), - connect.static(options.base), - connect.directory(options.base) - ]; - } - } - }, - testserver: {}, - docserver: { - options: { - port: 8880, - hostname: '0.0.0.0', - base: './docs/api', - middleware: function(connect, options) { - return [ - //uncomment to enable CSP - // util.csp(), - //util.rewrite(), - connect.static(options.base), - connect.directory(options.base) - ]; - } - } - }, - }, - - open: { - dev: { - path: 'http://localhost:9990/belle/' - }, - docs: { - path: 'http://localhost:8880/index.html' - } - }, - - distdir: 'build/belle', - vsdir: '../Umbraco.Web.UI/umbraco', - pkg: grunt.file.readJSON('package.json'), - banner: - '/*! <%= pkg.title || pkg.name %>\n' + - '<%= pkg.homepage ? " * " + pkg.homepage + "\\n" : "" %>' + - ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>;\n' + - ' * Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %>\n */\n', - src: { - js: ['src/**/*.js', 'src/*.js'], - - common: ['src/common/**/*.js'], - controllers: ['src/**/*.controller.js'], - - specs: ['test/**/*.spec.js'], - scenarios: ['test/**/*.scenario.js'], - samples: ['sample files/*.js'], - html: ['src/index.html', 'src/install.html'], - - everything: ['src/**/*.*', 'test/**/*.*', 'docs/**/*.*'], - - tpl: { - app: ['src/views/**/*.html'], - common: ['src/common/**/*.tpl.html'] - }, - less: ['src/less/belle.less'], // recess:build doesn't accept ** in its file patterns - prod: ['<%= distdir %>/js/*.js'] - }, - - clean: { - pre: ['<%= distdir %>/*'], - post: ['<%= distdir %>/js/*.dev.js'] - }, - - copy: { - assets: { - files: [{ dest: '<%= distdir %>/assets', src: '**', expand: true, cwd: 'src/assets/' }] - }, - - config: { - files: [{ dest: '<%= distdir %>/../config', src: '**', expand: true, cwd: 'src/config/' }] - }, - - installer: { - files: [{ dest: '<%= distdir %>/views/install', src: '**/*.html', expand: true, cwd: 'src/installer/steps' }] - }, - - canvasdesigner: { - files: [ - { dest: '<%= distdir %>/preview', src: '**/*.html', expand: true, cwd: 'src/canvasdesigner' }, - { dest: '<%= distdir %>/preview/editors', src: '**/*.html', expand: true, cwd: 'src/canvasdesigner/editors' }, - { dest: '<%= distdir %>/assets/less', src: '**/*.less', expand: true, cwd: 'src/canvasdesigner/editors' }, - { dest: '<%= distdir %>/js', src: 'canvasdesigner.config.js', expand: true, cwd: 'src/canvasdesigner/config' }, - { dest: '<%= distdir %>/js', src: 'canvasdesigner.palettes.js', expand: true, cwd: 'src/canvasdesigner/config' }, - { dest: '<%= distdir %>/js', src: 'canvasdesigner.front.js', expand: true, cwd: 'src/canvasdesigner' } - ] - }, - - vendor: { - files: [{ dest: '<%= distdir %>/lib', src: '**', expand: true, cwd: 'lib/' }] - }, - - views: { - files: [{ dest: '<%= distdir %>/views', src: ['**/*.*', '!**/*.controller.js'], expand: true, cwd: 'src/views' }] - }, - - app: { - files: [ - { dest: '<%= distdir %>/js', src: '*.js', expand: true, cwd: 'src/' } - ] - }, - - mocks: { - files: [{ dest: '<%= distdir %>/js', src: '*.js', expand: true, cwd: 'src/common/mocks/' }] - }, - - vs: { - files: [ - //everything except the index.html root file! - //then we need to figure out how to not copy all the test stuff either!? - { dest: '<%= vsdir %>/assets', src: '**', expand: true, cwd: '<%= distdir %>/assets' }, - { dest: '<%= vsdir %>/js', src: '**', expand: true, cwd: '<%= distdir %>/js' }, - { dest: '<%= vsdir %>/views', src: '**', expand: true, cwd: '<%= distdir %>/views' }, - { dest: '<%= vsdir %>/preview', src: '**', expand: true, cwd: '<%= distdir %>/preview' }, - { dest: '<%= vsdir %>/lib', src: '**', expand: true, cwd: '<%= distdir %>/lib' } - ] - } - }, - - karma: { - unit: { configFile: 'test/config/karma.conf.js', keepalive: true }, - e2e: { configFile: 'test/config/e2e.js', keepalive: true }, - watch: { configFile: 'test/config/unit.js', singleRun: false, autoWatch: true, keepalive: true } - }, - - concat: { - index: { - src: ['src/index.html'], - dest: '<%= distdir %>/index.html', - options: { - process: true - } - }, - install: { - src: ['src/installer/installer.html'], - dest: '<%= distdir %>/installer.html', - options: { - process: true - } - }, - - installJs: { - src: ['src/installer/**/*.js'], - dest: '<%= distdir %>/js/umbraco.installer.js', - options: { - banner: "<%= banner %>\n(function() { \n\n angular.module('umbraco.install', []); \n", - footer: "\n\n})();" - } - }, - - canvasdesignerJs: { - src: ['src/canvasdesigner/canvasdesigner.global.js', 'src/canvasdesigner/canvasdesigner.controller.js', 'src/canvasdesigner/editors/*.js', 'src/canvasdesigner/lib/*.js'], - dest: '<%= distdir %>/js/canvasdesigner.panel.js' - }, - - controllers: { - src: ['src/controllers/**/*.controller.js', 'src/views/**/*.controller.js'], - dest: '<%= distdir %>/js/umbraco.controllers.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - services: { - src: ['src/common/services/*.js'], - dest: '<%= distdir %>/js/umbraco.services.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - security: { - src: ['src/common/security/*.js'], - dest: '<%= distdir %>/js/umbraco.security.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - resources: { - src: ['src/common/resources/*.js'], - dest: '<%= distdir %>/js/umbraco.resources.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - testing: { - src: ['src/common/mocks/*/*.js'], - dest: '<%= distdir %>/js/umbraco.testing.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - directives: { - src: ['src/common/directives/**/*.js'], - dest: '<%= distdir %>/js/umbraco.directives.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - filters: { - src: ['src/common/filters/*.js'], - dest: '<%= distdir %>/js/umbraco.filters.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - } - }, - - uglify: { - options: { - mangle: true - }, - combine: { - files: { - '<%= distdir %>/js/umbraco.min.js': ['<%= distdir %>/js/umbraco.*.js'] - } - } - }, - - recess: { - build: { - files: { - '<%= distdir %>/assets/css/<%= pkg.name %>.css': - ['<%= src.less %>'] - }, - options: { - compile: true, - compress: true - } - }, - nonodes: { - files: { - '<%= distdir %>/assets/css/nonodes.style.min.css': - ['src/less/pages/nonodes.less'] - }, - options: { - compile: true, - compress: true - } - }, - installer: { - files: { - '<%= distdir %>/assets/css/installer.css': - ['src/less/installer.less'] - }, - options: { - compile: true, - compress: true - } - }, - canvasdesigner: { - files: { - '<%= distdir %>/assets/css/canvasdesigner.css': - ['src/less/canvas-designer.less', 'src/less/helveticons.less'] - }, - options: { - compile: true, - compress: true - } - } - }, - - postcss: { - options: { - processors: [ - // add vendor prefixes - require('autoprefixer-core')({ - browsers: 'last 2 versions' - }) - ] - }, - dist: { - src: '<%= distdir %>/assets/css/<%= pkg.name %>.css' - } - }, - - ngTemplateCache: { - views: { - files: { - '<%= distdir %>/js/umbraco.views.js': 'src/views/**/*.html' - }, - options: { - trim: 'src/', - module: 'umbraco.views' - } - } - }, - - watch: { - docs: { - files: ['docs/src/**/*.md'], - tasks: ['watch-docs', 'timestamp'] - }, - css: { - files: 'src/**/*.less', - tasks: ['watch-less', 'timestamp'], - options: { - livereload: true, - }, - }, - js: { - files: ['src/**/*.js', 'src/*.js'], - tasks: ['watch-js', 'timestamp'], - }, - test: { - files: ['test/**/*.js'], - tasks: ['watch-test', 'timestamp'], - }, - installer: { - files: ['src/installer/**/*.*'], - tasks: ['watch-installer', 'timestamp'], - }, - canvasdesigner: { - files: ['src/canvasdesigner/**/*.*'], - tasks: ['watch-canvasdesigner', 'timestamp'], - }, - html: { - files: ['src/views/**/*.html', 'src/*.html'], - tasks: ['watch-html', 'timestamp'] - }, - options: { - interval: 500 - } - }, - - - ngdocs: { - options: { - dest: 'docs/api', - startPage: '/api', - title: "Umbraco Backoffice UI API Documentation", - html5Mode: false, - styles: [ - 'docs/umb-docs.css' - ], - image: "https://our.umbraco.org/assets/images/logo.svg" - }, - api: { - src: ['src/common/**/*.js', 'docs/src/api/**/*.ngdoc'], - title: 'API Documentation' - }, - tutorials: { - src: [], - title: '' - } - }, - - eslint:{ - src: ['<%= src.common %>','<%= src.controllers %>'], - options: {quiet: true} - }, - - jshint: { - dev: { - files: { - src: ['<%= src.common %>'] - }, - options: { - curly: true, - eqeqeq: true, - immed: true, - latedef: "nofunc", - newcap: true, - noarg: true, - sub: true, - boss: true, - //NOTE: This is required so it doesn't barf on reserved words like delete when doing $http.delete - es5: true, - eqnull: true, - //NOTE: we need to use eval sometimes so ignore it - evil: true, - //NOTE: we need to check for strings such as "javascript:" so don't throw errors regarding those - scripturl: true, - //NOTE: we ignore tabs vs spaces because enforcing that causes lots of errors depending on the text editor being used - smarttabs: true, - globals: {} - } - }, - build: { - files: { - src: ['<%= src.prod %>'] - }, - options: { - curly: true, - eqeqeq: true, - immed: true, - latedef: "nofunc", - newcap: true, - noarg: true, - sub: true, - boss: true, - //NOTE: This is required so it doesn't barf on reserved words like delete when doing $http.delete - es5: true, - eqnull: true, - //NOTE: we need to use eval sometimes so ignore it - evil: true, - //NOTE: we need to check for strings such as "javascript:" so don't throw errors regarding those - scripturl: true, - //NOTE: we ignore tabs vs spaces because enforcing that causes lots of errors depending on the text editor being used - smarttabs: true, - globalstrict: true, - globals: { $: false, jQuery: false, define: false, require: false, window: false } - } - } - }, - - bower: { - dev: { - dest: '<%= distdir %>/lib', - options: { - expand: true, - ignorePackages: ['bootstrap'], - packageSpecific: { - 'moment': { - keepExpandedHierarchy: false, - files: ['min/moment-with-locales.js'] - }, - 'typeahead.js': { - keepExpandedHierarchy: false, - files: ['dist/typeahead.bundle.min.js'] - }, - 'underscore': { - files: ['underscore-min.js', 'underscore-min.map'] - }, - 'rgrove-lazyload': { - files: ['lazyload.js'] - }, - 'bootstrap-social': { - files: ['bootstrap-social.css'] - }, - 'font-awesome': { - files: ['css/font-awesome.min.css', 'fonts/*'] - }, - "jquery": { - keepExpandedHierarchy: false, - files: ['dist/jquery.min.js', 'dist/jquery.min.map'] - }, - 'jquery-ui': { - keepExpandedHierarchy: false, - files: ['jquery-ui.min.js'] - }, - 'jquery-migrate': { - keepExpandedHierarchy: false, - files: ['jquery-migrate.min.js'] - }, - 'tinymce': { - files: ['plugins/**', 'themes/**', 'tinymce.min.js'] - }, - 'angular-dynamic-locale': { - files: ['tmhDynamicLocale.min.js', 'tmhDynamicLocale.min.js.map'] - }, - 'ng-file-upload': { - keepExpandedHierarchy: false, - files: ['ng-file-upload.min.js'] - }, - 'angular-local-storage': { - keepExpandedHierarchy: false, - files: ['dist/angular-local-storage.min.js'] - }, - 'codemirror': { - files: [ - 'lib/codemirror.js', - 'lib/codemirror.css', - - 'mode/css/*', - 'mode/javascript/*', - 'mode/xml/*', - 'mode/htmlmixed/*', - - 'addon/search/*', - 'addon/edit/*', - 'addon/selection/*', - 'addon/dialog/*' - ] - }, - 'ace-builds': { - files: [ - 'src-min-noconflict/ace.js', - - 'src-min-noconflict/ext-language_tools.js', - 'src-min-noconflict/ext-searchbox.js', - 'src-min-noconflict/ext-settings_menu.js', - - 'src-min-noconflict/snippets/text.js', - 'src-min-noconflict/snippets/javascript.js', - - 'src-min-noconflict/theme-chrome.js', - - 'src-min-noconflict/mode-razor.js', - 'src-min-noconflict/mode-javascript.js', - - 'src-min-noconflict/worker-javascript.js', - ] - } - } - } - }, - options: { - expand: true - } - }, - - "bower-install-simple": { - options: { - color: true - }, - "dev": {} - } - }); - - - - grunt.loadNpmTasks('grunt-contrib-concat'); - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-clean'); - grunt.loadNpmTasks('grunt-contrib-copy'); - grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-recess'); - grunt.loadNpmTasks('grunt-postcss'); - - grunt.loadNpmTasks('grunt-karma'); - - grunt.loadNpmTasks('grunt-open'); - grunt.loadNpmTasks('grunt-contrib-connect'); - grunt.loadNpmTasks("grunt-bower-install-simple"); - grunt.loadNpmTasks('grunt-bower'); - grunt.loadNpmTasks('grunt-ngdocs'); - - grunt.loadNpmTasks('grunt-eslint'); - grunt.loadNpmTasks('grunt-hustler'); -}; From 06a87bc341d6a66b61242dccab8bd598283c99ed Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 29 Aug 2017 14:56:17 +0200 Subject: [PATCH 31/64] update readme --- README.md | 4 +- src/Umbraco.Web.UI.Client/README.md | 97 ----------------------------- 2 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/README.md diff --git a/README.md b/README.md index a9559c1850..a724b3531a 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,7 @@ Umbraco is a free open source Content Management System built on the ASP.NET pla ## Building Umbraco from source ## -The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `grunt vs` in `src\Umbraco.Web.UI.Client`. - -If you're interested in making changes to Belle without running Visual Studio make sure to read the [Belle ReadMe file](src/Umbraco.Web.UI.Client/README.md). +The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. Note that you can always [download a nightly build](http://nightly.umbraco.org/?container=umbraco-750) so you don't have to build the code yourself. diff --git a/src/Umbraco.Web.UI.Client/README.md b/src/Umbraco.Web.UI.Client/README.md deleted file mode 100644 index 6001d65271..0000000000 --- a/src/Umbraco.Web.UI.Client/README.md +++ /dev/null @@ -1,97 +0,0 @@ -#Belle - -Umbraco 7 UI, codename "Belle" Built on AngularJS, bower, Lazyload.js and Twitter Bootstrap - - -##Introduction -Slides from the initial demonstration of Belle done at the Umbraco DK Fest can be found here: - -http://rawgithub.com/umbraco/Umbraco.Web.Ui.Client/build/master/Presentation/index.html - - -##Running the site with mocked data - -This won't require any database or setup, as everything is running through node. All you have to do is install -node and grunt on either windows or OSX and the entire setup is ready for you. - - -###Install node.js -We need node to run tests and automated less compiling and other automated tasks. go to http://nodejs.org. Node.js is a powerfull javascript engine, which allows us to run all our tests and tasks written in javascript locally. - -*note:* On windows you might need to restart explorer.exe to register node. - - -###Install dependencies -Next we need to install all the required packages. This is done with the package tool, included with node.js, open /src/Umbraco.Web.UI.Client in cmd.exe or osx terminal and run the command: - - npm install - -this will fetch all needed packages to your local machine. - - -###Install grunt globally -Grunt is a task runner for node.js, and we use it for all automated tasks in the build process. For convenience we need to install it globally on your machine, so it can be used directly in cmd.exe or the terminal. - -So run the command: - - npm install grunt-cli -g - -*note:* On windows you might need to restart explorer.exe to register the grunt cmd. - -*note:* On OSX you might need to run: - - sudo npm install grunt-cli -g - -Now that you have node and grunt installed, you can open `/src/Umbraco.Web.UI.Client` in either `cmd.exe` or terminal and run: - - grunt dev - -This will build the site, merge less files, run tests and create the /Build folder, and finally open the site in your -browser. - - -##Limitations -The current prototype simply uses in-memory storage, so no database dependencies. It is aimed at showing UI, not a complete functional client-server setup. - - -##Project Structure - -All project files are located in /src/Umbraco.Web.UI.Client which only contains client-side files, everything -related to asp.net are in /src/Umbraco.Web.UI - -after building Belle files are located in /build/belle, with all files following AngularJs -conventions: - -###Folders -- */Umbraco.Web.Ui.Client/build/lib:* Dependencies -- */Umbraco.Web.Ui.Client/build/js:* Application javascript files -- */Umbraco.Web.Ui.Client/build/views/common/:* Main application views -- */Umbraco.Web.Ui.Client/build/views/[sectioname]/pagename Editors html -- */Umbraco.Web.Ui.Client/build/views/propertyeditors:* Property Editors html - - -###Files -- */Umbraco.Web.Ui.Client/build/js/app.js:* Main umbraco application / modules -- */Umbraco.Web.Ui.Client/build/js/loader.js:* lazyload configuration for dependencies -- */Umbraco.Web.Ui.Client/build/js/routes.js:* Application routes -- */Umbraco.Web.Ui.Client/build/js/umbraco.controllers.js:* Application controllers -- */Umbraco.Web.Ui.Client/build/js/umbraco.services.js:* Application services -- */Umbraco.Web.Ui.Client/build/js/umbraco.filters.js:* Application filters -- */Umbraco.Web.Ui.Client/build/js/umbraco.directives.js:* Application directives -- */Umbraco.Web.Ui.Client/build/js/umbraco.resources.js:* Application resources, like content, media, users, members etc -- */Umbraco.Web.Ui.Client/build/js/umbraco.mocks.js:* Fake Application resources, for running the app without a server - -##Getting started -The current app is built, following conventions from angularJs and bootstrap. To get started with the applicaton you will need to atleast know the basics of these frameworks - -###AngularJS -- Excellent introduction videos on http://www.egghead.io/ -- Official guide at: http://docs.angularjs.org/guide/ - -###Require.js -- Introduction: http://javascriptplayground.com/blog/2012/07/requirejs-amd-tutorial-introduction -- Require.js website: http://requirejs.org/ - - - - From 6a95da31b03da7b75038b351164e1a57d5c2b8cf Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 1 Sep 2017 00:55:30 +1000 Subject: [PATCH 32/64] U4-10374 EntityService, ContentService and MediaService should use a prefix query for GetDescendants instead of a Contains query --- src/Umbraco.Core/Services/ContentService.cs | 25 +++++++++-- src/Umbraco.Core/Services/EntityService.cs | 46 ++++++++++++++++----- src/Umbraco.Core/Services/MediaService.cs | 9 +++- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 42ea80a717..84651f5a6f 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -676,7 +676,17 @@ namespace Umbraco.Core.Services // get query - if the id is System Root, then just get all var query = Query.Builder; if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + { + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar)); + } + // get filter IQuery filterQuery = null; @@ -711,7 +721,16 @@ namespace Umbraco.Core.Services // get query - if the id is System Root, then just get all var query = Query.Builder; if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + { + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar)); + } return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } @@ -1645,7 +1664,7 @@ namespace Umbraco.Core.Services { using (new WriteLock(Locker)) { - var nodeObjectType = new Guid(Constants.ObjectTypes.Document); + var nodeObjectType = Constants.ObjectTypes.DocumentGuid; using (var uow = UowProvider.GetUnitOfWork()) { diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 6e4b5ef17c..ae4f1bbf72 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -348,11 +348,22 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); - + var query = Query.Builder; //if the id is System Root, then just get all if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + { + //lookup the path so we can use it in the prefix query below + var itemPaths = repository.GetAllPaths(objectTypeId, id).ToArray(); + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var itemPath = itemPaths[0].Path; + + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", itemPath), TextColumnType.NVarchar)); + } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) @@ -381,15 +392,30 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); - + var query = Query.Builder; if (idsA.All(x => x != Constants.System.Root)) { + //lookup the paths so we can use it in the prefix query below + var itemPaths = repository.GetAllPaths(objectTypeId, idsA).ToArray(); + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var clauses = new List>>(); foreach (var id in idsA) - { - var qid = id; - clauses.Add(x => x.Path.SqlContains(string.Format(",{0},", qid), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); + { + //if the id is root then don't add any clauses + if (id != Constants.System.Root) + { + var itemPath = itemPaths.FirstOrDefault(x => x.Id == id); + if (itemPath == null) continue; + var path = itemPath.Path; + var qid = id; + clauses.Add(x => x.Path.SqlStartsWith(string.Format("{0},", path), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); + } } query.WhereAny(clauses); } @@ -442,7 +468,7 @@ namespace Umbraco.Core.Services return contents; } } - + /// /// Gets a collection of descendents by the parents Id /// @@ -601,13 +627,13 @@ namespace Umbraco.Core.Services return repository.GetAllPaths(objectTypeId, keys); } } - + /// - /// Gets a collection of + /// Gets a collection of /// /// Guid id of the UmbracoObjectType /// - /// An enumerable list of objects + /// An enumerable list of objects public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) { var umbracoObjectType = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index ee4cddb382..ecf2c750af 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -593,7 +593,14 @@ namespace Umbraco.Core.Services //if the id is System Root, then just get all if (id != Constants.System.Root) { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var mediaPath = entityRepository.GetAllPaths(Constants.ObjectTypes.MediaGuid, id).ToArray(); + if (mediaPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", mediaPath[0]), TextColumnType.NVarchar)); } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) From 207d6b53b53b797c81f6ee1494f9e1fc92d389dc Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Sep 2017 11:22:56 +0200 Subject: [PATCH 33/64] Fixed .umb-panel-header-content-wrapper height The .umb-panel-header-content-wrapper was 1 pixel higher than the parent .umb-panel-header, now both heights are 99px. --- src/Umbraco.Web.UI.Client/src/less/panel.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 7c18b79c81..0b6505dfa7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -356,7 +356,7 @@ .umb-panel-header-content-wrapper { display: flex; flex-direction: column; - height: 100px; + height: 99px; padding: 0 20px; } From 2121b7c8a0d2980b0498ead1e1927f92a48ce134 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 4 Sep 2017 14:07:06 +0200 Subject: [PATCH 34/64] Node version --- build/BuildBelle.bat | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index 7f13d548dc..4d0be2c571 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -5,8 +5,8 @@ SETLOCAL SET toolsFolder=%CD%\tools\ ECHO Current folder: %CD% -SET nodeFileName=node-v6.9.1-win-x86.7z -SET nodeExtractFolder=%toolsFolder%node.js.691 +SET nodeFileName=node-v6.11.2-win-x86.7z +SET nodeExtractFolder=%toolsFolder%node.js.6112 SET nuGetExecutable=%CD%\tools\nuget.exe IF NOT EXIST "%nuGetExecutable%" ( @@ -25,8 +25,8 @@ FOR /f "delims=" %%A in ('dir "%toolsFolder%7-Zip.CommandLine.*" /b') DO SET "se MOVE "%sevenZipExePath%tools\7za.exe" "%toolsFolder%7za.exe" IF NOT EXIST "%nodeExtractFolder%" ( - ECHO Downloading http://nodejs.org/dist/v6.9.1/%nodeFileName% to %toolsFolder%%nodeFileName% - powershell -Command "(New-Object Net.WebClient).DownloadFile('http://nodejs.org/dist/v6.9.1/%nodeFileName%', '%toolsFolder%%nodeFileName%')" + ECHO Downloading http://nodejs.org/dist/v6.11.2/%nodeFileName% to %toolsFolder%%nodeFileName% + powershell -Command "(New-Object Net.WebClient).DownloadFile('http://nodejs.org/dist/v6.11.2/%nodeFileName%', '%toolsFolder%%nodeFileName%')" ECHO Extracting %nodeFileName% to %nodeExtractFolder% "%toolsFolder%\7za.exe" x "%toolsFolder%\%nodeFileName%" -o"%nodeExtractFolder%" -aos > nul ) From 344b52d10fcec4514e6d9d74dfd16d51df72b7e9 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 4 Sep 2017 17:04:48 +0200 Subject: [PATCH 35/64] deploy-391 - allow for id reservation --- src/Umbraco.Core/Constants-ObjectTypes.cs | 2 ++ .../Repositories/ContentRepository.cs | 19 +++++++++- .../Repositories/MediaRepository.cs | 35 ++++++++++++++----- src/Umbraco.Core/Services/EntityService.cs | 29 +++++++++++++++ src/Umbraco.Core/Services/IEntityService.cs | 10 +++++- 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 4a79437c61..1fbc1ec62b 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -222,6 +222,8 @@ namespace Umbraco.Core /// Guid for a Forms DataSource. /// public static readonly Guid LanguageGuid = new Guid(Language); + + public static readonly Guid IdReservationGuid = new Guid("92849B1E-3904-4713-9356-F646F87C25F4"); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index eac9d197be..a01cbaa2e8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -435,7 +435,24 @@ namespace Umbraco.Core.Persistence.Repositories nodeDto.Path = parent.Path; nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + // note: + // there used to be a check on Database.IsNew(nodeDto) here to either Insert or Update, + // but I cannot figure out what was the point, as the node should obviously be new if + // we reach that point - removed. + + // see if there's a reserved identifier for this unique id + var sql = new Sql("SELECT id FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", nodeDto.UniqueId, Constants.ObjectTypes.IdReservationGuid); + var id = Database.ExecuteScalar(sql); + if (id > 0) + { + nodeDto.NodeId = id; + Database.Update(nodeDto); + } + else + { + Database.Insert(nodeDto); + } //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 598b16d78d..b22e8981b9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -84,7 +84,7 @@ namespace Umbraco.Core.Persistence.Repositories #endregion #region Overrides of PetaPocoRepositoryBase - + protected override Sql GetBaseQuery(BaseQueryType queryType) { var sql = new Sql(); @@ -153,7 +153,7 @@ namespace Umbraco.Core.Persistence.Repositories /// This is the underlying method that processes most queries for this repository /// /// - /// The full SQL to select all media data + /// The full SQL to select all media data /// /// /// The Id SQL to just return all media ids - used to process the properties for the media item @@ -164,7 +164,7 @@ namespace Umbraco.Core.Persistence.Repositories { // fetch returns a list so it's ok to iterate it in this method var dtos = Database.Fetch(sqlFull); - + //This is a tuple list identifying if the content item came from the cache or not var content = new List>(); var defs = new DocumentDefinitionCollection(); @@ -180,7 +180,7 @@ namespace Umbraco.Core.Persistence.Repositories if (withCache) { var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - //only use this cached version if the dto returned is the same version - this is just a safety check, media doesn't + //only use this cached version if the dto returned is the same version - this is just a safety check, media doesn't //store different versions, but just in case someone corrupts some data we'll double check to be sure. if (cached != null && cached.Version == dto.VersionId) { @@ -303,7 +303,7 @@ namespace Umbraco.Core.Persistence.Repositories .From(SqlSyntax) .InnerJoin(SqlSyntax) .On(SqlSyntax, left => left.NodeId, right => right.NodeId); - + if (contentTypeIdsA.Length > 0) { xmlIdsQuery.InnerJoin(SqlSyntax) @@ -314,7 +314,7 @@ namespace Umbraco.Core.Persistence.Repositories } xmlIdsQuery.Where(dto => dto.NodeObjectType == mediaObjectType, SqlSyntax); - + var allXmlIds = Database.Fetch(xmlIdsQuery); var toRemove = allXmlIds.Except(allMediaIds).ToArray(); @@ -380,7 +380,24 @@ namespace Umbraco.Core.Persistence.Repositories nodeDto.Path = parent.Path; nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + // note: + // there used to be a check on Database.IsNew(nodeDto) here to either Insert or Update, + // but I cannot figure out what was the point, as the node should obviously be new if + // we reach that point - removed. + + // see if there's a reserved identifier for this unique id + var sql = new Sql("SELECT id FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", nodeDto.UniqueId, Constants.ObjectTypes.IdReservationGuid); + var id = Database.ExecuteScalar(sql); + if (id > 0) + { + nodeDto.NodeId = id; + Database.Update(nodeDto); + } + else + { + Database.Insert(nodeDto); + } //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); @@ -564,7 +581,7 @@ namespace Umbraco.Core.Persistence.Repositories private IMedia CreateMediaFromDto(ContentVersionDto dto, Sql docSql) { var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - + var media = MediaFactory.BuildEntity(dto, contentType); var docDef = new DocumentDefinition(dto, contentType); @@ -584,7 +601,7 @@ namespace Umbraco.Core.Persistence.Repositories if (EnsureUniqueNaming == false) return nodeName; - var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", new { objectType = NodeObjectTypeId, parentId }); return SimilarNodeName.GetUniqueName(names, id, nodeName); diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 04d6c276b2..c10311dc01 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -646,5 +646,34 @@ namespace Umbraco.Core.Services return exists; } } + + /// + public int ReserveId(Guid key) + { + NodeDto node; + using (var scope = UowProvider.ScopeProvider.CreateScope()) + { + var sql = new Sql("SELECT * FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", key, Constants.ObjectTypes.IdReservationGuid); + node = scope.Database.SingleOrDefault(sql); + if (node != null) throw new InvalidOperationException("An identifier has already been reserved for this Udi."); + node = new NodeDto + { + UniqueId = key, + Text = "RESERVED.ID", + NodeObjectType = Constants.ObjectTypes.IdReservationGuid, + + CreateDate = DateTime.Now, + UserId = 0, + ParentId = -1, + Level = 1, + Path = "-1", + SortOrder = 0, + Trashed = false + }; + scope.Database.Insert(node); + scope.Complete(); + } + return node.NodeId; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 82e5227cf2..d1da84a1c8 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -170,7 +170,7 @@ namespace Umbraco.Core.Services /// /// /// - /// + /// /// IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); @@ -270,5 +270,13 @@ namespace Umbraco.Core.Services /// /// Type of the entity Type GetEntityType(UmbracoObjectTypes umbracoObjectType); + + /// + /// Reserves an identifier for a key. + /// + /// They key. + /// The identifier. + /// When a new content or a media is saved with the key, it will have the reserved identifier. + int ReserveId(Guid key); } } \ No newline at end of file From 84fd5dc2ee52a713f5fe168ab38c1bcab8fbb285 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 4 Sep 2017 19:49:28 +0200 Subject: [PATCH 36/64] perfs - IContentService.IsPublishable --- src/Umbraco.Core/EnumerableExtensions.cs | 11 +++ src/Umbraco.Core/Services/ContentService.cs | 67 +++++++------------ .../Services/ContentServiceTests.cs | 15 +++++ 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index e8565f3bc7..bb115b394d 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -295,5 +295,16 @@ namespace Umbraco.Core return list1Groups.Count == list2Groups.Count && list1Groups.All(g => g.Count() == list2Groups[g.Key].Count()); } + + public static IEnumerable SkipLast(this IEnumerable source) + { + using (var e = source.GetEnumerator()) + { + if (e.MoveNext() == false) yield break; + + for (var value = e.Current; e.MoveNext(); value = e.Current) + yield return value; + } + } } } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 09e6068ce1..913a83c1cd 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -903,15 +903,30 @@ namespace Umbraco.Core.Services /// True if the Content can be published, otherwise False public bool IsPublishable(IContent content) { - //If the passed in content has yet to be saved we "fallback" to checking the Parent - //because if the Parent is publishable then the current content can be Saved and Published - if (content.HasIdentity == false) - { - var parent = GetById(content.ParentId); - return IsPublishable(parent, true); - } + // get ids from path + // skip the first one that has to be -1 - and we don't care + // skip the last one that has to be "this" - and it's ok to stop at the parent + var ids = content.Path.Split(',').Skip(1).SkipLast().Select(int.Parse).ToArray(); + if (ids.Length == 0) + return false; - return IsPublishable(content, false); + // if the first one is recycle bin, fail fast + if (ids[0] == Constants.System.RecycleBinContent) + return false; + + // fixme - move to repository? + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var sql = new Sql(@" + SELECT id + FROM umbracoNode + JOIN cmsDocument ON umbracoNode.id=cmsDocument.nodeId AND cmsDocument.published=@0 + WHERE umbracoNode.trashed=@1 AND umbracoNode.id IN (@2)", + true, false, ids); + Console.WriteLine(sql.SQL); + var x = uow.Database.Fetch(sql); + return ids.Length == x.Count; + } } /// @@ -1062,7 +1077,7 @@ namespace Umbraco.Core.Services descendant.WriterId = userId; descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); - + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); } @@ -2311,40 +2326,6 @@ namespace Umbraco.Core.Services } } - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// - /// Check current is only used when falling back to checking the Parent of non-saved content, as - /// non-saved content doesn't have a valid path yet. - /// - /// to check if anscestors are published - /// Boolean indicating whether the passed in content should also be checked for published versions - /// True if the Content can be published, otherwise False - private bool IsPublishable(IContent content, bool checkCurrent) - { - var ids = content.Path.Split(',').Select(int.Parse).ToList(); - foreach (var id in ids) - { - //If Id equals that of the recycle bin we return false because nothing in the bin can be published - if (id == Constants.System.RecycleBinContent) - return false; - - //We don't check the System Root, so just continue - if (id == Constants.System.Root) continue; - - //If the current id equals that of the passed in content and if current shouldn't be checked we skip it. - if (checkCurrent == false && id == content.Id) continue; - - //Check if the content for the current id is published - escape the loop if we encounter content that isn't published - var hasPublishedVersion = HasPublishedVersion(id); - if (hasPublishedVersion == false) - return false; - } - - return true; - } - private PublishStatusType CheckAndLogIsPublishable(IContent content) { //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 79bf91716e..6c1f388ee5 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -998,6 +998,21 @@ namespace Umbraco.Tests.Services Assert.That(content.Published, Is.True); } + [Test] + public void IsPublishable() + { + // Arrange + var contentService = ServiceContext.ContentService; + var parent = contentService.CreateContent("parent", -1, "umbTextpage"); + contentService.SaveAndPublishWithStatus(parent); + var content = contentService.CreateContent("child", parent, "umbTextpage"); + contentService.Save(content); + + Assert.IsTrue(contentService.IsPublishable(content)); + contentService.UnPublish(parent); + Assert.IsFalse(contentService.IsPublishable(content)); + } + [Test] public void Can_Publish_Content_WithEvents() { From e511554a427d20d675662a87add4abbd0fca2afb Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 4 Sep 2017 20:10:19 +0200 Subject: [PATCH 37/64] Adds less as a dependency so that the build can complete --- src/Umbraco.Web.UI.Client/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index d95652bba2..98b1102aa4 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -40,6 +40,7 @@ "karma": "^1.7.0", "karma-jasmine": "^1.1.0", "karma-phantomjs-launcher": "^1.0.4", + "less": "^2.6.1", "lodash": "^4.16.3", "merge-stream": "^1.0.1", "run-sequence": "^2.1.0" From ad19eb8dff7b48ee7898a4e98c20aa87b0ef3f32 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 4 Sep 2017 20:14:31 +0200 Subject: [PATCH 38/64] Update node version and remove unused npm reference --- build/BuildBelle.bat | 45 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index 4d0be2c571..cc6ce2202c 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -5,8 +5,10 @@ SETLOCAL SET toolsFolder=%CD%\tools\ ECHO Current folder: %CD% -SET nodeFileName=node-v6.11.2-win-x86.7z -SET nodeExtractFolder=%toolsFolder%node.js.6112 +SET nodeVersion=6.11.2 + +SET nodeFileName=node-v%nodeVersion%-win-x86.7z +SET nodeExtractFolder=%toolsFolder%node.js.%nodeVersion% SET nuGetExecutable=%CD%\tools\nuget.exe IF NOT EXIST "%nuGetExecutable%" ( @@ -25,44 +27,41 @@ FOR /f "delims=" %%A in ('dir "%toolsFolder%7-Zip.CommandLine.*" /b') DO SET "se MOVE "%sevenZipExePath%tools\7za.exe" "%toolsFolder%7za.exe" IF NOT EXIST "%nodeExtractFolder%" ( - ECHO Downloading http://nodejs.org/dist/v6.11.2/%nodeFileName% to %toolsFolder%%nodeFileName% - powershell -Command "(New-Object Net.WebClient).DownloadFile('http://nodejs.org/dist/v6.11.2/%nodeFileName%', '%toolsFolder%%nodeFileName%')" + ECHO Downloading http://nodejs.org/dist/v%nodeVersion%/%nodeFileName% to %toolsFolder%%nodeFileName% + powershell -Command "(New-Object Net.WebClient).DownloadFile('http://nodejs.org/dist/v%nodeVersion%/%nodeFileName%', '%toolsFolder%%nodeFileName%')" ECHO Extracting %nodeFileName% to %nodeExtractFolder% "%toolsFolder%\7za.exe" x "%toolsFolder%\%nodeFileName%" -o"%nodeExtractFolder%" -aos > nul ) FOR /f "delims=" %%A in ('dir "%nodeExtractFolder%\node*" /b') DO SET "nodePath=%nodeExtractFolder%\%%A" - -SET drive=%CD:~0,2% -SET nuGetFolder=%drive%\packages\ -FOR /f "delims=" %%A in ('dir "%nuGetFolder%npm.*" /b') DO SET "npmPath=%nuGetFolder%%%A\" - -IF [%npmPath%] == [] GOTO :installnpm -IF NOT [%npmPath%] == [] GOTO :build - -:installnpm - ECHO Downloading npm - ECHO Configured packages folder: %nuGetFolder% - ECHO Installing Npm NuGet Package - "%nuGetExecutable%" install Npm -OutputDirectory %nuGetFolder% -Verbosity detailed - REM Ensures that we look for the just downloaded NPM, not whatever the user has installed on their machine - FOR /f "delims=" %%A in ('dir %nuGetFolder%npm.* /b') DO SET "npmPath=%nuGetFolder%%%A\" - GOTO :build - + :build - ECHO Adding Npm and Node to path + ECHO Adding Node to path REM SETLOCAL is on, so changes to the path not persist to the actual user's path - PATH="%npmPath%";"%nodePath%";%PATH% + PATH="%nodePath%";%PATH% + + ECHO Node version is: + call node -v + + ECHO npm version is: + call npm -v + SET buildFolder=%CD% ECHO Change directory to %CD%\..\src\Umbraco.Web.UI.Client\ CD %CD%\..\src\Umbraco.Web.UI.Client\ ECHO Do npm install and the gulp build of Belle + + ECHO Clean npm cache call npm cache clean --quiet + ECHO Installing gulp cli call npm install -g gulp-cli --quiet + ECHO Installing bower call npm install -g bower --quiet + ECHO Doing npm install call npm install --quiet + ECHO Executing gulp build call gulp build --buildversion=%release% ECHO Move back to the build folder From 8ff89c4f48b104b09766815aecf1a34647c0c90f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Sep 2017 16:41:06 +1000 Subject: [PATCH 39/64] U4-10385 Examine indexing performance bottleneck: IUserService.GetProfileById which is not cached --- src/Umbraco.Core/Models/Membership/User.cs | 2 +- src/Umbraco.Core/Services/UserService.cs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index d1270ad050..e01b070d18 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Models.Membership /// [Serializable] [DataContract(IsReference = true)] - public class User : Entity, IUser + public class User : Entity, IUser, IProfile { /// /// Constructor for creating a new/empty user diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 8e34fbe5f1..5abe69e3b9 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -701,12 +701,11 @@ namespace Umbraco.Core.Services /// Id of the User to retrieve /// public IProfile GetProfileById(int id) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateUserRepository(uow); - return repository.GetProfile(id); - } + { + //This is called a TON. Go get the full user from cache which should already be IProfile + var fullUser = GetUserById(id); + var asProfile = fullUser as IProfile; + return asProfile ?? new UserProfile(fullUser.Id, fullUser.Name); } /// From 4b7bd6482efd999588ccac27c174d8ab6c6de3b2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Sep 2017 18:52:03 +1000 Subject: [PATCH 40/64] Creates new EmailSender which should take the place throughout the codebase for sending emails in the Core. Have replaced most places where SMTP is used directly (but not the notifications part since that is more complicated right now). Added the internal event and special usage so we can raise an event to send an email if smtp is not configured. --- src/Umbraco.Core/EmailSender.cs | 114 ++++++++++++++++++ src/Umbraco.Core/Events/SendEmailEventArgs.cs | 27 +++++ src/Umbraco.Core/IEmailSender.cs | 13 ++ .../Security/BackOfficeUserManager.cs | 83 ++++++++++--- src/Umbraco.Core/Security/EmailService.cs | 42 +++++-- .../Security/UmbracoEmailMessage.cs | 17 +++ src/Umbraco.Core/Umbraco.Core.csproj | 4 + .../users/views/users/users.controller.js | 23 ++-- .../Editors/BackOfficeServerVariables.cs | 2 +- src/Umbraco.Web/Editors/UsersController.cs | 20 +-- .../EmailNotificationMethod.cs | 11 +- .../Security/Identity/AppBuilderExtensions.cs | 6 +- .../umbraco.presentation/library.cs | 6 +- .../businesslogic/translation/Translation.cs | 4 +- 14 files changed, 313 insertions(+), 59 deletions(-) create mode 100644 src/Umbraco.Core/EmailSender.cs create mode 100644 src/Umbraco.Core/Events/SendEmailEventArgs.cs create mode 100644 src/Umbraco.Core/IEmailSender.cs create mode 100644 src/Umbraco.Core/Security/UmbracoEmailMessage.cs diff --git a/src/Umbraco.Core/EmailSender.cs b/src/Umbraco.Core/EmailSender.cs new file mode 100644 index 0000000000..6f381f693c --- /dev/null +++ b/src/Umbraco.Core/EmailSender.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Routing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; + +namespace Umbraco.Core +{ + /// + /// A utility class for sending emails + /// + public class EmailSender : IEmailSender + { + //TODO: This should encapsulate a BackgroundTaskRunner with a queue to send these emails! + + private readonly bool _enableEvents; + + /// + /// Default constructor + /// + public EmailSender() : this(false) + { + } + + internal EmailSender(bool enableEvents) + { + _enableEvents = enableEvents; + } + + private static readonly Lazy SmtpConfigured = new Lazy(() => GlobalSettings.HasSmtpServerConfigured(HttpRuntime.AppDomainAppVirtualPath)); + + /// + /// Sends the message non-async + /// + /// + public void Send(MailMessage message) + { + if (SmtpConfigured.Value == false && _enableEvents) + { + OnSendEmail(new SendEmailEventArgs(message)); + } + else + { + using (var client = new SmtpClient()) + { + client.Send(message); + } + } + } + + /// + /// Sends the message async + /// + /// + /// + public async Task SendAsync(MailMessage message) + { + if (SmtpConfigured.Value == false && _enableEvents) + { + OnSendEmail(new SendEmailEventArgs(message)); + } + else + { + using (var client = new SmtpClient()) + { + if (client.DeliveryMethod == SmtpDeliveryMethod.Network) + { + await client.SendMailAsync(message); + } + else + { + client.Send(message); + } + } + } + } + + /// + /// Returns true if the application should be able to send a required application email + /// + /// + /// We assume this is possible if either an event handler is registered or an smtp server is configured + /// + internal static bool CanSendRequiredEmail + { + get { return EventHandlerRegistered || SmtpConfigured.Value; } + } + + /// + /// returns true if an event handler has been registered + /// + internal static bool EventHandlerRegistered + { + get { return SendEmail != null; } + } + + /// + /// An event that is raised when no smtp server is configured if events are enabled + /// + internal static event EventHandler SendEmail; + + private static void OnSendEmail(SendEmailEventArgs e) + { + var handler = SendEmail; + if (handler != null) handler(null, e); + } + } +} diff --git a/src/Umbraco.Core/Events/SendEmailEventArgs.cs b/src/Umbraco.Core/Events/SendEmailEventArgs.cs new file mode 100644 index 0000000000..790e5d4940 --- /dev/null +++ b/src/Umbraco.Core/Events/SendEmailEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Mail; + +namespace Umbraco.Core.Events +{ + public class SendEmailEventArgs : EventArgs + { + public SmtpClient Smtp { get; private set; } + public MailMessage Message { get; private set; } + + ///// + ///// A flag indicating that the mail sending was handled by an event handler + ///// + //public bool SendingHandledExternally { get; set; } + + public SendEmailEventArgs(MailMessage message) + { + Message = message; + } + + public SendEmailEventArgs(SmtpClient smtp, MailMessage message) + { + Smtp = smtp; + Message = message; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/IEmailSender.cs b/src/Umbraco.Core/IEmailSender.cs new file mode 100644 index 0000000000..7e6565a53b --- /dev/null +++ b/src/Umbraco.Core/IEmailSender.cs @@ -0,0 +1,13 @@ +using System.Net.Mail; +using System.Threading.Tasks; + +namespace Umbraco.Core +{ + /// + /// Simple abstraction to send an email message + /// + public interface IEmailSender + { + Task SendAsync(MailMessage message); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 5e04659b43..5a82df5b66 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -7,6 +7,8 @@ using System.Web.Security; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security.DataProtection; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models.Identity; using Umbraco.Core.Services; @@ -24,14 +26,25 @@ namespace Umbraco.Core.Security { } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the constructor specifying all dependencies instead")] public BackOfficeUserManager( IUserStore store, IdentityFactoryOptions options, MembershipProviderBase membershipProvider) + : this(store, options, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content) + { + } + + public BackOfficeUserManager( + IUserStore store, + IdentityFactoryOptions options, + MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig) : base(store) { - if (options == null) throw new ArgumentNullException("options");; - InitUserManager(this, membershipProvider, options); + if (options == null) throw new ArgumentNullException("options"); ; + InitUserManager(this, membershipProvider, contentSectionConfig, options); } #region Static Create methods @@ -46,7 +59,8 @@ namespace Umbraco.Core.Security { return Create(options, userService, ApplicationContext.Current.Services.EntityService, - externalLoginService, membershipProvider); + externalLoginService, membershipProvider, + UmbracoConfig.For.UmbracoSettings().Content); } /// @@ -57,20 +71,34 @@ namespace Umbraco.Core.Security /// /// /// + /// /// public static BackOfficeUserManager Create( IdentityFactoryOptions options, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, - MembershipProviderBase membershipProvider) + MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig) { if (options == null) throw new ArgumentNullException("options"); if (userService == null) throw new ArgumentNullException("userService"); if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); - var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, entityService, externalLoginService, membershipProvider)); - manager.InitUserManager(manager, membershipProvider, options); + var manager = new BackOfficeUserManager( + new BackOfficeUserStore(userService, entityService, externalLoginService, membershipProvider)); + manager.InitUserManager(manager, membershipProvider, contentSectionConfig, options); + return manager; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + BackOfficeUserStore customUserStore, + MembershipProviderBase membershipProvider) + { + var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); return manager; } @@ -80,33 +108,46 @@ namespace Umbraco.Core.Security /// /// /// + /// /// public static BackOfficeUserManager Create( - IdentityFactoryOptions options, - BackOfficeUserStore customUserStore, - MembershipProviderBase membershipProvider) + IdentityFactoryOptions options, + BackOfficeUserStore customUserStore, + MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig) { - var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); + var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider, contentSectionConfig); return manager; } #endregion + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + protected void InitUserManager( + BackOfficeUserManager manager, + MembershipProviderBase membershipProvider, + IdentityFactoryOptions options) + { + InitUserManager(manager, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content, options); + } + /// /// Initializes the user manager with the correct options /// /// /// + /// /// /// protected void InitUserManager( BackOfficeUserManager manager, MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig, IdentityFactoryOptions options) { //NOTE: This method is mostly here for backwards compat - base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider); + base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider, contentSectionConfig); } - } @@ -151,6 +192,16 @@ namespace Umbraco.Core.Security } #endregion + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + protected void InitUserManager( + BackOfficeUserManager manager, + MembershipProviderBase membershipProvider, + IDataProtectionProvider dataProtectionProvider) + { + InitUserManager(manager, membershipProvider, dataProtectionProvider, UmbracoConfig.For.UmbracoSettings().Content); + } + /// /// Initializes the user manager with the correct options /// @@ -159,11 +210,13 @@ namespace Umbraco.Core.Security /// The for the users called UsersMembershipProvider /// /// + /// /// protected void InitUserManager( BackOfficeUserManager manager, MembershipProviderBase membershipProvider, - IDataProtectionProvider dataProtectionProvider) + IDataProtectionProvider dataProtectionProvider, + IContentSection contentSectionConfig) { // Configure validation logic for usernames manager.UserValidator = new BackOfficeUserValidator(manager) @@ -193,7 +246,9 @@ namespace Umbraco.Core.Security //custom identity factory for creating the identity object for which we auth against in the back office manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); - manager.EmailService = new EmailService(); + manager.EmailService = new EmailService( + contentSectionConfig.NotificationEmailAddress, + new EmailSender()); //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs index f8f9af10ae..51d1b82207 100644 --- a/src/Umbraco.Core/Security/EmailService.cs +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -1,16 +1,37 @@ -using System.Net.Mail; +using System; +using System.ComponentModel; +using System.Net.Mail; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Umbraco.Core.Configuration; namespace Umbraco.Core.Security { + /// + /// The implementation for Umbraco + /// public class EmailService : IIdentityMessageService { + private readonly string _notificationEmailAddress; + private readonly IEmailSender _defaultEmailSender; + + public EmailService(string notificationEmailAddress, IEmailSender defaultEmailSender) + { + _notificationEmailAddress = notificationEmailAddress; + _defaultEmailSender = defaultEmailSender; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the constructor specifying all dependencies")] + public EmailService() + : this(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, new EmailSender()) + { + } + public async Task SendAsync(IdentityMessage message) { var mailMessage = new MailMessage( - UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, + _notificationEmailAddress, message.Destination, message.Subject, message.Body) @@ -21,16 +42,15 @@ namespace Umbraco.Core.Security try { - using (var client = new SmtpClient()) + //check if it's a custom message and if so use it's own defined mail sender + var umbMsg = message as UmbracoEmailMessage; + if (umbMsg != null) { - if (client.DeliveryMethod == SmtpDeliveryMethod.Network) - { - await client.SendMailAsync(mailMessage); - } - else - { - client.Send(mailMessage); - } + await umbMsg.MailSender.SendAsync(mailMessage); + } + else + { + await _defaultEmailSender.SendAsync(mailMessage); } } finally diff --git a/src/Umbraco.Core/Security/UmbracoEmailMessage.cs b/src/Umbraco.Core/Security/UmbracoEmailMessage.cs new file mode 100644 index 0000000000..9ef6205ebf --- /dev/null +++ b/src/Umbraco.Core/Security/UmbracoEmailMessage.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A custom implementation for IdentityMessage that allows the customization of how an email is sent + /// + internal class UmbracoEmailMessage : IdentityMessage + { + public IEmailSender MailSender { get; private set; } + + public UmbracoEmailMessage(IEmailSender mailSender) + { + MailSender = mailSender; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7817b24ff1..918fac7ccd 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -348,14 +348,17 @@ + + + @@ -688,6 +691,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 3ea6c46580..6868aa7426 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -50,24 +50,31 @@ "selected": true }; - //don't set this if no email is configured - if (Umbraco.Sys.ServerVariables.umbracoSettings.emailServerConfigured) { + //don't show the invite button if no email is configured + if (Umbraco.Sys.ServerVariables.umbracoSettings.showUserInvite) { vm.defaultButton = { labelKey: "user_inviteUser", - handler: function () { + handler: function() { vm.setUsersViewState('inviteUser'); } }; + vm.subButtons = [ + { + labelKey: "user_createUser", + handler: function () { + vm.setUsersViewState('createUser'); + } + } + ]; } - - vm.subButtons = [ - { + else { + vm.defaultButton = { labelKey: "user_createUser", handler: function () { vm.setUsersViewState('createUser'); } - } - ]; + }; + } vm.toggleFilter = toggleFilter; vm.setUsersViewState = setUsersViewState; diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index ab41246753..03ba2cdb99 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -289,7 +289,7 @@ namespace Umbraco.Web.Editors {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')}, {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset}, {"loginBackgroundImage", UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage}, - {"emailServerConfigured", GlobalSettings.HasSmtpServerConfigured(_httpContext.Request.ApplicationPath)}, + {"showUserInvite", EmailSender.CanSendRequiredEmail}, } }, { diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index cb2963124f..a6c8698a3c 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -345,9 +345,8 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - - var hasSmtp = GlobalSettings.HasSmtpServerConfigured(RequestContext.VirtualPathRoot); - if (hasSmtp == false) + + if (EmailSender.CanSendRequiredEmail == false) { throw new HttpResponseException( Request.CreateNotificationValidationErrorResponse("No Email server is configured")); @@ -442,12 +441,15 @@ namespace Umbraco.Web.Editors UserExtensions.GetUserCulture(to.Language, Services.TextService), new[] { userDisplay.Name, from, message, inviteUri.ToString() }); - await UserManager.EmailService.SendAsync(new IdentityMessage - { - Body = emailBody, - Destination = userDisplay.Email, - Subject = emailSubject - }); + await UserManager.EmailService.SendAsync( + //send the special UmbracoEmailMessage which configures it's own sender + //to allow for events to handle sending the message if no smtp is configured + new UmbracoEmailMessage(new EmailSender(true)) + { + Body = emailBody, + Destination = userDisplay.Email, + Subject = emailSubject + }); } diff --git a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs index c43fb0d3a1..998525c96a 100644 --- a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods var subject = _textService.Localize("healthcheck/scheduledHealthCheckEmailSubject"); - using (var client = new SmtpClient()) + var mailSender = new EmailSender(); using (var mailMessage = new MailMessage(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, RecipientEmail, string.IsNullOrEmpty(subject) ? "Umbraco Health Check Status" : subject, @@ -77,14 +77,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods && message.Contains("<") && message.Contains("(); @@ -119,7 +120,8 @@ namespace Umbraco.Web.Security.Identity (options, owinContext) => BackOfficeUserManager.Create( options, customUserStore, - userMembershipProvider)); + userMembershipProvider, + UmbracoConfig.For.UmbracoSettings().Content)); app.SetBackOfficeUserManagerType(); diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index 4ab89c5c7d..651cf9a9f4 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -1613,7 +1613,8 @@ namespace umbraco public static void SendMail(string fromMail, string toMail, string subject, string body, bool isHtml) { try - { + { + var mailSender = new EmailSender(); using (var mail = new MailMessage()) { mail.From = new MailAddress(fromMail.Trim()); @@ -1622,8 +1623,7 @@ namespace umbraco mail.Subject = subject; mail.IsBodyHtml = isHtml; mail.Body = body; - using (var smtpClient = new SmtpClient()) - smtpClient.Send(mail); + mailSender.Send(mail); } } catch (Exception ee) diff --git a/src/umbraco.cms/businesslogic/translation/Translation.cs b/src/umbraco.cms/businesslogic/translation/Translation.cs index 1cddf4e8e9..9c31cdb47d 100644 --- a/src/umbraco.cms/businesslogic/translation/Translation.cs +++ b/src/umbraco.cms/businesslogic/translation/Translation.cs @@ -59,6 +59,7 @@ namespace umbraco.cms.businesslogic.translation { try { + var mailSender = new EmailSender(); using (var mail = new MailMessage()) { mail.From = new MailAddress(user.Email.Trim()); @@ -66,8 +67,7 @@ namespace umbraco.cms.businesslogic.translation mail.Subject = ui.Text("translation", "mailSubject", subjectVars, translator); ; mail.IsBodyHtml = false; mail.Body = ui.Text("translation", "mailBody", bodyVars, translator); ; - using (var smtpClient = new SmtpClient()) - smtpClient.Send(mail); + mailSender.Send(mail); } } catch (Exception ex) From 2590ee51ec7541866fd0c67d95684d4519a3e729 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Sep 2017 19:20:33 +1000 Subject: [PATCH 41/64] cleans up event args and makes internal --- build/UmbracoVersion.txt | 2 +- src/SolutionInfo.cs | 2 +- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- src/Umbraco.Core/Events/SendEmailEventArgs.cs | 16 ++-------------- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index ae27482bf3..89b1ced747 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,3 +1,3 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) 7.7.0 -beta003 \ No newline at end of file +beta004 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 2256adc07d..157530ffe5 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.7.0")] -[assembly: AssemblyInformationalVersion("7.7.0-beta003")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.7.0-beta004")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index ac5375bfe9..33adcb49b7 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Configuration /// Gets the version comment (like beta or RC). /// /// The version comment. - public static string CurrentComment { get { return "beta003"; } } + public static string CurrentComment { get { return "beta004"; } } // Get the version of the umbraco.dll by looking at a class in that dll // Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx diff --git a/src/Umbraco.Core/Events/SendEmailEventArgs.cs b/src/Umbraco.Core/Events/SendEmailEventArgs.cs index 790e5d4940..8c6a2138d5 100644 --- a/src/Umbraco.Core/Events/SendEmailEventArgs.cs +++ b/src/Umbraco.Core/Events/SendEmailEventArgs.cs @@ -3,25 +3,13 @@ using System.Net.Mail; namespace Umbraco.Core.Events { - public class SendEmailEventArgs : EventArgs + internal class SendEmailEventArgs : EventArgs { - public SmtpClient Smtp { get; private set; } public MailMessage Message { get; private set; } - ///// - ///// A flag indicating that the mail sending was handled by an event handler - ///// - //public bool SendingHandledExternally { get; set; } - public SendEmailEventArgs(MailMessage message) { Message = message; - } - - public SendEmailEventArgs(SmtpClient smtp, MailMessage message) - { - Smtp = smtp; - Message = message; - } + } } } \ No newline at end of file From 1f7a5ff90acb5a15b331b07f5a949fab2c255b23 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Sep 2017 19:28:18 +1000 Subject: [PATCH 42/64] bumping version --- build/UmbracoVersion.txt | 2 +- src/SolutionInfo.cs | 2 +- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 89b1ced747..6f8b223965 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,3 +1,3 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) 7.7.0 -beta004 \ No newline at end of file +beta005 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 157530ffe5..65df52011e 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.7.0")] -[assembly: AssemblyInformationalVersion("7.7.0-beta004")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.7.0-beta005")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 33adcb49b7..2b51d86830 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Configuration /// Gets the version comment (like beta or RC). /// /// The version comment. - public static string CurrentComment { get { return "beta004"; } } + public static string CurrentComment { get { return "beta005"; } } // Get the version of the umbraco.dll by looking at a class in that dll // Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx From 0dee1f0fac4ff7e148c73d9024465cfcf968f35d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Sep 2017 22:35:40 +1000 Subject: [PATCH 43/64] U4-10368 Need option to set username different than e-mail for AD compatibility --- .../UmbracoSettings/ISecuritySection.cs | 11 +++++++- .../UmbracoSettings/SecurityElement.cs | 25 +++++++++++++++++++ .../src/views/users/user.controller.js | 4 +-- .../src/views/users/user.html | 2 +- .../Editors/BackOfficeServerVariables.cs | 1 + 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs index 4cc9ba402a..31ee4611d0 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs @@ -13,6 +13,15 @@ string AuthCookieName { get; } - string AuthCookieDomain { get; } + string AuthCookieDomain { get; } + + /// + /// A boolean indicating that by default the email address will be the username + /// + /// + /// Even if this is true and the username is different from the email in the database, the username field will still be shown. + /// When this is false, the username and email fields will be shown in the user section. + /// + bool UsernameIsEmail { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs index 1918d740ba..dc2ba7e983 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs @@ -25,6 +25,19 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return GetOptionalTextElement("allowPasswordReset", true); } } + /// + /// A boolean indicating that by default the email address will be the username + /// + /// + /// Even if this is true and the username is different from the email in the database, the username field will still be shown. + /// When this is false, the username and email fields will be shown in the user section. + /// + [ConfigurationProperty("usernameIsEmail")] + internal InnerTextConfigurationElement UsernameIsEmail + { + get { return GetOptionalTextElement("usernameIsEmail", true); } + } + [ConfigurationProperty("authCookieName")] internal InnerTextConfigurationElement AuthCookieName { @@ -55,6 +68,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return AllowPasswordReset; } } + /// + /// A boolean indicating that by default the email address will be the username + /// + /// + /// Even if this is true and the username is different from the email in the database, the username field will still be shown. + /// When this is false, the username and email fields will be shown in the user section. + /// + bool ISecuritySection.UsernameIsEmail + { + get { return UsernameIsEmail; } + } + string ISecuritySection.AuthCookieName { get { return AuthCookieName; } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index cf83aa4db5..540f2b85df 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -15,7 +15,7 @@ vm.labels = {}; vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); - vm.emailIsUsername = true; + vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail; //create the initial model for change password vm.changePasswordModel = { @@ -68,7 +68,7 @@ setUserDisplayState(); formatDatesToLocal(vm.user); - vm.emailIsUsername = user.email === user.username; + vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail || user.email === user.username; //go get the config for the membership provider and add it to the model authResource.getMembershipProviderConfig().then(function (data) { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index ebe493fce8..b91aec14ae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -43,7 +43,7 @@ - + Date: Tue, 5 Sep 2017 22:46:13 +1000 Subject: [PATCH 44/64] updates config template --- src/Umbraco.Web.UI.Client/src/views/users/user.controller.js | 2 +- src/Umbraco.Web.UI/config/umbracoSettings.Release.config | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 540f2b85df..15d2353c46 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -68,7 +68,7 @@ setUserDisplayState(); formatDatesToLocal(vm.user); - vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail || user.email === user.username; + vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail && user.email === user.username; //go get the config for the membership provider and add it to the model authResource.getMembershipProviderConfig().then(function (data) { diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index 6687e71f4c..4e0c24434c 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -66,6 +66,8 @@ false + + true false From e7c231c5a87071ae24feb6900997509cc3a97715 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 5 Sep 2017 15:23:18 +0200 Subject: [PATCH 45/64] Support IdReservation in IdkMap --- src/Umbraco.Core/Constants-ObjectTypes.cs | 10 +++++++++- src/Umbraco.Core/Models/UmbracoObjectTypes.cs | 9 ++++++++- src/Umbraco.Core/Services/IdkMap.cs | 8 ++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 1fbc1ec62b..a1e6e55ee9 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -223,7 +223,15 @@ namespace Umbraco.Core /// public static readonly Guid LanguageGuid = new Guid(Language); - public static readonly Guid IdReservationGuid = new Guid("92849B1E-3904-4713-9356-F646F87C25F4"); + /// + /// Guid for an Identifier Reservation. + /// + public const string IdReservation = "92849B1E-3904-4713-9356-F646F87C25F4"; + + /// + /// Guid for an Identifier Reservation. + /// + public static readonly Guid IdReservationGuid = new Guid(IdReservation); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index d0136a10a4..d4caaef768 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -185,6 +185,13 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Language)] [FriendlyName("Language")] - Language + Language, + + /// + /// Reserved Identifier + /// + [UmbracoObjectType(Constants.ObjectTypes.IdReservation)] + [FriendlyName("Identifier Reservation")] + IdReservation } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IdkMap.cs b/src/Umbraco.Core/Services/IdkMap.cs index 015bb104dc..411316fca2 100644 --- a/src/Umbraco.Core/Services/IdkMap.cs +++ b/src/Umbraco.Core/Services/IdkMap.cs @@ -46,6 +46,10 @@ namespace Umbraco.Core.Services if (val == null) return Attempt.Fail(); + // cache reservations, when something is saved this cache is cleared anyways + //if (umbracoObjectType == UmbracoObjectTypes.IdReservation) + // Attempt.Succeed(val.Value); + try { _locker.EnterWriteLock(); @@ -95,6 +99,10 @@ namespace Umbraco.Core.Services if (val == null) return Attempt.Fail(); + // cache reservations, when something is saved this cache is cleared anyways + //if (umbracoObjectType == UmbracoObjectTypes.IdReservation) + // Attempt.Succeed(val.Value); + try { _locker.EnterWriteLock(); From f3f31f2569b5b5ef07e4a47d4ebc21fb0e9110ea Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Sep 2017 23:31:26 +1000 Subject: [PATCH 46/64] Gets invite and create user working with new setting and allowing specifying a separate username. --- .../src/views/users/user.html | 2 +- .../users/views/users/users.controller.js | 2 + .../src/views/users/views/users/users.html | 7 +++ src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 20 +++--- .../umbraco/config/lang/en_us.xml | 1 + src/Umbraco.Web/Editors/UsersController.cs | 63 ++++++++++++++----- .../Models/ContentEditing/UserInvite.cs | 12 +++- 7 files changed, 81 insertions(+), 26 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index b91aec14ae..e043b50d2c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -47,7 +47,7 @@ + + + Required + + + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 5d0779f8b6..77615cf11a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -81,14 +81,14 @@ Domain '%0%' has been updated Edit Current Domains - Inherit Culture - or inherit culture from parent nodes. Will also apply
+ or inherit culture from parent nodes. Will also apply
to the current node, unless a domain below applies too.]]>
Domains @@ -339,11 +339,11 @@ Number of columns Number of rows - Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, + Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, by referring this ID using a <asp:content /> element.]]> - Select a placeholder id from the list below. You can only + Select a placeholder id from the list below. You can only choose Id's from the current template's master.]]> Click on the image to see full size @@ -380,15 +380,15 @@ - %0%' below
You can add additional languages under the 'languages' in the menu on the left + %0%' below
You can add additional languages under the 'languages' in the menu on the left ]]>
Culture Name Edit the key of the dictionary item. - @@ -398,6 +398,8 @@ Confirm your password Name the %0%... Enter a name... + Enter an email... + Enter a username... Label... Enter a description... Type to search... diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index ed0edf8b6d..70e738ffeb 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -400,6 +400,7 @@ Name the %0%... Enter a name... Enter an email... + Enter a username... Label... Enter a description... Type to search... diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index cb2963124f..3ca2041ea5 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -265,13 +265,18 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - - var existing = Services.UserService.GetByEmail(userSave.Email); - if (existing != null) + + if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail) { - ModelState.AddModelError("Email", "A user with the email already exists"); - throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + //ensure they are the same if we're using it + userSave.Username = userSave.Email; } + else + { + //first validate the username if were showing it + CheckUniqueUsername(userSave.Username, null); + } + CheckUniqueEmail(userSave.Email, null); //Perform authorization here to see if the current user can actually save this user with the info being requested var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); @@ -283,7 +288,7 @@ namespace Umbraco.Web.Editors //we want to create the user with the UserManager, this ensures the 'empty' (special) password //format is applied without us having to duplicate that logic - var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Email, userSave.Email, GlobalSettings.DefaultUILanguage); + var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Username, userSave.Email, GlobalSettings.DefaultUILanguage); identityUser.Name = userSave.Name; var created = await UserManager.CreateAsync(identityUser); @@ -345,7 +350,7 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - + var hasSmtp = GlobalSettings.HasSmtpServerConfigured(RequestContext.VirtualPathRoot); if (hasSmtp == false) { @@ -353,13 +358,19 @@ namespace Umbraco.Web.Editors Request.CreateNotificationValidationErrorResponse("No Email server is configured")); } - var user = Services.UserService.GetByEmail(userSave.Email); - if (user != null && (user.LastLoginDate != default(DateTime) || user.EmailConfirmedDate.HasValue)) + IUser user; + if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail) { - ModelState.AddModelError("Email", "A user with the email already exists"); - throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + //ensure it's the same + userSave.Username = userSave.Email; } - + else + { + //first validate the username if we're showing it + user = CheckUniqueUsername(userSave.Username, u => u.LastLoginDate != default(DateTime) || u.EmailConfirmedDate.HasValue); + } + user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default(DateTime) || u.EmailConfirmedDate.HasValue); + //Perform authorization here to see if the current user can actually save this user with the info being requested var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, user, null, null, userSave.UserGroups); @@ -372,7 +383,7 @@ namespace Umbraco.Web.Editors { //we want to create the user with the UserManager, this ensures the 'empty' (special) password //format is applied without us having to duplicate that logic - var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Email, userSave.Email, GlobalSettings.DefaultUILanguage); + var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Username, userSave.Email, GlobalSettings.DefaultUILanguage); identityUser.Name = userSave.Name; var created = await UserManager.CreateAsync(identityUser); @@ -402,7 +413,31 @@ namespace Umbraco.Web.Editors return display; } - + + private IUser CheckUniqueEmail(string email, Func extraCheck) + { + var user = Services.UserService.GetByEmail(email); + if (user != null && (extraCheck == null || extraCheck(user))) + { + ModelState.AddModelError("Email", "A user with the email already exists"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + return user; + } + + private IUser CheckUniqueUsername(string username, Func extraCheck) + { + var user = Services.UserService.GetByUsername(username); + if (user != null && (extraCheck == null || extraCheck(user))) + { + ModelState.AddModelError( + UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail ? "Email" : "Username", + "A user with the username already exists"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + return user; + } + private HttpContextBase EnsureHttpContext() { var attempt = this.TryGetHttpContext(); diff --git a/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs b/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs index 06895ccc68..368067814d 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; +using Umbraco.Core; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Models.ContentEditing { @@ -18,7 +20,10 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "email", IsRequired = true)] [Required] [EmailAddress] - public string Email { get; set; } + public string Email { get; set; } + + [DataMember(Name = "username")] + public string Username { get; set; } [DataMember(Name = "message")] public string Message { get; set; } @@ -26,7 +31,10 @@ namespace Umbraco.Web.Models.ContentEditing public IEnumerable Validate(ValidationContext validationContext) { if (UserGroups.Any() == false) - yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); + yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); + + if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail == false && Username.IsNullOrWhiteSpace()) + yield return new ValidationResult("A username cannot be empty", new[] { "Username" }); } } } \ No newline at end of file From d32093136faa0aba6bf60d71b8d370088435ca58 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Sep 2017 23:41:58 +1000 Subject: [PATCH 47/64] U4-10214 Disable ability to delete admin group --- .../src/views/users/views/groups/groups.html | 1 + src/Umbraco.Web/Editors/UserGroupsController.cs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index dddd931b1d..119c4f559d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -84,6 +84,7 @@
diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index 33b1d9dc08..18777395d3 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -133,7 +133,10 @@ namespace Umbraco.Web.Editors [UserGroupAuthorization("userGroupIds")] public HttpResponseMessage PostDeleteUserGroups([FromUri] int[] userGroupIds) { - var userGroups = Services.UserService.GetAllUserGroups(userGroupIds).ToArray(); + var userGroups = Services.UserService.GetAllUserGroups(userGroupIds) + //never delete the admin group + .Where(x => x.Alias != Constants.Security.AdminGroupAlias) + .ToArray(); foreach (var userGroup in userGroups) { Services.UserService.DeleteUserGroup(userGroup); From 1578bc8cc7d5fe2d70cd00b255d278e77d6b822d Mon Sep 17 00:00:00 2001 From: Tom Fulton Date: Tue, 5 Sep 2017 13:20:18 -0400 Subject: [PATCH 48/64] U4-8886 - TinyMCE - highlight media picker toolbar icon when image selected --- src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 8753bb5316..86de5b586d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -87,6 +87,7 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro editor.addButton('umbmediapicker', { icon: 'custom icon-picture', tooltip: 'Media Picker', + stateSelector: 'img', onclick: function () { var selectedElm = editor.selection.getNode(), From 616b9b0e924922784ce133e5a2fab9c5bb860973 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Sep 2017 14:50:44 +1000 Subject: [PATCH 49/64] U4-10325 Media insert in grid should use UDIs --- src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs | 2 +- .../src/views/propertyeditors/grid/editors/media.controller.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index 389c620637..3026064dec 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -9,7 +9,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Core.Configuration.Grid { - class GridEditorsConfig : IGridEditorsConfig + internal class GridEditorsConfig : IGridEditorsConfig { private readonly ILogger _logger; private readonly IRuntimeCacheProvider _runtimeCache; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 7198166c25..21f9534848 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -26,6 +26,7 @@ angular.module("umbraco") $scope.control.value = { focalPoint: selectedImage.focalPoint, id: selectedImage.id, + udi: selectedImage.udi, image: selectedImage.image, altText: selectedImage.altText }; From 3b741d3075b6389960657fb66000bc301f98749a Mon Sep 17 00:00:00 2001 From: mikkelhm Date: Wed, 6 Sep 2017 09:37:35 +0200 Subject: [PATCH 50/64] Disallow selection of the admin group, as it should never be allowed --- .../src/views/users/views/groups/groups.controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js index 52ddda140d..956688997f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js @@ -59,10 +59,14 @@ function selectUserGroup(userGroup, selection, event) { - // only allow selection if user is member of the group or admin + // Only allow selection if user is member of the group or admin if (currentUser.userGroups.indexOf(userGroup.group.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { return; } + // Disallow selection of the admin group, the checkbox is not visible in the UI, but clicking(and thous selecting) is still possible. + // Currently selection can only be used for deleting, and the Controller will also disallow deleting the admin group. + if (userGroup.group.alias === "admin") + return; if (userGroup.selected) { var index = selection.indexOf(userGroup.group.id); From 8d6c8ef282799dfe96082a3b31b205a967194543 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 6 Sep 2017 14:20:09 +0200 Subject: [PATCH 51/64] perfs - support LocalDb --- src/Umbraco.Core/CoreBootManager.cs | 23 + src/Umbraco.Core/Persistence/LocalDb.cs | 959 ++++++++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + 3 files changed, 983 insertions(+) create mode 100644 src/Umbraco.Core/Persistence/LocalDb.cs diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index dcce15f419..a0455d820a 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Configuration; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Web; using AutoMapper; @@ -399,6 +400,28 @@ namespace Umbraco.Core if (ApplicationContext.IsConfigured == false) return; if (ApplicationContext.DatabaseContext.IsDatabaseConfigured == false) return; + // deal with localdb + var databaseContext = ApplicationContext.DatabaseContext; + var localdbex = new Regex(@"\(localdb\)\\([a-zA-Z0-9-_]+)(;|$)"); + var m = localdbex.Match(databaseContext.ConnectionString); + if (m.Success) + { + var instanceName = m.Groups[1].Value; + ProfilingLogger.Logger.Info(string.Format("LocalDb instance \"{0}\"", instanceName)); + + var localDb = new LocalDb(); + if (localDb.IsAvailable == false) + throw new UmbracoStartupFailedException("Umbraco cannot start. LocalDb is not available."); + + if (localDb.InstanceExists(m.Groups[1].Value) == false) + { + if (localDb.CreateInstance(instanceName) == false) + throw new UmbracoStartupFailedException(string.Format("Umbraco cannot start. LocalDb cannot create instance \"{0}\".", instanceName)); + if (localDb.StartInstance(instanceName) == false) + throw new UmbracoStartupFailedException(string.Format("Umbraco cannot start. LocalDb cannot start instance \"{0}\".", instanceName)); + } + } + //try now if (ApplicationContext.DatabaseContext.CanConnect) return; diff --git a/src/Umbraco.Core/Persistence/LocalDb.cs b/src/Umbraco.Core/Persistence/LocalDb.cs new file mode 100644 index 0000000000..084122ac3a --- /dev/null +++ b/src/Umbraco.Core/Persistence/LocalDb.cs @@ -0,0 +1,959 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Umbraco.Core.Persistence +{ + /// + /// Manages LocalDB databases. + /// + /// + /// Latest version is SQL Server 2016 Express LocalDB, + /// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb + /// which can be installed by downloading the Express installer from https://www.microsoft.com/en-us/sql-server/sql-server-downloads + /// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This installs + /// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server Management + /// Studio which can be used to connect to LocalDB databases. + /// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. + /// + public class LocalDb + { + private int _version; + private bool _hasVersion; + + #region Availability & Version + + /// + /// Gets the LocalDb installed version. + /// + /// If more than one version is installed, returns the highest available. Returns + /// the major version as an integer e.g. 11, 12... + /// Thrown when LocalDb is not available. + public int Version + { + get + { + EnsureVersion(); + if (_version <= 0) + throw new InvalidOperationException("LocalDb is not available."); + return _version; + } + } + + /// + /// Ensures that the LocalDb version is detected. + /// + private void EnsureVersion() + { + if (_hasVersion) return; + DetectVersion(); + _hasVersion = true; + } + + /// + /// Gets a value indicating whether LocalDb is available. + /// + public bool IsAvailable + { + get + { + EnsureVersion(); + return _version > 0; + } + } + + /// + /// Ensures that LocalDb is available. + /// + /// Thrown when LocalDb is not available. + private void EnsureAvailable() + { + if (IsAvailable == false) + throw new InvalidOperationException("LocalDb is not available."); + } + + /// + /// Detects LocalDb installed version. + /// + /// If more than one version is installed, the highest available is detected. + private void DetectVersion() + { + _hasVersion = true; + _version = -1; + + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + if (programFiles == null) return; + + // detect 14, 13, 12, 11 + for (var i = 14; i > 10; i--) + { + var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", i)); + if (File.Exists(path) == false) continue; + _version = i; + break; + } + } + + #endregion + + #region Instances + + /// + /// Gets the name of existing LocalDb instances. + /// + /// The name of existing LocalDb instances. + /// Thrown when LocalDb is not available. + public string[] GetInstances() + { + EnsureAvailable(); + string output, error; + var rc = ExecuteSqlLocalDb("i", out output, out error); // info + if (rc != 0 || error != string.Empty) return null; + return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Gets a value indicating whether a LocalDb instance exists. + /// + /// The name of the instance. + /// A value indicating whether a LocalDb instance with the specified name exists. + /// Thrown when LocalDb is not available. + public bool InstanceExists(string instanceName) + { + EnsureAvailable(); + var instances = GetInstances(); + return instances != null && instances.Contains(instanceName); + } + + /// + /// Creates a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was created without errors. + /// Thrown when LocalDb is not available. + public bool CreateInstance(string instanceName) + { + EnsureAvailable(); + string output, error; + return ExecuteSqlLocalDb(string.Format("c \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Drops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was dropped without errors. + /// Thrown when LocalDb is not available. + /// + /// When an instance is dropped all the attached database files are deleted. + /// Successful if the instance does not exist. + /// + public bool DropInstance(string instanceName) + { + EnsureAvailable(); + var instance = GetInstance(instanceName); + if (instance == null) return true; + instance.DropDatabases(); // else the files remain + + // -i force NOWAIT, -k kills + string output, error; + return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty + && ExecuteSqlLocalDb(string.Format("d \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was stopped without errors. + /// Thrown when LocalDb is not available. + /// + /// Successful if the instance does not exist. + /// + public bool StopInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) return true; + + // -i force NOWAIT, -k kills + string output, error; + return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was started without errors. + /// Thrown when LocalDb is not available. + /// + /// Failed if the instance does not exist. + /// + public bool StartInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) return false; + string output, error; + return ExecuteSqlLocalDb(string.Format("s \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Gets a LocalDb instance. + /// + /// The name of the instance. + /// The instance with the specified name if it exists, otherwise null. + /// Thrown when LocalDb is not available. + public Instance GetInstance(string instanceName) + { + EnsureAvailable(); + return InstanceExists(instanceName) ? new Instance(instanceName) : null; + } + + #endregion + + #region Databases + + /// + /// Represents a LocalDb instance. + /// + /// + /// LocalDb is assumed to be available, and the instance is assumed to exist. + /// + public class Instance + { + private readonly string _masterCstr; + + /// + /// Gets the name of the instance. + /// + public string InstanceName { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// + public Instance(string instanceName) + { + InstanceName = instanceName; + _masterCstr = string.Format(@"Server=(localdb)\{0};Integrated Security=True;", instanceName); + } + + /// + /// Gets a LocalDb connection string. + /// + /// The name of the database. + /// The connection string for the specified database. + /// + /// The database should exist in the LocalDb instance. + /// + public string GetConnectionString(string databaseName) + { + return _masterCstr + string.Format(@"Database={0};", databaseName); + } + + /// + /// Gets a LocalDb connection string for an attached database. + /// + /// The name of the database. + /// The directory containing database files. + /// The connection string for the specified database. + /// + /// The database should not exist in the LocalDb instance. + /// It will be attached with its name being its MDF filename (full path), uppercased, when + /// the first connection is opened, and remain attached until explicitely detached. + /// + public string GetAttachedConnectionString(string databaseName, string filesPath) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + return _masterCstr + string.Format(@"AttachDbFileName='{0}';", mdfFilename); + } + + /// + /// Gets the name of existing databases. + /// + /// The name of existing databases. + public string[] GetDatabases() + { + var userDatabases = new List(); + + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var databases = new Dictionary(); + + SetCommand(cmd, @" + SELECT name, filename FROM sys.sysdatabases"); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + databases[reader.GetString(0)] = reader.GetString(1); + } + } + + foreach (var database in databases) + { + var dbname = database.Key; + + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + continue; + + // fixme - shall we deal with stale databases? + // fixme - is it always ok to assume file names? + //var mdf = database.Value; + //var ldf = mdf.Replace(".mdf", "_log.ldf"); + //if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + // continue; + + //ExecuteDropDatabase(cmd, dbname, mdf, ldf); + //count++; + + userDatabases.Add(dbname); + } + } + + return userDatabases.ToArray(); + } + + /// + /// Gets a value indicating whether a database exists. + /// + /// The name of the database. + /// A value indicating whether a database with the specified name exists. + /// + /// A database exists if it is registered in the instance, and its files exist. If the database + /// is registered but some of its files are missing, the database is dropped. + /// + public bool DatabaseExists(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) return false; + + // it can exist, even though its files have been deleted + // if files exist assume all is ok (should we try to connect?) + var ldf = GetLogFilename(mdf); + if (File.Exists(mdf) && File.Exists(ldf)) + return true; + + ExecuteDropDatabase(cmd, databaseName, mdf, ldf); + } + + return false; + } + + /// + /// Creates a new database. + /// + /// The name of the database. + /// The directory containing database files. + /// A value indicating whether the database was created without errors. + /// + /// Failed if a database with the specified name already exists in the instance, + /// or if the database files already exist in the specified directory. + /// + public bool CreateDatabase(string databaseName, string filesPath) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) return false; + + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + CREATE DATABASE {0} + ON (NAME=N{1}, FILENAME={2}) + LOG ON (NAME=N{3}, FILENAME={4})", + QuotedName(databaseName), + QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''), + QuotedName(logName, '\''), QuotedName(ldfFilename, '\''))); + + var unused = cmd.ExecuteNonQuery(); + } + return true; + } + + /// + /// Drops a database. + /// + /// The name of the database. + /// A value indicating whether the database was dropped without errors. + /// + /// Successful if the database does not exist. + /// Deletes the database files. + /// + public bool DropDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + SetCommand(cmd, @" + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", + databaseName); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) return true; + + ExecuteDropDatabase(cmd, databaseName, mdf); + } + + return true; + } + + /// + /// Drops stale databases. + /// + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropStaleDatabases() + { + return DropDatabases(true); + } + + /// + /// Drops databases. + /// + /// A value indicating whether to delete only stale database. + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropDatabases(bool staleOnly = false) + { + var count = 0; + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var databases = new Dictionary(); + + SetCommand(cmd, @" + SELECT name, filename FROM sys.sysdatabases"); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + databases[reader.GetString(0)] = reader.GetString(1); + } + } + + foreach (var database in databases) + { + var dbname = database.Key; + + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + continue; + + var mdf = database.Value; + var ldf = mdf.Replace(".mdf", "_log.ldf"); + if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + continue; + + ExecuteDropDatabase(cmd, dbname, mdf, ldf); + count++; + } + } + + return count; + } + + /// + /// Detaches a database. + /// + /// The name of the database. + /// The directory containing the database files. + /// Thrown when a database with the specified name does not exist. + public string DetachDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + throw new InvalidOperationException("Database does not exist."); + + DetachDatabase(cmd, databaseName); + + return Path.GetDirectoryName(mdf); + } + } + + /// + /// Attaches a database. + /// + /// The name of the database. + /// The directory containing database files. + /// Thrown when a database with the specified name already exists. + public void AttachDatabase(string databaseName, string filesPath) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) + throw new InvalidOperationException("Database already exists."); + + AttachDatabase(cmd, databaseName, filesPath); + } + } + + /// + /// Gets the file names of a database. + /// + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + public void GetFilenames(string databaseName, + out string mdfName, out string ldfName, + out string mdfFilename, out string ldfFilename) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename); + } + } + + /// + /// Kills all existing connections. + /// + /// The name of the database. + public void KillConnections(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + SetCommand(cmd, @" + DECLARE @sql VARCHAR(MAX); + SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';' + FROM master.sys.sysprocesses + WHERE DBId = DB_ID(@0) AND SPId <> @@SPId; + EXEC(@sql);", + databaseName); + cmd.ExecuteNonQuery(); + } + } + + /// + /// Gets a database. + /// + /// The Sql Command. + /// The name of the database. + /// The full filename of the MDF file, if the database exists, otherwise null. + private static string GetDatabase(SqlCommand cmd, string databaseName) + { + SetCommand(cmd, @" + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", + databaseName); + + string mdf = null; + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + mdf = reader.GetString(1) ?? string.Empty; + while (reader.Read()) + { + } + } + + return mdf; + } + + /// + /// Drops a database and its files. + /// + /// The Sql command. + /// The name of the database. + /// The name of the database (MDF) file. + /// The name of the log (LDF) file. + private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string ldf = null) + { + try + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE", + QuotedName(databaseName))); + + var unused1 = cmd.ExecuteNonQuery(); + } + catch (SqlException e) + { + if (e.Message.Contains("Unable to open the physical file") && e.Message.Contains("Operating system error 2:")) + { + // quite probably, the files were missing + // yet, it should be possible to drop the database anyways + // but we'll have to deal with the files + } + else + { + // no idea, throw + throw; + } + } + + // cannot use parameters on DROP DATABASE + // ie "DROP DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + DROP DATABASE {0}", + QuotedName(databaseName))); + + var unused2 = cmd.ExecuteNonQuery(); + + // be absolutely sure + if (File.Exists(mdf)) File.Delete(mdf); + ldf = ldf ?? GetLogFilename(mdf); + if (File.Exists(ldf)) File.Delete(ldf); + } + + /// + /// Gets the log (LDF) filename corresponding to a database (MDF) filename. + /// + /// The MDF filename. + /// + private static string GetLogFilename(string mdfFilename) + { + if (mdfFilename.EndsWith(".mdf") == false) + throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", "mdfFilename"); + return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; + } + + /// + /// Detaches a database. + /// + /// The Sql command. + /// The name of the database. + private static void DetachDatabase(SqlCommand cmd, string databaseName) + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE", + QuotedName(databaseName))); + + var unused1 = cmd.ExecuteNonQuery(); + + SetCommand(cmd, @" + EXEC sp_detach_db @dbname=@0", + databaseName); + + var unused2 = cmd.ExecuteNonQuery(); + } + + /// + /// Attaches a database. + /// + /// The Sql command. + /// The name of the database. + /// The directory containing database files. + private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + CREATE DATABASE {0} + ON (NAME=N{1}, FILENAME={2}) + LOG ON (NAME=N{3}, FILENAME={4}) + FOR ATTACH", + QuotedName(databaseName), + QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''), + QuotedName(logName, '\''), QuotedName(ldfFilename, '\''))); + + var unused = cmd.ExecuteNonQuery(); + } + + /// + /// Sets a database command. + /// + /// The command. + /// The command text. + /// The command arguments. + /// + /// The command text must refer to arguments as @0, @1... each referring + /// to the corresponding position in . + /// + private static void SetCommand(SqlCommand cmd, string sql, params object[] args) + { + cmd.CommandType = CommandType.Text; + cmd.CommandText = sql; + cmd.Parameters.Clear(); + for (var i = 0; i < args.Length; i++) + cmd.Parameters.AddWithValue("@" + i, args[i]); + } + + /// + /// Gets the file names of a database. + /// + /// The Sql command. + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + private void GetFilenames(SqlCommand cmd, string databaseName, + out string mdfName, out string ldfName, + out string mdfFilename, out string ldfFilename) + { + mdfName = ldfName = mdfFilename = ldfFilename = null; + + SetCommand(cmd, @" + SELECT DB_NAME(database_id), type_desc, name, physical_name + FROM master.sys.master_files + WHERE database_id=DB_ID(@0)", + databaseName); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var type = reader.GetString(1); + if (type == "ROWS") + { + mdfName = reader.GetString(2); + ldfName = reader.GetString(3); + } + else if (type == "LOG") + { + ldfName = reader.GetString(2); + ldfFilename = reader.GetString(3); + } + } + } + } + } + + /// + /// Copy database files. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The name of the target database. + /// The directory containing target database files. + /// The source database files extension. + /// The target database files extension. + /// A value indicating whether to overwrite the target files. + /// A value indicating whether to delete the source files. + /// + /// The , , + /// and parameters are optional. If they result in target being identical + /// to source, no copy is performed. If is false, nothing happens, otherwise the source + /// files are deleted. + /// If target is not identical to source, files are copied or moved, depending on the value of . + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public void CopyDatabaseFiles(string databaseName, string filesPath, + string targetDatabaseName = null, string targetFilesPath = null, + string sourceExtension = null, string targetExtension = null, + bool overwrite = false, bool delete = false) + { + var nop = (targetFilesPath == null || targetFilesPath == filesPath) + && (targetDatabaseName == null || targetDatabaseName == databaseName) + && (sourceExtension == null && targetExtension == null || sourceExtension == targetExtension); + if (nop && delete == false) return; + + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, + out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + if (sourceExtension != null) + { + mdfFilename += "." + sourceExtension; + ldfFilename += "." + sourceExtension; + } + + if (nop) + { + // delete + if (File.Exists(mdfFilename)) File.Delete(mdfFilename); + if (File.Exists(ldfFilename)) File.Delete(ldfFilename); + } + else + { + // copy or copy+delete ie move + string targetLogName, targetBaseFilename, targetLogFilename, targetMdfFilename, targetLdfFilename; + GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, + out targetLogName, out targetBaseFilename, out targetLogFilename, out targetMdfFilename, out targetLdfFilename); + + if (targetExtension != null) + { + targetMdfFilename += "." + targetExtension; + targetLdfFilename += "." + targetExtension; + } + + if (delete) + { + if (overwrite && File.Exists(targetMdfFilename)) File.Delete(targetMdfFilename); + if (overwrite && File.Exists(targetLdfFilename)) File.Delete(targetLdfFilename); + File.Move(mdfFilename, targetMdfFilename); + File.Move(ldfFilename, targetLdfFilename); + } + else + { + File.Copy(mdfFilename, targetMdfFilename, overwrite); + File.Copy(ldfFilename, targetLdfFilename, overwrite); + } + } + } + + /// + /// Gets a value indicating whether database files exist. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The database files extension. + /// A value indicating whether the database files exist. + /// + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public bool DatabaseFilesExist(string databaseName, string filesPath, string extension = null) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, + out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + if (extension != null) + { + mdfFilename += "." + extension; + ldfFilename += "." + extension; + } + + return File.Exists(mdfFilename) && File.Exists(ldfFilename); + } + + /// + /// Gets the name of the database files. + /// + /// The name of the database. + /// The directory containing database files. + /// The name of the log. + /// The base filename (the MDF filename without the .mdf extension). + /// The base log filename (the LDF filename without the .ldf extension). + /// The MDF filename. + /// The LDF filename. + private static void GetDatabaseFiles(string databaseName, string filesPath, + out string logName, + out string baseFilename, out string baseLogFilename, + out string mdfFilename, out string ldfFilename) + { + logName = databaseName + "_log"; + baseFilename = Path.Combine(filesPath, databaseName); + baseLogFilename = Path.Combine(filesPath, logName); + mdfFilename = baseFilename + ".mdf"; + ldfFilename = baseFilename + "_log.ldf"; + } + + #endregion + + #region SqlLocalDB + + /// + /// Executes the SqlLocalDB command. + /// + /// The arguments. + /// The command standard output. + /// The command error output. + /// The process exit code. + /// + /// Execution is successful if the exit code is zero, and error is empty. + /// + private int ExecuteSqlLocalDb(string args, out string output, out string error) + { + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + if (programFiles == null) + { + output = string.Empty; + error = "SqlLocalDB.exe not found"; + return -1; + } + + var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", _version)); + + var p = new Process + { + StartInfo = + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = path, + Arguments = args, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + } + }; + p.Start(); + output = p.StandardOutput.ReadToEnd(); + error = p.StandardError.ReadToEnd(); + p.WaitForExit(); + + return p.ExitCode; + } + + /// + /// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited identifier. + /// + /// The name to quote. + /// A quote character. + /// + /// + /// This is a C# implementation of T-SQL QUOTEDNAME. + /// is optional, it can be '[' (default), ']', '\'' or '"'. + /// + private static string QuotedName(string name, char quote = '[') + { + switch (quote) + { + case '[': + case ']': + return "[" + name.Replace("]", "]]") + "]"; + case '\'': + return "'" + name.Replace("'", "''") + "'"; + case '"': + return "\"" + name.Replace("\"", "\"\"") + "\""; + default: + throw new NotSupportedException("Not a valid quote character."); + } + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ec6bfebd5b..8854c3c4ab 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -496,6 +496,7 @@ + From dbda6689b68bb429e8463e7a35b5d1811e37fc0a Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 5 Sep 2017 14:51:56 +0200 Subject: [PATCH 52/64] perfs - suspendable --- src/Umbraco.Core/Umbraco.Core.csproj | 3 + .../Cache/CacheRefresherEventHandler.cs | 2 + src/Umbraco.Web/Cache/PageCacheRefresher.cs | 41 +++---- .../Scheduling/ScheduledPublishing.cs | 3 + src/Umbraco.Web/Search/ExamineEvents.cs | 88 +++++++------ src/Umbraco.Web/Suspendable.cs | 116 ++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + .../umbraco.presentation/content.cs | 1 + 8 files changed, 196 insertions(+), 59 deletions(-) create mode 100644 src/Umbraco.Web/Suspendable.cs diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8854c3c4ab..77e84da945 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -134,6 +134,9 @@ + + ..\Umbraco.Web.UI\Bin\umbraco.dll + diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index beab3e6cf0..6fcdf2fcf3 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -823,6 +823,8 @@ namespace Umbraco.Web.Cache var handler = FindHandler(e); if (handler == null) continue; + ApplicationContext.Current.ProfilingLogger.Logger.Info("Handling " + e.Sender + " " + e.EventName); + handler.Invoke(null, new[] { e.Sender, e.Args }); } } diff --git a/src/Umbraco.Web/Cache/PageCacheRefresher.cs b/src/Umbraco.Web/Cache/PageCacheRefresher.cs index e884c9b3b8..d9bb7e9b88 100644 --- a/src/Umbraco.Web/Cache/PageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PageCacheRefresher.cs @@ -50,8 +50,9 @@ namespace Umbraco.Web.Cache /// public override void RefreshAll() { - content.Instance.RefreshContentFromDatabase(); - XmlPublishedContent.ClearRequest(); + if (Suspendable.PageCacheRefresher.CanRefreshDocumentCacheFromDatabase) + content.Instance.RefreshContentFromDatabase(); + ClearCaches(); base.RefreshAll(); } @@ -61,11 +62,9 @@ namespace Umbraco.Web.Cache /// The id. public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.UpdateDocumentCache(id); - XmlPublishedContent.ClearRequest(); - DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); - DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.UpdateDocumentCache(id); + ClearCaches(); base.Refresh(id); } @@ -75,35 +74,35 @@ namespace Umbraco.Web.Cache /// The id. public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.ClearDocumentCache(id, false); - XmlPublishedContent.ClearRequest(); - DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); - DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ClearAllIsolatedCacheByEntityType(); + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.ClearDocumentCache(id, false); + ClearCaches(); base.Remove(id); } public override void Refresh(IContent instance) { - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.UpdateDocumentCache(new Document(instance)); - XmlPublishedContent.ClearRequest(); - DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); - DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ClearAllIsolatedCacheByEntityType(); + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.UpdateDocumentCache(new Document(instance)); + ClearCaches(); base.Refresh(instance); } public override void Remove(IContent instance) + { + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.ClearDocumentCache(new Document(instance), false); + ClearCaches(); + base.Remove(instance); + } + + private void ClearCaches() { ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.ClearDocumentCache(new Document(instance), false); XmlPublishedContent.ClearRequest(); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); ClearAllIsolatedCacheByEntityType(); - base.Remove(instance); } } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 24359d2b50..96cf724d02 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -27,6 +27,9 @@ namespace Umbraco.Web.Scheduling { if (_appContext == null) return true; // repeat... + if (Suspendable.ScheduledPublishing.CanRun == false) + return true; // repeat, later + switch (_appContext.GetCurrentServerRole()) { case ServerRole.Slave: diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index 7fbbf29b89..e018bac214 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -24,7 +24,6 @@ namespace Umbraco.Web.Search /// public sealed class ExamineEvents : ApplicationEventHandler { - /// /// Once the application has started we should bind to all events and initialize the providers. /// @@ -32,9 +31,9 @@ namespace Umbraco.Web.Search /// /// /// We need to do this on the Started event as to guarantee that all resolvers are setup properly. - /// + /// protected override void ApplicationStarted(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext) - { + { LogHelper.Info("Initializing Examine and binding to business logic events"); var registeredProviders = ExamineManager.Instance.IndexProviderCollection @@ -46,14 +45,14 @@ namespace Umbraco.Web.Search if (registeredProviders == 0) return; - //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part + //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part // in a load balanced environment. CacheRefresherBase.CacheUpdated += UnpublishedPageCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += PublishedPageCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += MediaCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += MemberCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += ContentTypeCacheRefresherCacheUpdated; - + var contentIndexer = ExamineManager.Instance.IndexProviderCollection[Constants.Examine.InternalIndexer] as UmbracoContentIndexer; if (contentIndexer != null) { @@ -77,6 +76,9 @@ namespace Umbraco.Web.Search /// static void ContentTypeCacheRefresherCacheUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + var indexersToUpdated = ExamineManager.Instance.IndexProviderCollection.OfType(); foreach (var provider in indexersToUpdated) { @@ -114,7 +116,7 @@ namespace Umbraco.Web.Search } } - //TODO: We need to update Examine to support re-indexing multiple items at once instead of one by one which will speed up + //TODO: We need to update Examine to support re-indexing multiple items at once instead of one by one which will speed up // the re-indexing process, we don't want to revert to rebuilding the whole thing! if (contentTypesChanged.Count > 0) @@ -129,8 +131,8 @@ namespace Umbraco.Web.Search { ReIndexForContent(contentItem, contentItem.HasPublishedVersion && contentItem.Trashed == false); } - } - } + } + } } if (mediaTypesChanged.Count > 0) { @@ -163,11 +165,14 @@ namespace Umbraco.Web.Search } } } - + } static void MemberCacheRefresherCacheUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -215,6 +220,9 @@ namespace Umbraco.Web.Search /// static void MediaCacheRefresherCacheUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -252,13 +260,13 @@ namespace Umbraco.Web.Search if (media1 != null) { ReIndexForMedia(media1, media1.Trashed == false); - } + } break; case MediaCacheRefresher.OperationType.Trashed: - + //keep if trashed for indexes supporting unpublished //(delete the index from all indexes not supporting unpublished content) - + DeleteIndexForEntity(payload.Id, true); //We then need to re-index this item for all indexes supporting unpublished content @@ -272,20 +280,20 @@ namespace Umbraco.Web.Search case MediaCacheRefresher.OperationType.Deleted: //permanently remove from all indexes - + DeleteIndexForEntity(payload.Id, false); break; default: throw new ArgumentOutOfRangeException(); - } - } + } + } } break; - case MessageType.RefreshByInstance: - case MessageType.RemoveByInstance: - case MessageType.RefreshAll: + case MessageType.RefreshByInstance: + case MessageType.RemoveByInstance: + case MessageType.RefreshAll: default: //We don't support these, these message types will not fire for media break; @@ -302,6 +310,9 @@ namespace Umbraco.Web.Search /// static void PublishedPageCacheRefresherCacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -312,8 +323,8 @@ namespace Umbraco.Web.Search } break; case MessageType.RemoveById: - - //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). + + //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). var c2 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); if (c2 != null) @@ -368,6 +379,9 @@ namespace Umbraco.Web.Search /// static void UnpublishedPageCacheRefresherCacheUpdated(UnpublishedPageCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -378,9 +392,9 @@ namespace Umbraco.Web.Search } break; case MessageType.RemoveById: - + // This is triggered when the item is permanently deleted - + DeleteIndexForEntity((int)e.MessageObject, false); break; case MessageType.RefreshByInstance: @@ -399,7 +413,7 @@ namespace Umbraco.Web.Search { DeleteIndexForEntity(c4.Id, false); } - break; + break; case MessageType.RefreshByJson: var jsonPayloads = UnpublishedPageCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); @@ -409,29 +423,28 @@ namespace Umbraco.Web.Search { switch (payload.Operation) { - case UnpublishedPageCacheRefresher.OperationType.Deleted: + case UnpublishedPageCacheRefresher.OperationType.Deleted: //permanently remove from all indexes - + DeleteIndexForEntity(payload.Id, false); break; default: throw new ArgumentOutOfRangeException(); - } - } + } + } } break; - case MessageType.RefreshAll: + case MessageType.RefreshAll: default: //We don't support these, these message types will not fire for unpublished content break; } } - private static void ReIndexForMember(IMember member) { ExamineManager.Instance.ReIndexNode( @@ -447,7 +460,7 @@ namespace Umbraco.Web.Search /// /// /// - + private static void IndexerDocumentWriting(object sender, DocumentWritingEventArgs e) { if (e.Fields.Keys.Contains("nodeName")) @@ -463,7 +476,7 @@ namespace Umbraco.Web.Search )); } } - + private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) { var xml = sender.ToXml(); @@ -497,7 +510,7 @@ namespace Umbraco.Web.Search //if keepIfUnpublished == true then only delete this item from indexes not supporting unpublished content, // otherwise if keepIfUnpublished == false then remove from all indexes - + .Where(x => keepIfUnpublished == false || x.SupportUnpublishedContent == false) .Where(x => x.EnableDefaultEventHandler)); } @@ -518,7 +531,7 @@ namespace Umbraco.Web.Search ExamineManager.Instance.ReIndexNode( xml, IndexTypes.Content, ExamineManager.Instance.IndexProviderCollection.OfType() - + //Index this item for all indexers if the content is published, otherwise if the item is not published // then only index this for indexers supporting unpublished content @@ -531,10 +544,10 @@ namespace Umbraco.Web.Search /// /// /// true if data is going to be returned from cache - /// + /// [Obsolete("This method is no longer used and will be removed from the core in future versions, the cacheOnly parameter has no effect. Use the other ToXDocument overload instead")] public static XDocument ToXDocument(Content node, bool cacheOnly) - { + { return ToXDocument(node); } @@ -542,7 +555,7 @@ namespace Umbraco.Web.Search /// Converts a content node to Xml /// /// - /// + /// private static XDocument ToXDocument(Content node) { if (TypeHelper.IsTypeAssignableFrom(node)) @@ -561,7 +574,7 @@ namespace Umbraco.Web.Search if (xNode.Attributes["nodeTypeAlias"] == null) { - //we'll add the nodeTypeAlias ourselves + //we'll add the nodeTypeAlias ourselves XmlAttribute d = xDoc.CreateAttribute("nodeTypeAlias"); d.Value = node.ContentType.Alias; xNode.Attributes.Append(d); @@ -569,6 +582,5 @@ namespace Umbraco.Web.Search return new XDocument(ExamineXmlExtensions.ToXElement(xNode)); } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Suspendable.cs b/src/Umbraco.Web/Suspendable.cs new file mode 100644 index 0000000000..29e8c3a540 --- /dev/null +++ b/src/Umbraco.Web/Suspendable.cs @@ -0,0 +1,116 @@ +using System; +using System.Diagnostics; +using Examine; +using Examine.Providers; +using Umbraco.Core; +using Umbraco.Web.Cache; + +namespace Umbraco.Web +{ + public static class Suspendable + { + public static class PageCacheRefresher + { + private static bool _tried, _suspended; + + public static bool CanRefreshDocumentCacheFromDatabase + { + get + { + // trying a full refresh + if (_suspended == false) return true; + _tried = true; // remember we tried + return false; + } + } + + public static bool CanUpdateDocumentCache + { + get + { + // trying a partial update + // ok if not suspended, or if we haven't done a full already + return _suspended == false || _tried == false; + } + } + + public static void SuspendDocumentCache() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (PageCacheRefresher), "Suspend document cache."); + _suspended = true; + } + + public static void ResumeDocumentCache() + { + _suspended = false; + + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (PageCacheRefresher), string.Format("Resume document cache (reload:{0}).", _tried ? "true" : "false")); + + if (_tried == false) return; + _tried = false; + + var pageRefresher = CacheRefreshersResolver.Current.GetById(new Guid(DistributedCache.PageCacheRefresherId)); + pageRefresher.RefreshAll(); + } + } + + public static class ExamineEvents + { + private static bool _tried, _suspended; + + public static bool CanIndex + { + get + { + if (_suspended == false) return true; + _tried = true; // remember we tried + return false; + } + } + + public static void SuspendIndexers() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ExamineEvents), "Suspend indexers."); + _suspended = true; + } + + public static void ResumeIndexers() + { + _suspended = false; + + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ExamineEvents), string.Format("Resume indexers (rebuild:{0}).", _tried ? "true" : "false")); + + if (_tried == false) return; + _tried = false; + + // fixme - could we fork this on a background thread? + foreach (BaseIndexProvider indexer in ExamineManager.Instance.IndexProviderCollection) + { + indexer.RebuildIndex(); + } + } + } + + public static class ScheduledPublishing + { + private static bool _suspended; + + public static bool CanRun + { + get { return _suspended == false; } + } + + public static void Suspend() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ScheduledPublishing), "Suspend scheduled publishing."); + _suspended = true; + } + + public static void Resume() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ScheduledPublishing), "Resume scheduled publishing."); + _suspended = false; + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ea3a0dfa7c..f8d6f9080e 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -435,6 +435,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 54bb1ac2f1..a7d40fa088 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; From b02b2a61f038b1f2e0564b6a9025df0fe7634148 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 7 Sep 2017 12:16:04 +0200 Subject: [PATCH 53/64] perfs - deploy context isCancelled --- src/Umbraco.Core/Deploy/IDeployContext.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Core/Deploy/IDeployContext.cs b/src/Umbraco.Core/Deploy/IDeployContext.cs index 7d4066e015..531ed9dae4 100644 --- a/src/Umbraco.Core/Deploy/IDeployContext.cs +++ b/src/Umbraco.Core/Deploy/IDeployContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; namespace Umbraco.Core.Deploy { @@ -38,5 +39,10 @@ namespace Umbraco.Core.Deploy /// The key of the item. /// The item with the specified key and type, if any, else null. T Item(string key) where T : class; + + ///// + ///// Gets the global deployment cancellation token. + ///// + //CancellationToken CancellationToken { get; } } } \ No newline at end of file From d6f8b878d29264d30f55782fb7552cd213ca33e7 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 7 Sep 2017 13:45:56 +0200 Subject: [PATCH 54/64] perfs - cleanup --- src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 6fcdf2fcf3..879f74bccd 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -822,9 +822,6 @@ namespace Umbraco.Web.Cache { var handler = FindHandler(e); if (handler == null) continue; - - ApplicationContext.Current.ProfilingLogger.Logger.Info("Handling " + e.Sender + " " + e.EventName); - handler.Invoke(null, new[] { e.Sender, e.Args }); } } @@ -833,7 +830,7 @@ namespace Umbraco.Web.Cache if (tempContext != null) tempContext.Dispose(); } - + } /// From 6ea92daa70cc834cc8e33409f5f404794de025b9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 7 Sep 2017 23:03:01 +1000 Subject: [PATCH 55/64] fixes merge removes unused file --- src/Umbraco.Core/Models/UmbracoObjectTypes.cs | 2 +- src/Umbraco.Web.UI/Umbraco/js/install.js | 30 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 src/Umbraco.Web.UI/Umbraco/js/install.js diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 4bcbcdc804..c7ad5eb2aa 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -193,7 +193,7 @@ namespace Umbraco.Core.Models [UmbracoObjectType(Constants.ObjectTypes.DocumentBlueprint, typeof(IContent))] [FriendlyName("DocumentBlueprint")] [UmbracoUdiType(Constants.UdiEntityType.DocumentBluePrint)] - DocumentBlueprint + DocumentBlueprint, /// /// Reserved Identifier diff --git a/src/Umbraco.Web.UI/Umbraco/js/install.js b/src/Umbraco.Web.UI/Umbraco/js/install.js deleted file mode 100644 index 41aab12c80..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/js/install.js +++ /dev/null @@ -1,30 +0,0 @@ -yepnope({ - - load: [ - 'lib/jquery/jquery-2.0.3.min.js', - - /* 1.1.5 */ - 'lib/angular/1.1.5/angular.min.js', - 'lib/angular/1.1.5/angular-cookies.min.js', - 'lib/angular/1.1.5/angular-mobile.min.js', - 'lib/angular/1.1.5/angular-mocks.js', - 'lib/angular/1.1.5/angular-sanitize.min.js', - 'lib/underscore/underscore.js', - 'js/umbraco.servervariables.js', - 'js/app.dev.js' - ], - - complete: function () { - jQuery(document).ready(function () { - - angular.module('umbraco.install', [ - 'umbraco.resources', - 'umbraco.services', - 'umbraco.httpbackend', - 'ngMobile' - ]); - - angular.bootstrap(document, ['umbraco.install']); - }); - } -}); \ No newline at end of file From a5c78f632560834097750430004cb737c7bd4d00 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 7 Sep 2017 20:27:52 +0200 Subject: [PATCH 56/64] fix bower.json after merge - copy correct clipboard.js file --- src/Umbraco.Web.UI.Client/bower.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 05d9db6f21..53ea30f9d5 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -30,7 +30,7 @@ "angular-local-storage": "~0.2.3", "moment": "~2.10.3", "ace-builds": "^1.2.3", - "font-awesome": "~4.2" + "font-awesome": "~4.2", "clipboard": "1.7.1" }, @@ -47,6 +47,7 @@ "sources": { "moment": "bower_components/moment/min/moment-with-locales.js", + "underscore": [ "bower_components/underscore/underscore-min.js", "bower_components/underscore/underscore-min.map" @@ -78,7 +79,10 @@ "ng-file-upload":"bower_components/ng-file-upload/ng-file-upload.min.js", "jquery-ui":"bower_components/jquery-ui/jquery-ui.min.js", - "jquery-migrate":"bower_components/jquery-migrate/jquery-migrate.min.js" + + "jquery-migrate":"bower_components/jquery-migrate/jquery-migrate.min.js", + + "clipboard": "bower_components/clipboard/dist/clipboard.min.js" } } } From 7003ef8d26b860b5db7e82ad0c0867683422bb57 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 7 Sep 2017 20:42:12 +0200 Subject: [PATCH 57/64] Remove circular dependency --- src/Umbraco.Core/Umbraco.Core.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 77e84da945..8854c3c4ab 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -134,9 +134,6 @@ - - ..\Umbraco.Web.UI\Bin\umbraco.dll - From 96c9ecbdd738e83edb5884596e66014c42aa4509 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 7 Sep 2017 21:39:41 +0200 Subject: [PATCH 58/64] fix calc functions --- src/Umbraco.Web.UI.Client/src/less/application/grid.less | 2 +- src/Umbraco.Web.UI.Client/src/less/canvas-designer.less | 2 +- src/Umbraco.Web.UI.Client/src/less/components/overlays.less | 2 +- src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less | 2 +- src/Umbraco.Web.UI.Client/src/less/sections.less | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/application/grid.less b/src/Umbraco.Web.UI.Client/src/less/application/grid.less index 8348b072b1..f6879bb679 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -125,7 +125,7 @@ body { @media (max-width: 500px) { #search-form .form-search { - width: ~"(calc(~'100%' - ~'80px'))"; + width: calc(100% - 80px); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index e7aa9d859f..095027d86b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -14,7 +14,7 @@ body { overflow: hidden; height: 100%; width: 100%; - width: ~"(calc(~'100%' - ~'80px'))"; // 80px is the fixed left menu for toggling the different browser sizes + width: calc(100% - 80px); // 80px is the fixed left menu for toggling the different browser sizes position: absolute; padding: 0; margin: 0; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index c7e91c1a44..0a8dfe6f71 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -155,7 +155,7 @@ @media (max-width: 500px) { .umb-overlay.umb-overlay-left { margin-left: 41px; - width: ~"(calc(~'100%' - ~'41px'))"; + width: calc(100% - 41px); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index e053d58a7f..1ca6bf6067 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -329,7 +329,7 @@ a.umb-package-details__back-link { .umb-package-details__main-content { flex: 1 1 auto; margin-right: 40px; - width: ~"(calc(~'100%' - ~'@{sidebarwidth}' - ~'40px'))"; // Make sure that the main content area doesn't gets affected by inline styling + width: calc(~'100%' - ~'@{sidebarwidth}' - ~'40px'); // Make sure that the main content area doesn't gets affected by inline styling } .umb-package-details__sidebar { diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index d73e24e4ea..85a33c84b8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -126,7 +126,7 @@ ul.sections li.help { bottom: 0; left: 0; display: block; - width: ~"(calc(~'100%' - ~'5px'))"; //subtract 4px orange border + 1px border-right for sections + width: calc(100% - 5px); //subtract 4px orange border + 1px border-right for sections } ul.sections li.help a { From dad4eafebda1340f92e24f01aeeee893bd882dc1 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 7 Sep 2017 22:25:45 +0200 Subject: [PATCH 59/64] Enable RDP to figure out gulp build problem --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index dc6e22edbf..4c9a33fd23 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,9 @@ version: '{build}' shallow_clone: true + +init: + - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + build_script: - cmd: >- SET SLN=%CD% From 8981fd23a8524dde9af55154d0a877677cbe0b9d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Sep 2017 11:41:49 +1000 Subject: [PATCH 60/64] makes new classes internal --- src/Umbraco.Core/Persistence/LocalDb.cs | 2 +- src/Umbraco.Web/Suspendable.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/LocalDb.cs b/src/Umbraco.Core/Persistence/LocalDb.cs index 084122ac3a..f706979e17 100644 --- a/src/Umbraco.Core/Persistence/LocalDb.cs +++ b/src/Umbraco.Core/Persistence/LocalDb.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence /// Studio which can be used to connect to LocalDB databases. /// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. /// - public class LocalDb + internal class LocalDb { private int _version; private bool _hasVersion; diff --git a/src/Umbraco.Web/Suspendable.cs b/src/Umbraco.Web/Suspendable.cs index 29e8c3a540..db4ef53485 100644 --- a/src/Umbraco.Web/Suspendable.cs +++ b/src/Umbraco.Web/Suspendable.cs @@ -7,7 +7,7 @@ using Umbraco.Web.Cache; namespace Umbraco.Web { - public static class Suspendable + internal static class Suspendable { public static class PageCacheRefresher { From 0713f12ba6abfdf5c23b2b86afb88568e9f392dd Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Sep 2017 12:02:20 +1000 Subject: [PATCH 61/64] makes new classes internal --- src/Umbraco.Core/Persistence/LocalDb.cs | 2 +- src/Umbraco.Web/Suspendable.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/LocalDb.cs b/src/Umbraco.Core/Persistence/LocalDb.cs index 084122ac3a..f706979e17 100644 --- a/src/Umbraco.Core/Persistence/LocalDb.cs +++ b/src/Umbraco.Core/Persistence/LocalDb.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence /// Studio which can be used to connect to LocalDB databases. /// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. /// - public class LocalDb + internal class LocalDb { private int _version; private bool _hasVersion; diff --git a/src/Umbraco.Web/Suspendable.cs b/src/Umbraco.Web/Suspendable.cs index 29e8c3a540..db4ef53485 100644 --- a/src/Umbraco.Web/Suspendable.cs +++ b/src/Umbraco.Web/Suspendable.cs @@ -7,7 +7,7 @@ using Umbraco.Web.Cache; namespace Umbraco.Web { - public static class Suspendable + internal static class Suspendable { public static class PageCacheRefresher { From 1d5348763d633d3381d4f3d12126ebd6866a6061 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Sep 2017 13:15:03 +1000 Subject: [PATCH 62/64] Fixes up invite message, fixes reset password quirk, ensures password values are cleared after changing password, ensures that the confirm password value is not posted to the server --- .../users/changepassword.directive.js | 29 +++---- .../common/resources/currentuser.resource.js | 75 ++++++++++--------- .../services/umbdataformatter.service.js | 3 +- .../src/views/common/dialogs/login.html | 4 +- .../common/overlays/user/user.controller.js | 6 +- .../components/users/change-password.html | 18 +++-- .../umbraco/config/lang/en_us.xml | 1 + 7 files changed, 73 insertions(+), 63 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js index f2834e4a36..d9a531c382 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js @@ -17,6 +17,8 @@ } */ + $scope.showReset = false; + //set defaults if they are not available if ($scope.config.disableToggle === undefined) { $scope.config.disableToggle = false; @@ -36,13 +38,13 @@ if ($scope.config.minPasswordLength === undefined) { $scope.config.minPasswordLength = 0; } - + //set the model defaults if (!angular.isObject($scope.passwordValues)) { //if it's not an object then just create a new one $scope.passwordValues = { newPassword: null, - oldPassword: null, + oldPassword: null, reset: null, answer: null }; @@ -61,11 +63,11 @@ //the value to compare to match passwords if (!isNew) { - $scope.confirm = ""; + $scope.passwordValues.confirm = ""; } else if ($scope.passwordValues.newPassword && $scope.passwordValues.newPassword.length > 0) { //if it is new and a new password has been set, then set the confirm password too - $scope.confirm = $scope.passwordValues.newPassword; + $scope.passwordValues.confirm = $scope.passwordValues.newPassword; } } @@ -86,6 +88,7 @@ $scope.changing = true; //if there was a previously generated password displaying, clear it $scope.passwordValues.generatedPassword = null; + $scope.passwordValues.confirm = null; }; $scope.cancelChange = function () { @@ -120,25 +123,13 @@ unsubscribe[u](); } }); - - $scope.showReset = function () { - return $scope.config.enableReset; - }; - + $scope.showOldPass = function () { return $scope.config.hasPassword && !$scope.config.allowManuallyChangingPassword && - !$scope.config.enablePasswordRetrieval && !$scope.passwordValues.reset; + !$scope.config.enablePasswordRetrieval && !$scope.showReset; }; - - $scope.showNewPass = function () { - return !$scope.passwordValues.reset; - }; - - $scope.showConfirmPass = function () { - return !$scope.passwordValues.reset; - }; - + //TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this directive $scope.showCancelBtn = function () { return $scope.config.disableToggle !== true && $scope.config.hasPassword; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js index fadbb88bb7..5ac6591c42 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js @@ -7,46 +7,49 @@ **/ function currentUserResource($q, $http, umbRequestHelper) { - //the factory object returned - return { + //the factory object returned + return { - performSetInvitedUserPassword: function (newPassword) { + performSetInvitedUserPassword: function (newPassword) { - if (!newPassword) { - return angularHelper.rejectedPromise({ errorMsg: 'newPassword cannot be empty' }); - } + if (!newPassword) { + return angularHelper.rejectedPromise({ errorMsg: 'newPassword cannot be empty' }); + } - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "currentUserApiBaseUrl", - "PostSetInvitedUserPassword"), - angular.toJson(newPassword)), - 'Failed to change password'); - }, + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "PostSetInvitedUserPassword"), + angular.toJson(newPassword)), + 'Failed to change password'); + }, - /** - * @ngdoc method - * @name umbraco.resources.currentUserResource#changePassword - * @methodOf umbraco.resources.currentUserResource - * - * @description - * Changes the current users password - * - * @returns {Promise} resourcePromise object containing the user array. - * - */ - changePassword: function (changePasswordArgs) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "currentUserApiBaseUrl", - "PostChangePassword"), - changePasswordArgs), - 'Failed to change password'); - } - - }; + /** + * @ngdoc method + * @name umbraco.resources.currentUserResource#changePassword + * @methodOf umbraco.resources.currentUserResource + * + * @description + * Changes the current users password + * + * @returns {Promise} resourcePromise object containing the user array. + * + */ + changePassword: function (changePasswordArgs) { + + changePasswordArgs = _.omit(changePasswordArgs, "confirm"); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "PostChangePassword"), + changePasswordArgs), + 'Failed to change password'); + } + + }; } angular.module('umbraco.resources').factory('currentUserResource', currentUserResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 75c7b02d55..525d920b09 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -82,6 +82,7 @@ //create the save model from the display model var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message', 'changePassword'); + saveModel.changePassword = _.omit(saveModel.changePassword, "confirm"); //make sure the userGroups are just a string array var currGroups = saveModel.userGroups; @@ -221,7 +222,7 @@ }); saveModel.email = propEmail.value; saveModel.username = propLogin.value; - saveModel.password = propPass.value; + saveModel.password = _.omit(propPass.value, "confirm"); var selectedGroups = []; for (var n in propGroups.value) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 3af0c30a08..58701f20f0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -13,7 +13,9 @@

Hi, {{invitedUser.name}}

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non libero vel turpis ultrices pharetra.

+

+ Welcome to Umbraco! Just need to get your password and avatar setup and then you're good to go +

- + + no-dirty-check + ng-change="$parent.$parent.showReset = !$parent.$parent.showReset"/> @@ -33,7 +34,7 @@ - + - - +
+ +
 
+    {{config | json}}   
+    
+
+    {{passwordValues | json}} 
+    
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 70e738ffeb..9f42eb7035 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1662,6 +1662,7 @@ To manage your website, simply open the Umbraco back office and start adding con User groups has been invited An invitation has been sent to the new user with details about how to log in to Umbraco. + Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. Writer Translator Change From c39e1175fd6d5327fcea65f2a8c286c0d7fd8de2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Sep 2017 13:43:38 +1000 Subject: [PATCH 63/64] Fixes up some more password changing quirks --- .../common/resources/currentuser.resource.js | 9 ++++--- .../services/umbdataformatter.service.js | 27 +++++++++++++++++-- .../components/users/change-password.html | 6 ----- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js index 5ac6591c42..233d510c3e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js @@ -5,7 +5,7 @@ * * **/ -function currentUserResource($q, $http, umbRequestHelper) { +function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) { //the factory object returned return { @@ -38,8 +38,11 @@ function currentUserResource($q, $http, umbRequestHelper) { */ changePassword: function (changePasswordArgs) { - changePasswordArgs = _.omit(changePasswordArgs, "confirm"); - + changePasswordArgs = umbDataFormatter.formatChangePasswordModel(changePasswordArgs); + if (!changePasswordArgs) { + throw 'No password data to change'; + } + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 525d920b09..5fc2416927 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -7,8 +7,30 @@ * @description A helper object used to format/transform JSON Umbraco data, mostly used for persisting data to the server **/ function umbDataFormatter() { + return { + formatChangePasswordModel: function(model) { + if (!model) { + return null; + } + var trimmed = _.omit(model, ["confirm", "generatedPassword"]) + + //ensure that the pass value is null if all child properties are null + var allNull = true; + var vals = _.values(trimmed); + for (var k = 0; k < vals.length; k++) { + if (vals[k] !== null && vals[k] !== undefined) { + allNull = false; + } + } + if (allNull) { + return null; + } + + return trimmed; + }, + formatContentTypePostData: function (displayModel, action) { //create the save model from the display model @@ -82,7 +104,7 @@ //create the save model from the display model var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message', 'changePassword'); - saveModel.changePassword = _.omit(saveModel.changePassword, "confirm"); + saveModel.changePassword = this.formatChangePasswordModel(saveModel.changePassword); //make sure the userGroups are just a string array var currGroups = saveModel.userGroups; @@ -222,7 +244,8 @@ }); saveModel.email = propEmail.value; saveModel.username = propLogin.value; - saveModel.password = _.omit(propPass.value, "confirm"); + + saveModel.password = this.formatChangePasswordModel(propPass.value); var selectedGroups = []; for (var n in propGroups.value) { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index 82d5594474..34c5db4d3c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -67,10 +67,4 @@
-
 
-    {{config | json}}   
-    
-
-    {{passwordValues | json}} 
-    
From 38c961209ef880fff45a40470390d8dc8771c516 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Sep 2017 13:48:32 +1000 Subject: [PATCH 64/64] Ensure the user's administratively reset password doesn't get re-shown in the UI - that was based on an actual password reset --- src/Umbraco.Web/Editors/PasswordChanger.cs | 4 ++-- src/Umbraco.Web/Editors/UsersController.cs | 12 ++---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index a6edd1bdd0..88e92c0ad2 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -98,8 +98,8 @@ namespace Umbraco.Web.Editors _logger.Warn(string.Format("Could not reset user password {0}", errors)); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, errors: " + errors, new[] { "resetPassword" }) }); } - - return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass }); + + return Attempt.Succeed(new PasswordChangedModel()); } //we're not resetting it so we need to try to change it. diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index b3ea8b1f1d..63484d1cec 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -553,8 +553,7 @@ namespace Umbraco.Web.Editors { userSave.Username = userSave.Email; } - - var resetPasswordValue = string.Empty; + if (userSave.ChangePassword != null) { var passwordChanger = new PasswordChanger(Logger, Services.UserService); @@ -562,9 +561,6 @@ namespace Umbraco.Web.Editors var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, found, userSave.ChangePassword, UserManager); if (passwordChangeResult.Success) { - //depending on how the provider is configured, the password may be reset so let's store that for later - resetPasswordValue = passwordChangeResult.Result.ResetPassword; - //need to re-get the user found = Services.UserService.GetUserById(intId.Result); } @@ -588,11 +584,7 @@ namespace Umbraco.Web.Editors Services.UserService.Save(user); var display = Mapper.Map(user); - - //re-map the password reset value (if any) - if (resetPasswordValue.IsNullOrWhiteSpace() == false) - display.ResetPasswordValue = resetPasswordValue; - + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/operationSavedHeader"), Services.TextService.Localize("speechBubbles/editUserSaved")); return display; }