self.assertEqual(default_features, ['deep-flatten', 'exclusive-lock',
'fast-diff', 'layering',
'object-map'])
+
+ def test_image_with_special_name(self):
+ rbd_name = 'test/rbd'
+ rbd_name_encoded = 'test%2Frbd'
+
+ self.create_image('rbd', rbd_name, 10240)
+ self.assertStatus(201)
+
+ img = self._get("/api/block/image/rbd/" + rbd_name_encoded)
+ self.assertStatus(200)
+
+ self._validate_image(img, name=rbd_name, size=10240,
+ num_objs=1, obj_size=4194304,
+ features_name=['deep-flatten',
+ 'exclusive-lock',
+ 'fast-diff', 'layering',
+ 'object-map'])
+
+ self.remove_image('rbd', rbd_name_encoded)
import os
import pkgutil
import sys
+from six import add_metaclass
+
+if sys.version_info >= (3, 0):
+ from urllib.parse import unquote # pylint: disable=no-name-in-module,import-error
+else:
+ from urllib import unquote # pylint: disable=no-name-in-module
+# pylint: disable=wrong-import-position
import cherrypy
-from six import add_metaclass
from .. import logger
from ..security import Scope, Permission
def _request_wrapper(func, method, json_response):
@wraps(func)
def inner(*args, **kwargs):
+ for key, value in kwargs.items():
+ # pylint: disable=undefined-variable
+ if (sys.version_info < (3, 0) and isinstance(value, unicode)) \
+ or isinstance(value, str):
+ kwargs[key] = unquote(value)
+
if method in ['GET', 'DELETE']:
ret = func(*args, **kwargs)
this.mode === this.rbdFormMode.copying
) {
this.route.params.subscribe((params: { pool: string; name: string; snap: string }) => {
- const poolName = params.pool;
- const rbdName = params.name;
- this.snapName = params.snap;
+ const poolName = decodeURIComponent(params.pool);
+ const rbdName = decodeURIComponent(params.name);
+ this.snapName = decodeURIComponent(params.snap);
this.rbdService.get(poolName, rbdName).subscribe((resp: RbdFormResponseModel) => {
this.setResponse(resp, this.snapName);
});
class="btn btn-sm btn-primary"
*ngIf="permission.update && (!permission.create || permission.create && selection.hasSingleSelection)"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}"
- routerLink="/rbd/edit/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}">
- <i class="fa fa-fw fa-pencil"></i><span i18n>Edit</span>
+ routerLink="/rbd/edit/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
+ <i class="fa fa-fw fa-pencil"></i>
+ <span i18n>Edit</span>
</button>
<button type="button"
class="btn btn-sm btn-primary"
*ngIf="permission.update"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
<a class="dropdown-item"
- routerLink="/rbd/edit/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}">
+ routerLink="/rbd/edit/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
<i class="fa fa-fw fa-pencil"></i>
<span i18n>Edit</span>
</a>
*ngIf="permission.create"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
<a class="dropdown-item"
- routerLink="/rbd/copy/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}">
+ routerLink="/rbd/copy/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
<i class="fa fa-fw fa-copy"></i>
<span i18n>Copy</span>
</a>
*ngIf="permission.create"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
<a class="dropdown-item"
- routerLink="/rbd/clone/{{ poolName }}/{{ rbdName }}/{{ selection.first()?.name }}">
+ routerLink="/rbd/clone/{{ poolName | encodeUri }}/{{ rbdName | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
<i class="fa fa-fw fa-clone"></i>
<span i18n>Clone</span>
</a>
<li role="menuitem"
*ngIf="permission.create"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
- <a class="dropdown-item" routerLink="/rbd/copy/{{ poolName }}/{{ rbdName }}/{{ selection.first()?.name }}">
- <i class="fa fa-fw fa-copy"></i><span i18n>Copy</span>
+ <a class="dropdown-item"
+ routerLink="/rbd/copy/{{ poolName | encodeUri }}/{{ rbdName | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
+ <i class="fa fa-fw fa-copy"></i>
+ <span i18n>Copy</span>
</a>
</li>
<li role="menuitem"
import { ComponentsModule } from '../../../shared/components/components.module';
import { DataTableModule } from '../../../shared/datatable/datatable.module';
import { Permissions } from '../../../shared/models/permissions';
+import { PipesModule } from '../../../shared/pipes/pipes.module';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { NotificationService } from '../../../shared/services/notification.service';
import { ServicesModule } from '../../../shared/services/services.module';
ServicesModule,
ApiModule,
HttpClientTestingModule,
- RouterTestingModule
+ RouterTestingModule,
+ PipesModule
],
providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }]
});
if (!params.hasOwnProperty('bucket')) {
return;
}
+ params.bucket = decodeURIComponent(params.bucket);
this.loading = true;
// Load the bucket data in 'edit' mode.
this.editing = true;
class="btn btn-sm btn-primary"
[ngClass]="{'disabled': !selection.hasSelection}"
*ngIf="permission.update && (!permission.create && !selection.hasMultiSelection || selection.hasSingleSelection)"
- routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket }}">
+ routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket | encodeUri }}">
<i class="fa fa-fw fa-pencil"></i>
<ng-container i18n>Edit</ng-container>
</button>
*ngIf="permission.update"
[ngClass]="{'disabled': !selection.hasSingleSelection}">
<a class="dropdown-item"
- routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket }}"
+ routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket | encodeUri }}"
i18n>
<i class="fa fa-fw fa-pencil"></i>
Edit
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { cdEncode } from '../decorators/cd-encode';
import { ApiModule } from './api.module';
+@cdEncode
@Injectable({
providedIn: ApiModule
})
const req = httpTesting.expectOne('api/block/image/poolName/rbdName/snap/snapshotName');
expect(req.request.method).toBe('DELETE');
});
+
+ describe('Encode decorator', () => {
+ it('should encode the imageName', () => {
+ service.get('poolName', 'rbd/name').subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName/rbd%2Fname');
+ expect(req.request.method).toBe('GET');
+ });
+ });
});
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { cdEncode } from '../decorators/cd-encode';
import { ApiModule } from './api.module';
+@cdEncode
@Injectable({
providedIn: ApiModule
})
import { forkJoin as observableForkJoin, of as observableOf } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
+import { cdEncode } from '../decorators/cd-encode';
import { ApiModule } from './api.module';
+@cdEncode
@Injectable({
providedIn: ApiModule
})
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { cdEncode } from '../decorators/cd-encode';
import { ApiModule } from './api.module';
+@cdEncode
@Injectable({
providedIn: ApiModule
})
import { forkJoin as observableForkJoin, of as observableOf } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
+import { cdEncode } from '../decorators/cd-encode';
import { ApiModule } from './api.module';
+@cdEncode
@Injectable({
providedIn: ApiModule
})
--- /dev/null
+import * as _ from 'lodash';
+
+/**
+ * This decorator can be used in a class or method.
+ * It will encode all the string parameters of all the methods of a class
+ * or, if applied on a method, the specified method.
+ *
+ * @export
+ * @param {Function} [target=null]
+ * @returns {*}
+ */
+export function cdEncode(target: Function = null): any {
+ if (target) {
+ encodeClass(target);
+ } else {
+ return encodeMethod();
+ }
+}
+
+function encodeClass(target: Function) {
+ for (const propertyName of Object.keys(target.prototype)) {
+ const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName);
+ const isMethod = descriptor.value instanceof Function;
+ if (!isMethod) {
+ continue;
+ }
+
+ const originalMethod = descriptor.value;
+ descriptor.value = function(...args: any[]) {
+ args.forEach((arg, i, argsArray) => {
+ if (_.isString(arg)) {
+ argsArray[i] = encodeURIComponent(arg);
+ }
+ });
+
+ const result = originalMethod.apply(this, args);
+ return result;
+ };
+
+ Object.defineProperty(target.prototype, propertyName, descriptor);
+ }
+}
+
+function encodeMethod() {
+ return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
+ if (descriptor === undefined) {
+ descriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
+ }
+ const originalMethod = descriptor.value;
+
+ descriptor.value = function() {
+ const args = [];
+
+ for (let i = 0; i < arguments.length; i++) {
+ if (_.isString(arguments[i])) {
+ args[i] = encodeURIComponent(arguments[i]);
+ } else {
+ args[i] = arguments[i];
+ }
+ }
+
+ const result = originalMethod.apply(this, args);
+ return result;
+ };
+ };
+}
--- /dev/null
+import { EncodeUriPipe } from './encode-uri.pipe';
+
+describe('EncodeUriPipe', () => {
+ it('create an instance', () => {
+ const pipe = new EncodeUriPipe();
+ expect(pipe).toBeTruthy();
+ });
+
+ it('should transforms the value', () => {
+ const pipe = new EncodeUriPipe();
+ expect(pipe.transform('rbd/name')).toBe('rbd%2Fname');
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'encodeUri'
+})
+export class EncodeUriPipe implements PipeTransform {
+ transform(value: any): any {
+ return encodeURIComponent(value);
+ }
+}
import { CephShortVersionPipe } from './ceph-short-version.pipe';
import { DimlessBinaryPipe } from './dimless-binary.pipe';
import { DimlessPipe } from './dimless.pipe';
+import { EncodeUriPipe } from './encode-uri.pipe';
import { FilterPipe } from './filter.pipe';
import { HealthColorPipe } from './health-color.pipe';
import { ListPipe } from './list.pipe';
ListPipe,
FilterPipe,
CdDatePipe,
- EmptyPipe
+ EmptyPipe,
+ EncodeUriPipe
],
exports: [
DimlessBinaryPipe,
ListPipe,
FilterPipe,
CdDatePipe,
- EmptyPipe
+ EmptyPipe,
+ EncodeUriPipe
],
providers: [
DatePipe,
RelativeDatePipe,
ListPipe,
CdDatePipe,
- EmptyPipe
+ EmptyPipe,
+ EncodeUriPipe
]
})
export class PipesModule {}