studiou-wc-free-photo-product.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. <?php
  2. /**
  3. * Plugin Name: QDR - Studiou WC Free Photo Product
  4. * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-free-photo-product
  5. * Description: Allows publishing special WooCommerce products with variants and custom raw image upload for photo printing
  6. * Version: 1.5.12
  7. * Requires at least: 6.9.4
  8. * Requires PHP: 8.2
  9. * Requires Plugins: woocommerce, product-variant-table-for-woocommerce
  10. * Author: Dalibor Votruba
  11. * Author URI: https://www.quadarax.com
  12. * License: GPL v2 or later
  13. * License URI: https://www.gnu.org/licenses/gpl-2.0.html
  14. * Text Domain: studiou-wc-free-photo-product
  15. * Domain Path: /languages
  16. * WC requires at least: 10.0
  17. * WC tested up to: 10.6
  18. * Woo: 12346:a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4
  19. */
  20. if (!defined('WPINC')) {
  21. die;
  22. }
  23. if (!defined('STUDIOU_WCFPP_VERSION')) define('STUDIOU_WCFPP_VERSION', '1.5.12');
  24. if (!defined('STUDIOU_WCFPP_PLUGIN_DIR')) define('STUDIOU_WCFPP_PLUGIN_DIR', plugin_dir_path(__FILE__));
  25. if (!defined('STUDIOU_WCFPP_PLUGIN_URL')) define('STUDIOU_WCFPP_PLUGIN_URL', plugin_dir_url(__FILE__));
  26. if (!defined('STUDIOU_WCFPP_PLUGIN_BASENAME')) define('STUDIOU_WCFPP_PLUGIN_BASENAME', plugin_basename(__FILE__));
  27. if (!defined('STUDIOU_WCFPP_CHUNK_SIZE')) define('STUDIOU_WCFPP_CHUNK_SIZE', 1024 * 1024); // 1MB chunks
  28. class Studiou_WC_Free_Photo_Product {
  29. /** @var Studiou_WC_FPP_DB */
  30. private $db;
  31. /** @var Studiou_WC_FPP_Product */
  32. private $product;
  33. /** @var Studiou_WC_FPP_Upload */
  34. private $upload;
  35. /** @var Studiou_WC_FPP_Shortcode */
  36. private $shortcode;
  37. /** @var Studiou_WC_FPP_Single_Product */
  38. private $single_product;
  39. /** @var Studiou_WC_FPP_Cart */
  40. private $cart;
  41. /** @var Studiou_WC_FPP_Admin */
  42. private $admin;
  43. /** @var Studiou_WC_FPP_Pricing */
  44. private $pricing;
  45. public function __construct() {
  46. $this->load_dependencies();
  47. add_action('admin_init', array($this, 'check_required_plugins'));
  48. add_action('admin_menu', array($this, 'add_admin_menu'), 99);
  49. add_action('admin_enqueue_scripts', array($this, 'register_admin_assets'));
  50. add_action('wp_enqueue_scripts', array($this, 'register_frontend_assets'));
  51. $this->init_components();
  52. add_action('before_woocommerce_init', array($this, 'declare_hpos_compatibility'));
  53. // Override WC core strings with FPP-specific wording (e.g. cart "Estimated total"
  54. // → "Součet" in cs_CZ). Translation lives in our .po so it stays locale-aware.
  55. add_filter('gettext_woocommerce', array($this, 'override_wc_strings'), 10, 2);
  56. }
  57. /**
  58. * Reword selected WooCommerce core strings via our own textdomain so translations
  59. * stay centralized in languages/studiou-wc-free-photo-product-*.po.
  60. */
  61. public function override_wc_strings($translation, $text) {
  62. if ($text === 'Estimated total') {
  63. return __('Total', 'studiou-wc-free-photo-product');
  64. }
  65. return $translation;
  66. }
  67. private function load_dependencies() {
  68. require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-db.php';
  69. require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-product.php';
  70. require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-upload.php';
  71. require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-shortcode.php';
  72. require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-single-product.php';
  73. require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-cart.php';
  74. require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-admin.php';
  75. require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-pricing.php';
  76. }
  77. private function init_components() {
  78. $this->db = new Studiou_WC_FPP_DB();
  79. $this->maybe_upgrade_db();
  80. $this->product = new Studiou_WC_FPP_Product();
  81. $this->upload = new Studiou_WC_FPP_Upload($this->db);
  82. $this->shortcode = new Studiou_WC_FPP_Shortcode();
  83. $this->single_product = new Studiou_WC_FPP_Single_Product($this->db);
  84. $this->cart = new Studiou_WC_FPP_Cart($this->db);
  85. $this->admin = new Studiou_WC_FPP_Admin($this->db);
  86. $this->pricing = new Studiou_WC_FPP_Pricing();
  87. }
  88. /**
  89. * Run dbDelta when the installed schema is older than the current plugin version.
  90. * dbDelta handles adding the batch_token column and its KEY on existing installs.
  91. */
  92. private function maybe_upgrade_db() {
  93. $installed = get_option('studiou_wcfpp_db_version', '');
  94. if (version_compare((string) $installed, STUDIOU_WCFPP_VERSION, '<')) {
  95. $this->db->create_tables();
  96. }
  97. }
  98. public function check_required_plugins() {
  99. if (!class_exists('WooCommerce')) {
  100. add_action('admin_notices', function () {
  101. echo '<div class="notice notice-error"><p>';
  102. echo esc_html__('QDR - Studiou WC Free Photo Product requires WooCommerce to be installed and activated.', 'studiou-wc-free-photo-product');
  103. echo '</p></div>';
  104. });
  105. }
  106. if (!self::is_pvtfw_active()) {
  107. add_action('admin_notices', function () {
  108. echo '<div class="notice notice-error"><p>';
  109. echo esc_html__('QDR - Studiou WC Free Photo Product requires the "Product Variant Table for WooCommerce" plugin to be installed and activated.', 'studiou-wc-free-photo-product');
  110. echo '</p></div>';
  111. });
  112. }
  113. }
  114. /**
  115. * Detects whether the Product Variant Table for WooCommerce (pvtfw) plugin is active,
  116. * regardless of its main file name, by matching the active-plugins list on the folder slug.
  117. */
  118. public static function is_pvtfw_active() {
  119. $slug = 'product-variant-table-for-woocommerce/';
  120. $active = (array) get_option('active_plugins', array());
  121. foreach ($active as $plugin) {
  122. if (strpos($plugin, $slug) === 0) {
  123. return true;
  124. }
  125. }
  126. if (is_multisite()) {
  127. $network = (array) get_site_option('active_sitewide_plugins', array());
  128. foreach (array_keys($network) as $plugin) {
  129. if (strpos($plugin, $slug) === 0) {
  130. return true;
  131. }
  132. }
  133. }
  134. return false;
  135. }
  136. public function declare_hpos_compatibility() {
  137. if (class_exists('Automattic\WooCommerce\Utilities\FeaturesUtil')) {
  138. \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
  139. 'custom_order_tables',
  140. __FILE__,
  141. true
  142. );
  143. }
  144. }
  145. public function add_admin_menu() {
  146. if (class_exists('WooCommerce')) {
  147. add_submenu_page(
  148. 'edit.php?post_type=product',
  149. __('Free Photo Files', 'studiou-wc-free-photo-product'),
  150. __('Free Photo Files', 'studiou-wc-free-photo-product'),
  151. 'manage_woocommerce',
  152. 'studiou-fpp-summary',
  153. array($this->admin, 'render_summary_page')
  154. );
  155. }
  156. }
  157. public function register_admin_assets($hook) {
  158. // Product edit page
  159. $screen = get_current_screen();
  160. if ($screen && ($screen->id === 'product' || $screen->post_type === 'product')) {
  161. wp_enqueue_style(
  162. 'studiou-wcfpp-admin',
  163. STUDIOU_WCFPP_PLUGIN_URL . 'assets/css/admin.css',
  164. array(),
  165. STUDIOU_WCFPP_VERSION
  166. );
  167. wp_enqueue_script(
  168. 'studiou-wcfpp-admin',
  169. STUDIOU_WCFPP_PLUGIN_URL . 'assets/js/admin.js',
  170. array('jquery'),
  171. STUDIOU_WCFPP_VERSION,
  172. true
  173. );
  174. wp_localize_script('studiou-wcfpp-admin', 'studiouWcfpp', array(
  175. 'ajaxUrl' => admin_url('admin-ajax.php'),
  176. 'nonce' => wp_create_nonce('studiou-wcfpp-nonce'),
  177. 'i18n' => array(
  178. 'confirmDelete' => __('Are you sure you want to delete the selected files?', 'studiou-wc-free-photo-product'),
  179. 'confirmDeleteSingle' => __('Are you sure you want to delete this file?', 'studiou-wc-free-photo-product'),
  180. 'confirmDownloadZip' => __('Download selected files as ZIP archive?', 'studiou-wc-free-photo-product'),
  181. 'confirmStatusChange' => __('Are you sure you want to change the status of the selected files?', 'studiou-wc-free-photo-product'),
  182. 'noFilesSelected' => __('No files selected.', 'studiou-wc-free-photo-product'),
  183. 'statusUpdated' => __('Status updated.', 'studiou-wc-free-photo-product'),
  184. 'error' => __('An error occurred.', 'studiou-wc-free-photo-product'),
  185. 'generatingZip' => __('Generating ZIP archive...', 'studiou-wc-free-photo-product'),
  186. 'generatingCsv' => __('Generating CSV file...', 'studiou-wc-free-photo-product'),
  187. 'deleting' => __('Deleting...', 'studiou-wc-free-photo-product'),
  188. ),
  189. ));
  190. }
  191. // Admin summary page
  192. if ('product_page_studiou-fpp-summary' === $hook) {
  193. wp_enqueue_style(
  194. 'studiou-wcfpp-admin',
  195. STUDIOU_WCFPP_PLUGIN_URL . 'assets/css/admin.css',
  196. array(),
  197. STUDIOU_WCFPP_VERSION
  198. );
  199. wp_enqueue_script(
  200. 'studiou-wcfpp-admin',
  201. STUDIOU_WCFPP_PLUGIN_URL . 'assets/js/admin.js',
  202. array('jquery'),
  203. STUDIOU_WCFPP_VERSION,
  204. true
  205. );
  206. wp_localize_script('studiou-wcfpp-admin', 'studiouWcfpp', array(
  207. 'ajaxUrl' => admin_url('admin-ajax.php'),
  208. 'nonce' => wp_create_nonce('studiou-wcfpp-nonce'),
  209. 'i18n' => array(
  210. 'confirmDelete' => __('Are you sure you want to delete the selected files?', 'studiou-wc-free-photo-product'),
  211. 'confirmDeleteSingle' => __('Are you sure you want to delete this file?', 'studiou-wc-free-photo-product'),
  212. 'confirmDownloadZip' => __('Download selected files as ZIP archive?', 'studiou-wc-free-photo-product'),
  213. 'confirmStatusChange' => __('Are you sure you want to change the status of the selected files?', 'studiou-wc-free-photo-product'),
  214. 'noFilesSelected' => __('No files selected.', 'studiou-wc-free-photo-product'),
  215. 'statusUpdated' => __('Status updated.', 'studiou-wc-free-photo-product'),
  216. 'error' => __('An error occurred.', 'studiou-wc-free-photo-product'),
  217. 'generatingZip' => __('Generating ZIP archive...', 'studiou-wc-free-photo-product'),
  218. 'generatingCsv' => __('Generating CSV file...', 'studiou-wc-free-photo-product'),
  219. 'deleting' => __('Deleting...', 'studiou-wc-free-photo-product'),
  220. ),
  221. ));
  222. }
  223. }
  224. public function register_frontend_assets() {
  225. wp_register_style(
  226. 'studiou-wcfpp-frontend',
  227. STUDIOU_WCFPP_PLUGIN_URL . 'assets/css/frontend.css',
  228. array(),
  229. STUDIOU_WCFPP_VERSION
  230. );
  231. wp_register_script(
  232. 'studiou-wcfpp-frontend',
  233. STUDIOU_WCFPP_PLUGIN_URL . 'assets/js/frontend.js',
  234. array('jquery'),
  235. STUDIOU_WCFPP_VERSION,
  236. true
  237. );
  238. wp_localize_script('studiou-wcfpp-frontend', 'studiouWcfppFront', array(
  239. 'ajaxUrl' => admin_url('admin-ajax.php'),
  240. 'nonce' => wp_create_nonce('studiou-wcfpp-front-nonce'),
  241. 'chunkSize' => STUDIOU_WCFPP_CHUNK_SIZE,
  242. 'cartUrl' => wc_get_cart_url(),
  243. 'currency' => array(
  244. // WooCommerce returns these with HTML entities (&#75;&#269; for Kč, &nbsp;
  245. // embedded in priceFormat). Decoded here so the plain-text price formatter
  246. // that fills <option> labels renders real characters instead of entities.
  247. 'symbol' => function_exists('get_woocommerce_currency_symbol') ? html_entity_decode(get_woocommerce_currency_symbol(), ENT_QUOTES | ENT_HTML5, 'UTF-8') : '',
  248. 'priceFormat' => function_exists('get_woocommerce_price_format') ? html_entity_decode(get_woocommerce_price_format(), ENT_QUOTES | ENT_HTML5, 'UTF-8') : '%1$s%2$s',
  249. 'decimalSeparator' => function_exists('wc_get_price_decimal_separator') ? wc_get_price_decimal_separator() : '.',
  250. 'thousandSeparator' => function_exists('wc_get_price_thousand_separator') ? wc_get_price_thousand_separator() : ',',
  251. 'decimals' => function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2,
  252. ),
  253. 'i18n' => array(
  254. 'uploading' => __('Uploading...', 'studiou-wc-free-photo-product'),
  255. 'uploadComplete' => __('Upload complete', 'studiou-wc-free-photo-product'),
  256. 'processing' => __('Processing...', 'studiou-wc-free-photo-product'),
  257. 'uploadError' => __('Upload failed. Please try again.', 'studiou-wc-free-photo-product'),
  258. 'fileTooLarge' => __('File is too large. Maximum size: %s MB', 'studiou-wc-free-photo-product'),
  259. 'selectVariant' => __('Please select a variant.', 'studiou-wc-free-photo-product'),
  260. 'uploadFile' => __('Please upload a file.', 'studiou-wc-free-photo-product'),
  261. 'addedToCart' => __('Product added to cart!', 'studiou-wc-free-photo-product'),
  262. 'addToCartError' => __('Could not add to cart. Please try again.', 'studiou-wc-free-photo-product'),
  263. 'dragDropText' => __('Drag & drop your file here or click to browse', 'studiou-wc-free-photo-product'),
  264. 'removeFile' => __('Remove', 'studiou-wc-free-photo-product'),
  265. 'close' => __('Close', 'studiou-wc-free-photo-product'),
  266. 'enlarge' => __('Enlarge', 'studiou-wc-free-photo-product'),
  267. 'addToCart' => __('Add to cart', 'studiou-wc-free-photo-product'),
  268. 'maxUploadsReached' => __('Maximum number of uploads reached (%d).', 'studiou-wc-free-photo-product'),
  269. 'outOfStock' => __('out of stock', 'studiou-wc-free-photo-product'),
  270. 'error' => __('An error occurred.', 'studiou-wc-free-photo-product'),
  271. 'qtyTiersTitle' => __('Quantity Discount', 'studiou-wc-free-photo-product'),
  272. 'qtyTiersFromQty' => __('From Qty', 'studiou-wc-free-photo-product'),
  273. 'qtyTiersDiscount' => __('Discount', 'studiou-wc-free-photo-product'),
  274. 'qtyTiersUnitPrice' => __('Unit Price', 'studiou-wc-free-photo-product'),
  275. ),
  276. ));
  277. }
  278. public static function activate() {
  279. require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-db.php';
  280. $db = new Studiou_WC_FPP_DB();
  281. $db->create_tables();
  282. // Create chunks temp directory
  283. $upload_dir = wp_upload_dir();
  284. $chunks_dir = $upload_dir['basedir'] . '/studiou-fpp-chunks';
  285. if (!file_exists($chunks_dir)) {
  286. wp_mkdir_p($chunks_dir);
  287. file_put_contents($chunks_dir . '/.htaccess', 'deny from all');
  288. file_put_contents($chunks_dir . '/index.php', '<?php // Silence is golden');
  289. }
  290. }
  291. public static function deactivate() {
  292. // Clean up temp chunks
  293. $upload_dir = wp_upload_dir();
  294. $chunks_dir = $upload_dir['basedir'] . '/studiou-fpp-chunks';
  295. if (is_dir($chunks_dir)) {
  296. $files = glob($chunks_dir . '/*');
  297. foreach ($files as $file) {
  298. if (is_file($file) && basename($file) !== '.htaccess' && basename($file) !== 'index.php') {
  299. unlink($file);
  300. }
  301. }
  302. }
  303. }
  304. }
  305. // Activation/deactivation hooks
  306. register_activation_hook(__FILE__, array('Studiou_WC_Free_Photo_Product', 'activate'));
  307. register_deactivation_hook(__FILE__, array('Studiou_WC_Free_Photo_Product', 'deactivate'));
  308. function run_studiou_wc_free_photo_product() {
  309. static $initialized = false;
  310. if ($initialized) {
  311. return;
  312. }
  313. $initialized = true;
  314. load_plugin_textdomain(
  315. 'studiou-wc-free-photo-product',
  316. false,
  317. dirname(plugin_basename(__FILE__)) . '/languages/'
  318. );
  319. new Studiou_WC_Free_Photo_Product();
  320. }
  321. add_action('plugins_loaded', 'run_studiou_wc_free_photo_product');