parent
403bb8bee6
commit
751e1b9c8c
15 changed files with 232 additions and 17 deletions
|
@ -24,3 +24,11 @@ demobot:
|
||||||
upstreams:
|
upstreams:
|
||||||
- name: vector
|
- name: vector
|
||||||
url: "https://scalar.vector.im/api"
|
url: "https://scalar.vector.im/api"
|
||||||
|
|
||||||
|
# IPs and CIDR ranges listed here will be blocked from being widgets.
|
||||||
|
# Note: Widgets may still be embedded with restricted content, although not through Dimension directly.
|
||||||
|
widgetBlacklist:
|
||||||
|
- 10.0.0.0/8
|
||||||
|
- 172.16.0.0/12
|
||||||
|
- 192.168.0.0/16
|
||||||
|
- 127.0.0.0/8
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -4441,6 +4441,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
||||||
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
|
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
|
||||||
},
|
},
|
||||||
|
"netmask": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU="
|
||||||
|
},
|
||||||
"ngx-modialog": {
|
"ngx-modialog": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/ngx-modialog/-/ngx-modialog-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/ngx-modialog/-/ngx-modialog-3.0.3.tgz",
|
||||||
|
@ -6659,8 +6664,7 @@
|
||||||
"querystring": {
|
"querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
|
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"querystring-es3": {
|
"querystring-es3": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
|
@ -8425,7 +8429,6 @@
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
|
||||||
"integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
|
"integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"punycode": "1.3.2",
|
"punycode": "1.3.2",
|
||||||
"querystring": "0.2.0"
|
"querystring": "0.2.0"
|
||||||
|
@ -8434,8 +8437,7 @@
|
||||||
"punycode": {
|
"punycode": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||||
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
|
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,10 +31,12 @@
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"matrix-js-sdk": "^0.8.2",
|
"matrix-js-sdk": "^0.8.2",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
|
"netmask": "^1.0.6",
|
||||||
"random-string": "^0.2.0",
|
"random-string": "^0.2.0",
|
||||||
"request": "^2.81.0",
|
"request": "^2.81.0",
|
||||||
"sequelize": "^4.7.5",
|
"sequelize": "^4.7.5",
|
||||||
"sqlite3": "^3.1.9",
|
"sqlite3": "^3.1.9",
|
||||||
|
"url": "^0.11.0",
|
||||||
"winston": "^2.3.1"
|
"winston": "^2.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -32,7 +32,7 @@ class Dimension {
|
||||||
this._app.use(bodyParser.json());
|
this._app.use(bodyParser.json());
|
||||||
|
|
||||||
// Register routes for angular app
|
// Register routes for angular app
|
||||||
this._app.get(['/riot', '/riot/*'], (req, res) => {
|
this._app.get(['/riot', '/riot/*', '/widget_wrapper', '/widget_wrapper/*'], (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, "..", "web-dist", "index.html"));
|
res.sendFile(path.join(__dirname, "..", "web-dist", "index.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
var IntegrationImpl = require("./integration/impl/index");
|
const IntegrationImpl = require("./integration/impl/index");
|
||||||
var Integrations = require("./integration/index");
|
const Integrations = require("./integration/index");
|
||||||
var _ = require("lodash");
|
const _ = require("lodash");
|
||||||
var log = require("./util/LogService");
|
const log = require("./util/LogService");
|
||||||
|
const request = require("request");
|
||||||
|
const dns = require("dns-then");
|
||||||
|
const urlParse = require("url");
|
||||||
|
const Netmask = require("netmask").Netmask;
|
||||||
|
const config = require("config");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API handler for the Dimension API
|
* API handler for the Dimension API
|
||||||
|
@ -26,6 +31,74 @@ class DimensionApi {
|
||||||
app.delete("/api/v1/dimension/integrations/:roomId/:type/:integrationType", this._removeIntegration.bind(this));
|
app.delete("/api/v1/dimension/integrations/:roomId/:type/:integrationType", this._removeIntegration.bind(this));
|
||||||
app.put("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._updateIntegrationState.bind(this));
|
app.put("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._updateIntegrationState.bind(this));
|
||||||
app.get("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._getIntegrationState.bind(this));
|
app.get("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._getIntegrationState.bind(this));
|
||||||
|
app.get("/api/v1/dimension/widgets/embeddable", this._checkEmbeddable.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkEmbeddable(req, res) {
|
||||||
|
// Unauthed endpoint.
|
||||||
|
|
||||||
|
var url = req.query.url;
|
||||||
|
var parts = urlParse.parse(url);
|
||||||
|
var processed = false;
|
||||||
|
|
||||||
|
// Only allow http and https
|
||||||
|
if (parts.protocol !== "http:" && parts.protocol !== "https:") {
|
||||||
|
res.status(400).send({error: "Invalid request scheme " + parts.protocol, canEmbed: false});
|
||||||
|
processed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the address is permitted for widgets
|
||||||
|
var hostname = parts.hostname.split(":")[0];
|
||||||
|
dns.resolve4(hostname).then(addresses => {
|
||||||
|
log.verbose("DimensionApi", "Hostname " + hostname + " resolves to " + addresses);
|
||||||
|
if (addresses.length == 0) {
|
||||||
|
res.status(400).send({error: "Unrecongized address", canEmbed: false});
|
||||||
|
processed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var ipOrCidr of config.get("widgetBlacklist")) {
|
||||||
|
var block = new Netmask(ipOrCidr);
|
||||||
|
for (var address of addresses) {
|
||||||
|
if (block.contains(address)) {
|
||||||
|
res.status(400).send({error: "Address not allowed", canEmbed: false});
|
||||||
|
processed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
log.verbose("DimensionApi", "Error resolving host " + hostname);
|
||||||
|
log.verbose("DimensionApi", err);
|
||||||
|
|
||||||
|
res.status(400).send({error: "DNS error", canEmbed: false});
|
||||||
|
processed = true;
|
||||||
|
}).then(() => {
|
||||||
|
if (processed) return;
|
||||||
|
|
||||||
|
// Verify that the content can actually be embedded (CORS)
|
||||||
|
request(url, (err, response) => {
|
||||||
|
if (err) {
|
||||||
|
log.verbose("DimensionApi", "Error contacting host " + hostname);
|
||||||
|
log.verbose("DimensionApi", err);
|
||||||
|
|
||||||
|
res.status(400).send({error: "Host error", canEmbed: false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
|
// 200 OK
|
||||||
|
var headers = response.headers;
|
||||||
|
var xFrameOptions = (headers['x-frame-options'] || '').toLowerCase();
|
||||||
|
|
||||||
|
if (xFrameOptions === 'sameorigin' || xFrameOptions === 'deny') {
|
||||||
|
res.status(400).send({error: "X-Frame-Options forbids embedding", canEmbed: false});
|
||||||
|
} else res.status(200).send({canEmbed: true});
|
||||||
|
} else {
|
||||||
|
res.status(400).send({error: "Unsuccessful status code: " + response.statusCode, canEmbed: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_getIntegration(integrationConfig, roomId, scalarToken) {
|
_getIntegration(integrationConfig, roomId, scalarToken) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { IrcApiService } from "./shared/irc-api.service";
|
||||||
import { TravisCiConfigComponent } from "./configs/travisci/travisci-config.component";
|
import { TravisCiConfigComponent } from "./configs/travisci/travisci-config.component";
|
||||||
import { CustomWidgetConfigComponent } from "./configs/widget/custom_widget/custom_widget-config.component";
|
import { CustomWidgetConfigComponent } from "./configs/widget/custom_widget/custom_widget-config.component";
|
||||||
import { MyFilterPipe } from "./shared/my-filter.pipe";
|
import { MyFilterPipe } from "./shared/my-filter.pipe";
|
||||||
|
import { WidgetWrapperComponent } from "./widget_wrapper/widget_wrapper.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -49,6 +50,7 @@ import { MyFilterPipe } from "./shared/my-filter.pipe";
|
||||||
TravisCiConfigComponent,
|
TravisCiConfigComponent,
|
||||||
CustomWidgetConfigComponent,
|
CustomWidgetConfigComponent,
|
||||||
MyFilterPipe,
|
MyFilterPipe,
|
||||||
|
WidgetWrapperComponent,
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
],
|
],
|
||||||
|
@ -57,6 +59,7 @@ import { MyFilterPipe } from "./shared/my-filter.pipe";
|
||||||
ScalarService,
|
ScalarService,
|
||||||
IntegrationService,
|
IntegrationService,
|
||||||
IrcApiService,
|
IrcApiService,
|
||||||
|
{provide: Window, useValue: window},
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
import { HomeComponent } from "./home/home.component";
|
import { HomeComponent } from "./home/home.component";
|
||||||
import { RiotComponent } from "./riot/riot.component";
|
import { RiotComponent } from "./riot/riot.component";
|
||||||
|
import { WidgetWrapperComponent } from "./widget_wrapper/widget_wrapper.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: "", component: HomeComponent},
|
{path: "", component: HomeComponent},
|
||||||
{path: "riot", component: RiotComponent},
|
{path: "riot", component: RiotComponent},
|
||||||
|
{path: "riot/widget_wrapper", component: WidgetWrapperComponent},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const routing = RouterModule.forRoot(routes);
|
export const routing = RouterModule.forRoot(routes);
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-12 removable" *ngFor="let widget of widgets trackById">
|
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
|
||||||
{{ widget.name || widget.url }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
|
{{ widget.name || widget.url }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
|
||||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
|
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
|
||||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
// component styles are encapsulated and only applied to their components
|
// component styles are encapsulated and only applied to their components
|
||||||
|
.widget-item {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ModalComponent, DialogRef } from "ngx-modialog";
|
import { ModalComponent, DialogRef } from "ngx-modialog";
|
||||||
import { WidgetComponent } from "../widget.component";
|
import { WidgetComponent, SCALAR_WIDGET_LINKS } from "../widget.component";
|
||||||
import { ScalarService } from "../../../shared/scalar.service";
|
import { ScalarService } from "../../../shared/scalar.service";
|
||||||
import { ConfigModalContext } from "../../../integration/integration.component";
|
import { ConfigModalContext } from "../../../integration/integration.component";
|
||||||
import { ToasterService } from "angular2-toaster";
|
import { ToasterService } from "angular2-toaster";
|
||||||
|
@ -21,23 +21,46 @@ export class CustomWidgetConfigComponent extends WidgetComponent implements Moda
|
||||||
public widgetUrl = "";
|
public widgetUrl = "";
|
||||||
|
|
||||||
private toggledWidgets: string[] = [];
|
private toggledWidgets: string[] = [];
|
||||||
|
private wrapperUrl = "";
|
||||||
|
|
||||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||||
private toaster: ToasterService,
|
private toaster: ToasterService,
|
||||||
scalarService: ScalarService) {
|
scalarService: ScalarService,
|
||||||
|
window: Window) {
|
||||||
super(scalarService, dialog.context.roomId);
|
super(scalarService, dialog.context.roomId);
|
||||||
|
|
||||||
this.getWidgetsOfType(WIDGET_DIM_CUSTOM, WIDGET_SCALAR_CUSTOM).then(widgets => {
|
this.getWidgetsOfType(WIDGET_DIM_CUSTOM, WIDGET_SCALAR_CUSTOM).then(widgets => {
|
||||||
this.widgets = widgets;
|
this.widgets = widgets;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.isUpdating = false;
|
this.isUpdating = false;
|
||||||
|
|
||||||
|
// Unwrap URLs for easy-editing
|
||||||
|
for (let widget of this.widgets) {
|
||||||
|
widget.url = this.getWrappedUrl(widget.url);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.wrapperUrl = window.location.origin + "/riot/widget_wrapper?url=";
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWrappedUrl(url: string): string {
|
||||||
|
const urls = [this.wrapperUrl].concat(SCALAR_WIDGET_LINKS);
|
||||||
|
for (var scalarUrl of urls) {
|
||||||
|
if (url.startsWith(scalarUrl)) {
|
||||||
|
return decodeURIComponent(url.substring(scalarUrl.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private wrapUrl(url: string): string {
|
||||||
|
return this.wrapperUrl + encodeURIComponent(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addWidget() {
|
public addWidget() {
|
||||||
let constructedWidget: Widget = {
|
let constructedWidget: Widget = {
|
||||||
id: "dimension-" + (new Date().getTime()),
|
id: "dimension-" + (new Date().getTime()),
|
||||||
url: this.widgetUrl,
|
url: this.wrapUrl(this.widgetUrl),
|
||||||
type: WIDGET_DIM_CUSTOM,
|
type: WIDGET_DIM_CUSTOM,
|
||||||
name: "Custom Widget",
|
name: "Custom Widget",
|
||||||
};
|
};
|
||||||
|
@ -45,6 +68,7 @@ export class CustomWidgetConfigComponent extends WidgetComponent implements Moda
|
||||||
this.isUpdating = true;
|
this.isUpdating = true;
|
||||||
this.scalarApi.setWidget(this.roomId, constructedWidget)
|
this.scalarApi.setWidget(this.roomId, constructedWidget)
|
||||||
.then(() => this.widgets.push(constructedWidget))
|
.then(() => this.widgets.push(constructedWidget))
|
||||||
|
.then(() => constructedWidget.url = this.getWrappedUrl(constructedWidget.url)) // unwrap for immediate editing
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isUpdating = false;
|
this.isUpdating = false;
|
||||||
this.widgetUrl = "";
|
this.widgetUrl = "";
|
||||||
|
@ -64,7 +88,7 @@ export class CustomWidgetConfigComponent extends WidgetComponent implements Moda
|
||||||
}
|
}
|
||||||
|
|
||||||
widget.name = widget.newName || "Custom Widget";
|
widget.name = widget.newName || "Custom Widget";
|
||||||
widget.url = widget.newUrl;
|
widget.url = this.wrapUrl(widget.newUrl);
|
||||||
|
|
||||||
this.isUpdating = true;
|
this.isUpdating = true;
|
||||||
this.scalarApi.setWidget(this.roomId, widget)
|
this.scalarApi.setWidget(this.roomId, widget)
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { ScalarService } from "../../shared/scalar.service";
|
import { ScalarService } from "../../shared/scalar.service";
|
||||||
import { Widget, ScalarToWidgets } from "../../shared/models/widget";
|
import { Widget, ScalarToWidgets } from "../../shared/models/widget";
|
||||||
|
|
||||||
|
export const SCALAR_WIDGET_LINKS = [
|
||||||
|
'https://scalar-staging.riot.im/scalar/api/widgets/generic.html?url=',
|
||||||
|
'https://scalar-staging.vector.im/scalar/api/widgets/generic.html?url=',
|
||||||
|
'https://scalar-develop.riot.im/scalar/api/widgets/generic.html?url=',
|
||||||
|
'https://demo.riot.im/scalar/api/widgets/generic.html?url=',
|
||||||
|
];
|
||||||
|
|
||||||
export class WidgetComponent {
|
export class WidgetComponent {
|
||||||
|
|
||||||
constructor(protected scalarApi: ScalarService, protected roomId: string) {
|
constructor(protected scalarApi: ScalarService, protected roomId: string) {
|
||||||
|
|
|
@ -34,4 +34,10 @@ export class ApiService {
|
||||||
return this.http.get(url, {params: {scalar_token: scalarToken}})
|
return this.http.get(url, {params: {scalar_token: scalarToken}})
|
||||||
.map(res => res.json()).toPromise();
|
.map(res => res.json()).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isEmbeddable(checkUrl: string): Promise<any> {
|
||||||
|
const url = "/api/v1/dimension/widgets/embeddable";
|
||||||
|
return this.http.get(url, {params: {url: checkUrl}})
|
||||||
|
.map(res => res.json()).toPromise();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
13
web/app/widget_wrapper/widget_wrapper.component.html
Normal file
13
web/app/widget_wrapper/widget_wrapper.component.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="control-page" *ngIf="isLoading || !canEmbed">
|
||||||
|
<div class="loading-badge" *ngIf="isLoading">
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin"></i>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
<div class="embed-failed" *ngIf="!isLoading && !canEmbed">
|
||||||
|
<p class="ban"><i class="fa fa-ban"></i></p>
|
||||||
|
<h4>Sorry, this content cannot be embedded</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<iframe [src]="embedUrl" *ngIf="!isLoading && canEmbed" frameborder="0" allowfullscreen></iframe>
|
||||||
|
</div>
|
41
web/app/widget_wrapper/widget_wrapper.component.scss
Normal file
41
web/app/widget_wrapper/widget_wrapper.component.scss
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// component styles are encapsulated and only applied to their components
|
||||||
|
.control-page {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #222;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-badge {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
position: relative;
|
||||||
|
top: calc(50% - 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-failed {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
top: calc(50% - 150px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-failed .ban {
|
||||||
|
font-size: 145px;
|
||||||
|
color: #bd362f;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
31
web/app/widget_wrapper/widget_wrapper.component.ts
Normal file
31
web/app/widget_wrapper/widget_wrapper.component.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { ApiService } from "../shared/api.service";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "my-widget-wrapper",
|
||||||
|
templateUrl: "widget_wrapper.component.html",
|
||||||
|
styleUrls: ["widget_wrapper.component.scss"],
|
||||||
|
})
|
||||||
|
export class WidgetWrapperComponent {
|
||||||
|
|
||||||
|
public isLoading = true;
|
||||||
|
public canEmbed = false;
|
||||||
|
public embedUrl: SafeUrl = null;
|
||||||
|
|
||||||
|
constructor(api: ApiService, activatedRoute: ActivatedRoute, sanitizer: DomSanitizer) {
|
||||||
|
let params: any = activatedRoute.snapshot.queryParams;
|
||||||
|
|
||||||
|
api.isEmbeddable(params.url).then(result => {
|
||||||
|
this.canEmbed = result.canEmbed;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.embedUrl = sanitizer.bypassSecurityTrustResourceUrl(params.url);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.canEmbed = false;
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue