Skip to content

Commit 89f4f16

Browse files
Merge pull request #8586 from christianbeeznest/feature/buycourses-yannick-observations
Plugin: Add BuyCourses service copy action
2 parents 6d9565c + 86520b3 commit 89f4f16

6 files changed

Lines changed: 210 additions & 8 deletions

File tree

public/plugin/BuyCourses/lang/en_US.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@
211211
$strings['YouNeedToBeRegisteredInAtLeastOneSession'] = 'You need to be registered in at least one session';
212212
$strings['IfYouWantToGetTheCertificateAndOrSkillsAsociatedToThisCourseYouNeedToBuyTheCertificateServiceYouCanGoToServiceCatalogClickingHere'] = "To obtain the certificate and/or the skills associated to this course, you need to buy the <b> Certificate </b> service. Go to the services catalogue to buy it by clicking <a target='_blank' href='%s'>here</a>";
213213
$strings['ServiceDeleted'] = 'Service deleted';
214+
$strings['ServiceCopied'] = 'Service copied successfully.';
215+
$strings['ServiceCopyFailed'] = 'The service could not be copied.';
216+
$strings['ServiceCopyInvalidSecurityToken'] = 'The copy request is no longer valid. Please try again.';
214217
$strings['YourCoursesNeedAtLeastOneLearningPath'] = 'The courses to which you are subscribed need at least one learning path that contains a final certificate item';
215218
$strings['GlobalTaxPerc'] = 'Global tax rate';
216219
$strings['GlobalTaxPercDescription'] = 'Default tax rate that will be used unless there is a specific tax rate for the course, session or service.';
@@ -596,4 +599,4 @@
596599
$strings['MaxMindAccountId'] = 'MaxMind account ID';
597600
$strings['MaxMindAccountIdHelp'] = 'Required only when MaxMind web service is selected.';
598601
$strings['MaxMindLicenseKey'] = 'MaxMind license key';
599-
$strings['MaxMindLicenseKeyHelp'] = 'Required only when MaxMind web service is selected. Keep this value private.';
602+
$strings['MaxMindLicenseKeyHelp'] = 'Required only when MaxMind web service is selected. Keep this value private.';

public/plugin/BuyCourses/lang/es.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@
211211
$strings['YouNeedToBeRegisteredInAtLeastOneSession'] = 'Necesitas estar registrado en al menos una sesión';
212212
$strings['IfYouWantToGetTheCertificateAndOrSkillsAsociatedToThisCourseYouNeedToBuyTheCertificateServiceYouCanGoToServiceCatalogClickingHere'] = "Si quieres obtener el certificado y/o las competencias asociadas a este curso, necesitas comprar el servicio de <b> Certificado </b>, puedes ir al catálogo de servicios para comprarlo haciendo click <a target='_blank' href='%s'>AQUÍ</a>";
213213
$strings['ServiceDeleted'] = 'Servicio eliminado';
214+
$strings['ServiceCopied'] = 'Servicio copiado correctamente.';
215+
$strings['ServiceCopyFailed'] = 'No se pudo copiar el servicio.';
216+
$strings['ServiceCopyInvalidSecurityToken'] = 'La solicitud de copia ya no es válida. Inténtalo nuevamente.';
214217
$strings['YourCoursesNeedAtLeastOneLearningPath'] = 'Los cursos en los que estás registrado necesitan tener al menos una lección que contenga un item de cerficado final';
215218
$strings['GlobalTaxPerc'] = 'Porcentaje del impuesto global';
216219
$strings['GlobalTaxPercDescription'] = 'Porcentaje por defecto que se usará, excepto si existe un impuesto específico en el curso, sesión o servicio.';

