Skip to content

Developer guide

Add-on ID kv_product_videos, addon.xml scheme 3.0, priority 1500. No core files are changed. Overrides are not used. Everything runs through PHP hooks, a backend post-controller, template hooks and schema extensions.

app/addons/kv_product_videos/
addon.xml settings, install/uninstall SQL
init.php hook registration
func.php public API + hook handlers
controllers/backend/products.post.php "Videos" tab on products.update / products.add
controllers/backend/kv_product_videos_license.php "Check now" / cron license check
schemas/exim/products.post.php "Videos" import/export field
schemas/exim/products.functions.php import/export functions
schemas/block_manager/blocks.post.php product block cache invalidation + license cache key
Tygh/Addons/KvProductVideos/VideoSource.php URL parsing, embed/watch URL builders
Tygh/Addons/KvProductVideos/MetadataFetcher.php oEmbed title/duration + poster download
Tygh/Addons/KvProductVideos/License/Client.php license check client (generated from the shared Kavio Labs template)
design/backend/templates/addons/kv_product_videos/
hooks/products/tabs_content.post.tpl tab inside the product form
hooks/index/styles.post.tpl
views/components/videos_tab.tpl
license/info.tpl License tab (points to the card)
license/card.tpl License card (add-on page) / banner
license/status_bar.tpl status chip/banner (not used: no own admin pages)
license/badge.tpl badge in Add-ons > Manage add-ons
hooks/addons/{tabs_content.pre,manage.post}.tpl card, badge
hooks/index/styles.post.tpl add-on styles + license.less (kv-license block)
design/backend/css/addons/kv_product_videos/styles.less
design/backend/css/addons/kv_product_videos/license.less card, chip and badge styles
var/themes_repository/responsive/templates/addons/kv_product_videos/
hooks/products/product_images.post.tpl gallery slides + thumbnails (data container)
hooks/products/product_data.post.tpl product list marker (badge / hover)
hooks/products/layout_content.post.tpl VideoObject JSON-LD
hooks/index/{styles,scripts}.post.tpl
blocks/product_tabs/kv_product_videos.tpl "Videos" product tab
components/player.tpl click-to-play player markup
var/themes_repository/responsive/css/addons/kv_product_videos/styles.less
js/addons/kv_product_videos/func.js
var/langs/en/addons/kv_product_videos.po

cscart_kv_product_videos

Column Type Notes
video_id int unsigned, PK, AI
product_id int unsigned indexed together with status and position
source_type varchar(16) youtube, vimeo, url (self-hosted link) or file (uploaded)
source_url varchar(1024) the link as entered (empty for file)
video_key varchar(128) YouTube ID, or Vimeo id / id:hash
file_path varchar(255) path inside the kv_product_videos storage (<product_id>/<file>)
mime_type varchar(64) for url and file
start_at int unsigned start offset in seconds parsed from the link
duration int unsigned seconds (from Vimeo oEmbed); used in JSON-LD
position int sort order
status char(1) A active / D disabled
timestamp, updated_timestamp int unsigned creation date is used as uploadDate

cscart_kv_product_videos_descriptions has the columns video_id, lang_code and title. The primary key is (video_id, lang_code).

Preview images are standard CS-Cart image pairs with the object type kv_product_video. The object ID is video_id and the type is M. Only the detailed image is stored, and thumbnails are generated by core.

Uploaded files go through the CS-Cart storage API. The storage type kv_product_videos (prefix images/kv_product_videos, root dir, CDN-enabled) is registered on demand by fn_kv_product_videos_get_storage(). PHP execution in images/ is already blocked by the core .htaccess.

Hook Handler Purpose
update_product_post fn_kv_product_videos_update_product_post Saves the Videos tab. It runs only in the admin area, for the submitted product ($_REQUEST['product_id'] or a new product), only when product_data[kv_pv_submitted]=Y is present, once per request, and only if fn_kv_product_videos_can_edit_product() allows it.
delete_product_post fn_kv_product_videos_delete_product_post Deletes videos, posters and the product’s upload folder.
clone_product fn_kv_product_videos_clone_product Copies rows, all-language titles, posters (fn_clone_image_pairs) and uploaded files.
get_products_post fn_kv_product_videos_get_products_post Storefront only. One query per product list. Adds $product['kv_pv_list'] (count, badge flag, hover source).
before_dispatch static License\Client::onBeforeDispatch Admin area and console only: runs the due license check and shows license notices.
addons:tabs_content pre, admin template License card at the top of the add-on page.
addons:manage post, admin template License badge, moved into the add-on’s row (4.17–4.21 have no per-row hook).
index:styles post, admin template license.less (kv-license block).

