Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions js/app/directives/icon.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* # passwordGen
*/
angular.module('passmanApp').directive('credentialIcon', [
'$window', 'UrlService', function($window, UrlService) {
'$window', 'UrlService', 'Utf8Base64Service', function($window, UrlService, Utf8Base64Service) {
return {
template: '<img ng-src="{{ iconUrl }}" class="icon-image-size">',
restrict: 'E',
Expand All @@ -46,7 +46,7 @@
scope.iconUrl = 'data:image/'+ scope.credential.icon.type +';base64,' + scope.credential.icon.content;
} else {
if(scope.credential.url) {
var url = window.btoa(angular.copy(scope.credential.url)).replace('/', '_');
var url = Utf8Base64Service.encode(scope.credential.url).replace('/', '_');
scope.iconUrl = UrlService.generateUrl('/api/v2/icon/') + url + '/' +
scope.credential.credential_id;
}
Expand Down
4 changes: 2 additions & 2 deletions js/app/directives/iconpicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* # passwordGen
*/
angular.module('passmanApp').directive('iconPicker', [
'$window', 'IconService', '$http', 'NotificationService', 'UrlService', '$translate', function($window, IconService, $http, NotificationService, UrlService, $translate) {
'$window', 'IconService', '$http', 'NotificationService', 'UrlService', '$translate', 'Utf8Base64Service', function($window, IconService, $http, NotificationService, UrlService, $translate, Utf8Base64Service) {
return {
templateUrl: 'views/partials/icon-picker.html',
restrict: 'A',
Expand Down Expand Up @@ -117,7 +117,7 @@

scope.refreshUrlIcon = function(){
NotificationService.showNotification($translate.instant('use.icon.refresh.trying'), 5000);
var queryUrl = UrlService.generateUrl('/api/v2/geticon/'+btoa(scope.credential.url));
var queryUrl = UrlService.generateUrl('/api/v2/geticon/' + Utf8Base64Service.encode(scope.credential.url));
$http.get(queryUrl).then(function (response) {
if(typeof response.data.content !== 'undefined'){
scope.customIcon = {};
Expand Down
4 changes: 2 additions & 2 deletions js/app/services/credentialservice.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* Service in the passmanApp.
*/
angular.module('passmanApp')
.service('CredentialService', ['$http', 'EncryptService', 'VaultService', 'FileService', 'UrlService', function ($http, EncryptService, VaultService, FileService, UrlService) {
.service('CredentialService', ['$http', 'EncryptService', 'VaultService', 'FileService', 'UrlService', 'Utf8Base64Service', function ($http, EncryptService, VaultService, FileService, UrlService, Utf8Base64Service) {
var credential = {
'credential_id': null,
'guid': null,
Expand Down Expand Up @@ -182,7 +182,7 @@
},
updateRevision: function (revision) {
var _revision = angular.copy(revision);
_revision.credential_data = window.btoa(JSON.stringify(_revision.credential_data));
_revision.credential_data = Utf8Base64Service.encode(JSON.stringify(_revision.credential_data));
var queryUrl = UrlService.generateUrl('/api/v2/credentials/' + revision.credential_data.guid + '/revision/' + revision.revision_id);
return $http.patch(queryUrl, _revision).then(function (response) {
if (response.data) {
Expand Down
58 changes: 58 additions & 0 deletions js/app/services/utf8base64service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Nextcloud - passman
*
* @copyright Copyright (c) 2026, Timo Triebensky (timo@binsky.org)
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

/* global TextEncoder, TextDecoder, Uint8Array */
(function () {
'use strict';

/**
* @ngdoc service
* @name passmanApp.Utf8Base64Service
* @description
* UTF-8 base64 for text that may contain characters outside Latin-1.
* window.btoa throws on those characters. decode runs window.atob and then reads the bytes as UTF-8,
* which is the same recovery as PHP base64_decode + json_decode. ASCII text matches the old btoa/atob pair.
* Since the old frontend stack does not provide js-base64, let's simply use the existing TextEncoder and TextDecoder APIs.
*/
angular.module('passmanApp')
.service('Utf8Base64Service', [function () {
return {
encode: function (value) {
var bytes = new TextEncoder().encode(value);
var binary = '';
var i;
for (i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
},
decode: function (value) {
var binary = window.atob(value);
var bytes = new Uint8Array(binary.length);
var i;
for (i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
}
};
}]);
}());
6 changes: 3 additions & 3 deletions js/app/services/vaultservice.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
* Service in the passmanApp.
*/
angular.module('passmanApp')
.service('VaultService', ['UrlService', '$http', function (UrlService, $http) {
.service('VaultService', ['UrlService', '$http', 'Utf8Base64Service', function (UrlService, $http, Utf8Base64Service) {
// AngularJS will instantiate a singleton by calling "new" on this function
var _this = this;
var _activeVault;
Expand Down Expand Up @@ -85,7 +85,7 @@
return $http.get(queryUrl).then(function (response) {
if (response.data) {
if (response.data.vault_settings) {
response.data.vault_settings = JSON.parse(window.atob(response.data.vault_settings));
response.data.vault_settings = JSON.parse(Utf8Base64Service.decode(response.data.vault_settings));
} else {
response.data.vault_settings = {};
}
Expand All @@ -100,7 +100,7 @@
delete _vault.defaultVaultPass;
delete _vault.defaultVault;
delete _vault.vaultKey;
_vault.vault_settings = window.btoa(JSON.stringify(_vault.vault_settings));
_vault.vault_settings = Utf8Base64Service.encode(JSON.stringify(_vault.vault_settings));
var queryUrl = UrlService.generateUrl('/api/v2/vaults/' + _vault.guid);
return $http.patch(queryUrl, _vault).then(function (response) {
if (response.data) {
Expand Down
1 change: 1 addition & 0 deletions templates/bookmarklet.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/filters/escapeHTML', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/filters/as', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/filters/toHHMMSS', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/utf8base64service', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/vaultservice', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/credentialservice', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/settingsservice', 'core');
Expand Down
1 change: 1 addition & 0 deletions templates/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/filters/as', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/filters/credentialsearch', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/filters/toHHMMSS', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/utf8base64service', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/cacheservice', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/vaultservice', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/credentialservice', 'core');
Expand Down
1 change: 1 addition & 0 deletions templates/public_share.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/filters/propsfilter', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/filters/byte', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/filters/escapeHTML', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/utf8base64service', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/vaultservice', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/credentialservice', 'core');
Util::addScript(MyAppTemplateConfig::APP_ID, 'app/services/settingsservice', 'core');
Expand Down
12 changes: 12 additions & 0 deletions tests/Unit/Db/CredentialRevisionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,16 @@ public function testJsonSerialize(): void {

$this->assertEquals($expected, $this->revision->jsonSerialize());
}

public function testJsonSerializeDecodesUtf8EuroInCredentialLabel(): void {
$this->revision->setCredentialData(
'eyJndWlkIjoiODMxRDM0RkMtRTdDMC00NTQxLUJENjgtNTA3NzQ3ODI0MTkyIiwibGFiZWwiOiJCYW5rIOKCrCJ9'
);

$serialized = $this->revision->jsonSerialize();

$this->assertIsObject($serialized['credential_data']);
$this->assertSame('Bank €', $serialized['credential_data']->label);
$this->assertSame('831D34FC-E7C0-4541-BD68-507747824192', $serialized['credential_data']->guid);
}
}
Loading