diff --git a/rest/index.php b/rest/index.php index d07466cee6263a6f6988777f9cdd73ae687fc0ed..226bfcd5c76e061de4634d437f801a4837f9e38b 100755 --- a/rest/index.php +++ b/rest/index.php @@ -99,6 +99,7 @@ $app->get('/users/{id}', \User\controllers\UserController::class . ':getById'); $app->put('/users/{id}', \User\controllers\UserController::class . ':update'); $app->delete('/users/{id}', \User\controllers\UserController::class . ':delete'); $app->get('/users/{id}/picture', \User\controllers\UserController::class . ':getPictureById'); +$app->put('/users/{id}/picture', \User\controllers\UserController::class . ':updatePicture'); $app->get('/users/{id}/substitute', \User\controllers\UserController::class . ':getSubstituteById'); $app->put('/users/{id}/preferences', \User\controllers\UserController::class . ':updatePreferences'); $app->put('/users/{id}/substitute', \User\controllers\UserController::class . ':updateSubstitute'); diff --git a/src/app/user/controllers/UserController.php b/src/app/user/controllers/UserController.php index fdead4ca42a556900097e35bd8125405d4bad33c..1e6877b9931bf01c1d807fc44be3ca4d60b5b0e8 100755 --- a/src/app/user/controllers/UserController.php +++ b/src/app/user/controllers/UserController.php @@ -176,30 +176,65 @@ class UserController 'email' => $body['email'] ]; - if ($GLOBALS['id'] == $args['id'] && !empty($body['picture'])) { - $infoContent = ''; - if (preg_match('/^data:image\/(\w+);base64,/', $body['picture'])) { - $infoContent = substr($body['picture'], 0, strpos($body['picture'], ',') + 1); - $body['picture'] = substr($body['picture'], strpos($body['picture'], ',') + 1); - } - $picture = base64_decode($body['picture']); - $finfo = new \finfo(FILEINFO_MIME_TYPE); - $mimeType = $finfo->buffer($picture); - $type = explode('/', $mimeType); + UserModel::update([ + 'set' => $set, + 'where' => ['id = ?'], + 'data' => [$args['id']] + ]); - if ($type[0] != 'image') { - return $response->withStatus(400)->withJson(['errors' => 'Picture is not an image']); - } + HistoryController::add([ + 'code' => 'OK', + 'objectType' => 'users', + 'objectId' => $args['id'], + 'type' => 'MODIFICATION', + 'message' => "{userUpdated} : {$body['firstname']} {$body['lastname']}" + ]); - if (!empty($body['pictureOrientation'])) { - $imagick = new \Imagick(); - $imagick->readImageBlob(base64_decode($body['picture'])); - $imagick->rotateImage(new \ImagickPixel(), $body['pictureOrientation']); - $body['picture'] = base64_encode($imagick->getImageBlob()); - } - $set['picture'] = $infoContent . $body['picture']; + return $response->withJson(['user' => UserController::getUserInformationsById(['id' => $args['id']])]); + } + + public function updatePicture(Request $request, Response $response, array $args) + { + if ($GLOBALS['id'] != $args['id']) { + return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']); } + $body = $request->getParsedBody(); + + if (!Validator::stringType()->notEmpty()->validate($body['picture'])) { + return $response->withStatus(400)->withJson(['errors' => 'Body picture is empty']); + } + + $user = UserModel::getById(['id' => $args['id'], 'select' => ['firstname', 'lastname']]); + if (empty($user)) { + return $response->withStatus(400)->withJson(['errors' => 'User does not exist']); + } + + $infoContent = ''; + if (preg_match('/^data:image\/(\w+);base64,/', $body['picture'])) { + $infoContent = substr($body['picture'], 0, strpos($body['picture'], ',') + 1); + $body['picture'] = substr($body['picture'], strpos($body['picture'], ',') + 1); + } + $picture = base64_decode($body['picture']); + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $mimeType = $finfo->buffer($picture); + $type = explode('/', $mimeType); + + if ($type[0] != 'image') { + return $response->withStatus(400)->withJson(['errors' => 'Picture is not an image']); + } + + if (!empty($body['pictureOrientation'])) { + $imagick = new \Imagick(); + $imagick->readImageBlob(base64_decode($body['picture'])); + $imagick->rotateImage(new \ImagickPixel(), $body['pictureOrientation']); + $body['picture'] = base64_encode($imagick->getImageBlob()); + } + + $set = [ + 'picture' => $infoContent . $body['picture'] + ]; + UserModel::update([ 'set' => $set, 'where' => ['id = ?'], @@ -211,10 +246,10 @@ class UserController 'objectType' => 'users', 'objectId' => $args['id'], 'type' => 'MODIFICATION', - 'message' => "{userUpdated} : {$body['firstname']} {$body['lastname']}" + 'message' => "{userUpdated} : {$user['firstname']} {$user['lastname']}" ]); - return $response->withJson(['user' => UserController::getUserInformationsById(['id' => $args['id']])]); + return $response->withJson(['success']); } public function delete(Request $request, Response $response, array $args) diff --git a/src/frontend/app/profile/profile.component.html b/src/frontend/app/profile/profile.component.html index 0bd3f49d47bf8d81b9b2b5d9876c01a53702fb74..146d6c83c01c606c3a3486dfc8b18e19bf79ec0e 100644 --- a/src/frontend/app/profile/profile.component.html +++ b/src/frontend/app/profile/profile.component.html @@ -7,8 +7,8 @@ <div class="user"> {{'lang.myProfil' | translate}} </div> - <div class="avatarProfile" - [ngStyle]="{'background': 'url(' + this.profileInfo.picture + ') no-repeat scroll center center / cover'}" + <div #avatarProfile class="avatarProfile" + [ngStyle]="{'background': 'url(' + this.avatarInfo.picture + ') no-repeat scroll center center / cover'}" (tap)="uploadFile.click();"> </div> <input #uploadFile type="file" style="display:none;" (change)="handleFileInput($event.target.files)"> @@ -93,8 +93,8 @@ <legend align="left">{{'lang.notifications' | translate}}</legend> <div class="form-container"> <div class="form-2-col"> - <mat-slide-toggle [checked]="this.profileInfo.preferences.notifications" - (change)="this.profileInfo.preferences.notifications=!this.profileInfo.preferences.notifications" + <mat-slide-toggle [checked]="this.preferenceInfo.notifications" + (change)="this.preferenceInfo.notifications=!this.preferenceInfo.notifications" color="primary">{{'lang.receiveNotif' | translate}}</mat-slide-toggle> </div> <div class="form-2-col" style="text-align: justify;font-size: 12px;"> @@ -110,7 +110,7 @@ <div class="form-2-col"> <mat-form-field> <mat-select #langSelect name="langUser" - [(ngModel)]="this.profileInfo.preferences.lang"> + [(ngModel)]="this.preferenceInfo.lang"> <mat-option *ngFor="let lang of this.profileInfo['availableLanguages']" [value]="lang">{{'lang.'+lang | translate }}</mat-option> </mat-select> @@ -129,7 +129,7 @@ <div class="form-2-col"> <mat-form-field> <mat-select name="writingMode" - [(ngModel)]="this.profileInfo.preferences.writingMode"> + [(ngModel)]="this.preferenceInfo.writingMode"> <mat-option value="direct">{{'lang.free' | translate}}</mat-option> <mat-option value="stylus">{{'lang.appleStylus' | translate}} <i class="fab fa-apple"></i> @@ -137,11 +137,11 @@ </mat-select> </mat-form-field> </div> - <div *ngIf="this.profileInfo.preferences.writingMode == 'stylus'" class="form-2-col" + <div *ngIf="this.preferenceInfo.writingMode == 'stylus'" class="form-2-col" style="text-align: justify;font-size: 12px;" [innerHTML]="'lang.freeModeInfo' | translate"> </div> - <div *ngIf="this.profileInfo.preferences.writingMode == 'direct'" class="form-2-col" + <div *ngIf="this.preferenceInfo.writingMode == 'direct'" class="form-2-col" style="text-align: justify;font-size: 12px;" [innerHTML]="'lang.standardModeInfo' | translate"> </div> @@ -156,7 +156,7 @@ <div class="form-2-col"> <mat-form-field> <mat-select name="writingSize" - [(ngModel)]="this.profileInfo.preferences.writingSize" + [(ngModel)]="this.preferenceInfo.writingSize" (selectionChange)="drawSample();"> <mat-option *ngFor='let in of counter(10) ;let i = index' [value]="i+1"> {{i+1}}</mat-option> @@ -176,7 +176,7 @@ <div class="form-2-col"> <mat-form-field> <mat-select name="writingColor" - [(ngModel)]="this.profileInfo.preferences.writingColor"> + [(ngModel)]="this.preferenceInfo.writingColor"> <mat-option style="color:#000000" value="#000000"> {{'lang.black' | translate}}</mat-option> <mat-option style="color:#1a75ff" value="#1a75ff"> @@ -188,7 +188,7 @@ </div> <div class="form-2-col"> <div style="height:50px;width:100px;margin:auto;" - [style.backgroundColor]="this.profileInfo.preferences.writingColor"> + [style.backgroundColor]="this.preferenceInfo.writingColor"> </div> </div> </div> diff --git a/src/frontend/app/profile/profile.component.ts b/src/frontend/app/profile/profile.component.ts index 96cbdc2d006f7836383da2463456394222f881a8..8adf05a009ac721a32381a4beeb0f412396a6b10 100644 --- a/src/frontend/app/profile/profile.component.ts +++ b/src/frontend/app/profile/profile.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit, Input, ViewChild } from '@angular/core'; +import { Component, OnInit, Input, ViewChild, Renderer2, ElementRef } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; -import { MatIconRegistry, MatSidenav, MatExpansionPanel, MatTabGroup } from '@angular/material'; +import { MatSidenav, MatExpansionPanel, MatTabGroup } from '@angular/material'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { SignaturesContentService } from '../service/signatures.service'; import { NotificationService } from '../service/notification.service'; @@ -9,9 +9,9 @@ import * as EXIF from 'exif-js'; import { TranslateService } from '@ngx-translate/core'; import { FiltersService } from '../service/filters.service'; import { Router } from '@angular/router'; -import { finalize, tap, exhaustMap, filter, catchError } from 'rxjs/operators'; +import { tap, exhaustMap, filter, catchError, takeUntil, finalize } from 'rxjs/operators'; import { AuthService } from '../service/auth.service'; -import { of } from 'rxjs'; +import { of, Subject } from 'rxjs'; @Component({ selector: 'app-my-profile', @@ -26,11 +26,17 @@ export class ProfileComponent implements OnInit { @ViewChild('passwordContent') passwordContent: MatExpansionPanel; + @ViewChild('avatarProfile') avatarProfile: ElementRef; profileInfo: any = { substitute: null, preferences: [] }; + preferenceInfo: any = {}; + avatarInfo: any = { + picture: '', + pictureOrientation : '' + }; hideCurrentPassword: Boolean = true; hideNewPassword: Boolean = true; hideNewPasswordConfirm: Boolean = true; @@ -65,17 +71,34 @@ export class ProfileComponent implements OnInit { msgButton = 'lang.validate'; loading: boolean = false; - constructor(private translate: TranslateService, public http: HttpClient, private router: Router, public sanitizer: DomSanitizer, public notificationService: NotificationService, public signaturesService: SignaturesContentService, public authService: AuthService, private cookieService: CookieService, public filtersService: FiltersService) { } + constructor(private translate: TranslateService, + public http: HttpClient, + private router: Router, + public sanitizer: DomSanitizer, + public notificationService: NotificationService, + public signaturesService: SignaturesContentService, + public authService: AuthService, + private cookieService: CookieService, + public filtersService: FiltersService, + private renderer: Renderer2) { } ngOnInit(): void { + this.initProfileInfo(); + } + + initProfileInfo() { this.profileInfo = JSON.parse(JSON.stringify(this.authService.user)); + this.preferenceInfo = this.profileInfo.preferences; + this.avatarInfo.picture = this.profileInfo.picture; + delete this.profileInfo.picture; + delete this.profileInfo.preferences; } closeProfile() { - $('.avatarProfile').css({ 'transform': 'rotate(0deg)' }); - $('.avatarProfile').css({ 'content': '' }); + this.renderer.setStyle(this.avatarProfile.nativeElement, 'transform', 'rotate(0deg)'); + this.renderer.setStyle(this.avatarProfile.nativeElement, 'content', ''); setTimeout(() => { - this.profileInfo = JSON.parse(JSON.stringify(this.authService.user)); + this.initProfileInfo(); }, 200); this.passwordContent.close(); @@ -187,34 +210,15 @@ export class ProfileComponent implements OnInit { submitProfile() { this.disableState = true; this.msgButton = 'lang.sending'; - const profileToSend = { - 'firstname': this.profileInfo.firstname, - 'lastname': this.profileInfo.lastname, - 'email': this.profileInfo.email, - 'picture': this.profileInfo.picture, - 'preferences': this.profileInfo.preferences, - 'substitute': this.profileInfo.substitute, - }; - - if (this.profileInfo.picture === this.authService.user.picture) { - profileToSend.picture = ''; - } else { - const orientation = $('.avatarProfile').css('content'); - profileToSend['pictureOrientation'] = orientation.replace(/\"/g, ''); - } - this.http.put('../rest/users/' + this.authService.user.id, profileToSend).pipe( - tap((data: any) => { - this.authService.user.picture = data.user.picture; - this.profileInfo.picture = data.user.picture; - }), - exhaustMap(() => this.http.put('../rest/users/' + this.authService.user.id + '/preferences', profileToSend.preferences)), + this.http.put('../rest/users/' + this.authService.user.id, this.profileInfo).pipe( + exhaustMap(() => this.http.put('../rest/users/' + this.authService.user.id + '/preferences', this.preferenceInfo)), tap(() => { this.disableState = false; this.msgButton = 'lang.validate'; - this.setLang(this.authService.user.preferences.lang); - this.cookieService.set('maarchParapheurLang', this.authService.user.preferences.lang); - $('.avatarProfile').css({ 'transform': 'rotate(0deg)' }); + this.setLang(this.preferenceInfo.lang); + this.cookieService.set('maarchParapheurLang', this.preferenceInfo.lang); + // this.renderer.setStyle(this.avatarProfile.nativeElement, 'transform', 'rotate(0deg)'); this.authService.updateUserInfoWithTokenRefresh(); }), exhaustMap(() => { @@ -248,6 +252,25 @@ export class ProfileComponent implements OnInit { ).subscribe(); } + changePicture() { + this.msgButton = 'lang.sending'; + this.disableState = true; + this.http.put('../rest/users/' + this.authService.user.id + '/picture', this.avatarInfo).pipe( + tap(() => { + this.authService.user.picture = this.avatarInfo.picture; + this.renderer.setStyle(this.avatarProfile.nativeElement, 'background-size', 'cover'); + this.renderer.setStyle(this.avatarProfile.nativeElement, 'background-position', 'center'); + this.renderer.setStyle(this.avatarProfile.nativeElement, 'transform', 'rotate(' + this.avatarInfo.pictureOrientation + 'deg)'); + this.renderer.setStyle(this.avatarProfile.nativeElement, 'content', '\'' + this.avatarInfo.pictureOrientation + '\''); + this.notificationService.success('lang.profileUpdated'); + }), + finalize(() => { + this.msgButton = 'lang.validate'; + this.disableState = false; + }) + ).subscribe(); + } + selectSubstitute(ev: any) { if (this.profileInfo.substitute !== null) { alert(this.translate.instant('lang.substitutionWarn')); @@ -286,37 +309,16 @@ export class ProfileComponent implements OnInit { handleFileInput(files: FileList) { this.passwordContent.close(); const fileToUpload = files.item(0); - $('.avatarProfile').css({ 'content': '' }); + this.renderer.setStyle(this.avatarProfile.nativeElement, 'content', ''); if (fileToUpload.size <= 5000000) { - if (['image/png', 'image/svg+xml', 'image/jpg', 'image/jpeg', 'image/gif'].indexOf(fileToUpload.type) !== -1) { + if (['image/png', 'image/jpg', 'image/jpeg', 'image/gif'].indexOf(fileToUpload.type) !== -1) { const myReader: FileReader = new FileReader(); myReader.onloadend = (e) => { - this.profileInfo.picture = myReader.result; const image = new Image(); - image.src = myReader.result.toString(); - image.onload = function () { - EXIF.getData((image as any), () => { - let deg = 0; - const orientation = EXIF.getTag(this, 'Orientation'); - switch (orientation) { - case 3: - deg = 180; - break; - case 6: - deg = 90; - break; - case 8: - deg = -90; - break; - } - $('.avatarProfile').css({ 'background-size': 'cover' }); - $('.avatarProfile').css({ 'background-position': 'center' }); - $('.avatarProfile').css({ 'transform': 'rotate(' + deg + 'deg)' }); - $('.avatarProfile').css({ 'content': '\'' + deg + '\'' }); - }); - }; + this.avatarInfo.picture = myReader.result; + image.onload = () => this.fixImgOrientation(image); }; myReader.readAsDataURL(fileToUpload); } else { @@ -327,12 +329,32 @@ export class ProfileComponent implements OnInit { } } + fixImgOrientation(image: any) { + EXIF.getData((image as any), () => { + let deg = 0; + const orientation = EXIF.getTag(image, 'Orientation'); + switch (orientation) { + case 3: + deg = 180; + break; + case 6: + deg = 90; + break; + case 8: + deg = -90; + break; + } + this.avatarInfo.pictureOrientation = deg; + this.changePicture(); + }); + } + drawSample() { const c = document.getElementById('sampleNote'); const ctx = (<HTMLCanvasElement>c).getContext('2d'); ctx.clearRect(0, 0, 150, 150); ctx.beginPath(); - ctx.lineWidth = this.profileInfo.preferences.writingSize; + ctx.lineWidth = this.preferenceInfo.writingSize; ctx.moveTo(0, 0); ctx.lineTo(150, 150); ctx.moveTo(150, 0);