Skip to content
Open
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
94 changes: 94 additions & 0 deletions plugins/Referrers/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
use Piwik\Archive;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Db;
use Piwik\Metrics;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Plugins\Actions\ArchivingHelper;
use Piwik\Plugins\Referrers\Columns\Metrics\VisitorsFromReferrerPercent;
Expand Down Expand Up @@ -749,13 +751,105 @@ public function getNumberOfDistinctWebsitesUrls($idSite, string $period, string
return $this->getNumeric(Archiver::METRIC_DISTINCT_URLS_RECORD_NAME, $idSite, $period, $date, $segment);
}

/**
* @unsanitized
*/
public function getCampaignUrlPresets($idSite, string $search = '')
{
Piwik::checkUserHasViewAccess($idSite);

if (!empty($search)) {
$optionName = $this->getCampaignUrlPresetsOptionName($idSite);
$sql = sprintf(
"SELECT option_value FROM `%s` WHERE option_name = '%s' AND option_value LIKE '%%%s%%'",
Common::prefixTable('option'),
$optionName,
$search
);
$optionValue = Db::fetchOne($sql);
} else {
$optionValue = Option::get($this->getCampaignUrlPresetsOptionName($idSite));
}

$presets = json_decode($optionValue, true);
if (!is_array($presets)) {
return [];
}

return Common::unsanitizeInputValues(array_slice(array_reverse($presets), 0, 5));
}

public function saveCampaignUrlPreset(
$idSite,
string $websiteUrl = '',
string $generatedUrl = '',
string $campaignName = '',
string $campaignKeyword = '',
string $campaignSource = '',
string $campaignMedium = '',
string $campaignId = '',
string $campaignContent = '',
string $campaignGroup = '',
string $campaignPlacement = ''
) {
Piwik::checkUserHasViewAccess($idSite);

$optionName = $this->getCampaignUrlPresetsOptionName($idSite);
$presets = json_decode(Option::get($optionName), true);
if (!is_array($presets)) {
$presets = [];
}

$presets[] = [
'websiteUrl' => $websiteUrl,
'generatedUrl' => $generatedUrl,
'campaignName' => $campaignName,
'campaignKeyword' => $campaignKeyword,
'campaignSource' => $campaignSource,
'campaignMedium' => $campaignMedium,
'campaignId' => $campaignId,
'campaignContent' => $campaignContent,
'campaignGroup' => $campaignGroup,
'campaignPlacement' => $campaignPlacement,
'login' => Piwik::getCurrentUserLogin(),
'savedAt' => date('Y-m-d H:i:s'),
];

Option::set($optionName, json_encode($presets));

return Common::unsanitizeInputValues(array_slice(array_reverse($presets), 0, 5));
}

public function deleteCampaignUrlPreset($idSite, int $index)
{
Piwik::checkUserHasViewAccess($idSite);

$optionName = $this->getCampaignUrlPresetsOptionName($idSite);
$presets = json_decode(Option::get($optionName), true);
if (!is_array($presets)) {
return [];
}

unset($presets[$index]);
$presets = array_values($presets);

Option::set($optionName, json_encode($presets));

return Common::unsanitizeInputValues(array_slice(array_reverse($presets), 0, 5));
}

private function getNumeric(string $name, $idSite, string $period, string $date, ?string $segment)
{
Piwik::checkUserHasViewAccess($idSite);
$archive = Archive::build($idSite, $period, $date, $segment);
return $archive->getDataTableFromNumeric($name);
}

private function getCampaignUrlPresetsOptionName($idSite): string
{
return 'Referrers.campaignUrlPresets.' . (int) $idSite;
}

