File: D:/web/matomo.vdk/plugins/PrivacyManager/vue/src/ManageGdpr/ManageGdpr.vue
<!--
Matomo - free/libre analytics platform
@link https://matomo.org
@license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
-->
<template>
<div class="manageGdpr">
<ContentBlock :content-title="translate('PrivacyManager_GdprTools')">
<div class="intro">
<p>
{{ translate('PrivacyManager_GdprToolsPageIntro1') }}
<br /><br />
{{ translate('PrivacyManager_GdprToolsPageIntro2') }}
<br />
</p>
<ol>
<li>{{ translate('PrivacyManager_GdprToolsPageIntroAccessRight') }}</li>
<li>{{ translate('PrivacyManager_GdprToolsPageIntroEraseRight') }}</li>
</ol>
<p>
<br />
<span
v-html="$sanitize(overviewHintText)"
/>
</p>
</div>
<h3>{{ translate('PrivacyManager_SearchForDataSubject') }}</h3>
<div class="form-group row">
<div class="col s12 input-field">
<div>
<label
for="gdprsite"
class="siteSelectorLabel"
>
{{ translate('PrivacyManager_SelectWebsite') }}
</label>
<div class="sites_autocomplete">
<SiteSelector
id="gdprsite"
v-model="site"
:show-all-sites-item="true"
:switch-site-on-select="false"
:show-selected-site="true"
@update:modelValue="changeSite($event)"
/>
</div>
</div>
</div>
</div>
<div class="form-group row segmentFilterGroup" v-if="isVisitorLogAndProfileEnabled" >
<div class="col s12">
<div>
<label style="margin: 8px 0;display: inline-block;">
{{ translate('PrivacyManager_FindDataSubjectsBy') }}
</label>
<div>
<SegmentGenerator
v-model="segment_filter"
:visit-segments-only="true"
:idsite="site.id"
:show-segment-editor="true"
/>
</div>
</div>
</div>
</div>
<SaveButton
class="findDataSubjects"
:value="translate('PrivacyManager_FindMatchingDataSubjects')"
@confirm="findDataSubjects()"
:disabled="!segment_filter"
:saving="isLoading"
v-if="isVisitorLogAndProfileEnabled"
>
</SaveButton>
<div v-else class="dataUnavailable system notification notification-icon
notification-info">
<strong>{{ translate('PrivacyManager_SiteDataNotAvailable')}}</strong>
<p>{{ translate('PrivacyManager_VisitorLogsProfilesDisabledMessage')}}</p>
<p
v-html="$sanitize(siteSettingsTextSingle)"
/>
</div>
</ContentBlock>
<div v-if="allWebsitesContainsDisabledSite" class="system notification notification-icon
notification-info">
<strong>{{ translate('PrivacyManager_SiteDataNotAvailableCertainSites')}}</strong>
<div class="notification-body">
<p>{{ translate('PrivacyManager_VisitorLogsProfilesSiteNamesDisabledMessage')}}</p>
<p
v-html="$sanitize(siteSettingsText)"
/>
</div>
</div>
<div v-show="!dataSubjects.length && hasSearched">
<h2>{{ translate('PrivacyManager_NoDataSubjectsFound') }}</h2>
</div>
<div v-show="dataSubjects.length">
<h2>{{ translate('PrivacyManager_MatchingDataSubjects') }}</h2>
<p>{{ translate('PrivacyManager_VisitsMatchedCriteria') }}
{{ translate('PrivacyManager_ExportingNote') }}
<br /> <br />
{{ translate('PrivacyManager_DeletionFromMatomoOnly') }}
<br /><br />
{{ translate('PrivacyManager_ResultIncludesAllVisits') }}
</p>
<table v-content-table>
<thead>
<tr>
<th class="checkInclude">
<div>
<Field
uicontrol="checkbox"
name="activateAll"
:model-value="toggleAll"
@update:model-value="toggleAll = $event; toggleActivateAll()"
:full-width="true"
>
</Field>
</div>
</th>
<th>{{ translate('General_Website') }}</th>
<th>{{ translate('General_VisitId') }}</th>
<th>{{ translate('General_VisitorID') }}</th>
<th>{{ translate('General_VisitorIP') }}</th>
<th>{{ translate('General_UserId') }}</th>
<th>{{ translate('General_Details') }}</th>
<th v-show="profileEnabled">{{ translate('General_Action') }}</th>
</tr>
</thead>
<tbody>
<tr v-show="dataSubjects.length > 400">
<td colspan="8">{{ translate('PrivacyManager_ResultTruncated', '400') }}</td>
</tr>
<tr
:title="`${translate('PrivacyManager_LastAction')}: ${dataSubject.lastActionDateTime}`"
v-for="(dataSubject, index) in dataSubjects"
:key="index"
>
<td class="checkInclude">
<div>
<Field
uicontrol="checkbox"
:name="`subject${dataSubject.idVisit}`"
v-model="dataSubjectsActive[index]"
:full-width="true"
>
</Field>
</div>
</td>
<td
class="site"
:title="`(${translate('General_Id')} ${dataSubject.idSite})`"
>{{ dataSubject.siteName }}</td>
<td class="visitId">{{ dataSubject.idVisit }}</td>
<td class="visitorId">
<a
:title="translate('PrivacyManager_AddVisitorIdToSearch')"
@click="addFilter('visitorId', dataSubject.visitorId)"
>{{ dataSubject.visitorId }}</a>
</td>
<td class="visitorIp">
<a
:title="translate('PrivacyManager_AddVisitorIPToSearch')"
@click="addFilter('visitIp', dataSubject.visitIp)"
>{{ dataSubject.visitIp }}</a>
</td>
<td class="userId">
<a
:title="translate('PrivacyManager_AddUserIdToSearch')"
@click="addFilter('userId', dataSubject.userId)"
>{{ dataSubject.userId }}</a>
</td>
<td>
<span
:title="`${dataSubject.deviceType} ${dataSubject.deviceModel}`"
style="margin-right:3.5px"
>
<img
height="16"
:src="dataSubject.deviceTypeIcon"
/>
</span>
<span
:title="dataSubject.operatingSystem"
style="margin-right:3.5px"
>
<img
height="16"
:src="dataSubject.operatingSystemIcon"
/>
</span>
<span
:title="`${dataSubject.browser} ${dataSubject.browserFamilyDescription}`"
style="margin-right:3.5px"
>
<img
height="16"
:src="dataSubject.browserIcon"
/>
</span>
<span
:title="`${dataSubject.country} ${dataSubject.region || ''}`"
>
<img
height="16"
:src="dataSubject.countryFlag"
/>
</span>
</td>
<td v-show="profileEnabled">
<a
class="visitorLogTooltip"
title="View visitor profile"
@click="showProfile(dataSubject.visitorId, dataSubject.idSite)"
>
<img src="plugins/Live/images/visitorProfileLaunch.png" style="margin-right:3.5px"/>
<span>{{ translate('Live_ViewVisitorProfile') }}</span>
</a>
</td>
</tr>
</tbody>
</table>
<SaveButton
class="exportDataSubjects"
style="margin-right:3.5px"
@confirm="exportDataSubject()"
:disabled="!hasActiveDataSubjects"
:value="translate('PrivacyManager_ExportSelectedVisits')"
>
</SaveButton>
<SaveButton
class="deleteDataSubjects"
@confirm="deleteDataSubject()"
:disabled="!hasActiveDataSubjects || isDeleting"
:value="translate('PrivacyManager_DeleteSelectedVisits')"
>
</SaveButton>
</div>
<div
class="ui-confirm"
id="confirmDeleteDataSubject"
ref="confirmDeleteDataSubject"
>
<h2>{{ translate('PrivacyManager_DeleteVisitsConfirm') }}</h2>
<input
role="yes"
type="button"
:value="translate('General_Yes')"
/>
<input
role="no"
type="button"
:value="translate('General_No')"
/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import {
translate,
Matomo,
AjaxHelper,
ContentBlock,
SiteSelector,
ContentTable,
NotificationsStore,
MatomoUrl,
SiteRef,
} from 'CoreHome';
import { SegmentGenerator } from 'SegmentEditor';
import { SaveButton, Field } from 'CorePluginsAdmin';
interface DataSubject {
browser: string;
browserFamilyDescription: string;
browserIcon: string;
country: string;
countryFlag: string;
deviceModel: string;
deviceType: string;
deviceTypeIcon: string;
idSite: string|number;
idVisit: string;
lastActionDateTime: string;
operatingSystem: string;
operatingSystemIcon: string;
region: string|null;
siteName: string;
userId: string|null;
visitIp: string;
visitorId: string;
}
interface ManageGdprState {
isLoading: boolean;
isDeleting: boolean;
site: SiteRef;
segment_filter: string;
dataSubjects: DataSubject[];
toggleAll: boolean;
hasSearched: boolean;
profileEnabled: boolean;
dataSubjectsActive: boolean[];
isVisitorLogAndProfileEnabled: boolean;
allWebsitesContainsDisabledSite: boolean;
}
interface VisitorLogProfileEnabledState {
value: boolean;
}
export default defineComponent({
components: {
ContentBlock,
SiteSelector,
SegmentGenerator,
SaveButton,
Field,
},
directives: {
ContentTable,
},
data(): ManageGdprState {
return {
isLoading: false,
isDeleting: false,
site: {
id: 'all',
name: translate('UsersManager_AllWebsites'),
},
segment_filter: 'visitId==',
dataSubjects: [],
toggleAll: true,
hasSearched: false,
profileEnabled: Matomo.visitorProfileEnabled,
dataSubjectsActive: [],
isVisitorLogAndProfileEnabled: true,
allWebsitesContainsDisabledSite: false,
};
},
created() {
this.changeSite(this.site);
},
watch: {
site(newSite) {
if (newSite.id === 'all') {
this.isVisitorLogAndProfileEnabled = true;
return;
}
this.allWebsitesContainsDisabledSite = false;
this.isLoading = true;
// always reset the search status on site change
this.dataSubjects = [];
this.hasSearched = false;
AjaxHelper.fetch<VisitorLogProfileEnabledState>({
method: 'Live.isVisitorProfileEnabled',
idSite: newSite.id,
}).then((isEnabled) => {
this.isVisitorLogAndProfileEnabled = isEnabled.value;
}).finally(() => {
this.isLoading = false;
});
},
},
setup() {
const sitesPromise = AjaxHelper.fetch<(string|number)[]>({
method: 'SitesManager.getSitesIdWithAdminAccess',
filter_limit: '-1',
});
return {
getSites() {
return sitesPromise;
},
};
},
methods: {
changeSite(newValue: SiteRef) {
AjaxHelper.fetch(
{
module: 'API',
method: 'Live.isVisitorProfileEnabled',
filter_limit: -1,
idSite: newValue.id,
},
{
createErrorNotification: false, // don't show errors from this API in UI
},
).then((response) => {
if (!response.value && this.segment_filter === 'userId==') {
this.segment_filter = 'visitId==';
} else if (response.value && this.segment_filter === 'visitId==') {
this.segment_filter = 'userId==';
}
}).catch(() => {
this.segment_filter = 'visitId==';
});
},
showSuccessNotification(message: string) {
const notificationInstanceId = NotificationsStore.show({
message,
context: 'success',
id: 'manageGdpr',
type: 'transient',
});
setTimeout(() => {
NotificationsStore.scrollToNotification(notificationInstanceId);
}, 200);
},
linkTo(action: string, module = 'PrivacyManager') {
return `?${MatomoUrl.stringify({
...MatomoUrl.urlParsed.value,
module,
action,
})}`;
},
toggleActivateAll() {
this.dataSubjectsActive.fill(this.toggleAll);
},
showProfile(visitorId: string, idSite: string|number) {
Matomo.helper.showVisitorProfilePopup(visitorId, idSite);
},
exportDataSubject() {
const visitsToDelete = this.activatedDataSubjects;
AjaxHelper.post<unknown[]>(
{
module: 'API',
method: 'PrivacyManager.exportDataSubjects',
format: 'json',
filter_limit: -1,
},
{
visits: visitsToDelete,
},
).then((visits) => {
this.showSuccessNotification(translate('PrivacyManager_VisitsSuccessfullyExported'));
Matomo.helper.sendContentAsDownload('exported_data_subjects.json', JSON.stringify(visits));
});
},
deleteDataSubject() {
Matomo.helper.modalConfirm(this.$refs.confirmDeleteDataSubject as HTMLElement, {
yes: () => {
this.isDeleting = true;
const visitsToDelete = this.activatedDataSubjects;
AjaxHelper.post(
{
module: 'API',
method: 'PrivacyManager.deleteDataSubjects',
filter_limit: -1,
},
{
visits: visitsToDelete,
},
).then(() => {
this.dataSubjects = [];
this.showSuccessNotification(translate('PrivacyManager_VisitsSuccessfullyDeleted'));
this.findDataSubjects();
}).finally(() => {
this.isDeleting = false;
});
},
});
},
addFilter(segment: string, value: string) {
this.segment_filter += `,${segment}==${value}`;
this.findDataSubjects();
},
findDataSubjects() {
this.dataSubjects = [];
this.dataSubjectsActive = [];
this.isLoading = true;
this.toggleAll = true;
this.hasSearched = false;
this.getSites().then((idsites) => {
let siteIds: QueryParameters[string] = this.site.id;
if (siteIds === 'all' && !Matomo.hasSuperUserAccess) {
// when superuser, we speed the request up a little and simply use 'all'
siteIds = idsites;
if (Array.isArray(idsites)) {
siteIds = idsites.join(',');
}
}
// API returns false if at least one sites logs/profiles are disabled
AjaxHelper.fetch<VisitorLogProfileEnabledState>({
method: 'Live.isVisitorProfileEnabled',
idSite: siteIds,
}).then((isEnabled) => {
this.allWebsitesContainsDisabledSite = !isEnabled.value;
});
AjaxHelper.fetch<DataSubject[]>({
idSite: siteIds,
module: 'API',
method: 'PrivacyManager.findDataSubjects',
segment: this.segment_filter,
}).then((visits) => {
this.hasSearched = true;
this.dataSubjectsActive = visits.map(() => true);
this.dataSubjects = visits;
}).finally(() => {
this.isLoading = false;
});
});
},
},
computed: {
hasActiveDataSubjects(): boolean {
return !!this.activatedDataSubjects.length;
},
activatedDataSubjects(): { idsite: string|number, idvisit: string|number }[] {
return this.dataSubjects.filter((v, i) => this.dataSubjectsActive[i]).map((v) => ({
idsite: v.idSite,
idvisit: v.idVisit,
}));
},
overviewHintText(): string {
return translate(
'PrivacyManager_GdprToolsOverviewHint',
`<a href="${this.linkTo('gdprOverview')}">`,
'</a>',
);
},
siteSettingsText(): string {
return translate(
'PrivacyManager_PleaseEnableVisitorLogsProfilesSites',
`<a href="${this.linkTo('index', 'SitesManager')}">`,
'</a>',
);
},
siteSettingsTextSingle(): string {
return translate(
'PrivacyManager_PleaseEnableVisitorLogsProfiles',
`<a href="${this.linkTo('index', 'SitesManager')}">`,
'</a>',
);
},
},
});
</script>