public/plugin/BuyCourses/lang/fr_FR.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@
211211
$strings['YouNeedToBeRegisteredInAtLeastOneSession'] = 'You need to be registered in at least one session';
212212
$strings['IfYouWantToGetTheCertificateAndOrSkillsAsociatedToThisCourseYouNeedToBuyTheCertificateServiceYouCanGoToServiceCatalogClickingHere'] = "Pour obtenir le certificat et/ou les compétences associés à ce cours, vous devez acheter le service <b> Certificat </b>. Allez au catalogue des services pour l'acheter en cliquant <a target='_blank' href='%s'>ici</a>";
213213
$strings['ServiceDeleted'] = 'Service deleted';
214+
$strings['ServiceCopied'] = 'Service copié avec succès.';
215+
$strings['ServiceCopyFailed'] = 'Le service n’a pas pu être copié.';
216+
$strings['ServiceCopyInvalidSecurityToken'] = 'La demande de copie n’est plus valide. Veuillez réessayer.';
214217
$strings['YourCoursesNeedAtLeastOneLearningPath'] = 'The courses to which you are subscribed need at least one learning path that contains a final certificate item';
215218
$strings['GlobalTaxPerc'] = 'Global tax rate';
216219
$strings['GlobalTaxPercDescription'] = 'Default tax rate that will be used unless there is a specific tax rate for the course, session or service.';

public/plugin/BuyCourses/src/buy_course_plugin.class.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3927,6 +3927,140 @@ public function saveServiceBenefitConfigurations(int $serviceId, array $serviceD
39273927
}
39283928
}
39293929

