1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-25 08:09:43 +02:00

Merge pull request #86 from documize/keycloak

Keycloak integration
This commit is contained in:
Harvey Kandola 2017-03-22 13:46:09 +00:00 committed by GitHub
commit 68107a4ea1
177 changed files with 6274 additions and 2035 deletions

View file

@ -8,7 +8,7 @@ The mission is to bring software dev inspired features (refactoring, testing, li
## Latest version
v0.43.1
v0.44.0
## OS Support
@ -18,7 +18,7 @@ v0.43.1
## Tech stack
- EmberJS (v2.11.2)
- EmberJS (v2.12.0)
- Go (v1.8)
- MySQL (v5.7.10+) or Percona (v5.7.16-10+)
@ -28,12 +28,18 @@ v0.43.1
- [Install for development guide](https://developers.documize.com/s/VzO9ZqMOCgABGyfW/installation/d/V16LOMucxwABhZF1/install-documize-for-development-guide)
- [Configuration guide](https://developers.documize.com/s/VzO9ZqMOCgABGyfW/installation/d/VzSL8cVZ4QAB2B4Y/configure-documize-guide)
## Keycloak Integration
Documize provides out-of-the-box integration with [Redhat Keycloak](http://www.keycloak.org) for open source identity and access management.
## Auth0 Integration
Documize is compatible with Auth0 identity as a service.
[![JWT Auth for open source projects](https://cdn.auth0.com/oss/badges/a0-badge-dark.png)](https://auth0.com/?utm_source=oss&utm_medium=gp&utm_campaign=oss)
Open Source Identity and Access Management
## Word Conversion to HTML
- [Code for `wordconvert` utility](https://github.com/documize/community/tree/master/cmd/wordconvert)

37
app/.eslintrc.js Normal file
View file

@ -0,0 +1,37 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 6,
sourceType: 'module'
},
extends: 'eslint:recommended',
env: {
browser: true,
jquery: true,
qunit: true,
embertest: true
},
rules: {
},
globals: {
"$": true,
"is": true,
"_": true,
"tinymce": true,
"CodeMirror": true,
"Drop": true,
"Mousetrap": true,
"Sortable": true,
"moment": true,
"Dropzone": true,
"Tooltip": true,
"server": true,
"authenticateUser": true,
"stubAudit": true,
"stubUserNotification": true,
"userLogin": true,
"Keycloak": true,
"Intercom": true,
"slug": true
}
};

5
app/.gitignore vendored
View file

@ -1,7 +1,8 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# See https://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/dist-prod
/tmp
# dependencies
@ -13,5 +14,5 @@
/connect.lock
/coverage/*
/libpeerconnection.log
npm-debug.log
npm-debug.log*
testem.log

View file

@ -1,51 +0,0 @@
{
"predef": [
"server",
"document",
"window",
"-Promise",
"moment",
"$",
"jQuery",
"_",
"is",
"Mousetrap",
"CodeMirror",
"Intercom",
"Materialize",
"tinymce",
"Tether",
"Tooltip",
"Drop",
"Dropzone",
"Sortable",
"datetimepicker",
"Waypoint",
"velocity"
],
"browser": true,
"boss": true,
"curly": true,
"debug": false,
"devel": true,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esnext": true,
"unused": true
}

View file

@ -1,21 +1,22 @@
---
language: node_js
node_js:
- "0.12"
- "6"
sudo: false
cache:
directories:
- node_modules
- $HOME/.npm
- $HOME/.cache # includes bowers cache
before_install:
- export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH
- "npm config set spin false"
- "npm install -g npm@^2"
- npm config set spin false
- npm install -g bower phantomjs-prebuilt
- bower --version
- phantomjs --version
install:
- npm install -g bower
- npm install
- bower install

View file

@ -21,15 +21,16 @@ const {
} = Ember;
export default Base.extend({
ajax: service(),
appMeta: service(),
localStorage: service(),
restore(data) {
// TODO: verify authentication data
if (data) {
return resolve(data);
}
return reject();
},
@ -51,16 +52,13 @@ export default Base.extend({
return Ember.RSVP.reject("invalid");
}
var headers = {
'Authorization': 'Basic ' + encoded
};
let headers = { 'Authorization': 'Basic ' + encoded };
return this.get('ajax').post('public/authenticate', {
headers
});
return this.get('ajax').post('public/authenticate', { headers });
},
invalidate() {
this.get('localStorage').clearAll();
return resolve();
}
});

View file

@ -0,0 +1,54 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import Base from 'ember-simple-auth/authenticators/base';
import netUtil from '../utils/net';
const {
isPresent,
RSVP: { resolve, reject },
inject: { service }
} = Ember;
export default Base.extend({
ajax: service(),
appMeta: service(),
kcAuth: service(),
localStorage: service(),
restore(data) {
// TODO: verify authentication data
if (data) {
return resolve(data);
}
return reject();
},
authenticate(data) {
data.domain = netUtil.getSubdomain();
if (!isPresent(data.token) || !isPresent(data.email)) {
return Ember.RSVP.reject("invalid");
}
return this.get('ajax').post('public/authenticate/keycloak', {
data: JSON.stringify(data),
contentType: 'json'
});
},
invalidate() {
this.get('localStorage').clearAll();
return this.get('kcAuth').logout();
}
});

View file

@ -0,0 +1,119 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import constants from '../../utils/constants';
import encoding from '../../utils/encoding';
const {
computed
} = Ember;
export default Ember.Component.extend({
appMeta: Ember.inject.service(),
isDocumizeProvider: computed.equal('authProvider', constants.AuthProvider.Documize),
isKeycloakProvider: computed.equal('authProvider', constants.AuthProvider.Keycloak),
KeycloakUrlError: computed.empty('keycloakConfig.url'),
KeycloakRealmError: computed.empty('keycloakConfig.realm'),
KeycloakClientIdError: computed.empty('keycloakConfig.clientId'),
KeycloakPublicKeyError: computed.empty('keycloakConfig.publicKey'),
KeycloakAdminUserError: computed.empty('keycloakConfig.adminUser'),
KeycloakAdminPasswordError: computed.empty('keycloakConfig.adminPassword'),
keycloakConfig: {
url: '',
realm: '',
clientId: '',
publicKey: '',
adminUser: '',
adminPassword: ''
},
didReceiveAttrs() {
this._super(...arguments);
let provider = this.get('authProvider');
switch (provider) {
case constants.AuthProvider.Documize:
// nothing to do
break;
case constants.AuthProvider.Keycloak: // eslint-disable-line no-case-declarations
let config = this.get('authConfig');
if (is.undefined(config) || is.null(config) || is.empty(config) ) {
config = {};
} else {
config = JSON.parse(config);
config.publicKey = encoding.Base64.decode(config.publicKey);
}
this.set('keycloakConfig', config);
break;
}
},
actions: {
onDocumize() {
this.set('authProvider', constants.AuthProvider.Documize);
},
onKeycloak() {
this.set('authProvider', constants.AuthProvider.Keycloak);
},
onSave() {
let provider = this.get('authProvider');
let config = this.get('authConfig');
switch (provider) {
case constants.AuthProvider.Documize:
config = {};
break;
case constants.AuthProvider.Keycloak:
if (this.get('KeycloakUrlError')) {
this.$("#keycloak-url").focus();
return;
}
if (this.get('KeycloakRealmError')) {
this.$("#keycloak-realm").focus();
return;
}
if (this.get('KeycloakClientIdError')) {
this.$("#keycloak-clientId").focus();
return;
}
if (this.get('KeycloakPublicKeyError')) {
this.$("#keycloak-publicKey").focus();
return;
}
if (this.get('KeycloakAdminUserError')) {
this.$("#keycloak-admin-user").focus();
return;
}
if (this.get('KeycloakAdminPasswordError')) {
this.$("#keycloak-admin-password").focus();
return;
}
config = Ember.copy(this.get('keycloakConfig'));
Ember.set(config, 'publicKey', encoding.Base64.encode(this.get('keycloakConfig.publicKey')));
break;
}
this.get('onSave')(provider, config).then(() => {
});
},
onSync() {
this.get('onSync')();
}
}
});

View file

@ -10,8 +10,9 @@
// https://documize.com
import Ember from 'ember';
import AuthProvider from '../../mixins/auth';
export default Ember.Component.extend({
export default Ember.Component.extend(AuthProvider, {
editUser: null,
deleteUser: null,
drop: null,

View file

@ -10,6 +10,7 @@
// https://documize.com
import Ember from 'ember';
import AuthProvider from '../../mixins/auth';
const {
isEmpty,
@ -18,7 +19,7 @@ const {
get
} = Ember;
export default Ember.Component.extend({
export default Ember.Component.extend(AuthProvider, {
newUser: { firstname: "", lastname: "", email: "", active: true },
firstnameEmpty: computed.empty('newUser.firstname'),
lastnameEmpty: computed.empty('newUser.lastname'),

View file

@ -22,18 +22,27 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
editMode: false,
docName: '',
docExcerpt: '',
hasNameError: computed.empty('docName'),
hasExcerptError: computed.empty('docExcerpt'),
keyUp(e) {
if (e.keyCode === 27) { // escape key
this.send('onCancel');
}
},
actions: {
toggleEdit() {
this.set('docName', this.get('document.name'));
this.set('docExcerpt', this.get('document.excerpt'));
this.set('editMode', true);
Ember.run.schedule('afterRender', () => {
$('#document-name').select();
});
},
onSaveDocument() {
onSave() {
if (this.get('hasNameError') || this.get('hasExcerptError')) {
return;
}
@ -46,7 +55,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
this.set('editMode', false);
},
cancel() {
onCancel() {
this.set('editMode', false);
}
}

View file

@ -21,9 +21,17 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
didReceiveAttrs() {
this._super(...arguments);
if (this.get('isDestroyed') || this.get('isDestroying')) {
return;
}
let page = this.get('page');
this.get('documentService').getPageMeta(page.get('documentId'), page.get('id')).then((meta) => {
if (this.get('isDestroyed') || this.get('isDestroying')) {
return;
}
this.set('meta', meta);
if (this.get('toEdit') === this.get('page.id') && this.get('isEditor')) {
this.send('onEdit');

View file

@ -23,7 +23,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
appMeta: Ember.inject.service(),
link: Ember.inject.service(),
hasPages: computed.notEmpty('pages'),
newSectionName: '',
newSectionName: 'Section',
newSectionNameMissing: computed.empty('newSectionName'),
newSectionLocation: '',
beforePage: '',
@ -34,7 +34,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
this.loadBlocks();
Ember.run.schedule('afterRender', () => {
Ember.run.schedule('afterRender', () => {
let jumpTo = "#page-" + this.get('pageId');
if (!$(jumpTo).inView()) {
$(jumpTo).velocity("scroll", { duration: 250, offset: -100 });
@ -51,13 +51,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
didInsertElement() {
this._super(...arguments);
$(".start-section:not(.start-section-empty-state)").hoverIntent({interval: 100, over: function() {
// in
$(this).find('.start-button').velocity("transition.slideDownIn", {duration: 300});
}, out: function() {
// out
$(this).find('.start-button').velocity("transition.slideUpOut", {duration: 300});
} });
this.setupAddWizard();
let self = this;
$(".tooltipped").each(function(i, el) {
@ -67,6 +61,8 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
willDestroyElement() {
this._super(...arguments);
$('.start-section:not(.start-section-empty-state)').off('.hoverIntent');
this.destroyTooltips();
},
@ -105,6 +101,20 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
});
},
setupAddWizard() {
Ember.run.schedule('afterRender', () => {
$('.start-section:not(.start-section-empty-state)').off('.hoverIntent');
$('.start-section:not(.start-section-empty-state)').hoverIntent({interval: 100, over: function() {
// in
$(this).find('.start-button').velocity("transition.slideDownIn", {duration: 300});
}, out: function() {
// out
$(this).find('.start-button').velocity("transition.slideUpOut", {duration: 300});
} });
});
},
addSection(model) {
// calculate sequence of page (position in document)
let sequence = 0;
@ -172,10 +182,10 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
},
onSavePage(page, meta) {
this.set('toEdit', '');
this.attrs.onSavePage(page, meta);
},
// Section wizard related
onShowSectionWizard(page) {
if (is.undefined(page)) {
page = { id: '0' };
@ -200,7 +210,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
}
$("#new-section-wizard").insertAfter(`#add-section-button-${page.id}`);
$("#new-section-wizard").velocity("transition.slideDownIn", {duration: 300, complete:
$("#new-section-wizard").velocity("transition.slideDownIn", { duration: 300, complete:
function() {
$("#new-section-name").focus();
}});
@ -209,6 +219,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
onHideSectionWizard() {
this.set('newSectionLocation', '');
this.set('beforePage', null);
$("#new-section-wizard").insertAfter('#wizard-placeholder');
$("#new-section-wizard").velocity("transition.slideUpOut", { duration: 300 });
},
@ -251,6 +262,8 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
} else {
this.set('toEdit', '');
}
this.setupAddWizard();
});
},
@ -289,12 +302,8 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
const promise = this.addSection(model);
promise.then((id) => {
this.set('pageId', id);
// if (model.page.pageType === 'section') {
// this.set('toEdit', id);
// } else {
// this.set('toEdit', '');
// }
this.setupAddWizard();
});
},

View file

@ -15,8 +15,8 @@ export default Ember.Component.extend({
selectedDocuments: [],
emptyState: Ember.computed('documents', function() {
return this.get('documents.length') === 0;
}),
return this.get('documents.length') === 0;
}),
didReceiveAttrs() {
this.set('selectedDocuments', []);

View file

@ -10,12 +10,13 @@
// https://documize.com
import Ember from 'ember';
import AuthMixin from '../../mixins/auth';
const {
inject: { service }
} = Ember;
export default Ember.Component.extend({
export default Ember.Component.extend(AuthMixin, {
folderService: service('folder'),
appMeta: service(),
users: [],
@ -103,7 +104,7 @@ export default Ember.Component.extend({
message = this.getDefaultInvitationMessage();
}
this.get('permissions').forEach((permission, index) => { /* jshint ignore:line */
this.get('permissions').forEach((permission, index) => { // eslint-disable-line no-unused-vars
Ember.set(permission, 'canView', $("#canView-" + permission.userId).prop('checked'));
Ember.set(permission, 'canEdit', $("#canEdit-" + permission.userId).prop('checked'));
});

View file

@ -12,12 +12,13 @@
import Ember from 'ember';
import NotifierMixin from '../../mixins/notifier';
import TooltipMixin from '../../mixins/tooltip';
import AuthMixin from '../../mixins/auth';
const {
computed
} = Ember;
export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
export default Ember.Component.extend(NotifierMixin, TooltipMixin, AuthMixin, {
folderService: Ember.inject.service('folder'),
session: Ember.inject.service(),
appMeta: Ember.inject.service(),
@ -37,7 +38,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
didReceiveAttrs() {
this.set('isFolderOwner', this.get('folder.userId') === this.get("session.user.id"));
let show = this.get('isFolderOwner') || this.get('hasSelectedDocuments') || this.get('folderService').get('canEditCurrentFolder');
let show = this.get('session.authenticated') || this.get('isFolderOwner') || this.get('hasSelectedDocuments') || this.get('folderService').get('canEditCurrentFolder');
this.set('showToolbar', show);
let targets = _.reject(this.get('folders'), {

View file

@ -64,7 +64,7 @@ export default Ember.Component.extend(NotifierMixin, {
});
this.on("error", function (x) {
console.log("Conversion failed for ", x.name, " obj ", x); // TODO proper error handling
console.log("Conversion failed for ", x.name, " obj ", x); // eslint-disable-line no-console
});
this.on("queuecomplete", function () {});

View file

@ -77,7 +77,7 @@ export default Ember.Component.extend(TooltipMixin, {
self.get('pinned').updateSequence(this.toArray()).then((pins) => {
self.set('pins', pins);
});
}
}
});
this.set('sortable', sortable);

View file

@ -19,8 +19,12 @@ const {
export default Ember.Component.extend({
drop: null,
busy: false,
mousetrap: null,
hasNameError: computed.empty('page.title'),
containerId: Ember.computed('page', function () {
let page = this.get('page');
return `base-editor-inline-container-${page.id}`;
}),
pageId: Ember.computed('page', function () {
let page = this.get('page');
return `page-editor-${page.id}`;
@ -43,16 +47,20 @@ export default Ember.Component.extend({
}),
didRender() {
let self = this;
Mousetrap.bind('esc', function () {
self.send('onCancel');
let msContainer = document.getElementById(this.get('containerId'));
let mousetrap = new Mousetrap(msContainer);
mousetrap.bind('esc', () => {
this.send('onCancel');
return false;
});
Mousetrap.bind(['ctrl+s', 'command+s'], function () {
self.send('onAction');
mousetrap.bind(['ctrl+s', 'command+s'], () => {
this.send('onAction');
return false;
});
this.set('mousetrap', mousetrap);
$('#' + this.get('pageId')).focus(function() {
$(this).select();
});
@ -65,8 +73,11 @@ export default Ember.Component.extend({
drop.destroy();
}
Mousetrap.unbind('esc');
Mousetrap.unbind(['ctrl+s', 'command+s']);
let mousetrap = this.get('mousetrap');
if (is.not.null(mousetrap)) {
mousetrap.unbind('esc');
mousetrap.unbind(['ctrl+s', 'command+s']);
}
},
actions: {

View file

@ -28,7 +28,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
try {
config = JSON.parse(this.get('meta.config'));
} catch (e) {}
} catch (e) {} // eslint-disable-line no-empty
if (is.empty(config)) {
config = {
@ -57,8 +57,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
if (response.apikey.length > 0 && response.url.length > 0 && response.username.length > 0) {
self.send('auth');
}
}, function (reason) { //jshint ignore: line
console.log(reason);
}, function (reason) { // eslint-disable-line no-unused-vars
self.set('waiting', false);
if (self.get('config.userId') > 0) {
self.send('auth');
@ -96,7 +95,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
});
});
self.set('waiting', false);
}, function (reason) { //jshint ignore: line
}, function (reason) { // eslint-disable-line no-unused-vars
self.set('workspaces', []);
self.set('waiting', false);
});
@ -116,7 +115,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
self.set('items', response);
self.set('config.itemCount', response.length);
self.set('waiting', false);
}, function (reason) { //jshint ignore: line
}, function (reason) { // eslint-disable-line no-unused-vars
if (self.get('isDestroyed') || self.get('isDestroying')) {
return;
}
@ -187,7 +186,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
self.set('config.userId', response.BaseEntity.id);
self.set('waiting', false);
self.getWorkspaces();
}, function (reason) { //jshint ignore: line
}, function (reason) { // eslint-disable-line no-unused-vars
self.set('authenticated', false);
self.set('user', null);
self.set('config.userId', 0);

View file

@ -56,7 +56,8 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
config.showMilestones = metaConfig.showMilestones;
config.showIssues = metaConfig.showIssues;
config.showCommits = metaConfig.showCommits;
} catch (e) {}
} catch (e) { // eslint-disable-line no-empty
}
if (_.isUndefined(config.showCommits)) {
config.showCommits = true;
@ -75,24 +76,24 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
.then(function () {
self.send('authStage2');
}, function (error) { //jshint ignore: line
console.log(error);
console.log(error); // eslint-disable-line no-console
self.send('auth');
});
} else {
if (config.userId !== self.get("session.session.authenticated.user.id")) {
console.log("github auth wrong user ID, switching");
console.log("github auth wrong user ID, switching"); // eslint-disable-line no-console
self.set('config.userId', self.get("session.session.authenticated.user.id"));
}
self.get('sectionService').fetch(page, "checkAuth", self.get('config'))
.then(function () {
self.send('authStage2');
}, function (error) { //jshint ignore: line
console.log(error);
}, function (error) {
console.log(error); // eslint-disable-line no-console
self.send('auth'); // require auth if the db token is invalid
});
}
}, function (error) { //jshint ignore: line
console.log(error);
}, function (error) {
console.log(error); // eslint-disable-line no-console
});
}
},
@ -163,11 +164,11 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
self.set('config.lists', lists);
self.set('busy', false);
}, function (error) { //jshint ignore: line
}, function (error) {
self.set('busy', false);
self.set('authenticated', false);
self.showNotification("Unable to fetch repositories");
console.log(error);
console.log(error); // eslint-disable-line no-console
});
},
@ -201,11 +202,11 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
self.set('busy', false);
self.set('owners', owners);
self.getOwnerLists();
}, function (error) { //jshint ignore: line
}, function (error) {
self.set('busy', false);
self.set('authenticated', false);
self.showNotification("Unable to fetch owners");
console.log(error);
console.log(error); // eslint-disable-line no-console
});
},
@ -253,7 +254,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
meta.set('rawBody', JSON.stringify(response));
self.set('busy', false);
self.attrs.onAction(page, meta);
}, function (reason) { //jshint ignore: line
}, function (reason) { // eslint-disable-line no-unused-vars
self.set('busy', false);
self.attrs.onAction(page, meta);
});

View file

@ -28,7 +28,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
try {
config = JSON.parse(this.get('meta.config'));
} catch (e) {}
} catch (e) {} // eslint-disable-line no-empty
if (is.empty(config)) {
config = {
@ -90,19 +90,19 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
if (is.not.undefined(group)) {
Ember.set(config, 'group', group);
}
}, function (reason) { //jshint ignore: line
}, function (reason) {
self.set('authenticated', false);
self.set('waiting', false);
self.set('config.APIToken', ''); // clear the api token
self.displayError(reason);
console.log("get options call failed");
console.log("get options call failed"); // eslint-disable-line no-console
});
}, function (reason) { //jshint ignore: line
}, function (reason) {
self.set('authenticated', false);
self.set('waiting', false);
self.set('config.APIToken', ''); // clear the api token
self.displayError(reason);
console.log("auth token invalid");
console.log("auth token invalid"); // eslint-disable-line no-console
});
},
@ -176,7 +176,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
self.set('waiting', false);
self.attrs.onAction(page, meta);
}, function (reason) { //jshint ignore: line
}, function (reason) { // eslint-disable-line no-unused-vars
self.set('authenticated', false);
self.set('waiting', false);
self.showNotification(`Something went wrong, try again!`);

View file

@ -43,7 +43,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
try {
config = JSON.parse(this.get('meta.config'));
} catch (e) {}
} catch (e) {} // eslint-disable-line no-empty
if (is.empty(config)) {
config = {
@ -77,8 +77,8 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
Trello.deauthorize();
});
}
}, function (error) { //jshint ignore: line
console.log(error);
}, function (error) {
console.log(error); // eslint-disable-line no-console
});
},
@ -132,7 +132,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
self.set('busy', false);
self.set('authenticated', false);
self.showNotification("Unable to fetch board lists");
console.log(error);
console.log(error); // eslint-disable-line no-console
});
},
@ -181,7 +181,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
Trello.members.get("me", function (user) {
self.set('config.user', user);
}, function (error) {
console.log(error);
console.log(error); // eslint-disable-line no-console
});
self.get('sectionService').fetch(page, "boards", self.get('config'))
@ -193,14 +193,14 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
self.set('busy', false);
self.set('authenticated', false);
self.showNotification("Unable to fetch boards");
console.log(error);
console.log(error); // eslint-disable-line no-console
});
},
error: function (error) {
self.set('busy', false);
self.set('authenticated', false);
self.showNotification("Unable to authenticate");
console.log(error);
console.log(error); // eslint-disable-line no-console
}
});
});
@ -233,7 +233,7 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
meta.set('rawBody', JSON.stringify(response));
self.set('busy', false);
self.attrs.onAction(page, meta);
}, function (reason) { //jshint ignore: line
}, function (reason) { // eslint-disable-line no-unused-vars
self.set('busy', false);
self.attrs.onAction(page, meta);
});