The kv_pv_submitted flag is printed after all rows of the tab. If PHP truncates the request because of max_input_vars, the flag is missing and nothing is changed or deleted.

fn_set_hook('kv_product_videos_get_videos_pre', $product_id, $params, $lang_code);
fn_set_hook('kv_product_videos_get_videos_post', $product_id, $params, $lang_code, $videos);
fn_set_hook('kv_product_videos_prepare_video_post', $video);
fn_set_hook('kv_product_videos_update_video_pre', $product_id, $data, $video_id, $lang_code, $video_file, $thumbnail_file);
fn_set_hook('kv_product_videos_update_video_post', $product_id, $data, $video_id, $lang_code, $record);
fn_set_hook('kv_product_videos_delete_video_pre', $video_id, $video);
fn_set_hook('kv_product_videos_get_structured_data_item', $product, $video, $item);

Register them in your add-on’s init.php as usual. For example, fn_register_hooks('kv_product_videos_get_structured_data_item') with fn_<your_addon>_kv_product_videos_get_structured_data_item(&$product, &$video, &$item).

Function Returns
fn_kv_product_videos_get_videos($product_id, $params = [], $lang_code) Videos keyed by video_id. $params: status (string or array), limit, get_images
fn_kv_product_videos_get_product_videos($product_id, $lang_code) Active videos prepared for the storefront: embed_url, embed_url_muted, background_url, src, play_src, watch_url, is_embed, main_pair. Respects Maximum videos. Cached per request. Usable as a Smarty modifier.
fn_kv_product_videos_update_video($product_id, $data, $video_id = 0, $lang_code, $video_file = [], $thumbnail_file = [], $fetch_metadata = null) Creates ($video_id = 0) or updates a video. $data: source_url, title, position, status, delete_thumbnail. Returns video_id or false
fn_kv_product_videos_delete_video($video_id) / fn_kv_product_videos_delete_product_videos($product_id)
fn_kv_product_videos_clone_videos($from_product_id, $to_product_id)
fn_kv_product_videos_get_structured_data($product) JSON strings (one VideoObject per video), safe to print inside a script tag
fn_kv_product_videos_can_edit_product($product_id) Edition-aware permission check (MV vendor ownership, CS-Cart shared products)
\Tygh\Addons\KvProductVideos\VideoSource::parseUrl($url) ['source_type','video_key','start_at','mime_type','source_url'] or null

Example: add a YouTube video from your own code.

fn_kv_product_videos_update_video($product_id, [
'source_url' => 'https://youtu.be/aqz-KE-bpKQ',
'title' => 'Product overview',
'status' => 'A',
], 0, DESCR_SL);

The core gallery template views/products/components/product_images.tpl has no hooks inside the gallery, and the add-on does not override it. Instead:

  1. hooks/products/product_images.post.tpl renders a hidden container .kv-pv-gallery-data after the gallery. It holds the video slides (.kv-pv-player), the thumbnail items, and a thumbnail-strip shell for products with a single image. It also contains <script class="cm-ajax-force">.
  2. js/addons/kv_product_videos/func.js runs from the bottom of the page, before DOM ready. After AJAX reloads (option changes, quick view) the forced inline script runs it too, before $.commonInit(). It moves the slides into .cm-preview-wrapper and the thumbnails into #images_preview_<id> at the same index. Then it renumbers data-ca-image-order.
  3. The core Owl carousel then starts as usual. The add-on listens to ce.product_image_gallery.ready and ce.product_image_gallery.image_changed. On these events it reveals the slides, stops videos that are no longer visible and handles muted autoplay.
  4. If the markup is unknown or the carousel is already running, the container is shown as a plain list of players below the images. This is the same as the Below the product images placement.

A player is a <button> poster. The iframe (youtube-nocookie.com or player.vimeo.com with dnt=1) or the <video> element is created only on click. The iframe URL is always built from a validated video ID and never from raw user input.

JSON-LD is printed by hooks/products/layout_content.post.tpl. The tag has data-no-defer and data-no-execute="§", so CS-Cart neither moves it nor evaluates it during AJAX updates.

Tygh.kvProductVideos.scan(context); // integrate pending gallery containers
Tygh.kvProductVideos.initLists(context); // product list badges/hover
Tygh.kvProductVideos.play(element, muted);
Tygh.kvProductVideos.stop(element);