/**
* Removes idsubdatatable_in_db metadata from a DataTable. Used by Social tables since
* they use fake subtable IDs.
Expand Down
84 changes: 84 additions & 0 deletions plugins/Referrers/tests/Integration/APITest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Plugins\Referrers\tests\Integration;

use Piwik\Option;
use Piwik\Plugins\Referrers\API;
use Piwik\Tests\Framework\Fixture;
use Piwik\Tests\Framework\Mock\FakeAccess;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;

/**
* @group Referrers
* @group ApiTest
* @group Plugins
*/
class APITest extends IntegrationTestCase
{
/**
* @var API
*/
private $api;

/**
* @var int
*/
private $siteId;

public function setUp(): void
{
parent::setUp();

Fixture::createSuperUser();
$this->siteId = Fixture::createWebsite('2014-01-01 01:02:03');
$this->api = API::getInstance();
}

public function tearDown(): void
{
Option::delete($this->getPresetOptionName());

parent::tearDown();
}

public function testSaveCampaignUrlPresetReturnsSavedPreset(): void
{
$result = $this->api->saveCampaignUrlPreset(
$this->siteId,
'https://example.com/landing-page',
'https://example.com/landing-page?mtm_campaign=Spring+sale',
'Spring sale',
'shoes',
'newsletter',
'email',
'spring-2026',
'hero-banner',
'vip',
'top-slot'
);

$this->assertIsArray($result);
$this->assertCount(1, $result);
$this->assertSame('Spring sale', $result[0]['campaignName']);
$this->assertSame('newsletter', $result[0]['campaignSource']);
}

public function provideContainerConfig()
{
return [
'Piwik\Access' => new FakeAccess(true),
];
}

private function getPresetOptionName(): string
{
return 'Referrers.campaignUrlPresets.' . $this->siteId;
}
}
124 changes: 123 additions & 1 deletion plugins/Referrers/vue/src/CampaignBuilder/CampaignBuilder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,55 @@
><code v-text="generatedUrl" /></pre>
</div>
</div>
<div v-show="presets.length">
<h3>Recent campaigns</h3>
<div>
<Field
uicontrol="text"
name="presetsearch"
title="Search saved campaigns"
v-model="presetSearch"
@update:model-value="loadPresets()"
>
</Field>
</div>
<ul class="recentCampaignPresets">
<li v-for="(preset, index) in presets" :key="index">
<span v-html="getPresetSummary(preset)"></span>
<span
class="recentCampaignPresetAction"
@click="applyPreset(preset)"
>
Apply
</span>
<a href="" @click.prevent="deletePreset(index)">Delete</a>
</li>
</ul>
</div>
</form>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { CopyToClipboard } from 'CoreHome';
import { AjaxHelper, CopyToClipboard } from 'CoreHome';
import { Field, SaveButton } from 'CorePluginsAdmin';

interface CampaignPreset {
websiteUrl: string;
generatedUrl: string;
campaignName: string;
campaignKeyword: string;
campaignSource: string;
campaignMedium: string;
campaignId: string;
campaignContent: string;
campaignGroup: string;
campaignPlacement: string;
login?: string;
savedAt?: string;
}

interface CampaignBuilderState {
websiteUrl: string;
campaignName: string;
Expand All @@ -155,6 +195,8 @@ interface CampaignBuilderState {
campaignGroup: string;
campaignPlacement: string;
generatedUrl: string;
presets: CampaignPreset[];
presetSearch: string;
}

const { $ } = window;
Expand Down Expand Up @@ -185,10 +227,13 @@ export default defineComponent({
campaignGroup: '',
campaignPlacement: '',
generatedUrl: '',
presets: [],
presetSearch: '',
};
},
created() {
this.reset();
this.loadPresets();
},
watch: {
generatedUrl() {
Expand Down Expand Up @@ -268,6 +313,83 @@ export default defineComponent({
generatedUrl += urlHash;

this.generatedUrl = generatedUrl;
this.savePreset();
},
getIdSite() {
const idSite = new URLSearchParams(window.location.search).get('idSite');
return Number(idSite || 0);
},
loadPresets() {
AjaxHelper.post(
{
module: 'API',
method: 'Referrers.getCampaignUrlPresets',
},
{
idSite: this.getIdSite(),
search: this.presetSearch,
},
).then((response) => {
this.presets = Array.isArray(response) ? response : [];
}).catch(() => {
this.presets = [];
});
},
savePreset() {
AjaxHelper.post(
{
module: 'API',
method: 'Referrers.saveCampaignUrlPreset',
},
{
idSite: this.getIdSite(),
websiteUrl: this.websiteUrl,
generatedUrl: this.generatedUrl,
campaignName: this.campaignName,
campaignKeyword: this.campaignKeyword,
campaignSource: this.campaignSource,
campaignMedium: this.campaignMedium,
campaignId: this.campaignId,
campaignContent: this.campaignContent,
campaignGroup: this.campaignGroup,
campaignPlacement: this.campaignPlacement,
},
).then((response) => {
this.presets = Array.isArray(response) ? response : [];
}).catch(() => {
});
},
applyPreset(preset: CampaignPreset) {
this.websiteUrl = preset.websiteUrl || this.websiteUrl;
this.campaignName = preset.campaignName || '';
this.campaignKeyword = preset.campaignKeyword || '';
this.campaignSource = preset.campaignSource || '';
this.campaignMedium = preset.campaignMedium || '';
this.campaignId = preset.campaignId || '';
this.campaignContent = preset.campaignContent || '';
this.campaignGroup = preset.campaignGroup || '';
this.campaignPlacement = preset.campaignPlacement || '';
this.generatedUrl = preset.generatedUrl || '';
},
getPresetSummary(preset: CampaignPreset) {
return `<strong>${preset.campaignName || 'Untitled campaign'}</strong>${
preset.websiteUrl ? ` for ${preset.websiteUrl}` : ''
}`;
},
deletePreset(index: number) {
AjaxHelper.post(
{
module: 'API',
method: 'Referrers.deleteCampaignUrlPreset',
},
{
idSite: this.getIdSite(),
index: this.presets.length - index - 1,
},
).then((response) => {
this.presets = Array.isArray(response) ? response : [];
}).catch(() => {
});
},
},
});
Expand Down
Loading