View file

@ -54,16 +54,16 @@ export default Ember.Component.extend({
}
},
codesample_languages: [
{text: 'HTML/XML', value: 'markup'},
{text: 'JavaScript', value: 'javascript'},
{text: 'CSS', value: 'css'},
{text: 'PHP', value: 'php'},
{text: 'Ruby', value: 'ruby'},
{text: 'Python', value: 'python'},
{text: 'Java', value: 'java'},
{text: 'C', value: 'c'},
{text: 'C#', value: 'csharp'},
{text: 'C++', value: 'cpp'}],
{text: 'HTML/XML', value: 'markup'},
{text: 'JavaScript', value: 'javascript'},
{text: 'CSS', value: 'css'},
{text: 'PHP', value: 'php'},
{text: 'Ruby', value: 'ruby'},
{text: 'Python', value: 'python'},
{text: 'Java', value: 'java'},
{text: 'C', value: 'c'},
{text: 'C#', value: 'csharp'},
{text: 'C++', value: 'cpp'}],
extended_valid_elements: "b,i,b/strong,i/em",
plugins: [
'advlist autolink lists link image charmap print preview hr anchor pagebreak',

View file

@ -17,7 +17,8 @@ export default Ember.Component.extend({
prompt: null,
optionValuePath: 'id',
optionLabelPath: 'name',
action: Ember.K, // action to fire on change
action() {}, // action to fire on change
// action: Ember.K, // action to fire on change
// shadow the passed-in `selection` to avoid
// leaking changes to it via a 2-way binding

View file

@ -10,6 +10,7 @@
// https://documize.com
import Ember from 'ember';
import AuthProvider from '../mixins/auth';
const {
computed,
@ -18,7 +19,7 @@ const {
isPresent
} = Ember;
export default Ember.Component.extend({
export default Ember.Component.extend(AuthProvider, {
password: { password: "", confirmation: "" },
hasFirstnameError: computed.empty('model.firstname'),
hasLastnameError: computed.empty('model.lastname'),
@ -44,6 +45,10 @@ export default Ember.Component.extend({
}
}),
didReceiveAttrs() {
this.set
},
actions: {
save() {
let password = this.get('password.password');

View file

@ -48,7 +48,6 @@ export default Ember.Component.extend(NotifierMixin, {
},
addFolder() {
console.log("adding folder!");
return true;
}
}

View file

@ -34,11 +34,10 @@ export function documentFileIcon(params) {
case "html":
html = "html.png";
break;
break;
case "css":
html = "css.png";
break;
break;
case "bat":
case "sh":

25
app/app/mixins/auth.js Normal file
View file

@ -0,0 +1,25 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import constants from '../utils/constants';
export default Ember.Mixin.create({
appMeta: Ember.inject.service(),
isAuthProviderDocumize: true,
IsAuthProviderKeycloak: false,
init() {
this._super(...arguments);
this.set('isAuthProviderDocumize', this.get('appMeta.authProvider') === constants.AuthProvider.Documize);
this.set('isAuthProviderKeycloak', this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak);
}
});

View file

@ -10,10 +10,27 @@
// https://documize.com
import Ember from 'ember';
import constants from '../../../utils/constants';
export default Ember.Route.extend({
appMeta: Ember.inject.service(),
beforeModel() {
if (this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak) {
this.transitionTo('auth.login');
}
},
setupController(controller, model) {
controller.set('model', model);
controller.set('sayThanks', false);
}
},
activate() {
$('body').addClass('background-color-off-white');
},
deactivate() {
$('body').removeClass('background-color-off-white');
}
});

View file

@ -0,0 +1,14 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
export default Ember.Controller.extend({});

View file

@ -0,0 +1,61 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import constants from '../../../utils/constants';
export default Ember.Route.extend({
session: Ember.inject.service(),
appMeta: Ember.inject.service(),
kcAuth: Ember.inject.service(),
localStorage: Ember.inject.service(),
queryParams: {
mode: {
refreshModel: true
}
},
message: '',
beforeModel(transition) {
return new Ember.RSVP.Promise((resolve) => {
this.set('mode', is.not.undefined(transition.queryParams.mode) ? transition.queryParams.mode : 'reject');
if (this.get('mode') === 'reject' || this.get('appMeta.authProvider') !== constants.AuthProvider.Keycloak) {
resolve();
}
this.get('kcAuth').fetchProfile().then((profile) => {
let data = this.get('kcAuth').mapProfile(profile);
this.get("session").authenticate('authenticator:keycloak', data).then(() => {
this.get('audit').record("logged-in-keycloak");
this.transitionTo('folders');
}, (reject) => {
this.set('message', reject.Error);
this.set('mode', 'reject');
resolve();
});
}, (reject) => {
this.set('mode', 'reject');
this.set('message', reject);
resolve();
});
});
},
model() {
return {
mode: this.get('mode'),
message: this.get('message')
}
}
});

View file

@ -0,0 +1,13 @@
{{#if (is-equal model.mode 'login')}}
<div class="sso-box">
<p>Authenticating with Keycloak...</p>
<img src="/assets/img/busy-gray.gif" />
</div>
{{/if}}
{{#if (is-equal model.mode 'reject')}}
<div class="sso-box">
<p>Keycloak authentication failure</p>
<p>{{model.message}}</p>
</div>
{{/if}}

View file

@ -10,39 +10,37 @@
// https://documize.com
import Ember from 'ember';
import AuthProvider from '../../../mixins/auth';
export default Ember.Controller.extend({
email: "",
password: "",
invalidCredentials: false,
export default Ember.Controller.extend(AuthProvider, {
appMeta: Ember.inject.service('app-meta'),
session: Ember.inject.service('session'),
audit: Ember.inject.service('audit'),
invalidCredentials: false,
reset() {
this.setProperties({
email: "",
password: ""
email: '',
password: ''
});
let dbhash = document.head.querySelector("[property=dbhash]").content;
if (dbhash.length > 0 && dbhash !== "{{.DBhash}}") {
this.transitionToRoute('setup');
}
},
actions: {
login() {
let creds = this.getProperties('email', 'password');
this.get('session').authenticate('authenticator:documize', creds)
.then((response) => {
this.get('audit').record("logged-in");
this.transitionToRoute('folders');
return response;
}).catch(() => {
this.set('invalidCredentials', true);
});
this.get('session').authenticate('authenticator:documize', creds).then((response) => {
this.get('audit').record("logged-in");
this.transitionToRoute('folders');
return response;
}).catch(() => {
this.set('invalidCredentials', true);
});
}
}
});

View file

@ -10,11 +10,58 @@
// https://documize.com
import Ember from 'ember';
import constants from '../../../utils/constants';
export default Ember.Route.extend({
appMeta: Ember.inject.service(),
kcAuth: Ember.inject.service(),
localStorage: Ember.inject.service(),
showLogin: false,
beforeModel(transition) {
return new Ember.RSVP.Promise((resolve) => {
let authProvider = this.get('appMeta.authProvider');
switch (authProvider) {
case constants.AuthProvider.Keycloak:
this.set('showLogin', false);
this.get('kcAuth').login().then(() => {
this.transitionTo('auth.keycloak', { queryParams: { mode: 'login' }});
resolve();
}, (reject) => {
transition.abort();
console.log (reject); // eslint-disable-line no-console
this.transitionTo('auth.keycloak', { queryParams: { mode: 'reject' }});
});
break;
default:
this.set('showLogin', true);
resolve();
break;
}
});
},
model() {
return {
showLogin: this.get('showLogin')
};
},
setupController: function (controller, model) {
controller.set('model', model);
controller.reset();
this.browser.setTitleAsPhrase("Login");
},
activate() {
$('body').addClass('background-color-off-white');
},
deactivate() {
$('body').removeClass('background-color-off-white');
}
});

View file

@ -1,23 +1,27 @@
<div class="auth-box">
<div class="logo">
<img src="/assets/img/logo-color.png" title="Documize" alt="Documize" class="responsive-img" />
{{#if model.showLogin}}
<div class="auth-box">
<div class="logo">
<img src="/assets/img/logo-color.png" title="Documize" alt="Documize" class="responsive-img" />
</div>
<div class="login-form">
<form id="login-form" {{action 'login' on="submit"}}>
<div class="input-control">
<label>Email</label>
{{focus-input type="email" value=email id="authEmail"}}
</div>
<div class="input-control">
<label>Password</label>
{{input type="password" value=password id="authPassword"}}
</div>
<div class="clearfix" />
<div class="margin-top-10 margin-bottom-20">
<button type="submit" class="regular-button button-blue">Sign in</button>
<span class="{{unless invalidCredentials "hide"}} color-red margin-left-20">Invalid credentials</span>
</div>
{{#if isAuthProviderDocumize}}
{{#link-to 'auth.forgot'}}Forgot your password?{{/link-to}}
{{/if}}
</form>
</div>
</div>
<div class="login-form">
<form id="login-form" {{action 'login' on="submit"}}>
<div class="input-control">
<label>Email</label>
{{focus-input type="email" value=email id="authEmail"}}
</div>
<div class="input-control">
<label>Password</label>
{{input type="password" value=password id="authPassword"}}
</div>
<div class="clearfix" />
<div class="margin-top-10 margin-bottom-20">
<button type="submit" class="regular-button button-blue">Sign in</button>
<span class="{{unless invalidCredentials "hide"}} color-red margin-left-20">Invalid credentials</span>
</div>
{{#link-to 'auth.forgot'}}Forgot your password?{{/link-to}}
</form>
</div>
</div>
{{/if}}

View file

@ -17,18 +17,19 @@ export default Ember.Route.extend({
appMeta: Ember.inject.service(),
activate: function () {
this.get('session').invalidate();
this.audit.record("logged-in");
this.audit.record("logged-out");
this.audit.stop();
if (config.environment === 'test') {
this.transitionTo('auth.login');
} else {
if (this.get("appMeta.allowAnonymousAccess")) {
this.transitionTo('folders');
} else {
this.get('session').invalidate().then(() => {
if (config.environment === 'test') {
this.transitionTo('auth.login');
} else {
if (this.get("appMeta.allowAnonymousAccess")) {
this.transitionTo('folders');
} else {
this.transitionTo('auth.login');
}
}
}
});
}
});

View file

@ -1 +1,4 @@
{{outlet}}
<div class="sso-box">
<p>Logging out...</p>
<img src="/assets/img/busy-gray.gif" />
</div>

View file

@ -15,4 +15,12 @@ export default Ember.Route.extend({
model: function (params) {
return params.token;
},
activate() {
$('body').addClass('background-color-off-white');
},
deactivate() {
$('body').removeClass('background-color-off-white');
}
});

View file

@ -24,5 +24,13 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, {
controller.set('serial', this.serial);
controller.set('slug', this.slug);
controller.set('folderId', this.folderId);
}
},
activate() {
$('body').addClass('background-color-off-white');
},
deactivate() {
$('body').removeClass('background-color-off-white');
}
});

View file

@ -20,7 +20,6 @@ export default Ember.Route.extend({
this.transitionTo('folders');
}, () => {
this.transitionTo('auth.login');
console.log(">>>>> Documize SSO failure");
});
},
});

View file

@ -0,0 +1,58 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import NotifierMixin from "../../../mixins/notifier";
import constants from '../../../utils/constants';
export default Ember.Controller.extend(NotifierMixin, {
global: Ember.inject.service(),
appMeta: Ember.inject.service(),
session: Ember.inject.service(),
handleProviderChange(data) {
this.get('session').logout();
this.set('appMeta.authProvider', data.authProvider);
this.set('appMeta.authConfig', data.authConfig);
window.location.href= '/';
},
actions: {
onSave(provider, config) {
if(this.get('session.isGlobalAdmin')) {
let data = { authProvider: provider, authConfig: JSON.stringify(config) };
return this.get('global').saveAuthConfig(data).then(() => {
this.showNotification('Saved');
if (provider !== this.get('appMeta.authProvider')) {
if (provider === constants.AuthProvider.Keycloak) {
this.get('global').syncExternalUsers().then(() => {
this.handleProviderChange(data);
});
} else {
this.handleProviderChange(data);
}
} else {
this.set('appMeta.authProvider', provider);
this.set('appMeta.authConfig', config);
}
});
}
},
onSync() {
return this.get('global').syncExternalUsers().then((response) => {
this.showNotification(response.message);
});
}
}
});

View file

@ -0,0 +1,48 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import constants from '../../../utils/constants';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
appMeta: Ember.inject.service(),
session: Ember.inject.service(),
global: Ember.inject.service(),
beforeModel() {
if (!this.get("session.isGlobalAdmin")) {
this.transitionTo('auth.login');
}
},
model() {
let data = {
authProvider: this.get('appMeta.authProvider'),
authConfig: null,
};
switch (data.authProvider) {
case constants.AuthProvider.Keycloak:
data.authConfig = this.get('appMeta.authConfig');
break;
case constants.AuthProvider.Documize:
data.authConfig = '';
break;
}
return data;
},
activate() {
document.title = "Authentication | Documize";
}
});

View file

@ -0,0 +1 @@
{{customize/auth-settings authProvider=model.authProvider authConfig=model.authConfig onSave=(action 'onSave') onSync=(action 'onSync')}}

View file

@ -1 +1 @@
{{general-settings model=model save=(action 'save')}}
{{customize/general-settings model=model save=(action 'save')}}

View file

@ -1 +1 @@
{{global-settings model=model saveSMTP=(action 'saveSMTP') saveLicense=(action 'saveLicense')}}
{{customize/global-settings model=model saveSMTP=(action 'saveSMTP') saveLicense=(action 'saveLicense')}}

View file

@ -9,6 +9,7 @@
{{#link-to 'customize.users' activeClass='selected' class="option" tagName="li"}}Users{{/link-to}}
{{#if session.isGlobalAdmin}}
{{#link-to 'customize.global' activeClass='selected' class="option" tagName="li"}}Global{{/link-to}}
{{#link-to 'customize.auth' activeClass='selected' class="option" tagName="li"}}Authentication{{/link-to}}
{{/if}}
</ul>
</div>

View file

@ -11,9 +11,12 @@
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import constants from '../../../utils/constants';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
userService: Ember.inject.service('user'),
global: Ember.inject.service('global'),
appMeta: Ember.inject.service(),
beforeModel: function () {
if (!this.session.isAdmin) {
@ -21,8 +24,20 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, {
}
},
model: function () {
return this.get('userService').getAll();
model() {
return new Ember.RSVP.Promise((resolve) => {
if (this.get('appMeta.authProvider') == constants.AuthProvider.Keycloak) {
this.get('global').syncExternalUsers().then(() => {
this.get('userService').getComplete().then((users) =>{
resolve(users);
});
});
} else {
this.get('userService').getComplete().then((users) =>{
resolve(users);
});
}
});
},
activate: function () {

View file

@ -1,5 +1,5 @@
{{user-settings add=(action 'add')}}
{{customize/user-settings add=(action 'add')}}
<div class="clearfix" />
{{settings/user-list users=model onDelete=(action "onDelete") onSave=(action "onSave") onPassword=(action "onPassword")}}
{{customize/user-list users=model onDelete=(action "onDelete") onSave=(action "onSave") onPassword=(action "onPassword")}}

View file

@ -20,8 +20,8 @@ export default Ember.Controller.extend(NotifierMixin, {
pages: [],
toggled: false,
queryParams: ['pageId', 'tab'],
pageId: '',
tab: 'index',
pageId: '',
tab: 'index',
actions: {
toggleSidebar() {
@ -113,11 +113,7 @@ export default Ember.Controller.extend(NotifierMixin, {
}
this.set('model.pages', _.sortBy(pages, "sequence"));
this.transitionToRoute('document.index',
this.get('model.folder.id'),
this.get('model.folder.slug'),
this.get('model.document.id'),
this.get('model.document.slug'));
this.get('target.router').refresh();
});
} else {
// page delete followed by re-leveling child pages

View file

@ -65,8 +65,6 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, {
actions: {
error(error /*, transition*/ ) {
console.log(error);
if (error) {
this.transitionTo('/not-found');
return false;

View file

@ -7,6 +7,6 @@
{{#layout/zone-content}}
{{folder/folder-toolbar folders=model.folders folder=model.folder hasSelectedDocuments=hasSelectedDocuments onDeleteDocument=(action
'onDeleteDocument') onMoveDocument=(action 'onMoveDocument')}} {{folder/documents-list documents=model.documents folder=model.folder
isFolderOwner=isFolderOwner onDocumentsChecked=(action 'onDocumentsChecked') }}
'onDeleteDocument') onMoveDocument=(action 'onMoveDocument')}}
{{folder/documents-list documents=model.documents folder=model.folder isFolderOwner=isFolderOwner onDocumentsChecked=(action 'onDocumentsChecked') }}
{{/layout/zone-content}}

View file

@ -10,8 +10,9 @@
// https://documize.com
import Ember from 'ember';
import AuthMixin from '../../mixins/auth';
export default Ember.Controller.extend({
export default Ember.Controller.extend(AuthMixin, {
tabGeneral: false,
tabShare: false,
tabPermissions: false,

View file

@ -81,7 +81,7 @@ export default Ember.Route.extend(NotifierMixin, {
folderPermissions.pushObject(u);
this.get('folderService').getPermissions(model.id).then((permissions) => {
permissions.forEach((permission, index) => { /* jshint ignore:line */
permissions.forEach((permission, index) => { // eslint-disable-line no-unused-vars
var folderPermission = folderPermissions.findBy('userId', permission.get('userId'));
if (is.not.undefined(folderPermission)) {
Ember.setProperties(folderPermission, {

View file

@ -7,7 +7,9 @@
<div class="sidebar-menu">
<ul class="options">
<li class="option {{if tabGeneral "selected"}}" {{action 'selectTab' 'tabGeneral'}}>General</li>
<li class="option {{if tabShare "selected"}}" {{action 'selectTab' 'tabShare'}}>Share</li>
{{#if isAuthProviderDocumize}}
<li class="option {{if tabShare "selected"}}" {{action 'selectTab' 'tabShare'}}>Share</li>
{{/if}}
<li class="option {{if tabPermissions "selected"}}" {{action 'selectTab' 'tabPermissions'}}>Permissions</li>
<li class="option {{if tabDelete "selected"}}" {{action 'selectTab' 'tabDelete'}}>Delete</li>
</ul>

View file

@ -26,9 +26,8 @@ export default Ember.Controller.extend(NotifierMixin, {
}).then(() => {
var credentials = Encoding.Base64.encode(":" + this.model.email + ":" + this.model.password);
window.location.href = "/auth/sso/" + encodeURIComponent(credentials);
}).catch((error) => {
}).catch((error) => { // eslint-disable-line no-unused-vars
// TODO notify user of the error within the GUI
console.log("Something went wrong attempting database creation, see server log: " + error);
});
}
}

View file

@ -58,18 +58,28 @@ export default Router.map(function () {
this.route('global', {
path: 'global'
});
this.route('auth', {
path: 'auth'
});
});
this.route('setup', {
path: 'setup'
});
this.route('secure', {
path: 'secure/:token'
});
this.route('auth', {
path: 'auth'
}, function () {
this.route('sso', {
path: 'sso/:token'
});
this.route('keycloak', {
path: 'keycloak'
});
this.route('login', {
path: 'login'
});

View file

@ -24,8 +24,12 @@ export default Ember.Route.extend(ApplicationRouteMixin, TooltipMixin, {
pinned: service(),
beforeModel(transition) {
this._super(...arguments);
return this.get('appMeta').boot(transition.targetName).then(data => {
if (this.get('session.session.authenticator') !== "authenticator:documize" && data.allowAnonymousAccess) {
if (this.get('session.session.authenticator') !== "authenticator:documize" &&
this.get('session.session.authenticator') !== "authenticator:keycloak" &&
data.allowAnonymousAccess) {
return this.get('session').authenticate('authenticator:anonymous', data);
}
@ -41,8 +45,8 @@ export default Ember.Route.extend(ApplicationRouteMixin, TooltipMixin, {
error(error, transition) {
if (error) {
console.log(error);
console.log(transition);
console.log(error); // eslint-disable-line no-console
console.log(transition); // eslint-disable-line no-console
if (netUtil.isAjaxAccessError(error)) {
localStorage.clear();
@ -50,8 +54,7 @@ export default Ember.Route.extend(ApplicationRouteMixin, TooltipMixin, {
}
}
// Return true to bubble this event to any parent route.
return true;
return true; // bubble this event to any parent route
}
}
});

View file

@ -11,6 +11,7 @@
import Ember from 'ember';
import config from '../config/environment';
import constants from '../utils/constants';
const {
String: { htmlSafe },
@ -21,7 +22,7 @@ const {
export default Ember.Service.extend({
ajax: service(),
localStorage: service(),
kcAuth: service(),
endpoint: `${config.apiHost}/${config.apiNamespace}`,
orgId: '',
title: '',
@ -30,6 +31,8 @@ export default Ember.Service.extend({
edition: 'Community',
valid: true,
allowAnonymousAccess: false,
authProvider: constants.AuthProvider.Documize,
authConfig: null,
setupMode: false,
invalidLicense() {
@ -40,7 +43,7 @@ export default Ember.Service.extend({
return [this.get('endpoint'), endpoint].join('/');
},
boot(requestedUrl) { // jshint ignore:line
boot(requestedUrl) { // eslint-disable-line no-unused-vars
let dbhash;
if (is.not.null(document.head.querySelector("[property=dbhash]"))) {
dbhash = document.head.querySelector("[property=dbhash]").content;
@ -59,6 +62,18 @@ export default Ember.Service.extend({
return resolve(this);
}
if (requestedUrl === 'secure') {
this.setProperties({
title: htmlSafe("Secure document viewing"),
allowAnonymousAccess: true,
setupMode: true
});
this.get('localStorage').clearAll();
return resolve(this);
}
return this.get('ajax').request('public/meta').then((response) => {
this.setProperties(response);
return response;

View file

@ -63,5 +63,25 @@ export default Ember.Service.extend({
data: license
});
}
}
},
// Saves auth config for Documize instance.
saveAuthConfig(config) {
if(this.get('sessionService.isGlobalAdmin')) {
return this.get('ajax').request(`global/auth`, {
method: 'PUT',
data: JSON.stringify(config)
});
}
},
syncExternalUsers() {
if(this.get('sessionService.isGlobalAdmin')) {
return this.get('ajax').request(`users/sync`, {
method: 'GET'
}).then((response) => {
return response;
});
}
},
});

106
app/app/services/kc-auth.js Normal file
View file

@ -0,0 +1,106 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import netUtil from '../utils/net';
const {
inject: { service }
} = Ember;
export default Ember.Service.extend({
sessionService: service('session'),
audit: service(),
ajax: service(),
appMeta: service(),
keycloak: null,
config: {},
boot() {
return new Ember.RSVP.Promise((resolve, reject) => {
if (is.not.undefined(this.get('keycloak')) && is.not.null(this.get('keycloak')) ) {
resolve(this.get('keycloak'));
return;
}
let keycloak = new Keycloak(JSON.parse(this.get('appMeta.authConfig')));
this.set('keycloak', keycloak);
keycloak.onTokenExpired = function () {
keycloak.clearToken();
};
keycloak.onAuthRefreshError = function () {
keycloak.clearToken();
};
this.get('keycloak').init().success(() => {
this.get('audit').record("initialized-keycloak");
resolve(this.get('keycloak'));
}).error((err) => {
reject(err);
});
});
},
login() {
return new Ember.RSVP.Promise((resolve, reject) => {
this.boot().then((keycloak) => {
let url = netUtil.getAppUrl(netUtil.getSubdomain()) + '/auth/keycloak?mode=login';
keycloak.login({redirectUri: url}).success(() => {
return resolve();
}).error(() => {
return reject(new Error('login failed'));
});
});
});
},
logout() {
return new Ember.RSVP.Promise((resolve, reject) => {
this.boot().then((keycloak) => {
keycloak.logout(JSON.parse(this.get('appMeta.authConfig'))).success(() => {
this.get('keycloak').clearToken();
resolve();
}).error((error) => {
this.get('keycloak').clearToken();
reject(error);
});
});
});
},
fetchProfile() {
return new Ember.RSVP.Promise((resolve, reject) => {
this.boot().then((keycloak) => {
keycloak.loadUserProfile().success((profile) => {
resolve(profile);
}).error((err) => {
reject(err);
});
});
});
},
mapProfile(profile) {
return {
domain: '',
token: this.get('keycloak').token,
remoteId: is.null(profile.id) || is.undefined(profile.id) ? profile.email: profile.id,
email: is.null(profile.email) || is.undefined(profile.email) ? '': profile.email,
username: is.null(profile.username) || is.undefined(profile.username) ? '': profile.username,
firstname: is.null(profile.firstName) || is.undefined(profile.firstName) ? profile.username: profile.firstName,
lastname: is.null(profile.lastName) || is.undefined(profile.lastName) ? profile.username: profile.lastName,
enabled: is.null(profile.enabled) || is.undefined(profile.enabled) ? true: profile.enabled
};
}
});

View file

@ -13,14 +13,14 @@ import Ember from 'ember';
export default Ember.Service.extend({
action: function(entry) {
console.log(entry);
console.log(entry); // eslint-disable-line no-console
},
error: function(entry) {
console.log(entry);
console.log(entry); // eslint-disable-line no-console
},
info: function(entry) {
console.log(entry);
console.log(entry); // eslint-disable-line no-console
}
});

View file

@ -21,7 +21,9 @@ export default SimpleAuthSession.extend({
ajax: service(),
appMeta: service(),
store: service(),
localStorage: service(),
folderPermissions: null,
currentFolder: null,
isMac: false,
isMobile: false,
authenticated: computed('user.id', function () {
@ -40,7 +42,7 @@ export default SimpleAuthSession.extend({
return data.get('global');
}),
init: function () {
init() {
this._super(...arguments);
this.set('isMac', is.mac());
@ -55,6 +57,7 @@ export default SimpleAuthSession.extend({
}
}),
folderPermissions: null,
currentFolder: null
logout() {
this.get('localStorage').clearAll();
}
});

View file

@ -46,9 +46,9 @@ export default Ember.Service.extend({
});
},
// Returns all users for organization.
// Returns all active users for organization.
getAll() {
return this.get('ajax').request(`users`).then((response) => {
return this.get('ajax').request(`users?active=1`).then((response) => {
return response.map((obj) => {
let data = this.get('store').normalize('user', obj);
return this.get('store').push(data);
@ -56,6 +56,17 @@ export default Ember.Service.extend({
});
},
// Returns all active and inactive users for organization.
getComplete() {
return this.get('ajax').request(`users?active=0`).then((response) => {
return response.map((obj) => {
let data = this.get('store').normalize('user', obj);
return this.get('store').push(data);
});
});
},
// Returns all users that can see folder.
getFolderUsers(folderId) {
let url = `users/folder/${folderId}`;

View file

@ -62,5 +62,11 @@
font-size: 1.1rem;
margin-bottom: 30px;
}
.document-sidebar-form-wrapper {
padding: 20px;
border: 1px solid $color-stroke;
@include border-radius(3px);
}
}
}

View file

@ -64,6 +64,7 @@
padding: 25px 50px;
box-shadow: 0 0 0 0.75pt $color-stroke,0 0 3pt 0.75pt $color-stroke;
background-color: $color-white;
overflow-x: scroll;
&:hover {
.page-title {

View file

@ -1,5 +1,4 @@
.onboarding-container
{
.onboarding-container {
width: 100%;
text-align: left;
color: $color-off-black;

View file

@ -39,6 +39,8 @@
}
.round-button-mono {
@extend .no-select;
@include ease-in();
display: inline-block;
position: relative;
overflow: hidden;
@ -52,9 +54,8 @@
vertical-align: middle;
border: 1px solid $color-stroke;
text-align: center;
@extend .no-select;
@include ease-in();
font-size: 1.2rem;
background-color: transparent;
&:hover {
@extend .z-depth-tiny;

View file

@ -0,0 +1,53 @@
<form class="form-bordered">
<div class="form-header">
<div class="title">Authentication</div>
<div class="tip">Determine the method for user authentication</div>
</div>
<div class="input-control">
<label>Provider</label>
<div class="tip">External authentication servers, services must be accessible from the server running this Documize instance</div>
{{#ui/ui-radio selected=isDocumizeProvider onClick=(action 'onDocumize')}}Documize &mdash; email/password{{/ui/ui-radio}}
{{#ui/ui-radio selected=isKeycloakProvider onClick=(action 'onKeycloak')}}Keycloak &mdash; bring your own authentication server{{/ui/ui-radio}}
</div>
{{#if isKeycloakProvider}}
<div class="form-header">
<div class="title">Keycloak Configuration</div>
<div class="tip">Connection parameters &mdash; create a documize user in Master realm with 'manage-users' role against target realm</div>
</div>
<div class="input-control">
<label>Keycloak Server URL</label>
<div class="tip">e.g. http://localhost:8888/auth</div>
{{focus-input id="keycloak-url" type="text" value=keycloakConfig.url class=(if KeycloakUrlError 'error')}}
</div>
<div class="input-control">
<label>Keycloak Realm</label>
<div class="tip">e.g. main</div>
{{input id="keycloak-realm" type="text" value=keycloakConfig.realm class=(if keycloakRealmError 'error')}}
</div>
<div class="input-control">
<label>Keycloak OIDC Client ID</label>
<div class="tip">e.g. account</div>
{{input id="keycloak-clientId" type="text" value=keycloakConfig.clientId class=(if KeycloakClientIdError 'error')}}
</div>
<div class="input-control">
<label>Keycloak Realm Public Key</label>
<div class="tip">Copy the RSA public key from Realm Settings &rarr; Keys</div>
{{textarea id="keycloak-publicKey" type="text" value=keycloakConfig.publicKey rows=7 class=(if KeycloakPublicKeyError 'error')}}
</div>
<div class="input-control">
<label>Keycloak Username</label>
<div class="tip">Used to connect with Keycloak and sync users with Documize</div>
{{input id="keycloak-admin-user" type="text" value=keycloakConfig.adminUser class=(if KeycloakAdminUserError 'error')}}
</div>
<div class="input-control">
<label>Keycloak Password</label>
<div class="tip">Used to connect with Keycloak and sync users with Documize</div>
{{input id="keycloak-admin-password" type="password" value=keycloakConfig.adminPassword class=(if KeycloakAdminPasswordError 'error')}}
</div>
{{/if}}
<div class="regular-button button-blue" {{action 'onSave'}}>save</div>
<div class="button-gap" />
<div class="regular-button button-green" {{action 'onSync'}}>sync users</div>
</form>

View file

@ -91,22 +91,24 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="input-control">
<label>Password</label>
<div class="tip">Optional new password</div>
{{input id="edit-password" type="password" value=password.password}}
{{#if isAuthProviderDocumize}}
<div class="row">
<div class="col-md-6">
<div class="input-control">
<label>Password</label>
<div class="tip">Optional new password</div>
{{input id="edit-password" type="password" value=password.password}}
</div>
</div>
<div class="col-md-6">
<div class="input-control">
<label>Confirm Password</label>
<div class="tip">Confirm new password</div>
{{input id="edit-confirmPassword" type="password" value=password.confirmation}}
</div>
</div>
</div>
<div class="col-md-6">
<div class="input-control">
<label>Confirm Password</label>
<div class="tip">Confirm new password</div>
{{input id="edit-confirmPassword" type="password" value=password.confirmation}}
</div>
</div>
</div>
{{/if}}
</form>
</div>
<div class="actions">

View file

@ -0,0 +1,21 @@
{{#if isAuthProviderDocumize}}
<form class="form-bordered">
<div class="form-header">
<div class="title">Add user</div>
<div class="tip">New users receive an invitation email with a random password</div>
</div>
<div class="input-control">
<label>Firstname</label>
{{focus-input id="newUserFirstname" type="text" value=newUser.firstname class=(if hasFirstnameEmptyError 'error')}}
</div>
<div class="input-control">
<label>Lastname</label>
{{input id="newUserLastname" type="text" value=newUser.lastname class=(if hasLastnameEmptyError 'error')}}
</div>
<div class="input-control">
<label>Email</label>
{{input id="newUserEmail" type="text" value=newUser.email class=(if hasEmailEmptyError 'error')}}
</div>
<div class="regular-button button-blue" {{ action 'add' }}>Add</div>
</form>
{{/if}}

View file

@ -4,20 +4,22 @@
<div class="doc-excerpt">{{document.excerpt}}</div>
</div>
{{else}}
<div class="edit-document-heading">
<div class="input-inline input-transparent edit-doc-title">
{{focus-input id="document-name" type="text" value=docName class=(if hasNameError 'error-inline') placeholder="Name" autocomplete="off"}}
</div>
<div class="input-inline input-transparent edit-doc-excerpt">
{{input id="document-excerpt" type="text" value=docExcerpt class=(if hasExcerptError 'error-inline') placeholder="Excerpt" autocomplete="off"}}
</div>
<div>
<div class="round-button-mono" {{action 'onSaveDocument'}}>
<i class="material-icons color-green">check</i>
<form {{action "onSave" on="submit"}}>
<div class="edit-document-heading">
<div class="input-inline input-transparent edit-doc-title">
{{focus-input id="document-name" type="text" value=docName class=(if hasNameError 'error-inline') placeholder="Name" autocomplete="off"}}
</div>
<div class="round-button-mono" {{action 'cancel'}}>
<i class="material-icons color-gray">close</i>
<div class="input-inline input-transparent edit-doc-excerpt">
{{input id="document-excerpt" type="text" value=docExcerpt class=(if hasExcerptError 'error-inline') placeholder="Excerpt" autocomplete="off"}}
</div>
<div>
<button type="submit" class="round-button-mono" {{action 'onSave'}}>
<i class="material-icons color-green">check</i>
</button>
<div class="round-button-mono" {{action 'onCancel'}}>
<i class="material-icons color-gray">close</i>
</div>
</div>
</div>
</div>
</form>
{{/unless}}

View file

@ -1,4 +1,5 @@
<div class="document-view">
{{#if hasPages}}
{{#each pages key="id" as |page index|}}
{{#if isEditor}}
@ -47,6 +48,8 @@
{{/if}}
{{/if}}
<div id="wizard-placeholder" class="hide margin-top-50" />
<div id="new-section-wizard" class="new-section-wizard">
<div class="input-inline input-transparent pull-left width-80">
{{input type="text" id="new-section-name" value=newSectionName class=(if newSectionNameMissing 'section-name error-inline' 'section-name') placeholder="Name" autocomplete="off"}}
@ -94,4 +97,5 @@
{{/if}}
</div>
</div>
</div>

View file

@ -24,7 +24,7 @@
<p>Are you sure you want to delete <span class="bold">{{page.title}}?</span></p>
<p>
{{input type="checkbox" id=checkId class="margin-left-20" checked=deleteChildren}}
<label for="{{checkId}}">&nbsp;Delete child pages</label>
<label for="{{checkId}}">&nbsp;Delete child sections</label>
</p>
{{/dropdown-dialog}}
{{#dropdown-dialog id=publishDialogId target=publishButtonId position="bottom right" button="Publish" color="flat-green" focusOn=blockTitleId onAction=(action 'onSavePageAsBlock')}}

View file

@ -37,7 +37,7 @@
<p>Are you sure you want to delete <span class="bold">{{page.title}}?</span></p>
<p>
{{input type="checkbox" id=checkId class="margin-left-20" checked=deleteChildren}}
<label for="{{checkId}}">&nbsp;Delete child pages</label>
<label for="{{checkId}}">&nbsp;Delete child sections</label>
</p>
{{/dropdown-dialog}}
{{#dropdown-dialog id=publishDialogId target=publishButtonId position="bottom right" button="Publish" color="flat-green" focusOn=blockTitleId onAction=(action 'onSavePageAsBlock')}}

View file

@ -5,18 +5,12 @@
</div>
<div class="actions">
{{#if showCancel}}
<div class="flat-button" {{action 'onCancel'}}>
cancel
</div>
<div class="flat-button" {{action 'onCancel'}}>cancel</div>
{{/if}}
{{#if hasSecondButton}}
<div class="flat-button {{color2}}" {{action 'onAction2'}}>
{{button2}}
</div>
<div class="flat-button {{color2}}" {{action 'onAction2'}}>{{button2}}</div>
{{/if}}
<div class="flat-button {{color}} dropdown-dialog-action-button" {{action 'onAction'}}>
{{button}}
</div>
<div class="flat-button {{color}} dropdown-dialog-action-button" {{action 'onAction'}}>{{button}}</div>
</div>
<div class="clearfix"></div>
</form>

View file

@ -43,11 +43,19 @@
{{/if}}
{{/if}}
{{#if isFolderOwner}}
{{#link-to 'settings' folder.id folder.slug (query-params tab="tabShare")}}
<div class="round-button-mono" id="folder-share-button" data-tooltip="Share" data-tooltip-position="top center">
<i class="material-icons color-gray">share</i>
</div>
{{/link-to}}
{{#if isAuthProviderDocumize}}
{{#link-to 'settings' folder.id folder.slug (query-params tab="tabShare")}}
<div class="round-button-mono" id="folder-share-button" data-tooltip="Share" data-tooltip-position="top center">
<i class="material-icons color-gray">share</i>
</div>
{{/link-to}}
{{else}}
{{#link-to 'settings' folder.id folder.slug (query-params tab="tabPermissions")}}
<div class="round-button-mono" id="folder-share-button" data-tooltip="Share" data-tooltip-position="top center">
<i class="material-icons color-gray">share</i>
</div>
{{/link-to}}
{{/if}}
<div class="button-gap"></div>
{{#link-to 'settings' folder.id folder.slug}}
<div class="round-button-mono" id="folder-settings-button" data-tooltip="Settings" data-tooltip-position="top center">

View file

@ -4,7 +4,6 @@
<img class="logo" src="/assets/img/logo-color.png"/>
<div class="stage-1">
<h2>Documize Sign-up</h2>
<p>Let's set up your account</p>
<div class="input-control">
<label>Firstname</label>
@ -13,11 +12,11 @@
</div>
<div class="input-control">
<label>Lastname</label>
<div class="tip">To customize the app for you</div>
<div class="tip">What the government calls you</div>
<input id="stage-1-lastname" type="text" value="" />
</div>
<div class="input-control">
<div id="stage-1-next" class="regular-button button-green">Next &rsaquo;</div>
<div id="stage-1-next" class="regular-button button-green">Next &rarr;</div>
</div>
</div>
@ -34,7 +33,7 @@
<input id="stage-2-password-confirm" type="password" value="" />
</div>
<div class="input-control">
<div id="stage-2-next" class="regular-button button-green">Sign In &rsaquo;</div>
<div id="stage-2-next" class="regular-button button-green">Sign In &rarr;</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
<div class="document-editor {{if blockMode 'document-editor-full'}}">
<div id={{containerId}} class="document-editor {{if blockMode 'document-editor-full'}}">
<div class="toolbar">
<div class="buttons pull-right">
{{#if busy}}

View file

@ -15,15 +15,17 @@
<label>Email</label>
{{input id="email" type="text" value=model.email class=(if hasEmailError 'error')}}
</div>
<div class="input-control">
<label>Password</label>
<div class="tip">Optional change your password</div>
{{input id="password" type="password" value=password.password class=hasPasswordError}}
</div>
<div class="input-control">
<label>Confirm Password</label>
<div class="tip">Confirm your new password</div>
{{input id="confirmPassword" type="password" value=password.confirmation class=hasConfirmPasswordError}}
</div>
{{#if isAuthProviderDocumize}}
<div class="input-control">
<label>Password</label>
<div class="tip">Optional change your password</div>
{{input id="password" type="password" value=password.password class=hasPasswordError}}
</div>
<div class="input-control">
<label>Confirm Password</label>
<div class="tip">Confirm your new password</div>
{{input id="confirmPassword" type="password" value=password.confirmation class=hasConfirmPasswordError}}
</div>
{{/if}}
<div class="regular-button button-blue" {{ action 'save' }}>save</div>
</div>

View file

@ -1,19 +0,0 @@
<form class="form-bordered">
<div class="form-header">
<div class="title">Add user</div>
<div class="tip">New users receive an invitation email with a random password</div>
</div>
<div class="input-control">
<label>Firstname</label>
{{focus-input id="newUserFirstname" type="text" value=newUser.firstname class=(if hasFirstnameEmptyError 'error')}}
</div>
<div class="input-control">
<label>Lastname</label>
{{input id="newUserLastname" type="text" value=newUser.lastname class=(if hasLastnameEmptyError 'error')}}
</div>
<div class="input-control">
<label>Email</label>
{{input id="newUserEmail" type="text" value=newUser.email class=(if hasEmailEmptyError 'error')}}
</div>
<div class="regular-button button-blue" {{ action 'add' }}>Add</div>
</form>

View file

@ -15,4 +15,16 @@ export default {
Private: 2,
Protected: 3
},
AuthProvider: {
Documize: 'documize',
Keycloak: 'keycloak'
},
DocumentActionType: {
Read: 1,
Feedback: 2,
Contribute: 3,
Approve: 4
}
};

View file

@ -9,9 +9,11 @@
//
// https://documize.com
// make url friendly slug from specified text.
// Make url friendly slug from specified text.
// Has to handle non-english character text "Общее" text!
function makeSlug(text) {
return text.toLowerCase().replace(/[^\w ]+/g, '').replace(/ +/g, '-');
return slug(text, { mode: 'rfc3986', lower: true});
//return text.toLowerCase().replace(/[^\w ]+/g, '').replace(/ +/g, '-');
}
function makeId(len) {
@ -33,4 +35,5 @@ export default {
makeSlug,
makeId,
endsWith
};
};

View file

@ -60,6 +60,8 @@ module.exports = function (defaults) {
app.import('vendor/waypoints.js');
app.import('vendor/velocity.js');
app.import('vendor/velocity.ui.js');
app.import('vendor/keycloak.js');
app.import('vendor/slug.js');
return app.toTree();
};

View file

@ -1,4 +0,0 @@
{
"node": true,
"browser": false
}

View file

@ -31,11 +31,11 @@ export default function () {
this.get('/documents', function (schema, request) {
let folder_id = request.queryParams.folder;
if (folder_id = "VzMuyEw_3WqiafcG") {
if (folder_id === "VzMuyEw_3WqiafcG") {
return schema.db.documents.where({ folderId: folder_id });
}
if (folder_id = 'V0Vy5Uw_3QeDAMW9') {
if (folder_id === 'V0Vy5Uw_3QeDAMW9') {
return null;
}
});

View file

@ -16,34 +16,33 @@
"engines": {
"node": ">= 0.10.0"
},
"author": "",
"author": "Documize Inc.",
"license": "AGPL",
"devDependencies": {
"broccoli-asset-rev": "^2.4.5",
"ember-ajax": "^2.4.1",
"ember-cli": "2.11.1",
"ember-cli": "2.12.0",
"ember-cli-app-version": "^2.0.0",
"ember-cli-babel": "^5.1.7",
"ember-cli-dependency-checker": "^1.3.0",
"ember-cli-eslint": "^3.0.0",
"ember-cli-htmlbars": "^1.1.1",
"ember-cli-htmlbars-inline-precompile": "^0.3.6",
"ember-cli-inject-live-reload": "^1.4.1",
"ember-cli-jshint": "^2.0.1",
"ember-cli-mirage": "^0.2.0",
"ember-cli-qunit": "^3.0.1",
"ember-cli-release": "^0.2.9",
"ember-cli-qunit": "^3.1.0",
"ember-cli-sass": "5.3.1",
"ember-cli-shims": "^1.0.2",
"ember-cli-sri": "^2.1.0",
"ember-cli-test-loader": "^1.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-data": "^2.11.0",
"ember-data": "^2.12.0",
"ember-export-application-global": "^1.0.5",
"ember-load-initializers": "^0.6.0",
"ember-resolver": "^2.0.3",
"ember-simple-auth": "git+https://github.com/documize/ember-simple-auth.git#21e638f9e33267d8944835002ee96884d34d568a",
"ember-source": "~2.11.0",
"loader.js": "^4.0.10"
"ember-simple-auth": "1.2.0",
"ember-source": "~2.12.0",
"loader.js": "^4.2.3"
},
"ember-addon": {
"paths": [

View file

@ -1 +1 @@
tinymce.PluginManager.add("advlist",function(e){function t(t){return e.$.contains(e.getBody(),t)}function n(e){return e&&/^(OL|UL|DL)$/.test(e.nodeName)&&t(e)}function r(e,t){var n=[];return t&&tinymce.each(t.split(/[ ,]/),function(e){n.push({text:e.replace(/\-/g," ").replace(/\b\w/g,function(e){return e.toUpperCase()}),data:"default"==e?"":e})}),n}function i(t,n){e.undoManager.transact(function(){var r,i=e.dom,o=e.selection;if(r=i.getParent(o.getNode(),"ol,ul"),!r||r.nodeName!=t||n===!1){var a={"list-style-type":n?n:""};e.execCommand("UL"==t?"InsertUnorderedList":"InsertOrderedList",!1,a)}r=i.getParent(o.getNode(),"ol,ul"),r&&tinymce.util.Tools.each(i.select("ol,ul",r).concat([r]),function(e){e.nodeName!==t&&n!==!1&&(e=i.rename(e,t)),i.setStyle(e,"listStyleType",n?n:null),e.removeAttribute("data-mce-style")}),e.focus()})}function o(t){var n=e.dom.getStyle(e.dom.getParent(e.selection.getNode(),"ol,ul"),"listStyleType")||"";t.control.items().each(function(e){e.active(e.settings.data===n)})}var a,s,l=function(e,t){var n=e.settings.plugins?e.settings.plugins:"";return tinymce.util.Tools.inArray(n.split(/[ ,]/),t)!==-1};a=r("OL",e.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),s=r("UL",e.getParam("advlist_bullet_styles","default,circle,disc,square"));var c=function(t){return function(){var r=this;e.on("NodeChange",function(e){var i=tinymce.util.Tools.grep(e.parents,n);r.active(i.length>0&&i[0].nodeName===t)})}};l(e,"lists")&&(e.addCommand("ApplyUnorderedListStyle",function(e,t){i("UL",t["list-style-type"])}),e.addCommand("ApplyOrderedListStyle",function(e,t){i("OL",t["list-style-type"])}),e.addButton("numlist",{type:a.length>0?"splitbutton":"button",tooltip:"Numbered list",menu:a,onPostRender:c("OL"),onshow:o,onselect:function(e){i("OL",e.control.settings.data)},onclick:function(){i("OL",!1)}}),e.addButton("bullist",{type:s.length>0?"splitbutton":"button",tooltip:"Bullet list",onPostRender:c("UL"),menu:s,onshow:o,onselect:function(e){i("UL",e.control.settings.data)},onclick:function(){i("UL",!1)}}))});
tinymce.PluginManager.add("advlist",function(e){function t(t){return e.$.contains(e.getBody(),t)}function n(e){return e&&/^(OL|UL|DL)$/.test(e.nodeName)&&t(e)}function r(e,t){var n=[];return t&&tinymce.each(t.split(/[ ,]/),function(e){n.push({text:e.replace(/\-/g," ").replace(/\b\w/g,function(e){return e.toUpperCase()}),data:"default"==e?"":e})}),n}function i(t,n){e.undoManager.transact(function(){var r,i=e.dom,o=e.selection;if(r=i.getParent(o.getNode(),"ol,ul"),!r||r.nodeName!=t||n===!1){var a={"list-style-type":n?n:""};e.execCommand("UL"==t?"InsertUnorderedList":"InsertOrderedList",!1,a)}r=i.getParent(o.getNode(),"ol,ul"),r&&tinymce.util.Tools.each(i.select("ol,ul",r).concat([r]),function(e){e.nodeName!==t&&n!==!1&&(e=i.rename(e,t)),i.setStyle(e,"listStyleType",n?n:null),e.removeAttribute("data-mce-style")}),e.focus()})}function o(t){var n=e.dom.getStyle(e.dom.getParent(e.selection.getNode(),"ol,ul"),"listStyleType")||"";t.control.items().each(function(e){e.active(e.settings.data===n)})}var a,s,l=function(e,t){var n=e.settings.plugins?e.settings.plugins:"";return tinymce.util.Tools.inArray(n.split(/[ ,]/),t)!==-1};a=r("OL",e.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),s=r("UL",e.getParam("advlist_bullet_styles","default,circle,disc,square"));var u=function(t){return function(){var r=this;e.on("NodeChange",function(e){var i=tinymce.util.Tools.grep(e.parents,n);r.active(i.length>0&&i[0].nodeName===t)})}};l(e,"lists")&&(e.addCommand("ApplyUnorderedListStyle",function(e,t){i("UL",t["list-style-type"])}),e.addCommand("ApplyOrderedListStyle",function(e,t){i("OL",t["list-style-type"])}),e.addButton("numlist",{type:a.length>0?"splitbutton":"button",tooltip:"Numbered list",menu:a,onPostRender:u("OL"),onshow:o,onselect:function(e){i("OL",e.control.settings.data)},onclick:function(){i("OL",!1)}}),e.addButton("bullist",{type:s.length>0?"splitbutton":"button",tooltip:"Bullet list",onPostRender:u("UL"),menu:s,onshow:o,onselect:function(e){i("UL",e.control.settings.data)},onclick:function(){i("UL",!1)}}))});

View file

@ -1 +1 @@
tinymce.PluginManager.add("anchor",function(e){var t=function(e){return!e.attr("href")&&(e.attr("id")||e.attr("name"))&&!e.firstChild},n=function(e){return function(n){for(var r=0;r<n.length;r++)t(n[r])&&n[r].attr("contenteditable",e)}},r=function(){var t=e.selection.getNode(),n="A"==t.tagName&&""===e.dom.getAttrib(t,"href"),r="";n&&(r=t.id||t.name||""),e.windowManager.open({title:"Anchor",body:{type:"textbox",name:"id",size:40,label:"Id",value:r},onsubmit:function(r){var i=r.data.id;n?(t.removeAttribute("name"),t.id=i):(e.selection.collapse(!0),e.execCommand("mceInsertContent",!1,e.dom.createHTML("a",{id:i})))}})};tinymce.Env.ceFalse&&e.on("PreInit",function(){e.parser.addNodeFilter("a",n("false")),e.serializer.addNodeFilter("a",n(null))}),e.addCommand("mceAnchor",r),e.addButton("anchor",{icon:"anchor",tooltip:"Anchor",onclick:r,stateSelector:"a:not([href])"}),e.addMenuItem("anchor",{icon:"anchor",text:"Anchor",context:"insert",onclick:r})});
tinymce.PluginManager.add("anchor",function(e){var t=function(e){return!e.attr("href")&&(e.attr("id")||e.attr("name"))&&!e.firstChild},n=function(e){return function(n){for(var r=0;r<n.length;r++)t(n[r])&&n[r].attr("contenteditable",e)}},r=function(e){return/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(e)},i=function(){var t=e.selection.getNode(),n="A"==t.tagName&&""===e.dom.getAttrib(t,"href"),i="";n&&(i=t.id||t.name||""),e.windowManager.open({title:"Anchor",body:{type:"textbox",name:"id",size:40,label:"Id",value:i},onsubmit:function(i){var o=i.data.id;return r(o)?void(n?(t.removeAttribute("name"),t.id=o):(e.selection.collapse(!0),e.execCommand("mceInsertContent",!1,e.dom.createHTML("a",{id:o})))):(i.preventDefault(),void e.windowManager.alert("Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."))}})};tinymce.Env.ceFalse&&e.on("PreInit",function(){e.parser.addNodeFilter("a",n("false")),e.serializer.addNodeFilter("a",n(null))}),e.addCommand("mceAnchor",i),e.addButton("anchor",{icon:"anchor",tooltip:"Anchor",onclick:i,stateSelector:"a:not([href])"}),e.addMenuItem("anchor",{icon:"anchor",text:"Anchor",context:"insert",onclick:i})});

View file

@ -1 +1 @@
tinymce.PluginManager.add("autolink",function(e){function t(e){i(e,-1,"(",!0)}function n(e){i(e,0,"",!0)}function r(e){i(e,-1,"",!1)}function i(e,t,n){function r(e,t){if(t<0&&(t=0),3==e.nodeType){var n=e.data.length;t>n&&(t=n)}return t}function i(e,t){1!=e.nodeType||e.hasChildNodes()?s.setStart(e,r(e,t)):s.setStartBefore(e)}function o(e,t){1!=e.nodeType||e.hasChildNodes()?s.setEnd(e,r(e,t)):s.setEndAfter(e)}var s,l,c,u,d,f,p,m,g,h;if("A"!=e.selection.getNode().tagName){if(s=e.selection.getRng(!0).cloneRange(),s.startOffset<5){if(m=s.endContainer.previousSibling,!m){if(!s.endContainer.firstChild||!s.endContainer.firstChild.nextSibling)return;m=s.endContainer.firstChild.nextSibling}if(g=m.length,i(m,g),o(m,g),s.endOffset<5)return;l=s.endOffset,u=m}else{if(u=s.endContainer,3!=u.nodeType&&u.firstChild){for(;3!=u.nodeType&&u.firstChild;)u=u.firstChild;3==u.nodeType&&(i(u,0),o(u,u.nodeValue.length))}l=1==s.endOffset?2:s.endOffset-1-t}c=l;do i(u,l>=2?l-2:0),o(u,l>=1?l-1:0),l-=1,h=s.toString();while(" "!=h&&""!==h&&160!=h.charCodeAt(0)&&l-2>=0&&h!=n);s.toString()==n||160==s.toString().charCodeAt(0)?(i(u,l),o(u,c),l+=1):0===s.startOffset?(i(u,0),o(u,c)):(i(u,l),o(u,c)),f=s.toString(),"."==f.charAt(f.length-1)&&o(u,c-1),f=s.toString(),p=f.match(a),p&&("www."==p[1]?p[1]="http://www.":/@$/.test(p[1])&&!/^mailto:/.test(p[1])&&(p[1]="mailto:"+p[1]),d=e.selection.getBookmark(),e.selection.setRng(s),e.execCommand("createlink",!1,p[1]+p[2]),e.settings.default_link_target&&e.dom.setAttrib(e.selection.getNode(),"target",e.settings.default_link_target),e.selection.moveToBookmark(d),e.nodeChanged())}}var o,a=/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i;return e.settings.autolink_pattern&&(a=e.settings.autolink_pattern),e.on("keydown",function(t){if(13==t.keyCode)return r(e)}),tinymce.Env.ie?void e.on("focus",function(){if(!o){o=!0;try{e.execCommand("AutoUrlDetect",!1,!0)}catch(e){}}}):(e.on("keypress",function(n){if(41==n.keyCode)return t(e)}),void e.on("keyup",function(t){if(32==t.keyCode)return n(e)}))});
tinymce.PluginManager.add("autolink",function(e){function t(e){i(e,-1,"(",!0)}function n(e){i(e,0,"",!0)}function r(e){i(e,-1,"",!1)}function i(e,t,n){function r(e,t){if(t<0&&(t=0),3==e.nodeType){var n=e.data.length;t>n&&(t=n)}return t}function i(e,t){1!=e.nodeType||e.hasChildNodes()?s.setStart(e,r(e,t)):s.setStartBefore(e)}function o(e,t){1!=e.nodeType||e.hasChildNodes()?s.setEnd(e,r(e,t)):s.setEndAfter(e)}var s,l,u,c,d,f,p,h,m,g;if("A"!=e.selection.getNode().tagName){if(s=e.selection.getRng(!0).cloneRange(),s.startOffset<5){if(h=s.endContainer.previousSibling,!h){if(!s.endContainer.firstChild||!s.endContainer.firstChild.nextSibling)return;h=s.endContainer.firstChild.nextSibling}if(m=h.length,i(h,m),o(h,m),s.endOffset<5)return;l=s.endOffset,c=h}else{if(c=s.endContainer,3!=c.nodeType&&c.firstChild){for(;3!=c.nodeType&&c.firstChild;)c=c.firstChild;3==c.nodeType&&(i(c,0),o(c,c.nodeValue.length))}l=1==s.endOffset?2:s.endOffset-1-t}u=l;do i(c,l>=2?l-2:0),o(c,l>=1?l-1:0),l-=1,g=s.toString();while(" "!=g&&""!==g&&160!=g.charCodeAt(0)&&l-2>=0&&g!=n);s.toString()==n||160==s.toString().charCodeAt(0)?(i(c,l),o(c,u),l+=1):0===s.startOffset?(i(c,0),o(c,u)):(i(c,l),o(c,u)),f=s.toString(),"."==f.charAt(f.length-1)&&o(c,u-1),f=s.toString(),p=f.match(a),p&&("www."==p[1]?p[1]="http://www.":/@$/.test(p[1])&&!/^mailto:/.test(p[1])&&(p[1]="mailto:"+p[1]),d=e.selection.getBookmark(),e.selection.setRng(s),e.execCommand("createlink",!1,p[1]+p[2]),e.settings.default_link_target&&e.dom.setAttrib(e.selection.getNode(),"target",e.settings.default_link_target),e.selection.moveToBookmark(d),e.nodeChanged())}}var o,a=/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i;return e.settings.autolink_pattern&&(a=e.settings.autolink_pattern),e.on("keydown",function(t){if(13==t.keyCode)return r(e)}),tinymce.Env.ie?void e.on("focus",function(){if(!o){o=!0;try{e.execCommand("AutoUrlDetect",!1,!0)}catch(e){}}}):(e.on("keypress",function(n){if(41==n.keyCode)return t(e)}),void e.on("keyup",function(t){if(32==t.keyCode)return n(e)}))});

View file

@ -1 +1 @@
tinymce.PluginManager.add("autoresize",function(e){function t(){return e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen()}function n(r){var a,s,l,c,u,d,f,p,m,g,h,v,b=tinymce.DOM;if(s=e.getDoc()){if(l=s.body,c=s.documentElement,u=i.autoresize_min_height,!l||r&&"setcontent"===r.type&&r.initial||t())return void(l&&c&&(l.style.overflowY="auto",c.style.overflowY="auto"));f=e.dom.getStyle(l,"margin-top",!0),p=e.dom.getStyle(l,"margin-bottom",!0),m=e.dom.getStyle(l,"padding-top",!0),g=e.dom.getStyle(l,"padding-bottom",!0),h=e.dom.getStyle(l,"border-top-width",!0),v=e.dom.getStyle(l,"border-bottom-width",!0),d=l.offsetHeight+parseInt(f,10)+parseInt(p,10)+parseInt(m,10)+parseInt(g,10)+parseInt(h,10)+parseInt(v,10),(isNaN(d)||d<=0)&&(d=tinymce.Env.ie?l.scrollHeight:tinymce.Env.webkit&&0===l.clientHeight?0:l.offsetHeight),d>i.autoresize_min_height&&(u=d),i.autoresize_max_height&&d>i.autoresize_max_height?(u=i.autoresize_max_height,l.style.overflowY="auto",c.style.overflowY="auto"):(l.style.overflowY="hidden",c.style.overflowY="hidden",l.scrollTop=0),u!==o&&(a=u-o,b.setStyle(e.iframeElement,"height",u+"px"),o=u,tinymce.isWebKit&&a<0&&n(r))}}function r(t,i,o){tinymce.util.Delay.setEditorTimeout(e,function(){n({}),t--?r(t,i,o):o&&o()},i)}var i=e.settings,o=0;e.settings.inline||(i.autoresize_min_height=parseInt(e.getParam("autoresize_min_height",e.getElement().offsetHeight),10),i.autoresize_max_height=parseInt(e.getParam("autoresize_max_height",0),10),e.on("init",function(){var t,n;t=e.getParam("autoresize_overflow_padding",1),n=e.getParam("autoresize_bottom_margin",50),t!==!1&&e.dom.setStyles(e.getBody(),{paddingLeft:t,paddingRight:t}),n!==!1&&e.dom.setStyles(e.getBody(),{paddingBottom:n})}),e.on("nodechange setcontent keyup FullscreenStateChanged",n),e.getParam("autoresize_on_init",!0)&&e.on("init",function(){r(20,100,function(){r(5,1e3)})}),e.addCommand("mceAutoResize",n))});
tinymce.PluginManager.add("autoresize",function(e){function t(){return e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen()}function n(r){var a,s,l,u,c,d,f,p,h,m,g,v,y=tinymce.DOM;if(s=e.getDoc()){if(l=s.body,u=s.documentElement,c=i.autoresize_min_height,!l||r&&"setcontent"===r.type&&r.initial||t())return void(l&&u&&(l.style.overflowY="auto",u.style.overflowY="auto"));f=e.dom.getStyle(l,"margin-top",!0),p=e.dom.getStyle(l,"margin-bottom",!0),h=e.dom.getStyle(l,"padding-top",!0),m=e.dom.getStyle(l,"padding-bottom",!0),g=e.dom.getStyle(l,"border-top-width",!0),v=e.dom.getStyle(l,"border-bottom-width",!0),d=l.offsetHeight+parseInt(f,10)+parseInt(p,10)+parseInt(h,10)+parseInt(m,10)+parseInt(g,10)+parseInt(v,10),(isNaN(d)||d<=0)&&(d=tinymce.Env.ie?l.scrollHeight:tinymce.Env.webkit&&0===l.clientHeight?0:l.offsetHeight),d>i.autoresize_min_height&&(c=d),i.autoresize_max_height&&d>i.autoresize_max_height?(c=i.autoresize_max_height,l.style.overflowY="auto",u.style.overflowY="auto"):(l.style.overflowY="hidden",u.style.overflowY="hidden",l.scrollTop=0),c!==o&&(a=c-o,y.setStyle(e.iframeElement,"height",c+"px"),o=c,tinymce.isWebKit&&a<0&&n(r))}}function r(t,i,o){tinymce.util.Delay.setEditorTimeout(e,function(){n({}),t--?r(t,i,o):o&&o()},i)}var i=e.settings,o=0;e.settings.inline||(i.autoresize_min_height=parseInt(e.getParam("autoresize_min_height",e.getElement().offsetHeight),10),i.autoresize_max_height=parseInt(e.getParam("autoresize_max_height",0),10),e.on("init",function(){var t,n;t=e.getParam("autoresize_overflow_padding",1),n=e.getParam("autoresize_bottom_margin",50),t!==!1&&e.dom.setStyles(e.getBody(),{paddingLeft:t,paddingRight:t}),n!==!1&&e.dom.setStyles(e.getBody(),{paddingBottom:n})}),e.on("nodechange setcontent keyup FullscreenStateChanged",n),e.getParam("autoresize_on_init",!0)&&e.on("init",function(){r(20,100,function(){r(5,1e3)})}),e.addCommand("mceAutoResize",n))});

View file

@ -1 +1 @@
tinymce._beforeUnloadHandler=function(){var e;return tinymce.each(tinymce.editors,function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e},tinymce.PluginManager.add("autosave",function(e){function t(e,t){var n={s:1e3,m:6e4};return e=/^(\d+)([ms]?)$/.exec(""+(e||t)),(e[2]?n[e[2]]:1)*parseInt(e,10)}function n(){var e=parseInt(p.getItem(u+"time"),10)||0;return!((new Date).getTime()-e>f.autosave_retention)||(r(!1),!1)}function r(t){p.removeItem(u+"draft"),p.removeItem(u+"time"),t!==!1&&e.fire("RemoveDraft")}function i(){!c()&&e.isDirty()&&(p.setItem(u+"draft",e.getContent({format:"raw",no_events:!0})),p.setItem(u+"time",(new Date).getTime()),e.fire("StoreDraft"))}function o(){n()&&(e.setContent(p.getItem(u+"draft"),{format:"raw"}),e.fire("RestoreDraft"))}function a(){d||(setInterval(function(){e.removed||i()},f.autosave_interval),d=!0)}function s(){var t=this;t.disabled(!n()),e.on("StoreDraft RestoreDraft RemoveDraft",function(){t.disabled(!n())}),a()}function l(){e.undoManager.beforeChange(),o(),r(),e.undoManager.add()}function c(t){var n=e.settings.forced_root_block;return t=tinymce.trim("undefined"==typeof t?e.getBody().innerHTML:t),""===t||new RegExp("^<"+n+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+n+">|<br>$","i").test(t)}var u,d,f=e.settings,p=tinymce.util.LocalStorage;u=f.autosave_prefix||"tinymce-autosave-{path}{query}-{id}-",u=u.replace(/\{path\}/g,document.location.pathname),u=u.replace(/\{query\}/g,document.location.search),u=u.replace(/\{id\}/g,e.id),f.autosave_interval=t(f.autosave_interval,"30s"),f.autosave_retention=t(f.autosave_retention,"20m"),e.addButton("restoredraft",{title:"Restore last draft",onclick:l,onPostRender:s}),e.addMenuItem("restoredraft",{text:"Restore last draft",onclick:l,onPostRender:s,context:"file"}),e.settings.autosave_restore_when_empty!==!1&&(e.on("init",function(){n()&&c()&&o()}),e.on("saveContent",function(){r()})),window.onbeforeunload=tinymce._beforeUnloadHandler,this.hasDraft=n,this.storeDraft=i,this.restoreDraft=o,this.removeDraft=r,this.isEmpty=c});
tinymce._beforeUnloadHandler=function(){var e;return tinymce.each(tinymce.editors,function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e},tinymce.PluginManager.add("autosave",function(e){function t(e,t){var n={s:1e3,m:6e4};return e=/^(\d+)([ms]?)$/.exec(""+(e||t)),(e[2]?n[e[2]]:1)*parseInt(e,10)}function n(){var e=parseInt(p.getItem(c+"time"),10)||0;return!((new Date).getTime()-e>f.autosave_retention)||(r(!1),!1)}function r(t){p.removeItem(c+"draft"),p.removeItem(c+"time"),t!==!1&&e.fire("RemoveDraft")}function i(){!u()&&e.isDirty()&&(p.setItem(c+"draft",e.getContent({format:"raw",no_events:!0})),p.setItem(c+"time",(new Date).getTime()),e.fire("StoreDraft"))}function o(){n()&&(e.setContent(p.getItem(c+"draft"),{format:"raw"}),e.fire("RestoreDraft"))}function a(){d||(setInterval(function(){e.removed||i()},f.autosave_interval),d=!0)}function s(){var t=this;t.disabled(!n()),e.on("StoreDraft RestoreDraft RemoveDraft",function(){t.disabled(!n())}),a()}function l(){e.undoManager.beforeChange(),o(),r(),e.undoManager.add()}function u(t){var n=e.settings.forced_root_block;return t=tinymce.trim("undefined"==typeof t?e.getBody().innerHTML:t),""===t||new RegExp("^<"+n+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+n+">|<br>$","i").test(t)}var c,d,f=e.settings,p=tinymce.util.LocalStorage;c=f.autosave_prefix||"tinymce-autosave-{path}{query}-{id}-",c=c.replace(/\{path\}/g,document.location.pathname),c=c.replace(/\{query\}/g,document.location.search),c=c.replace(/\{id\}/g,e.id),f.autosave_interval=t(f.autosave_interval,"30s"),f.autosave_retention=t(f.autosave_retention,"20m"),e.addButton("restoredraft",{title:"Restore last draft",onclick:l,onPostRender:s}),e.addMenuItem("restoredraft",{text:"Restore last draft",onclick:l,onPostRender:s,context:"file"}),e.settings.autosave_restore_when_empty!==!1&&(e.on("init",function(){n()&&u()&&o()}),e.on("saveContent",function(){r()})),window.onbeforeunload=tinymce._beforeUnloadHandler,this.hasDraft=n,this.storeDraft=i,this.restoreDraft=o,this.removeDraft=r,this.isEmpty=u});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more