All classes start with kv-pv-. The player keeps the product image box ratio through an inline padding-top. Without a known size it falls back to 16:9. Override the styles in your theme’s custom CSS.

The add-on extends blocks.post.php with the kv_product_videos and kv_product_videos_descriptions tables. Cached product blocks (badge and hover markers) therefore refresh when videos change. It also adds the license state to the products block cache key (callable_handlers kv_pv_license → fn_kv_product_videos_is_licensed).

Tygh\Addons\KvProductVideos\License\Client verifies the license against the Kavio Labs license server. The client, the “Check now”/cron controller controllers/backend/kv_product_videos_license.php and the License tab template design/backend/templates/addons/kv_product_videos/license/info.tpl are generated from the shared Kavio Labs template; don’t edit them by hand.

  • Admin license UI (generated, REVISION 3; don’t edit by hand): design/backend/templates/addons/kv_product_videos/license/card.tpl (License card at the top of the add-on page, every tab; hooks/addons/tabs_content.pre.tpl), license/badge.tpl (badge in Add-ons > Manage add-ons; hooks/addons/manage.post.tpl), license/status_bar.tpl (chip/banner for the add-on’s own admin pages), design/backend/css/addons/kv_product_videos/license.less (loaded by the kv-license block of hooks/index/styles.post.tpl). The add-on has no admin pages of its own, so it uses no status bar. All of them render nothing for vendors and for administrators without the “Manage settings” privilege. The “what is switched off” sentence is the language variable kv_product_videos.license_off_effect. License\Client assigns the view data to $kv_lic_kv_product_videos in its before_dispatch handler (only for administrators with the Manage settings privilege); the templates only read this variable, so they render nothing (and no error) while an older version of the PHP code is loaded, for example in the request that uploads a new zip over the installed add-on. Templates never call add-on functions; PHP code can call fn_kv_product_videos_license_ui($context).
  • fn_kv_product_videos_is_licensed() (func.php) is the gate. It reads the stored state only and never contacts the server.
  • fn_kv_product_videos_get_product_videos() returns no videos (gallery slides and thumbnails, the “Videos” tab, VideoObject structured data); fn_kv_product_videos_get_products_post() adds no list badge or hover data.
  • The core products block is cached; its cache key includes the license state (blocks.post.php, callable_handlers), so the badge appears or disappears at once.
  • Admin video management (fn_kv_product_videos_get_videos(), the Videos tab, import/export) is not gated. Core 4.21 product videos are not affected.
  • Client::boot() (init.php) registers a before_dispatch handler for the admin area and the console only. On admin page views and console requests it runs the check when it is due (key changed, or 24 hours after the last check; 1 hour after a failure) and shows the admin notice once per session.
  • Request: POST https://cs-cart.kaviolabs.com/index.php?dispatch=kv_licenses.check with JSON {"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent through Tygh\Http (3 s connect, 5 s total; kept out of the CS-Cart request log). The answer is accepted only when its Ed25519 signature matches a public key embedded in the client and it echoes the nonce.
  • State: storage key kv_license_kv_product_videos (?:storage_data), or var/kv_storage/kv_product_videos/license.json while the full page cache is active, protected by an HMAC with the store’s crypt key.
  • Cron: php admin.php --dispatch=kv_product_videos_license.check.
  • Development stores only (http_host and https_host are development hosts): the constants KV_LICENSE_SERVER_URL, KV_LICENSE_TEST_HOST (act as if the development storefronts ran on that host) and KV_LICENSE_TEST_TIME_OFFSET (seconds). They are ignored on production hosts.
  • All SQL goes through db_* with placeholders.
  • Video and image uploads go through fn_filter_uploaded_data(). Videos are limited to mp4, m4v, webm and ogv. Images are limited to the formats the image library supports, and to the image size limit in 4.18+.
  • Links are accepted only with http/https and without whitespace or quote characters. Titles are stripped of HTML. All template output is escaped.
  • Remote metadata requests run only in the admin area. They use short timeouts (5 s connect, 10 s total) and a host allow-list for downloaded images (i.ytimg.com, img.youtube.com, i.vimeocdn.com). Failures are ignored.
  • Every hook handler is wrapped in try/catch, so the add-on never breaks the storefront, product saving, cloning or deletion.

Tygh/Addons/KvProductVideos/License/Ed25519.php (checks the signature of the license server’s answers on servers whose PHP has no sodium extension) is derived from paragonie/sodium_compat, ISC License, Copyright (c) 2016-2023 Paragon Initiative Enterprises and Copyright (c) 2013-2019 Frank Denis. The full license notice is in the file header.