Add ability to add custom css/javascript
authorChocobozzz <me@florianbigard.com>
Thu, 22 Feb 2018 09:22:53 +0000 (10:22 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 22 Feb 2018 09:22:53 +0000 (10:22 +0100)
15 files changed:
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
client/src/app/app.component.html
client/src/app/app.component.ts
client/src/app/core/server/server.service.ts
config/default.yaml
config/production.yaml.example
server/controllers/api/config.ts
server/initializers/constants.ts
server/tests/api/check-params/config.ts
server/tests/api/server/config.ts
shared/models/config/custom-config.model.ts
shared/models/config/customization.model.ts [new file with mode: 0644]
shared/models/config/server-config.model.ts

index 0fe2aa20324ec04ec2f726ce82e8d70205083dde..8dca9bc042ec5fe6f2cd829ce6d2f626c51e22f0 100644 (file)
     </div>
   </ng-template>
 
+  <div class="inner-form-title">Customizations</div>
+
+  <div class="form-group">
+    <label for="customizationJavascript">JavaScript</label>
+    <textarea
+        id="customizationJavascript" formControlName="customizationJavascript"
+        [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }"
+    ></textarea>
+    <div *ngIf="formErrors.customizationJavascript" class="form-error">
+      {{ formErrors.customizationJavascript }}
+    </div>
+  </div>
+
+  <div class="form-group">
+    <label for="customizationCSS">CSS</label>
+    <textarea
+        id="customizationCSS" formControlName="customizationCSS"
+        [ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
+    ></textarea>
+    <div *ngIf="formErrors.customizationCSS" class="form-error">
+      {{ formErrors.customizationCSS }}
+    </div>
+  </div>
+
   <input type="submit" value="Update configuration" [disabled]="!form.valid">
 </form>
index 0195f44eb2b52db4e120d08458d4f6ca5e8496cb..e72f30c6965c2db90daf944a1c28f28cfef526e7 100644 (file)
@@ -29,3 +29,9 @@ input[type=submit] {
   margin-top: 30px;
   margin-bottom: 10px;
 }
+
+textarea {
+  @include peertube-textarea(500px, 150px);
+
+  display: block;
+}
index cd8c926f7b48e0885cddd813162526843abc0838..02726853616d7c765e7418e8695c62b696bdd9c7 100644 (file)
@@ -49,7 +49,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
     signupLimit: '',
     adminEmail: '',
     userVideoQuota: '',
-    transcodingThreads: ''
+    transcodingThreads: '',
+    customizationJavascript: '',
+    customizationCSS: ''
   }
   validationMessages = {
     instanceName: INSTANCE_NAME.MESSAGES,
@@ -84,7 +86,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
       adminEmail: [ '', ADMIN_EMAIL.VALIDATORS ],
       userVideoQuota: [ '', USER_VIDEO_QUOTA.VALIDATORS ],
       transcodingThreads: [ '', TRANSCODING_THREADS.VALIDATORS ],
-      transcodingEnabled: [ ]
+      transcodingEnabled: [ ],
+      customizationJavascript: [ '' ],
+      customizationCSS: [ '' ]
     }
 
     for (const resolution of this.resolutions) {
@@ -125,7 +129,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
       instance: {
         name: this.form.value['instanceName'],
         description: this.form.value['instanceDescription'],
-        terms: this.form.value['instanceTerms']
+        terms: this.form.value['instanceTerms'],
+        customizations: {
+          javascript: this.form.value['customizationJavascript'],
+          css: this.form.value['customizationCSS']
+        }
       },
       cache: {
         previews: {
@@ -183,7 +191,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
       adminEmail: this.customConfig.admin.email,
       userVideoQuota: this.customConfig.user.videoQuota,
       transcodingThreads: this.customConfig.transcoding.threads,
-      transcodingEnabled: this.customConfig.transcoding.enabled
+      transcodingEnabled: this.customConfig.transcoding.enabled,
+      customizationJavascript: this.customConfig.instance.customizations.javascript,
+      customizationCSS: this.customConfig.instance.customizations.css
     }
 
     for (const resolution of this.resolutions) {
index dafc452668a8e9630df2491ca3cf58da48a546f2..0e1882ad359ef8ef66b58bfabc7b06888d239a88 100644 (file)
@@ -1,3 +1,5 @@
+<div *ngIf="customCSS" [innerHTML]="customCSS"></div>
+
 <div>
   <div class="header">
 
index 3af33ba2b47c8d1922b36edf13e07ec07bef0a4c..25936146c8c6f5970b2bfcc1a2349b7fef52762e 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, OnInit } from '@angular/core'
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
 import { GuardsCheckStart, Router } from '@angular/router'
 import { AuthService, ServerService } from '@app/core'
 import { isInSmallView } from '@app/shared/misc/utils'
@@ -24,10 +25,13 @@ export class AppComponent implements OnInit {
 
   isMenuDisplayed = true
 
+  customCSS: SafeHtml
+
   constructor (
     private router: Router,
     private authService: AuthService,
-    private serverService: ServerService
+    private serverService: ServerService,
+    private domSanitizer: DomSanitizer
   ) {}
 
   get serverVersion () {
@@ -66,6 +70,26 @@ export class AppComponent implements OnInit {
         }
       }
     )
+
+    this.serverService.configLoaded
+      .subscribe(() => {
+        const config = this.serverService.getConfig()
+
+        // We test customCSS in case or the admin removed the css
+        if (this.customCSS || config.instance.customizations.css) {
+          const styleTag = '<style>' + config.instance.customizations.css + '</style>'
+          this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
+        }
+
+        if (config.instance.customizations.javascript) {
+          try {
+            // tslint:disable:no-eval
+            eval(config.instance.customizations.javascript)
+          } catch (err) {
+            console.error('Cannot eval custom JavaScript.', err)
+          }
+        }
+      })
   }
 
   toggleMenu () {
index f54e63efd5c867a2fb6ca7d49340de848f27597e..3c94f09c6d4890a0fb27bce36c6dfcef64b66f49 100644 (file)
@@ -12,6 +12,7 @@ export class ServerService {
   private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
   private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
 
+  configLoaded = new ReplaySubject<boolean>(1)
   videoPrivaciesLoaded = new ReplaySubject<boolean>(1)
   videoCategoriesLoaded = new ReplaySubject<boolean>(1)
   videoLicencesLoaded = new ReplaySubject<boolean>(1)
@@ -19,7 +20,11 @@ export class ServerService {
 
   private config: ServerConfig = {
     instance: {
-      name: 'PeerTube'
+      name: 'PeerTube',
+      customizations: {
+        javascript: '',
+        css: ''
+      }
     },
     serverVersion: 'Unknown',
     signup: {
@@ -56,7 +61,11 @@ export class ServerService {
   loadConfig () {
     this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL)
       .do(this.saveConfigLocally)
-      .subscribe(data => this.config = data)
+      .subscribe(data => {
+        this.config = data
+
+        this.configLoaded.next(true)
+      })
   }
 
   loadVideoCategories () {
index 9c1136621612d4a8e1b2fd5c3abc9de78183f562..0d0e788dd8e043416948e3f0ac99d97b045a8716 100644 (file)
@@ -74,3 +74,6 @@ instance:
   name: 'PeerTube'
   description: 'Welcome to this PeerTube instance!' # Support markdown
   terms: 'No terms for now.' # Support markdown
+  customizations:
+    javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
+    css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
index 9d233a84715fc5de96ebf215f9138bcade4575cc..0b15c7b960787b4244ab5292eb28a1ba0410f45a 100644 (file)
@@ -74,3 +74,6 @@ instance:
   name: 'PeerTube'
   description: '' # Support markdown
   terms: '' # Support markdown
+  customizations:
+    javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
+    css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
index 532afb8c0832f0119e327e65938aa5a13e64c910..8cfaf3e29759ba3a558884ab11170e3c95c7c476 100644 (file)
@@ -43,7 +43,11 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
 
   const json: ServerConfig = {
     instance: {
-      name: CONFIG.INSTANCE.NAME
+      name: CONFIG.INSTANCE.NAME,
+      customizations: {
+        javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
+        css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
+      }
     },
     serverVersion: packageJSON.version,
     signup: {
@@ -132,7 +136,11 @@ function customConfig (): CustomConfig {
     instance: {
       name: CONFIG.INSTANCE.NAME,
       description: CONFIG.INSTANCE.DESCRIPTION,
-      terms: CONFIG.INSTANCE.TERMS
+      terms: CONFIG.INSTANCE.TERMS,
+      customizations: {
+        css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
+        javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
+      }
     },
     cache: {
       previews: {
index ac001bbc7cb06c2aadd52614dc6032623702e112..328a3e70a6a4c4592a13c6e96366e64eb8ae3fd5 100644 (file)
@@ -158,7 +158,11 @@ const CONFIG = {
   INSTANCE: {
     get NAME () { return config.get<string>('instance.name') },
     get DESCRIPTION () { return config.get<string>('instance.description') },
-    get TERMS () { return config.get<string>('instance.terms') }
+    get TERMS () { return config.get<string>('instance.terms') },
+    CUSTOMIZATIONS: {
+      get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
+      get CSS () { return config.get<string>('instance.customizations.css') }
+    }
   }
 }
 
index efc1e4e09bf0499f9319ba5c80a113b0562403fa..c1c0a3f59cc426d953555fd6b339965a29995127 100644 (file)
@@ -17,7 +17,11 @@ describe('Test config API validators', function () {
     instance: {
       name: 'PeerTube updated',
       description: 'my super description',
-      terms: 'my super terms'
+      terms: 'my super terms',
+      customizations: {
+        javascript: 'alert("coucou")',
+        css: 'body { background-color: red; }'
+      }
     },
     cache: {
       previews: {
index f1f7afef92fde66b7588afab418ecd10c3241d3e..048135a345f2bc82497e3a59d31fe347dd864d70 100644 (file)
@@ -3,6 +3,7 @@
 import 'mocha'
 import * as chai from 'chai'
 import { About } from '../../../../shared/models/config/about.model'
+import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
 import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils'
 const expect = chai.expect
 
@@ -48,11 +49,13 @@ describe('Test config', function () {
 
   it('Should get the customized configuration', async function () {
     const res = await getCustomConfig(server.url, server.accessToken)
-    const data = res.body
+    const data = res.body as CustomConfig
 
     expect(data.instance.name).to.equal('PeerTube')
     expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
     expect(data.instance.terms).to.equal('No terms for now.')
+    expect(data.instance.customizations.css).to.be.empty
+    expect(data.instance.customizations.javascript).to.be.empty
     expect(data.cache.previews.size).to.equal(1)
     expect(data.signup.enabled).to.be.true
     expect(data.signup.limit).to.equal(4)
@@ -72,7 +75,11 @@ describe('Test config', function () {
       instance: {
         name: 'PeerTube updated',
         description: 'my super description',
-        terms: 'my super terms'
+        terms: 'my super terms',
+        customizations: {
+          javascript: 'alert("coucou")',
+          css: 'body { background-color: red; }'
+        }
       },
       cache: {
         previews: {
@@ -109,6 +116,8 @@ describe('Test config', function () {
     expect(data.instance.name).to.equal('PeerTube updated')
     expect(data.instance.description).to.equal('my super description')
     expect(data.instance.terms).to.equal('my super terms')
+    expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
+    expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
     expect(data.cache.previews.size).to.equal(2)
     expect(data.signup.enabled).to.be.false
     expect(data.signup.limit).to.equal(5)
@@ -136,6 +145,8 @@ describe('Test config', function () {
     expect(data.instance.name).to.equal('PeerTube updated')
     expect(data.instance.description).to.equal('my super description')
     expect(data.instance.terms).to.equal('my super terms')
+    expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
+    expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
     expect(data.cache.previews.size).to.equal(2)
     expect(data.signup.enabled).to.be.false
     expect(data.signup.limit).to.equal(5)
@@ -167,6 +178,11 @@ describe('Test config', function () {
     const res = await getCustomConfig(server.url, server.accessToken)
     const data = res.body
 
+    expect(data.instance.name).to.equal('PeerTube')
+    expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
+    expect(data.instance.terms).to.equal('No terms for now.')
+    expect(data.instance.customizations.css).to.be.empty
+    expect(data.instance.customizations.javascript).to.be.empty
     expect(data.cache.previews.size).to.equal(1)
     expect(data.signup.enabled).to.be.true
     expect(data.signup.limit).to.equal(4)
index 6ef0fc5a20795a9b0914010a7eb1713887658368..46d0a86efa31c77e8b8fb81a730bf9478557bafa 100644 (file)
@@ -3,6 +3,10 @@ export interface CustomConfig {
     name: string
     description: string
     terms: string
+    customizations: {
+      javascript?: string
+      css?: string
+    }
   }
 
   cache: {
diff --git a/shared/models/config/customization.model.ts b/shared/models/config/customization.model.ts
new file mode 100644 (file)
index 0000000..4e4d0d1
--- /dev/null
@@ -0,0 +1,8 @@
+export interface Customization {
+  instance: {
+    customization: {
+      javascript: string
+      css: string
+    }
+  }
+}
index 988dd71e3635ed1056df6f7159a87aa8887adcdb..004cf6ddb909ce5812eaca8b8e219c6a2d971d9a 100644 (file)
@@ -2,7 +2,11 @@ export interface ServerConfig {
   serverVersion: string
 
   instance: {
-    name: string
+    name: string;
+    customizations: {
+      javascript: string
+      css: string
+    }
   }
 
   signup: {