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
2 changes: 1 addition & 1 deletion lib/ContactsMenu/Providers/DetailsProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public function process(IEntry $entry) {

$contactsUrl = $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('contacts.contacts.direct', [
'contact' => $uid . '~' . $addressBookUri
'contact' => base64_encode($uid . '~' . $addressBookUri),
])
);

Expand Down
5 changes: 5 additions & 0 deletions lib/Controller/ContactsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public function __construct(
* @param string $uuid
*/
public function direct(string $contact): RedirectResponse {
// Keep compatibility with old routing scheme
if (str_contains($contact, '~')) {
$contact = base64_encode($contact);
}

$url = $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('contacts.page.index') . $this->l10n->t('All contacts') . '/' . $contact
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/AppContent/ChartContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default {
transformData() {
const contactsByUid = {}
const contacts = Object.keys(this.contactsList).map(key => {
const [uid, addressbook] = key.split('~')
const [uid, addressbook] = Buffer.from(key, 'base64').toString('utf-8').split('~')
if (!contactsByUid[addressbook]) {
contactsByUid[addressbook] = {}
}
Expand Down
4 changes: 0 additions & 4 deletions src/components/AppContent/ContactsContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</EmptyContent>
</AppContent>

<AppContent v-else :show-details="showDetails" @update:showDetails="hideDetails">

Check warning on line 44 in src/components/AppContent/ContactsContent.vue

View workflow job for this annotation

GitHub Actions / NPM lint

v-on event '@update:showDetails' must be hyphenated
<!-- contacts list -->
<template #list>
<ContactsList :list="contactsList"
Expand Down Expand Up @@ -116,10 +116,6 @@
return this.$store.getters.getSortedContacts
},

selectedContact() {
return this.$route.params.selectedContact
},

/**
* Is this a real group ?
* Aka not a dynamically generated one like `All contacts`
Expand Down Expand Up @@ -157,7 +153,7 @@
* Forward the newContact event to the parent
*/
newContact() {
this.$emit('new-contact')

Check warning on line 156 in src/components/AppContent/ContactsContent.vue

View workflow job for this annotation

GitHub Actions / NPM lint

The "new-contact" event has been triggered but not declared on `emits` option

Check warning on line 156 in src/components/AppContent/ContactsContent.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Custom event name 'new-contact' must be camelCase
},

/**
Expand Down
4 changes: 2 additions & 2 deletions src/components/AppNavigation/GroupNavigationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@

methods: {
/**
* @param groups

Check warning on line 138 in src/components/AppNavigation/GroupNavigationItem.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Missing JSDoc @param "groups" description
* @param groupId

Check warning on line 139 in src/components/AppNavigation/GroupNavigationItem.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Missing JSDoc @param "groupId" description
*/
isInGroup(groups, groupId) {
return groups.includes(groupId)
Expand All @@ -151,9 +151,9 @@
async onDrop(event, group) {
try {
const contactFromDropData = JSON.parse(event.dataTransfer.getData('item'))
const contactFromStore = this.$store.getters.getContact(`${contactFromDropData.uid}~${contactFromDropData.addressbookId}`)
const contactFromStore = this.$store.getters.getContact(Buffer.from(`${contactFromDropData.uid}~${contactFromDropData.addressbookId}`, 'utf-8').toString('base64'))
if (contactFromStore && !this.isInGroup(contactFromStore.groups, group.id)) {
const contact = this.$store.getters.getContact(`${contactFromDropData.uid}~${contactFromDropData.addressbookId}`)
const contact = this.$store.getters.getContact(Buffer.from(`${contactFromDropData.uid}~${contactFromDropData.addressbookId}`, 'utf-8').toString('base64'))
await this.$store.dispatch('updateContactGroups', {
groupNames: [...contactFromStore.groups, group.id],
contact,
Expand Down Expand Up @@ -235,7 +235,7 @@
* Open mailto: for contacts in a group
*
* @param {object} group of contacts to be emailed
* @param {string} mode

Check warning on line 238 in src/components/AppNavigation/GroupNavigationItem.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Missing JSDoc @param "mode" description
*/
emailGroup(group, mode = 'to') {
const emails = []
Expand Down
2 changes: 1 addition & 1 deletion src/components/ContactDetails/ContactDetailsProperty.vue
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export default {
}
if (this.propName === 'x-managersname') {
if (this.property.getParameter('uid')) {
return this.property.getParameter('uid') + '~' + this.contact.addressbook.id
return Buffer.from(this.property.getParameter('uid') + '~' + this.contact.addressbook.id, 'utf-8').toString('base64')
}
// Try to find the matching contact by display name
// TODO: this only *shows* the display name but doesn't assign the missing UID
Expand Down
13 changes: 6 additions & 7 deletions src/components/ContactsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,15 @@ import Merging from './ContactsList/Merging.vue'
import IconCancelRaw from '@mdi/svg/svg/cancel.svg?raw'
// eslint-disable-next-line import/no-unresolved
import IconDeleteRaw from '@mdi/svg/svg/delete-outline.svg'
import RouterMixin from '../mixins/RouterMixin.js'

export default {
name: 'ContactsList',

mixins: [
RouterMixin,
],

components: {
AppContentList,
NcNoteCard,
Expand Down Expand Up @@ -153,12 +158,6 @@ export default {
},

computed: {
selectedContact() {
return this.$route.params.selectedContact
},
selectedGroup() {
return this.$route.params.selectedGroup
},
filteredList() {
const contactsList = this.list
.filter(item => this.matchSearch(this.contacts[item.key]))
Expand Down Expand Up @@ -242,7 +241,7 @@ export default {
* @param {string} key the contact unique key
*/
scrollToContact(key) {
const item = this.$el.querySelector('#' + btoa(key).slice(0, -2))
const item = this.$el.querySelector('#' + key.slice(0, -2))

// if the item is not visible in the list or barely visible
if (!(item && item.getBoundingClientRect().y > 50)) { // header height
Expand Down
13 changes: 6 additions & 7 deletions src/components/ContactsList/ContactsListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,15 @@ import {
NcAvatar,
} from '@nextcloud/vue'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import RouterMixin from '../../mixins/RouterMixin.js'

export default {
name: 'ContactsListItem',

mixins: [
RouterMixin,
],

components: {
ListItem,
NcAvatar,
Expand Down Expand Up @@ -86,19 +91,13 @@ export default {
},

computed: {
selectedGroup() {
return this.$route.params.selectedGroup
},
selectedContact() {
return this.$route.params.selectedContact
},
// contact is not draggable when it has not been saved on server as it can't be added to groups/circles before
isDraggable() {
return !!this.source.dav && this.source.addressbook.id !== 'z-server-generated--system'
},
// usable and valid html id for scrollTo
id() {
return window.btoa(this.source.key).slice(0, -2)
return this.source.key.slice(0, -2)
},
getTel() {
return this.source.properties.find(property => property.name === 'tel')?.getFirstValue()
Expand Down
3 changes: 3 additions & 0 deletions src/mixins/RouterMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ export default {
selectedCircle() {
return this.$route.params.selectedCircle
},
selectedChart() {
return this.$route.params.selectedChart
},
},
}
2 changes: 1 addition & 1 deletion src/models/contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export default class Contact {
* @memberof Contact
*/
get key() {
return this.uid + '~' + this.addressbook.id
return Buffer.from(this.uid + '~' + this.addressbook.id, 'utf8').toString('base64')
}

/**
Expand Down
25 changes: 4 additions & 21 deletions src/views/Contacts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import RootNavigation from '../components/AppNavigation/RootNavigation.vue'
import SettingsImportContacts from '../components/AppNavigation/Settings/SettingsImportContacts.vue'
import IconAdd from 'vue-material-design-icons/Plus.vue'

import RouterMixin from '../mixins/RouterMixin.js'

import Contact from '../models/contact.js'
import rfcProps from '../models/rfcProps.js'

Expand Down Expand Up @@ -99,28 +101,9 @@ export default {

mixins: [
IsMobileMixin,
RouterMixin,
],

// passed by the router
props: {
selectedCircle: {
type: String,
default: undefined,
},
selectedGroup: {
type: String,
default: undefined,
},
selectedContact: {
type: String,
default: undefined,
},
selectedChart: {
type: String,
default: undefined,
},
},

data() {
return {
// The object shorthand syntax is breaking builds (bug in @babel/preset-env)
Expand Down Expand Up @@ -376,7 +359,7 @@ export default {
}

const inList = this.contactsList.findIndex(contact => contact.key === this.selectedContact) > -1
if (this.selectedContact === undefined || !inList) {
if (!this.selectedContact || !inList) {
// Unknown contact
if (this.selectedContact && !inList) {
showError(t('contacts', 'Contact not found'))
Expand Down
85 changes: 81 additions & 4 deletions tests/unit/ContactsMenu/Provider/DetailsProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function testProcessContact() {
$uid = 'e3a71614-c602-4eb5-9994-47eec551542b';
$abUri = 'contacts-1';
$iconUrl = 'core/img/actions/info.svg';
$resultUri = "$domain/index.php/apps/contacts/direct/contact/$uid~$abUri";
$resultUri = "$domain/index.php/apps/contacts/direct/contact/ZTNhNzE2MTQtYzYwMi00ZWI1LTk5OTQtNDdlZWM1NTE1NDJifmNvbnRhY3RzLTE=";

$entry->expects($this->exactly(3))
->method('getProperty')
Expand Down Expand Up @@ -95,16 +95,93 @@ public function testProcessContact() {
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('contacts.contacts.direct', [
'contact' => $uid . '~' . $abUri
'contact' => 'ZTNhNzE2MTQtYzYwMi00ZWI1LTk5OTQtNDdlZWM1NTE1NDJifmNvbnRhY3RzLTE=',
])
->willReturn("/apps/contacts/direct/contact/$uid~$abUri");
->willReturn('/apps/contacts/direct/contact/ZTNhNzE2MTQtYzYwMi00ZWI1LTk5OTQtNDdlZWM1NTE1NDJifmNvbnRhY3RzLTE=');

// Action icon and contact absolute urls
$this->urlGenerator->expects($this->exactly(2))
->method('getAbsoluteURL')
->will($this->returnValueMap([
[$iconUrl, "$domain/$iconUrl"],
["/apps/contacts/direct/contact/$uid~$abUri", $resultUri]
['/apps/contacts/direct/contact/ZTNhNzE2MTQtYzYwMi00ZWI1LTk5OTQtNDdlZWM1NTE1NDJifmNvbnRhY3RzLTE=', $resultUri]
]));

// Translations
$this->l10n->expects($this->once())
->method('t')
->with('Details')
->willReturnArgument(0);

$this->actionFactory->expects($this->once())
->method('newLinkAction')
->with($this->equalTo("$domain/$iconUrl"), $this->equalTo('Details'), $this->equalTo($resultUri))
->willReturn($action);
$action->expects($this->once())
->method('setPriority')
->with($this->equalTo(0));
$entry->expects($this->once())
->method('addAction')
->with($action);

$this->provider->process($entry);
}

public function testProcessContactWithUnicodeUid() {
$entry = $this->createMock(IEntry::class);
$action = $this->createMock(ILinkAction::class);
$addressbook = $this->createMock(IAddressBook::class);

// DATA
$domain = 'https://cloud.example.com';
$uid = 'e3a71614-c602-4eb5-9994-47eec551542b-é';
$abUri = 'contacts-1';
$iconUrl = 'core/img/actions/info.svg';
$resultUri = "$domain/index.php/apps/contacts/direct/contact/ZTNhNzE2MTQtYzYwMi00ZWI1LTk5OTQtNDdlZWM1NTE1NDJiLcOpfmNvbnRhY3RzLTE=";


$entry->expects($this->exactly(3))
->method('getProperty')
->will($this->returnValueMap([
['UID', $uid],
['isLocalSystemBook', null],
['addressbook-key', 1]
]));

$addressbook->expects($this->once())
->method('getKey')
->willReturn(1);

$addressbook->expects($this->once())
->method('getUri')
->willReturn($abUri);

$this->manager->expects($this->once())
->method('getUserAddressbooks')
->willReturn([1 => $addressbook]);

// Action icon
$this->urlGenerator->expects($this->once())
->method('imagePath')
->with('core', 'actions/info.svg')
->willReturn($iconUrl);

//
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('contacts.contacts.direct', [
// Taken from node: Buffer.from('e3a71614-c602-4eb5-9994-47eec551542b-é~contacts-1', 'utf-8').toString('base64')
// To ensure interop between JavaScript and PHP
'contact' => 'ZTNhNzE2MTQtYzYwMi00ZWI1LTk5OTQtNDdlZWM1NTE1NDJiLcOpfmNvbnRhY3RzLTE=',
])
->willReturn('/apps/contacts/direct/contact/ZTNhNzE2MTQtYzYwMi00ZWI1LTk5OTQtNDdlZWM1NTE1NDJiLcOpfmNvbnRhY3RzLTE=');

// Action icon and contact absolute urls
$this->urlGenerator->expects($this->exactly(2))
->method('getAbsoluteURL')
->will($this->returnValueMap([
[$iconUrl, "$domain/$iconUrl"],
['/apps/contacts/direct/contact/ZTNhNzE2MTQtYzYwMi00ZWI1LTk5OTQtNDdlZWM1NTE1NDJiLcOpfmNvbnRhY3RzLTE=', $resultUri]
]));

// Translations
Expand Down
22 changes: 15 additions & 7 deletions tests/unit/Controller/ContactsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;

class ContactsControllerTest extends TestCase {
Expand All @@ -38,10 +39,17 @@ protected function setUp(): void {
);
}

public static function provideDirectContactData(): array {
return [
['uuid~addressbook', 'dXVpZH5hZGRyZXNzYm9vaw=='],
['dXVpZH5hZGRyZXNzYm9vaw==', 'dXVpZH5hZGRyZXNzYm9vaw=='],
];
}

public function testRedirect() {
$contact = 'uuid~addressbook';

/**
* @dataProvider provideDirectContactData
*/
public function testRedirect(string $contact, string $expectedContact): void {
$this->l10n->method('t')
->with('All contacts')
->willReturn('All contacts');
Expand All @@ -53,11 +61,11 @@ public function testRedirect() {

$this->urlGenerator->expects($this->once())
->method('getAbsoluteURL')
->with('/index.php/apps/contacts/All contacts/' . $contact)
->willReturn('/index.php/apps/contacts/All contacts/' . $contact);
->with('/index.php/apps/contacts/All contacts/' . $expectedContact)
->willReturn('/index.php/apps/contacts/All contacts/' . $expectedContact);

$result = $this->controller->direct('uuid~addressbook');
$result = $this->controller->direct($contact);
$this->assertTrue($result instanceof RedirectResponse);
$this->assertEquals('/index.php/apps/contacts/All contacts/' . $contact, $result->getRedirectURL());
$this->assertEquals('/index.php/apps/contacts/All contacts/' . $expectedContact, $result->getRedirectURL());
}
}
Loading