Skip to content

Commit

Permalink
#24
Browse files Browse the repository at this point in the history
  • Loading branch information
Hakenadu committed Mar 16, 2023
1 parent f0174a5 commit e3a7cda
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 183 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ For the app published at https://plantuml.mseiche.de/ I'm using the following *f
```json
{
"share": {
"description": "<p>Your PlantUML spec will be stored symmetrically encrypted via <a href=\"https:\/\/en.wikipedia.org\/wiki\/WebDAV\">WebDAV<\/a>.<\/p><p>The information needed to decrypt the stored data is the id which is sent by your browser when accessing the data.<\/p><p class=\"mb-0\">Anyhow if you use this functionality you agree to my <a href=\"https:\/\/mseiche.de\/terms-of-service\">Terms of Service<\/a><\/p>"
"description": "<p>Your PlantUML spec will be stored symmetrically encrypted via <a href=\"https:\/\/en.wikipedia.org\/wiki\/WebDAV\">WebDAV<\/a>.<\/p><p>The information needed to decrypt the stored data is the id which is sent by your browser when accessing the data.<\/p><p class=\"mb-0\">Anyhow if you use this functionality you agree to my <a href=\"https:\/\/mseiche.de\/terms-of-service\">Terms of Service<\/a><\/p>",
"imageOnlyLinks": {
"visible": true,
"If an image only link is used, the key is inserted as a query parameter for a GET request. The key will therefore most likely appear in my reverse proxy logs when the Link is used to download the image."
}
},
"footer": {
"actions": [
Expand Down
5 changes: 4 additions & 1 deletion config/frontend-config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"share": {
"description": "just a placeholder"
"description": "just a placeholder",
"imageOnlyLinks": {
"visible": true
}
},
"footer": {
"actions": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.github.hakenadu.plantuml.controller;

import java.util.UUID;

import org.springframework.context.annotation.Profile;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.github.hakenadu.plantuml.service.ImageService;
import com.github.hakenadu.plantuml.service.document.DocumentService;
import com.github.hakenadu.plantuml.service.document.exception.DocumentServiceException;
import com.github.hakenadu.plantuml.service.exception.ImageServiceException;

@Profile({ "local", "webdav", "redis" })
@RestController
@RequestMapping("/documents/{id}")
@CrossOrigin
public class DocumentImagesController {

private final DocumentService documentService;
private final ImageService imageService;

public DocumentImagesController(final DocumentService documentService, final ImageService imageService) {
this.documentService = documentService;
this.imageService = imageService;
}

@GetMapping(path = "/images/svg", produces = "image/svg+xml")
public byte[] getSvg(final @PathVariable UUID id, final @RequestParam String key)
throws ImageServiceException, DocumentServiceException {
return imageService.getSvg(documentService.getDocument(id, key));
}

@GetMapping(path = "/images/png", produces = MediaType.IMAGE_PNG_VALUE)
public byte[] getPng(final @PathVariable UUID id, final @RequestParam String key)
throws ImageServiceException, DocumentServiceException {
return imageService.getPng(documentService.getDocument(id, key));
}
}
252 changes: 128 additions & 124 deletions plantuml-editor-frontend/src/app/services/config.service.ts
Original file line number Diff line number Diff line change
@@ -1,124 +1,128 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable, of} from 'rxjs';
import {environment} from '../../environments/environment';
import {DomSanitizer, SafeHtml, SafeUrl} from '@angular/platform-browser';
import {map} from 'rxjs/operators';

export type IconConfig = MaterialIconConfig | ImgIconConfig;

export interface MaterialIconConfig {
type: 'material';
name: string;
}

export interface ImgIconConfig {
type: 'img';
src: string | SafeUrl;
width?: string;
height?: string;
}

export type FooterActionConfig = PopupFooterActionConfig | LinkFooterActionConfig;

export interface PopupFooterActionConfig {
type: 'popup';
icon: IconConfig;
tooltip?: string;
content: string | SafeHtml;
}

export interface LinkFooterActionConfig {
type: 'link';
icon: IconConfig;
tooltip?: string;
href: string | SafeUrl;
}

export interface IntroConfig {
description?: string | SafeHtml;
slideshow?: {
showMessage?: boolean,
visible?: boolean
};
}

export interface FooterConfig {
actions?: FooterActionConfig[];
}

export interface ShareConfig {
description?: string | SafeHtml;
}

export interface FrontendConfig {
intro?: IntroConfig;
footer?: FooterConfig;
share?: ShareConfig;
}

@Injectable({
providedIn: 'root'
})
export class ConfigService {

private _config?: FrontendConfig;
private _config$: Observable<FrontendConfig>;

constructor(private httpClient: HttpClient,
private domSanitizer: DomSanitizer) {
if (environment.configUrl !== undefined && environment.configUrl !== null) {
this._config$ = this.httpClient.get<FrontendConfig>(`${environment.configUrl}/frontend/`);
} else { // for local development read config from environment.ts
const config = environment.config;
this._config$ = of(config ? <FrontendConfig>config : {});
}
this._config$.pipe(map(config => this.sanitize(config))).subscribe(config => this._config = config);
}

private sanitizeFooterConfig(config: FrontendConfig) {
if (!config?.footer?.actions) {
return;
}

for (const action of config.footer.actions) {
switch (action.type) {
case 'popup':
action.content = this.domSanitizer.bypassSecurityTrustHtml(<string>action.content);
break;
case 'link':
action.href = this.domSanitizer.bypassSecurityTrustUrl(<string>action.href);
break;
}
if (action.icon.type === 'img') {
action.icon.src = this.domSanitizer.bypassSecurityTrustUrl(<string>action.icon.src)
}
}
}

private sanitizeIntroConfig(config: FrontendConfig) {
if (config?.intro?.description) {
config.intro.description = this.domSanitizer.bypassSecurityTrustHtml(<string>config.intro.description);
}
}

private sanitizeShareConfig(config: FrontendConfig) {
if (config?.share?.description) {
config.share.description = this.domSanitizer.bypassSecurityTrustHtml(<string>config.share.description);
}
}

private sanitize(config: FrontendConfig): FrontendConfig {
this.sanitizeIntroConfig(config);
this.sanitizeFooterConfig(config);
this.sanitizeShareConfig(config);
return config;
}

get config$(): Observable<FrontendConfig> {
if (this._config) {
return of(this._config);
}
return this._config$;
}
}
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable, of} from 'rxjs';
import {environment} from '../../environments/environment';
import {DomSanitizer, SafeHtml, SafeUrl} from '@angular/platform-browser';
import {map} from 'rxjs/operators';

export type IconConfig = MaterialIconConfig | ImgIconConfig;

export interface MaterialIconConfig {
type: 'material';
name: string;
}

export interface ImgIconConfig {
type: 'img';
src: string | SafeUrl;
width?: string;
height?: string;
}

export type FooterActionConfig = PopupFooterActionConfig | LinkFooterActionConfig;

export interface PopupFooterActionConfig {
type: 'popup';
icon: IconConfig;
tooltip?: string;
content: string | SafeHtml;
}

export interface LinkFooterActionConfig {
type: 'link';
icon: IconConfig;
tooltip?: string;
href: string | SafeUrl;
}

export interface IntroConfig {
description?: string | SafeHtml;
slideshow?: {
showMessage?: boolean,
visible?: boolean
};
}

export interface FooterConfig {
actions?: FooterActionConfig[];
}

export interface ShareConfig {
description?: string | SafeHtml;
imageOnlyLinks?: {
visible?: boolean;
warningMessage?: string;
}
}

export interface FrontendConfig {
intro?: IntroConfig;
footer?: FooterConfig;
share?: ShareConfig;
}

@Injectable({
providedIn: 'root'
})
export class ConfigService {

private _config?: FrontendConfig;
private _config$: Observable<FrontendConfig>;

constructor(private httpClient: HttpClient,
private domSanitizer: DomSanitizer) {
if (environment.configUrl !== undefined && environment.configUrl !== null) {
this._config$ = this.httpClient.get<FrontendConfig>(`${environment.configUrl}/frontend/`);
} else { // for local development read config from environment.ts
const config = environment.config;
this._config$ = of(config ? <FrontendConfig>config : {});
}
this._config$.pipe(map(config => this.sanitize(config))).subscribe(config => this._config = config);
}

private sanitizeFooterConfig(config: FrontendConfig) {
if (!config?.footer?.actions) {
return;
}

for (const action of config.footer.actions) {
switch (action.type) {
case 'popup':
action.content = this.domSanitizer.bypassSecurityTrustHtml(<string>action.content);
break;
case 'link':
action.href = this.domSanitizer.bypassSecurityTrustUrl(<string>action.href);
break;
}
if (action.icon.type === 'img') {
action.icon.src = this.domSanitizer.bypassSecurityTrustUrl(<string>action.icon.src)
}
}
}

private sanitizeIntroConfig(config: FrontendConfig) {
if (config?.intro?.description) {
config.intro.description = this.domSanitizer.bypassSecurityTrustHtml(<string>config.intro.description);
}
}

private sanitizeShareConfig(config: FrontendConfig) {
if (config?.share?.description) {
config.share.description = this.domSanitizer.bypassSecurityTrustHtml(<string>config.share.description);
}
}

private sanitize(config: FrontendConfig): FrontendConfig {
this.sanitizeIntroConfig(config);
this.sanitizeFooterConfig(config);
this.sanitizeShareConfig(config);
return config;
}

get config$(): Observable<FrontendConfig> {
if (this._config) {
return of(this._config);
}
return this._config$;
}
}
34 changes: 31 additions & 3 deletions plantuml-editor-frontend/src/app/share/share.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ <h2 mat-dialog-title>Share your PlantUML source</h2>

<mat-divider class="mt-2 mb-2"></mat-divider>

<div class="row">
<div class="row align-items-center">
<div class="col-12 col-md">
<mat-form-field appearance="outline">
<mat-label>Key</mat-label>
Expand All @@ -16,15 +16,43 @@ <h2 mat-dialog-title>Share your PlantUML source</h2>
</mat-form-field>
</div>
<div class="col-12 col-md-auto mt-2 mt-md-0">
<mat-checkbox [formControl]="imageFullsize"
<mat-checkbox *ngIf="!imageOnlyLink.value"
[formControl]="imageFullsize"
color="primary"
matTooltip="if this box is checked the receiver of your link will have the image shown in fullsize with the editor hidden">
image fullsize
</mat-checkbox>
<div *ngIf="config?.share?.imageOnlyLinks?.visible && !imageFullsize.value">
<mat-checkbox [formControl]="imageOnlyLink"
color="primary"
matTooltip="if this box is checked an image download link is generated">
image only link
</mat-checkbox>
<mat-button-toggle-group *ngIf="imageOnlyLink.value"
[formControl]="imageType"
class="ms-2">
<mat-button-toggle value="svg"
matTooltip="select this toggle to generate the plantuml image as scalable vector graphic">
svg
</mat-button-toggle>
<mat-button-toggle value="png"
matTooltip="select this toggle to generate the plantuml image as portable network graphic">
png
</mat-button-toggle>
</mat-button-toggle-group>
</div>
</div>
</div>

<div *ngIf="link" class="mt-3 mb-0 p-2 alert alert-success flex-grow-1 d-flex flex-row align-items-center pe-2">
<div *ngIf="config?.share?.imageOnlyLinks?.warningMessage && imageOnlyLink.value"
class="mt-3 mb-0 p-2 flex-grow-1 d-flex flex-row align-items-center alert alert-warning">
<mat-icon>gpp_maybe</mat-icon>
<p class="m-0 ms-2">
{{config?.share?.imageOnlyLinks?.warningMessage}}
</p>
</div>

<div *ngIf="link" class="mt-3 mb-0 p-2 alert alert-success flex-grow-1 d-flex flex-row align-items-center">
<button mat-icon-button
(click)="copyLinkToClipboard()">
<mat-icon>content_copy</mat-icon>
Expand Down
Loading

0 comments on commit e3a7cda

Please sign in to comment.