HEX
Server: Microsoft-IIS/10.0
System: Windows NT ITPWINWEBSVR22 10.0 build 20348 (Windows Server 2022) AMD64
User: www.conferencesearch.co.uk (0)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: D:/web/matomo.circ/plugins/Marketplace/vue/src/PluginDetailsModal/PluginDetailsModal.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 ref="root" class="modal" id="pluginDetailsModal">
    <div
      v-if="!isLoading"
      class="modal-content"
      :class="{ 'modal-content--simple-header': !hasHeaderMetadata }"
    >

      <div class="modal-content__header">
        <span class="btn-close modal-close"><i class="icon-close"></i></span>

        <h2>
          {{ plugin && plugin.displayName ? plugin.displayName : 'Plugin details' }}
        </h2>
        <div class="plugin-metadata-part1" v-if="hasHeaderMetadata">
          <h3 class="sr-only">Plugin details — part 1</h3>
          <dl>
            <div class="pair" v-if="showReviews">
              <dt>{{ translate('Marketplace_Reviews') }}</dt>
              <dd>
                <img class="star-icon reviews-icon"
                     src="plugins/Marketplace/images/star.svg"
                     alt=""
                /><a
                  v-on:click="scrollElementIntoView('#reviews')"
                >{{ pluginReviews.averageRating }}</a>
              </dd>
            </div>

            <div class="pair" v-if="!plugin.isBundle">
              <dt>{{ translate('CorePluginsAdmin_Version') }}</dt>
              <dd>{{ plugin.latestVersion }}</dd>
            </div>

            <div class="pair" v-if="plugin.numDownloads > 0">
              <dt>{{ translate('General_Downloads') }}</dt>
              <dd>{{ plugin.numDownloadsPretty }}</dd>
            </div>

            <div class="pair" v-if="plugin.lastUpdated && !plugin.isBundle">
              <dt>{{ translate('Marketplace_LastUpdated') }}</dt>
              <dd>{{ plugin.lastUpdated }}</dd>
            </div>

            <div class="pair" v-if="!plugin.isBundle">
              <dt>{{ translate('Marketplace_Developer') }}</dt>
              <dd>
                {{ pluginOwner }}
              </dd>
            </div>
          </dl>
        </div>
      </div>

      <div
        class="modal-content__main"
        :class="{'modal-content__main--with-free-trial': showFreeTrialDropdown }"
      >
        <div class="plugin-description">
          <MissingReqsNotice v-if="showMissingRequirementsNoticeIfApplicable" :plugin="plugin" />

          <div v-if="isMultiServerEnvironment" class="alert alert-warning">
            {{ translate('Marketplace_MultiServerEnvironmentWarning') }}
          </div>
          <div v-else-if="!isAutoUpdatePossible" class="alert alert-warning">
            {{
              translate(
                'Marketplace_AutoUpdateDisabledWarning',
                '\'[General]enable_auto_update=1\'',
                '\'config/config.ini.php\''
              )
            }}
          </div>

          <div v-if="showMissingLicenseDescription" class="alert alert-danger">
            {{ translate('Marketplace_PluginLicenseMissingDescription') }}
          </div>
          <div v-else-if="showExceededLicenseDescription" class="alert alert-warning">
            {{ translate('Marketplace_PluginLicenseExceededDescription') }}
          </div>
          <div v-else-if="plugin.licenseStatus === 'Pending' && !isMultiServerEnvironment"
               class="alert alert-warning"
               v-html="$sanitize(getPendingLicenseHelpText(plugin.displayName))"
          >
          </div>
          <div v-else-if="plugin.licenseStatus === 'Cancelled' && !isMultiServerEnvironment"
               class="alert alert-warning"
               v-html="$sanitize(getCancelledLicenseHelpText(plugin.displayName))"
          >
          </div>
          <div v-else-if="
          !plugin.hasDownloadLink
          && !isMultiServerEnvironment
          && (plugin.licenseStatus || !plugin.isPaid)"
               class="alert alert-warning"
               v-html="$sanitize(getDownloadLinkMissingHelpText(plugin.displayName))"
          >
          </div>

          <div v-html="$sanitize(pluginDescription)"></div>
        </div>

        <div class="plugin-metadata-part2">
          <hr />
          <h3 class="sr-only">Plugin details — part 2</h3>
          <dl>
            <div class="pair" v-if="!plugin.isBundle">
              <dt>{{ translate('CorePluginsAdmin_Version') }}</dt>
              <dd>{{ plugin.latestVersion }}</dd>
            </div>

            <div class="pair" v-if="pluginKeywords">
              <dt>{{ translate('Marketplace_PluginKeywords') }}</dt>
              <dd>{{ pluginKeywords.join(', ') }}</dd>
            </div>

            <template v-if="!plugin.isBundle">
              <div class="pair">
                <dt>{{ translate('Marketplace_Authors') }}</dt>
                <dd>
                  <template v-for="(author, index) in pluginAuthors" :key="`author-${index}`">
                    <a
                      v-if="author.homepage"
                      target="_blank"
                      rel="noreferrer noopener"
                      :href="author.homepage"
                    >{{ author.name }}</a>
                    <a
                      v-else-if="author.email && isValidEmail(author.email)"
                      :href="`mailto:${ encodeURIComponent(author.email) }`"
                    >{{ author.name }}</a>
                    <span v-else>{{ author.name }}</span>
                    <span v-if="index < pluginAuthors.length - 1">, </span>
                  </template>
                </dd>
              </div>

              <div class="pair">
                <dt>{{ translate('CorePluginsAdmin_Websites') }}</dt>
                <dd>
                  <a
                    v-if="plugin.homepage"
                    target="_blank"
                    rel="noreferrer noopener"
                    :href="plugin.homepage"
                  >{{ translate('Marketplace_PluginWebsite') }}</a>
                  <template v-if="pluginChangelogUrl">
                    <template v-if="plugin.homepage">, </template>
                    <a
                      target="_blank"
                      rel="noreferrer noopener"
                      :href="externalRawLink(pluginChangelogUrl)"
                    >{{ translate('CorePluginsAdmin_Changelog') }}</a>
                  </template>

                  <template v-if="plugin.repositoryUrl">
                    <template v-if="plugin.homepage || pluginChangelogUrl">, </template>
                    <a
                      target="_blank"
                      rel="noreferrer noopener"
                      :href="externalRawLink(plugin.repositoryUrl)"
                    >GitHub</a>
                  </template>
                </dd>
              </div>

              <div class="pair" v-if="pluginActivity && pluginActivity.numCommits">
                <dt>{{ translate('CorePluginsAdmin_Activity') }}</dt>
                <dd>
                  {{ plugin.activity.numCommits }} commits

                  <template
                    v-if="pluginActivity?.numContributors > 1">
                    {{
                      ' ' + translate('Marketplace_ByXDevelopers', pluginActivity.numContributors)
                    }}
                  </template>
                  <template
                    v-if="pluginActivity?.lastCommitDate">
                    {{
                      ' ' + translate('Marketplace_LastCommitTime', pluginActivity.lastCommitDate)
                    }}
                  </template>
                </dd>
              </div>

              <div class="pair" v-if="showLicenseName">
                <dt>{{ translate('Marketplace_License') }}</dt>
                <dd>
                  <a v-if="pluginLatestVersion.license?.url" rel="noreferrer noopener"
                     :href="pluginLatestVersion.license?.url"
                     target="_blank">{{ pluginLatestVersion.license?.name }}</a>
                  <span v-else>{{ pluginLatestVersion.license?.name }}</span>
                </dd>
              </div>

              <template v-if="pluginSupport.length">
                <div
                  class="pair"
                  v-for="(support, index) in pluginSupport"
                  :key="`support-${index}`"
                >
                  <template v-if="support.name && support.value">
                    <dt v-html="$sanitize(support.name)"></dt>
                    <dd v-if="this.isValidHttpUrl(support.value)">
                      <a
                        target="_blank"
                        rel="noreferrer noopener"
                        :href="externalRawLink($sanitize(support.value))"
                      >{{ $sanitize(support.value) }}</a>
                    </dd>
                    <dd v-else-if="this.isValidEmail(support.value)">
                      <a
                        :href="`mailto:${ encodeURIComponent(support.value) }`"
                      >{{ $sanitize(support.value) }}</a>
                    </dd>
                    <dd v-else v-html="$sanitize(support.value)"></dd>
                  </template>
                </div>
              </template>
            </template> <!-- v-if="!plugin.isBundle" -->
          </dl>
        </div>

        <div class="plugin-screenshots" v-if="pluginScreenshots.length">
          <hr />
          <h3>{{ translate('Marketplace_Screenshots') }}</h3>
          <div class="thumbnails">
            <figure
              v-for="screenshot in pluginScreenshots"
              :key="`screenshot-${screenshot}`"
            >
              <img :src="`${screenshot}?w=800`" width="800" alt="">
              <figcaption>{{ this.getScreenshotBaseName(screenshot) }}</figcaption>
            </figure>
          </div>
        </div>

        <div class="plugin-documentation" v-if="pluginDocumentation">
          <hr />
          <h3>{{ translate('General_Documentation') }}</h3>
          <div v-html="$sanitize(pluginDocumentation)"></div>
        </div>

        <div class="plugin-faq" v-if="pluginFaq">
          <hr />
          <h3>{{ translate('General_Faq') }}</h3>
          <div v-html="$sanitize(pluginFaq)"></div>
        </div>

        <div class="plugin-reviews" id="reviews" v-if="showReviews">
          <hr />
          <h3>{{ translate('Marketplace_Reviews') }}</h3>
          <iframe class="reviewIframe"
                  :style="pluginReviews.height ? `height: ${pluginReviews.height}px;` : '' "
                  :id="pluginReviews.embedUrl.replace(/[\W_]+/g, ' ')"
                  :src="pluginReviews.embedUrl"></iframe>
        </div>

      </div>
      <div
        class="modal-content__footer"
        :class="{'modal-content__footer--with-free-trial': showFreeTrialDropdown }"
      >
        <img v-if="showFreeTrialDropdown && isMatomoPlugin"
             class="matomo-badge matomo-badge-modal"
             src="plugins/Marketplace/images/matomo-badge.png"
             aria-label="Matomo plugin"
             alt=""
        />
        <div class="cta-container cta-container-modal">
          <div v-if="showFreeTrialDropdown" class="free-trial">
            <div class="free-trial-lead-in">{{ translate('Marketplace_TryFreeTrialTitle') }}</div>
            <select
              class="free-trial-dropdown"
              :title="`${translate('Marketplace_ShownPriceIsExclTax')} ${translate(
                'Marketplace_CurrentNumPiwikUsers',
                numUsers
                )}`"
              v-model="selectedPluginShopVariationUrl"
              @change="changeSelectedPluginShopVariationUrl"
            >
              <option v-for="(variation, index) in plugin.shop.variations" :key="`var-${index}`"
                      :value="variation.addToCartUrl"
                      :title="`${translate(
                      'Marketplace_PriceExclTax',
                      variation.price,
                      variation.currency
                    )} ${translate('Marketplace_CurrentNumPiwikUsers', numUsers)}`"
              >{{ variation.name }} - {{ variation.prettyPrice }} / {{ variation.period }}</option>
            </select>
          </div>

          <CTAContainer
            :is-super-user="isSuperUser"
            :is-plugins-admin-enabled="isPluginsAdminEnabled"
            :is-multi-server-environment="isMultiServerEnvironment"
            :is-valid-consumer="isValidConsumer"
            :is-auto-update-possible="isAutoUpdatePossible"
            :activate-nonce="activateNonce"
            :deactivate-nonce="deactivateNonce"
            :install-nonce="installNonce"
            :update-nonce="updateNonce"
            :plugin="plugin"
            :in-modal="true"
            :shop-variation-url="selectedShopVariationUrl"
            @requestTrial="emitTrialEvent('requestTrial')"
            @startFreeTrial="emitTrialEvent('startFreeTrial')"
          />
        </div>
        <img v-if="!showFreeTrialDropdown && isMatomoPlugin"
             class="matomo-badge matomo-badge-modal"
             src="plugins/Marketplace/images/matomo-badge.png"
             aria-label="Matomo plugin"
             alt=""
        />
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { MatomoUrl, translate, externalLink } from 'CoreHome';
import {
  IPluginShopDetails,
  IPluginShopReviews,
  IPluginShopVariation,
  PluginDetails,
  TObject,
  TObjectArray,
} from '../types';
import CTAContainer from '../PluginList/CTAContainer.vue';
import MissingReqsNotice from '../MissingReqsNotice/MissingReqsNotice.vue';
import ChangeEvent = JQuery.ChangeEvent;

const { $ } = window;

interface PluginDetailsModalState {
  isLoading: boolean;
  currentPluginShopVariationUrl: string;
}

export default defineComponent({
  components: { MissingReqsNotice, CTAContainer },
  props: {
    modelValue: {
      type: Object,
      default: () => ({}),
    },
    activateNonce: {
      type: String,
      required: true,
    },
    deactivateNonce: {
      type: String,
      required: true,
    },
    installNonce: {
      type: String,
      required: true,
    },
    updateNonce: {
      type: String,
      required: true,
    },
    isAutoUpdatePossible: {
      type: Boolean,
      required: true,
    },
    isValidConsumer: {
      type: Boolean,
      required: true,
    },
    isMultiServerEnvironment: {
      type: Boolean,
      required: true,
    },
    isPluginsAdminEnabled: {
      type: Boolean,
      required: true,
    },
    isSuperUser: {
      type: Boolean,
      required: true,
    },
    hasSomeAdminAccess: {
      type: Boolean,
      required: true,
    },
    numUsers: {
      type: Number,
      required: true,
    },
  },
  data(): PluginDetailsModalState {
    return {
      isLoading: true,
      currentPluginShopVariationUrl: '',
    };
  },
  emits: [
    'requestTrial',
    'startFreeTrial',
    'update:modelValue',
  ],
  watch: {
    modelValue(newValue) {
      if (newValue) {
        this.showPluginDetailsDialog();
      }
    },
    isLoading(newValue) {
      if (newValue === false) {
        this.applyExternalTarget();
        this.applyIframeResize();
      }
    },
  },
  computed: {
    plugin(): PluginDetails {
      return this.modelValue as PluginDetails;
    },
    pluginLatestVersion(): TObject {
      const versions: TObjectArray = this.plugin.versions || [{}];
      return versions[versions.length - 1] as TObject;
    },
    pluginReadmeHtml(): TObject {
      return this.pluginLatestVersion?.readmeHtml as TObject || {};
    },
    pluginDescription(): string {
      return this.pluginReadmeHtml?.description as string || '';
    },
    pluginDocumentation(): string {
      return this.pluginReadmeHtml?.documentation as string || '';
    },
    pluginFaq(): string {
      return this.pluginReadmeHtml?.faq as string || '';
    },
    pluginShop(): IPluginShopDetails {
      return this.plugin.shop;
    },
    pluginShopVariations(): IPluginShopVariation[] {
      return this.pluginShop?.variations || [];
    },
    pluginReviews(): IPluginShopReviews | TObject {
      return this.pluginShop?.reviews || {};
    },
    pluginKeywords(): string[] {
      return this.plugin?.keywords || [];
    },
    pluginAuthors(): TObjectArray {
      const authors = this.plugin.authors || [];
      return authors.filter((author) => author.name);
    },
    pluginActivity(): TObject {
      return this.plugin.activity || {};
    },
    pluginChangelogUrl(): string {
      return this.plugin.changelog.url as string || '';
    },
    pluginSupport(): TObjectArray[] {
      return this.plugin.support || [];
    },
    isMatomoPlugin(): boolean {
      return ['piwik', 'matomo-org'].includes(this.plugin.owner);
    },
    pluginOwner(): string {
      return this.isMatomoPlugin ? 'Matomo' : this.plugin.owner;
    },
    showReviews(): boolean {
      return (this.pluginReviews
        && this.pluginReviews.embedUrl
        && this.pluginReviews.averageRating
      ) as boolean;
    },
    showMissingLicenseDescription(): boolean {
      return this.hasSomeAdminAccess && this.plugin.isMissingLicense;
    },
    showExceededLicenseDescription(): boolean {
      return this.hasSomeAdminAccess && this.plugin.hasExceededLicense;
    },
    showMissingRequirementsNoticeIfApplicable(): boolean {
      return this.isSuperUser && (this.plugin.isDownloadable || this.plugin.isInstalled);
    },
    showLicenseName(): boolean {
      const license: TObject = this.pluginLatestVersion?.license as TObject || {};
      return !!license.name;
    },
    showFreeTrialDropdown(): boolean {
      return (
        this.isSuperUser
        && !this.plugin.isMissingLicense
        && !this.plugin.isInstalled
        && !this.plugin.hasExceededLicense
        && this.plugin.isEligibleForFreeTrial
      ) as boolean;
    },
    pluginScreenshots(): string[] {
      return this.plugin.screenshots || [];
    },
    hasHeaderMetadata(): boolean {
      return (this.showReviews
        || !this.plugin.isBundle
        || (this.plugin.numDownloads || 0) > 0
        || (this.plugin.lastUpdated && !this.plugin.isBundle)
      ) as boolean;
    },
    pluginShopVariationsPretty(): string[] {
      return this.pluginShopVariations.map(
        (variation) => `${variation.name} - ${variation.prettyPrice} / ${variation.period}`,
      );
    },
    pluginShopRecommendedVariation(): IPluginShopVariation | null {
      const recommendedVariations = this.pluginShopVariations.filter((v) => v.recommended);
      const defaultVariation = this.pluginShopVariations.length
        ? this.pluginShopVariations[0]
        : null;
      return recommendedVariations.length ? recommendedVariations[0] : defaultVariation;
    },
    selectedPluginShopVariationUrl(): string {
      return this.currentPluginShopVariationUrl
        ? this.currentPluginShopVariationUrl
        : this.pluginShopRecommendedVariation?.addToCartUrl || '';
    },
    selectedShopVariationUrl(): string {
      return this.selectedPluginShopVariationUrl || '';
    },
  },
  methods: {
    changeSelectedPluginShopVariationUrl(event: ChangeEvent) {
      if (event) {
        this.currentPluginShopVariationUrl = event.target.value;
      }
    },
    applyExternalTarget() {
      setTimeout(() => {
        const root = this.$refs.root as HTMLElement;
        $('.modal-content__main a', root).each((index, a) => {
          const link = $(a).attr('href');

          if (link && link.indexOf('http') === 0) {
            $(a).attr('target', '_blank');
          }
        });
      });
    },
    scrollElementIntoView(selector: string) {
      setTimeout(() => {
        const root = this.$refs.root as HTMLElement;
        const elements = $(selector, root);

        if (elements.length && elements[0] && elements[0].scrollIntoView) {
          elements[0].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
        }
      });
    },
    isValidEmail(email: string) {
      // regex from https://stackoverflow.com/a/46181
      // eslint-disable-next-line max-len
      return email.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/);
    },
    isValidHttpUrl(input: string) {
      try {
        const url = new URL(input);
        return url.protocol === 'http:' || url.protocol === 'https:';
      } catch (err) {
        return false;
      }
    },
    getProtocolAndDomain(url: string) {
      const urlObj = new URL(url);
      return `${urlObj.protocol}//${urlObj.hostname}`;
    },
    applyIframeResize() {
      setTimeout(() => {
        const { iFrameResize } = window;
        if (this.pluginReviews) {
          $(() => {
            const $iFrames = $('#pluginDetailsModal iframe.reviewIframe');
            for (let i = 0; i < $iFrames.length; i += 1) {
              // eslint-disable-next-line max-len
              iFrameResize({ checkOrigin: [this.getProtocolAndDomain(this.pluginReviews.embedUrl as string)] }, $iFrames[i]);
            }
          });
        }
      });
    },
    getScreenshotBaseName(screenshot: string) {
      const filename = screenshot.split('/').pop() || '';
      return filename.substring(0, filename.lastIndexOf('.')).split('_').join(' ');
    },
    emitTrialEvent(eventName: 'requestTrial'|'startFreeTrial') {
      const { plugin } = this;

      $('#pluginDetailsModal').modal('close');

      setTimeout(() => {
        this.$emit(eventName, plugin);
      }, 250);
    },
    showPluginDetailsDialog() {
      $('#pluginDetailsModal').modal({
        dismissible: true,
        onCloseEnd: () => {
          MatomoUrl.updateHash({
            ...MatomoUrl.hashParsed.value,
            showPlugin: null,
          });
          this.$emit('update:modelValue', null);
          this.isLoading = true;
        },
      }).modal('open');

      setTimeout(() => {
        this.isLoading = false;
      }, 10); // just to prevent showing the modal when the plugin data are not yet passed in
    },
    getPendingLicenseHelpText(pluginName: string) {
      return translate(
        'Marketplace_PluginLicenseStatusPending',
        pluginName,
        externalLink('https://shop.matomo.org/my-account/'),
        '</a>',
      );
    },
    getCancelledLicenseHelpText(pluginName: string) {
      return translate(
        'Marketplace_PluginLicenseStatusCancelled',
        pluginName,
        externalLink('https://shop.matomo.org/my-account/'),
        '</a>',
      );
    },
    getDownloadLinkMissingHelpText(pluginName: string) {
      return translate(
        'Marketplace_PluginDownloadLinkMissingDescription',
        pluginName,
        externalLink('https://matomo.org/faq/plugins/faq_21/'),
        '</a>',
      );
    },
  },
});
</script>