3930+
public function copyService(int $id)
3931+
{
3932+
if ($id <= 0) {
3933+
return false;
3934+
}
3935+
3936+
$servicesTable = Database::get_main_table(self::TABLE_SERVICES);
3937+
$service = Database::select(
3938+
'*',
3939+
$servicesTable,
3940+
[
3941+
'where' => ['id = ?' => $id],
3942+
],
3943+
'first'
3944+
);
3945+
3946+
if (empty($service) || !is_array($service)) {
3947+
return false;
3948+
}
3949+
3950+
$sourceName = trim((string) ($service['name'] ?? ''));
3951+
if ('' === $sourceName) {
3952+
$sourceName = $this->get_lang('Service');
3953+
}
3954+
3955+
$sourceImageName = (string) ($service['image'] ?? '');
3956+
3957+
$copiedServiceId = Database::insert(
3958+
$servicesTable,
3959+
[
3960+
'name' => $sourceName.' (copy)',
3961+
'description' => (string) ($service['description'] ?? ''),
3962+
'price' => isset($service['price']) ? (float) $service['price'] : 0.0,
3963+
'tax_perc' => '' !== (string) ($service['tax_perc'] ?? '') ? (int) $service['tax_perc'] : null,
3964+
'duration_days' => isset($service['duration_days']) ? (int) $service['duration_days'] : 0,
3965+
'renewable' => !empty($service['renewable']) ? 1 : 0,
3966+
'total_charges' => isset($service['total_charges']) ? (int) $service['total_charges'] : 0,
3967+
'allow_trial' => !empty($service['allow_trial']) ? 1 : 0,
3968+
'trial_period' => (string) ($service['trial_period'] ?? ''),
3969+
'trial_frequency' => isset($service['trial_frequency']) ? (int) $service['trial_frequency'] : 0,
3970+
'trial_total_charges' => isset($service['trial_total_charges']) ? (int) $service['trial_total_charges'] : 0,
3971+
'max_subscribers' => isset($service['max_subscribers']) ? (int) $service['max_subscribers'] : 0,
3972+
'subscription_behavior_json' => (string) ($service['subscription_behavior_json'] ?? ''),
3973+
'stripe_price_id' => (string) ($service['stripe_price_id'] ?? ''),
3974+
'display_on_course_creation_page' => !empty($service['display_on_course_creation_page']) ? 1 : 0,
3975+
'applies_to' => isset($service['applies_to']) ? (int) $service['applies_to'] : self::SERVICE_TYPE_NONE,
3976+
'owner_id' => isset($service['owner_id']) ? (int) $service['owner_id'] : api_get_user_id(),
3977+
'visibility' => !empty($service['visibility']) ? 1 : 0,
3978+
'image' => '',
3979+
'video_url' => (string) ($service['video_url'] ?? ''),
3980+
'service_information' => (string) ($service['service_information'] ?? ''),
3981+
]
3982+
);
3983+
3984+
if (!$copiedServiceId) {
3985+
return false;
3986+
}
3987+
3988+
$copiedImageName = 'simg-'.(int) $copiedServiceId.'.png';
3989+
if ($this->copyServiceImage($sourceImageName, $copiedImageName)) {
3990+
Database::update(
3991+
$servicesTable,
3992+
['image' => $copiedImageName],
3993+
['id = ?' => (int) $copiedServiceId]
3994+
);
3995+
}
3996+
3997+
$this->copyServiceBenefitConfigurations($id, (int) $copiedServiceId);
3998+
3999+
return (int) $copiedServiceId;
4000+
}
4001+
4002+
private function copyServiceBenefitConfigurations(int $sourceServiceId, int $targetServiceId): void
4003+
{
4004+
if ($sourceServiceId <= 0 || $targetServiceId <= 0) {
4005+
return;
4006+
}
4007+
4008+
if (!$this->hasPluginTable(self::TABLE_SERVICE_REL_EXTRA_FIELD)) {
4009+
return;
4010+
}
4011+
4012+
$serviceRelTable = Database::get_main_table(self::TABLE_SERVICE_REL_EXTRA_FIELD);
4013+
$rows = Database::select(
4014+
'*',
4015+
$serviceRelTable,
4016+
[
4017+
'where' => ['service_id = ?' => $sourceServiceId],
4018+
]
4019+
);
4020+
4021+
foreach ($rows as $row) {
4022+
$extraFieldId = isset($row['extra_field_id']) ? (int) $row['extra_field_id'] : 0;
4023+
$grantedValue = isset($row['granted_value']) ? (int) $row['granted_value'] : 0;
4024+
4025+
if ($extraFieldId <= 0 || $grantedValue <= 0) {
4026+
continue;
4027+
}
4028+
4029+
Database::insert($serviceRelTable, [
4030+
'service_id' => $targetServiceId,
4031+
'extra_field_id' => $extraFieldId,
4032+
'granted_value' => $grantedValue,
4033+
]);
4034+
}
4035+
}
4036+
4037+
private function copyServiceImage(string $sourceImageName, string $targetImageName): bool
4038+
{
4039+
if ('' === $sourceImageName || '' === $targetImageName) {
4040+
return false;
4041+
}
4042+
4043+
if (!$this->serviceImageExists($sourceImageName)) {
4044+
return false;
4045+
}
4046+
4047+
try {
4048+
$pluginsFilesystem = Container::getPluginsFileSystem();
4049+
$directory = $this->getServiceImagesDirectory();
4050+
4051+
if (!$pluginsFilesystem->directoryExists($directory)) {
4052+
$pluginsFilesystem->createDirectory($directory);
4053+
}
4054+
4055+
$content = $pluginsFilesystem->read($this->getServiceImageStoragePath($sourceImageName));
4056+
$pluginsFilesystem->write($this->getServiceImageStoragePath($targetImageName), $content);
4057+
} catch (\Throwable) {
4058+
return false;
4059+
}
4060+
4061+
return true;
4062+
}
4063+
39304064
/**
39314065
* update a service.
39324066
*

public/plugin/BuyCourses/src/list_service.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
$plugin = BuyCoursesPlugin::create();
1717
$httpRequest = Container::getRequest();
1818

19+
$copyServiceTokenKey = 'buycourses_copy_service_token';
20+
if (empty($_SESSION[$copyServiceTokenKey])) {
21+
$_SESSION[$copyServiceTokenKey] = bin2hex(random_bytes(32));
22+
}
23+
1924
$includeSession = 'true' === $plugin->get('include_sessions');
2025
$includeServices = 'true' === $plugin->get('include_services');
2126

@@ -27,6 +32,40 @@
2732

2833
api_protect_admin_script(true);
2934

35+
if ('POST' === $httpRequest->getMethod() && 'copy_service' === $httpRequest->request->get('action')) {
36+
$serviceId = $httpRequest->request->getInt('id');
37+
$postedToken = (string) $httpRequest->request->get('copy_service_token', '');
38+
$sessionToken = (string) ($_SESSION[$copyServiceTokenKey] ?? '');
39+
40+
if ('' === $sessionToken || !hash_equals($sessionToken, $postedToken)) {
41+
Display::addFlash(
42+
Display::return_message($plugin->get_lang('ServiceCopyInvalidSecurityToken'), 'warning')
43+
);
44+
45+
header('Location: list_service.php');
46+
exit;
47+
}
48+
49+
$_SESSION[$copyServiceTokenKey] = bin2hex(random_bytes(32));
50+
51+
$copiedServiceId = $plugin->copyService($serviceId);
52+
if (false === $copiedServiceId) {
53+
Display::addFlash(
54+
Display::return_message($plugin->get_lang('ServiceCopyFailed'), 'error')
55+
);
56+
57+
header('Location: list_service.php');
58+
exit;
59+
}
60+
61+
Display::addFlash(
62+
Display::return_message($plugin->get_lang('ServiceCopied'), 'success')
63+
);
64+
65+
header('Location: services_edit.php?id='.(int) $copiedServiceId);
66+
exit;
67+
}
68+
3069
$pageSize = BuyCoursesPlugin::PAGINATION_PAGE_SIZE;
3170
$currentPage = max(1, $httpRequest->query->getInt('page', 1));
3271
$first = $pageSize * ($currentPage - 1);
@@ -72,6 +111,7 @@
72111
$tpl->assign('courses', []);
73112
$tpl->assign('sessions', []);
74113
$tpl->assign('services', $services);
114+
$tpl->assign('copy_service_token', (string) ($_SESSION[$copyServiceTokenKey] ?? ''));
75115

76116
$tpl->assign('course_pagination', '');
77117
$tpl->assign('session_pagination', '');

public/plugin/BuyCourses/view/list.tpl

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -603,13 +603,32 @@
603603
{% endif %}
604604

605605
<td class="px-6 py-4 text-right">
606-
<a
607-
href="{{ url('index') ~ 'plugin/BuyCourses/src/services_edit.php?' ~ {'id': item.id}|url_encode }}"
608-
class="inline-flex items-center justify-center gap-2 rounded-xl bg-primary px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2"
609-
>
610-
<em class="fa fa-wrench fa-fw"></em>
611-
{{ 'Edit'|get_lang }}
612-
</a>
606+
<div class="inline-flex flex-wrap items-center justify-end gap-2">
607+
<a
608+
href="{{ url('index') ~ 'plugin/BuyCourses/src/services_edit.php?' ~ {'id': item.id}|url_encode }}"
609+
class="inline-flex items-center justify-center gap-2 rounded-xl bg-primary px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2"
610+
>
611+
<em class="fa fa-wrench fa-fw"></em>
612+
{{ 'Edit'|get_lang }}
613+
</a>
614+
615+
<form
616+
method="post"
617+
action="{{ url('index') ~ 'plugin/BuyCourses/src/list_service.php' }}"
618+
class="inline-flex"
619+
>
620+
<input type="hidden" name="action" value="copy_service">
621+
<input type="hidden" name="id" value="{{ item.id }}">
622+
<input type="hidden" name="copy_service_token" value="{{ copy_service_token }}">
623+
<button
624+
type="submit"
625+
class="inline-flex items-center justify-center gap-2 rounded-xl border border-gray-25 bg-white px-4 py-2.5 text-sm font-semibold text-gray-90 shadow-sm transition hover:border-primary/30 hover:text-primary focus:outline-none focus:ring-2 focus:ring-primary/20 focus:ring-offset-2"
626+
>
627+
<em class="fa fa-copy fa-fw"></em>
628+
{{ 'Copy'|get_lang }}
629+
</button>
630+
</form>
631+
</div>
613632
</td>
614633
</tr>
615634
{% else %}

0 commit comments

Comments
 (0)