diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index ef2d8a87..45d2d6fb 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -1031,9 +1031,9 @@ }, "image_log.json": { "type": "-", - "size": 977, + "size": 128703, "lmtime": 0, - "modified": false + "modified": true }, "images.inc.php": { "type": "-", @@ -1756,6 +1756,12 @@ "dashgoals": {}, "dashproducts": {}, "dashtrends": {}, + ".DS_Store": { + "type": "-", + "size": 22532, + "lmtime": 0, + "modified": false + }, "ets_blog": {}, "ets_multilayerslider": {}, "followup": {}, @@ -1775,306 +1781,6 @@ "lmtime": 0, "modified": false }, - "pagesnotfound": {}, - "productcomments": {}, - "ps_accounts": {}, - "ps_banner": {}, - "ps_buybuttonlite": {}, - "ps_categorytree": {}, - "ps_checkout": {}, - "ps_checkpayment": {}, - "ps_contactinfo": {}, - "ps_crossselling": {}, - "ps_currencyselector": {}, - "ps_customeraccountlinks": {}, - "ps_customersignin": {}, - "ps_customtext": {}, - "ps_dataprivacy": {}, - "ps_emailalerts": {}, - "ps_emailsubscription": {}, - "ps_facebook": {}, - "ps_facetedsearch": { - "_dev": { - "back": { - "blocklayered.css": { - "type": "-", - "size": 1577, - "lmtime": 1737287962008, - "modified": false - }, - "blocklayered.css.map": { - "type": "-", - "size": 4300, - "lmtime": 1737287962008, - "modified": false - }, - "blocklayered.scss": { - "type": "-", - "size": 1810, - "lmtime": 0, - "modified": false - }, - "index.js": { - "type": "-", - "size": 8041, - "lmtime": 0, - "modified": false - } - }, - "front": { - "events.js": { - "type": "-", - "size": 1114, - "lmtime": 0, - "modified": false - }, - "facet.css": { - "type": "-", - "size": 1529, - "lmtime": 1737287962008, - "modified": false - }, - "facet.css.map": { - "type": "-", - "size": 3483, - "lmtime": 1737287962008, - "modified": false - }, - "facet.scss": { - "type": "-", - "size": 1398, - "lmtime": 0, - "modified": false - }, - "index.js": { - "type": "-", - "size": 911, - "lmtime": 0, - "modified": false - }, - "overlay.css": { - "type": "-", - "size": 1560, - "lmtime": 1737287962008, - "modified": false - }, - "overlay.css.map": { - "type": "-", - "size": 3763, - "lmtime": 1737287962008, - "modified": false - }, - "overlay.js": { - "type": "-", - "size": 1225, - "lmtime": 0, - "modified": false - }, - "overlay.scss": { - "type": "-", - "size": 1522, - "lmtime": 0, - "modified": false - }, - "slider.css": { - "type": "-", - "size": 1194, - "lmtime": 1737287962008, - "modified": false - }, - "slider.css.map": { - "type": "-", - "size": 2760, - "lmtime": 1737287962008, - "modified": false - }, - "slider.js": { - "type": "-", - "size": 3816, - "lmtime": 0, - "modified": false - }, - "slider.scss": { - "type": "-", - "size": 1179, - "lmtime": 0, - "modified": false - }, - "urlparser.js": { - "type": "-", - "size": 1051, - "lmtime": 0, - "modified": false - } - } - } - }, - "ps_faviconnotificationbo": {}, - "ps_featuredproducts": {}, - "psgdpr": { - "views": { - "templates": { - "hook": { - "displayGDPRConsent.tpl": { - "type": "-", - "size": 4075, - "lmtime": 1737912422958, - "modified": false - } - } - } - } - }, - "ps_imageslider": {}, - "ps_languageselector": {}, - "ps_linklist": {}, - "ps_mainmenu": {}, - "ps_mbo": {}, - "ps_metrics": {}, - "ps_reminder": {}, - "ps_searchbar": {}, - "ps_sharebuttons": {}, - "ps_shoppingcart": {}, - "ps_socialfollow": {}, - "ps_themecusto": {}, - "ps_wirepayment": {}, - "psxmarketingwithgoogle": {}, - "raty": {}, - "referralprogram": {}, - "regenerateimage": {}, - "santandercredit": { - "CERT": {}, - "config": {}, - "config_pl.xml": { - "type": "-", - "size": 507, - "lmtime": 0, - "modified": false - }, - "config.xml": { - "type": "-", - "size": 579, - "lmtime": 0, - "modified": false - }, - "controllers": {}, - "doc": {}, - "images": {}, - "index.php": { - "type": "-", - "size": 1272, - "lmtime": 0, - "modified": false - }, - "js": {}, - "logo.gif": { - "type": "-", - "size": 1217, - "lmtime": 0, - "modified": false - }, - "logo.png": { - "type": "-", - "size": 3571, - "lmtime": 0, - "modified": false - }, - "santandercredit.php": { - "type": "-", - "size": 59073, - "lmtime": 1741559739588, - "modified": false - }, - "services": {}, - "sql": {}, - "translations": {}, - "upgrade": {}, - "views": { - "templates": { - "hook": { - "displayOrderDetail.tpl": { - "type": "-", - "size": 5988, - "lmtime": 0, - "modified": false - }, - "ehpDisplayAdminOrder.tpl": { - "type": "-", - "size": 698, - "lmtime": 0, - "modified": false - }, - "ehpDisplayDetails.tpl": { - "type": "-", - "size": 2862, - "lmtime": 0, - "modified": false - }, - "ehpDisplayLog.tpl": { - "type": "-", - "size": 2857, - "lmtime": 0, - "modified": false - }, - "index.php": { - "type": "-", - "size": 1272, - "lmtime": 0, - "modified": false - }, - "infos2.tpl": { - "type": "-", - "size": 4834, - "lmtime": 0, - "modified": false - }, - "infos.tpl": { - "type": "-", - "size": 5007, - "lmtime": 0, - "modified": false - }, - "santanderCreditInfo.tpl": { - "type": "-", - "size": 1154, - "lmtime": 0, - "modified": false - }, - "santanderCreditPayment.tpl": { - "type": "-", - "size": 3580, - "lmtime": 0, - "modified": false - }, - "santanderCreditProduct.tpl": { - "type": "-", - "size": 1740, - "lmtime": 1741560111947, - "modified": false - } - } - } - } - }, - "statsbestcategories": {}, - "statsbestcustomers": {}, - "statsbestmanufacturers": {}, - "statsbestproducts": {}, - "statsbestsuppliers": {}, - "statsbestvouchers": {}, - "statscarrier": {}, - "statscatalog": {}, - "statscheckup": {}, - "statsdata": {}, - "statsforecast": {}, - "statsnewsletter": {}, - "statspersonalinfos": {}, - "statsproduct": {}, - "statsregistrations": {}, - "statssales": {}, - "statssearch": {}, - "statsstock": {}, - "welcome": {}, "leobootstrapmenu": { "classes": { "BtmegamenuGroup.php": { @@ -2916,6 +2622,1245 @@ "lmtime": 1736978940000, "modified": false } + }, + "pagesnotfound": {}, + "productcomments": {}, + "ps_accounts": {}, + "ps_banner": {}, + "ps_buybuttonlite": {}, + "ps_categorytree": {}, + "ps_checkout": {}, + "ps_checkpayment": {}, + "ps_contactinfo": {}, + "ps_crossselling": {}, + "ps_currencyselector": {}, + "ps_customeraccountlinks": {}, + "ps_customersignin": {}, + "ps_customtext": {}, + "ps_dataprivacy": {}, + "ps_emailalerts": { + "composer.json": { + "type": "-", + "size": 635, + "lmtime": 1742566965000, + "modified": false + }, + "composer.lock": { + "type": "-", + "size": 96251, + "lmtime": 1742566964000, + "modified": false + }, + "config_pl.xml": { + "type": "-", + "size": 575, + "lmtime": 1742566964000, + "modified": false + }, + "config.xml": { + "type": "-", + "size": 604, + "lmtime": 1742566964000, + "modified": false + }, + "CONTRIBUTORS.md": { + "type": "-", + "size": 163, + "lmtime": 1742566963000, + "modified": false + }, + "controllers": { + "front": { + "account.php": { + "type": "-", + "size": 2239, + "lmtime": 1742566971000, + "modified": false + }, + "actions.php": { + "type": "-", + "size": 5710, + "lmtime": 1742566970000, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566971000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566970000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566965000, + "modified": false + }, + "js": { + "admin": { + "ps_emailalerts.js": { + "type": "-", + "size": 2225, + "lmtime": 1742566968000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566968000, + "modified": false + }, + "mailalerts.js": { + "type": "-", + "size": 3411, + "lmtime": 1742566968000, + "modified": false + } + }, + "LICENSE.md": { + "type": "-", + "size": 10329, + "lmtime": 1742566963000, + "modified": false + }, + "logo.png": { + "type": "-", + "size": 4942, + "lmtime": 1742566964000, + "modified": false + }, + "logo.webp": { + "type": "-", + "size": 1568, + "lmtime": 1742566964000, + "modified": false + }, + "MailAlert.php": { + "type": "-", + "size": 13614, + "lmtime": 1742566964000, + "modified": false + }, + "mailalerts-account.php": { + "type": "-", + "size": 2685, + "lmtime": 1742566965000, + "modified": false + }, + "mailalerts-ajax.php": { + "type": "-", + "size": 1111, + "lmtime": 1742566964000, + "modified": false + }, + "mailalerts-extra.php": { + "type": "-", + "size": 3372, + "lmtime": 1742566964000, + "modified": false + }, + "mails": { + "en": { + "customer_qty.html": { + "type": "-", + "size": 5446, + "lmtime": 1742566973000, + "modified": false + }, + "customer_qty.txt": { + "type": "-", + "size": 278, + "lmtime": 1742566975000, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566974000, + "modified": false + }, + "new_order.html": { + "type": "-", + "size": 16873, + "lmtime": 1742566974000, + "modified": false + }, + "new_order.txt": { + "type": "-", + "size": 606, + "lmtime": 1742566973000, + "modified": false + }, + "order_changed.html": { + "type": "-", + "size": 6172, + "lmtime": 1742566973000, + "modified": false + }, + "order_changed.txt": { + "type": "-", + "size": 535, + "lmtime": 1742566973000, + "modified": false + }, + "productcoverage.html": { + "type": "-", + "size": 5407, + "lmtime": 1742566974000, + "modified": false + }, + "productcoverage.txt": { + "type": "-", + "size": 229, + "lmtime": 1742566973000, + "modified": false + }, + "productoutofstock.html": { + "type": "-", + "size": 5501, + "lmtime": 1742566974000, + "modified": false + }, + "productoutofstock.txt": { + "type": "-", + "size": 303, + "lmtime": 1742566974000, + "modified": false + }, + "return_slip.html": { + "type": "-", + "size": 7614, + "lmtime": 1742566974000, + "modified": false + }, + "return_slip.txt": { + "type": "-", + "size": 420, + "lmtime": 1742566974000, + "modified": false + } + }, + "pl": { + "customer_qty.html": { + "type": "-", + "size": 26018, + "lmtime": 1736978509245, + "modified": false + }, + "customer_qty.txt": { + "type": "-", + "size": 384, + "lmtime": 1736978509245, + "modified": false + }, + "new_order.html": { + "type": "-", + "size": 59197, + "lmtime": 1736978509246, + "modified": false + }, + "new_order.txt": { + "type": "-", + "size": 832, + "lmtime": 1736978509247, + "modified": false + }, + "order_changed.html": { + "type": "-", + "size": 28750, + "lmtime": 1736978509247, + "modified": false + }, + "order_changed.txt": { + "type": "-", + "size": 591, + "lmtime": 1736978509248, + "modified": false + }, + "productcoverage.html": { + "type": "-", + "size": 22913, + "lmtime": 1736978509248, + "modified": false + }, + "productcoverage.txt": { + "type": "-", + "size": 325, + "lmtime": 1736978509249, + "modified": false + }, + "productoutofstock.html": { + "type": "-", + "size": 26620, + "lmtime": 1736978509249, + "modified": false + }, + "productoutofstock.txt": { + "type": "-", + "size": 433, + "lmtime": 1736978509250, + "modified": false + }, + "return_slip.html": { + "type": "-", + "size": 45652, + "lmtime": 1736978509250, + "modified": false + }, + "return_slip.txt": { + "type": "-", + "size": 510, + "lmtime": 1736978509251, + "modified": false + } + }, + "sk": { + "customer_qty.html": { + "type": "-", + "size": 5472, + "lmtime": 1742566975000, + "modified": false + }, + "customer_qty.txt": { + "type": "-", + "size": 294, + "lmtime": 1742566976000, + "modified": false + }, + "new_order.html": { + "type": "-", + "size": 16921, + "lmtime": 1742566975000, + "modified": false + }, + "new_order.txt": { + "type": "-", + "size": 630, + "lmtime": 1742566975000, + "modified": false + }, + "order_changed.html": { + "type": "-", + "size": 6559, + "lmtime": 1742566975000, + "modified": false + }, + "order_changed.txt": { + "type": "-", + "size": 564, + "lmtime": 1742566975000, + "modified": false + }, + "productcoverage.html": { + "type": "-", + "size": 5444, + "lmtime": 1742566975000, + "modified": false + }, + "productcoverage.txt": { + "type": "-", + "size": 263, + "lmtime": 1742566975000, + "modified": false + }, + "productoutofstock.html": { + "type": "-", + "size": 5504, + "lmtime": 1742566976000, + "modified": false + }, + "productoutofstock.txt": { + "type": "-", + "size": 308, + "lmtime": 1742566976000, + "modified": false + }, + "return_slip.html": { + "type": "-", + "size": 7677, + "lmtime": 1742566976000, + "modified": false + }, + "return_slip.txt": { + "type": "-", + "size": 478, + "lmtime": 1742566976000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566971000, + "modified": false + } + }, + "ps_emailalerts.php": { + "type": "-", + "size": 60692, + "lmtime": 1742566965000, + "modified": false + }, + "README.md": { + "type": "-", + "size": 1525, + "lmtime": 1742566964000, + "modified": false + }, + "tests": { + "php": { + "phpstan": { + "phpstan-1.7.6.neon": { + "type": "-", + "size": 547, + "lmtime": 1742566967000, + "modified": false + }, + "phpstan-1.7.7.neon": { + "type": "-", + "size": 323, + "lmtime": 1742566967000, + "modified": false + }, + "phpstan-1.7.8.neon": { + "type": "-", + "size": 323, + "lmtime": 1742566966000, + "modified": false + }, + "phpstan-8.0.neon": { + "type": "-", + "size": 322, + "lmtime": 1742566967000, + "modified": false + }, + "phpstan-latest.neon": { + "type": "-", + "size": 323, + "lmtime": 1742566967000, + "modified": false + }, + "phpstan.neon": { + "type": "-", + "size": 440, + "lmtime": 1742566967000, + "modified": false + } + }, + "phpstan.sh": { + "type": "-", + "size": 1165, + "lmtime": 1742566966000, + "modified": false + } + } + }, + "translations": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566971000, + "modified": false + }, + "pl.php": { + "type": "-", + "size": 0, + "lmtime": 1742566970000, + "modified": false + } + }, + "upgrade": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566968000, + "modified": false + }, + "install-2.1.0.php": { + "type": "-", + "size": 1095, + "lmtime": 1742566967000, + "modified": false + }, + "install-2.3.4.php": { + "type": "-", + "size": 981, + "lmtime": 1742566968000, + "modified": false + }, + "upgrade-2.3.1.php": { + "type": "-", + "size": 1039, + "lmtime": 1742566968000, + "modified": false + }, + "upgrade-2.3.3.php": { + "type": "-", + "size": 1041, + "lmtime": 1742566968000, + "modified": false + }, + "upgrade-2.4.0.php": { + "type": "-", + "size": 1997, + "lmtime": 1742566967000, + "modified": false + }, + "upgrade-2.4.1.php": { + "type": "-", + "size": 977, + "lmtime": 1742566967000, + "modified": false + } + }, + "vendor": { + "autoload.php": { + "type": "-", + "size": 178, + "lmtime": 1742566965000, + "modified": false + }, + "composer": { + "autoload_classmap.php": { + "type": "-", + "size": 147, + "lmtime": 1742566966000, + "modified": false + }, + "autoload_namespaces.php": { + "type": "-", + "size": 149, + "lmtime": 1742566966000, + "modified": false + }, + "autoload_psr4.php": { + "type": "-", + "size": 143, + "lmtime": 1742566966000, + "modified": false + }, + "autoload_real.php": { + "type": "-", + "size": 1453, + "lmtime": 1742566966000, + "modified": false + }, + "autoload_static.php": { + "type": "-", + "size": 317, + "lmtime": 1742566965000, + "modified": false + }, + "ClassLoader.php": { + "type": "-", + "size": 13420, + "lmtime": 1742566966000, + "modified": false + }, + "installed.json": { + "type": "-", + "size": 3, + "lmtime": 1742566966000, + "modified": false + }, + "LICENSE": { + "type": "-", + "size": 1070, + "lmtime": 1742566965000, + "modified": false + } + } + }, + "views": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566968000, + "modified": false + }, + "templates": { + "admin": { + "_configure": { + "helpers": { + "form": { + "form.tpl": { + "type": "-", + "size": 3381, + "lmtime": 1742566969000, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566970000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566969000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566969000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566969000, + "modified": false + } + }, + "front": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566969000, + "modified": false + }, + "mailalerts-account-line.tpl": { + "type": "-", + "size": 1513, + "lmtime": 1742566969000, + "modified": false + }, + "mailalerts-account.tpl": { + "type": "-", + "size": 1427, + "lmtime": 1742566969000, + "modified": false + } + }, + "hook": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566970000, + "modified": false + }, + "my-account-footer.tpl": { + "type": "-", + "size": 966, + "lmtime": 1742566970000, + "modified": false + }, + "my-account.tpl": { + "type": "-", + "size": 966, + "lmtime": 1742566970000, + "modified": false + }, + "product.tpl": { + "type": "-", + "size": 2064, + "lmtime": 1742566970000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566969000, + "modified": false + } + } + } + }, + "ps_emailsubscription": {}, + "ps_facebook": {}, + "ps_facetedsearch": { + "_dev": { + "back": { + "blocklayered.css": { + "type": "-", + "size": 1577, + "lmtime": 1737287962008, + "modified": false + }, + "blocklayered.css.map": { + "type": "-", + "size": 4300, + "lmtime": 1737287962008, + "modified": false + }, + "blocklayered.scss": { + "type": "-", + "size": 1810, + "lmtime": 0, + "modified": false + }, + "index.js": { + "type": "-", + "size": 8041, + "lmtime": 0, + "modified": false + } + }, + "front": { + "events.js": { + "type": "-", + "size": 1114, + "lmtime": 0, + "modified": false + }, + "facet.css": { + "type": "-", + "size": 1529, + "lmtime": 1737287962008, + "modified": false + }, + "facet.css.map": { + "type": "-", + "size": 3483, + "lmtime": 1737287962008, + "modified": false + }, + "facet.scss": { + "type": "-", + "size": 1398, + "lmtime": 0, + "modified": false + }, + "index.js": { + "type": "-", + "size": 911, + "lmtime": 0, + "modified": false + }, + "overlay.css": { + "type": "-", + "size": 1560, + "lmtime": 1737287962008, + "modified": false + }, + "overlay.css.map": { + "type": "-", + "size": 3763, + "lmtime": 1737287962008, + "modified": false + }, + "overlay.js": { + "type": "-", + "size": 1225, + "lmtime": 0, + "modified": false + }, + "overlay.scss": { + "type": "-", + "size": 1522, + "lmtime": 0, + "modified": false + }, + "slider.css": { + "type": "-", + "size": 1194, + "lmtime": 1737287962008, + "modified": false + }, + "slider.css.map": { + "type": "-", + "size": 2760, + "lmtime": 1737287962008, + "modified": false + }, + "slider.js": { + "type": "-", + "size": 3816, + "lmtime": 0, + "modified": false + }, + "slider.scss": { + "type": "-", + "size": 1179, + "lmtime": 0, + "modified": false + }, + "urlparser.js": { + "type": "-", + "size": 1051, + "lmtime": 0, + "modified": false + } + } + } + }, + "ps_faviconnotificationbo": {}, + "ps_featuredproducts": {}, + "psgdpr": { + "views": { + "templates": { + "hook": { + "displayGDPRConsent.tpl": { + "type": "-", + "size": 4075, + "lmtime": 1737912422958, + "modified": false + } + } + } + } + }, + "ps_imageslider": {}, + "ps_languageselector": {}, + "ps_linklist": {}, + "ps_mainmenu": {}, + "ps_mbo": {}, + "ps_metrics": {}, + "ps_reminder": {}, + "ps_searchbar": {}, + "ps_sharebuttons": {}, + "ps_shoppingcart": {}, + "ps_socialfollow": {}, + "ps_themecusto": {}, + "ps_wirepayment": {}, + "psxmarketingwithgoogle": {}, + "raty": {}, + "referralprogram": {}, + "regenerateimage": {}, + "santandercredit": { + "CERT": {}, + "config": {}, + "config_pl.xml": { + "type": "-", + "size": 507, + "lmtime": 0, + "modified": false + }, + "config.xml": { + "type": "-", + "size": 579, + "lmtime": 0, + "modified": false + }, + "controllers": {}, + "doc": {}, + "images": {}, + "index.php": { + "type": "-", + "size": 1272, + "lmtime": 0, + "modified": false + }, + "js": {}, + "logo.gif": { + "type": "-", + "size": 1217, + "lmtime": 0, + "modified": false + }, + "logo.png": { + "type": "-", + "size": 3571, + "lmtime": 0, + "modified": false + }, + "santandercredit.php": { + "type": "-", + "size": 59073, + "lmtime": 1741559739588, + "modified": false + }, + "services": {}, + "sql": {}, + "translations": {}, + "upgrade": {}, + "views": { + "templates": { + "hook": { + "displayOrderDetail.tpl": { + "type": "-", + "size": 5988, + "lmtime": 0, + "modified": false + }, + "ehpDisplayAdminOrder.tpl": { + "type": "-", + "size": 698, + "lmtime": 0, + "modified": false + }, + "ehpDisplayDetails.tpl": { + "type": "-", + "size": 2862, + "lmtime": 0, + "modified": false + }, + "ehpDisplayLog.tpl": { + "type": "-", + "size": 2857, + "lmtime": 0, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1272, + "lmtime": 0, + "modified": false + }, + "infos2.tpl": { + "type": "-", + "size": 4834, + "lmtime": 0, + "modified": false + }, + "infos.tpl": { + "type": "-", + "size": 5007, + "lmtime": 0, + "modified": false + }, + "santanderCreditInfo.tpl": { + "type": "-", + "size": 1154, + "lmtime": 0, + "modified": false + }, + "santanderCreditPayment.tpl": { + "type": "-", + "size": 3580, + "lmtime": 0, + "modified": false + }, + "santanderCreditProduct.tpl": { + "type": "-", + "size": 1740, + "lmtime": 1741560111947, + "modified": false + } + } + } + } + }, + "statsbestcategories": {}, + "statsbestcustomers": {}, + "statsbestmanufacturers": {}, + "statsbestproducts": {}, + "statsbestsuppliers": {}, + "statsbestvouchers": {}, + "statscarrier": {}, + "statscatalog": {}, + "statscheckup": {}, + "statsdata": {}, + "statsforecast": {}, + "statsnewsletter": {}, + "statspersonalinfos": {}, + "statsproduct": {}, + "statsregistrations": {}, + "statssales": {}, + "statssearch": {}, + "statsstock": {}, + "welcome": {}, + "ps_cashondelivery": { + "composer.json": { + "type": "-", + "size": 1072, + "lmtime": 1742566353000, + "modified": false + }, + "composer.lock": { + "type": "-", + "size": 51498, + "lmtime": 1742566352000, + "modified": false + }, + "config_pl.xml": { + "type": "-", + "size": 482, + "lmtime": 1742566352000, + "modified": false + }, + "config.xml": { + "type": "-", + "size": 496, + "lmtime": 1742566352000, + "modified": false + }, + "controllers": { + "front": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566357000, + "modified": false + }, + "validation.php": { + "type": "-", + "size": 4122, + "lmtime": 1742566357000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566357000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566352000, + "modified": false + }, + "LICENSE.md": { + "type": "-", + "size": 10329, + "lmtime": 1742566351000, + "modified": false + }, + "logo.png": { + "type": "-", + "size": 8755, + "lmtime": 1742566352000, + "modified": false + }, + "logo.webp": { + "type": "-", + "size": 716, + "lmtime": 1742566352000, + "modified": false + }, + "ps_cashondelivery.php": { + "type": "-", + "size": 12104, + "lmtime": 1742566352000, + "modified": false + }, + "README.md": { + "type": "-", + "size": 863, + "lmtime": 1742567048000, + "modified": false + }, + "tests": { + "index.php": { + "type": "-", + "size": 1271, + "lmtime": 1742566354000, + "modified": false + }, + "phpstan": { + "index.php": { + "type": "-", + "size": 1271, + "lmtime": 1742566354000, + "modified": false + }, + "phpstan-1.7.6.neon": { + "type": "-", + "size": 799, + "lmtime": 1742566355000, + "modified": false + }, + "phpstan-1.7.7.neon": { + "type": "-", + "size": 416, + "lmtime": 1742566355000, + "modified": false + }, + "phpstan-1.7.8.neon": { + "type": "-", + "size": 185, + "lmtime": 1742566354000, + "modified": false + }, + "phpstan-latest.neon": { + "type": "-", + "size": 185, + "lmtime": 1742566354000, + "modified": false + }, + "phpstan.neon": { + "type": "-", + "size": 341, + "lmtime": 1742566354000, + "modified": false + } + }, + "phpstan.sh": { + "type": "-", + "size": 1074, + "lmtime": 1742566354000, + "modified": false + } + }, + "translations": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566357000, + "modified": false + }, + "pl.php": { + "type": "-", + "size": 0, + "lmtime": 1742566356000, + "modified": false + } + }, + "upgrade": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566355000, + "modified": false + }, + "upgrade-1.0.7.php": { + "type": "-", + "size": 1252, + "lmtime": 1742566355000, + "modified": false + }, + "upgrade-2.0.0.php": { + "type": "-", + "size": 1906, + "lmtime": 1742566355000, + "modified": false + } + }, + "vendor": { + "autoload.php": { + "type": "-", + "size": 178, + "lmtime": 1742566353000, + "modified": false + }, + "composer": { + "autoload_classmap.php": { + "type": "-", + "size": 317, + "lmtime": 1742566354000, + "modified": false + }, + "autoload_namespaces.php": { + "type": "-", + "size": 149, + "lmtime": 1742566354000, + "modified": false + }, + "autoload_psr4.php": { + "type": "-", + "size": 143, + "lmtime": 1742566354000, + "modified": false + }, + "autoload_real.php": { + "type": "-", + "size": 1453, + "lmtime": 1742566353000, + "modified": false + }, + "autoload_static.php": { + "type": "-", + "size": 656, + "lmtime": 1742566353000, + "modified": false + }, + "ClassLoader.php": { + "type": "-", + "size": 13420, + "lmtime": 1742566353000, + "modified": false + }, + "installed.json": { + "type": "-", + "size": 3, + "lmtime": 1742566353000, + "modified": false + }, + "LICENSE": { + "type": "-", + "size": 1070, + "lmtime": 1742566353000, + "modified": false + } + } + }, + "views": { + "img": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566355000, + "modified": false + }, + "orderstate": { + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566356000, + "modified": false + }, + "PS_OS_COD_VALIDATION.gif": { + "type": "-", + "size": 1028, + "lmtime": 1742566356000, + "modified": false + } + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566355000, + "modified": false + }, + "templates": { + "hook": { + "displayOrderConfirmation.tpl": { + "type": "-", + "size": 1390, + "lmtime": 1742566356000, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566356000, + "modified": false + }, + "paymentOptions-additionalInformation.tpl": { + "type": "-", + "size": 999, + "lmtime": 1742566356000, + "modified": false + }, + "payment_return.tpl": { + "type": "-", + "size": 1470, + "lmtime": 1742566356000, + "modified": false + }, + "ps_cashondelivery_intro.tpl": { + "type": "-", + "size": 1090, + "lmtime": 1742566356000, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1127, + "lmtime": 1742566356000, + "modified": false + } + } + } } }, "override": {}, diff --git a/modules/import_api/classes/convertor.php b/modules/import_api/classes/convertor.php new file mode 100644 index 00000000..8986f9dd --- /dev/null +++ b/modules/import_api/classes/convertor.php @@ -0,0 +1,448 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ + +class Convertor +{ + private $settings; + private $creator; + private $id_lang; + + public function __construct($settings, $creator) + { + $this->settings = $settings; + $this->creator = $creator; + $this->id_lang = (int)Configuration::get('PS_LANG_DEFAULT'); + } + + public function convertToFrmProduct($original_product) + { + $product_category = array(); + if (!empty($original_product['category_path'])) { + + foreach ($original_product['category_path'] as $path) { + + if ($path['parent']) { + $parent_id = $this->creator->getCategoryId($path['parent'], $this->settings['top_category_id']); + } else { + $parent_id = $this->settings['top_category_id']; + } + + if ($this->settings['import_api_settings']['category_path']) { + $categories = explode('->', $path['value']); + } else { + $categories = array($path['value']); + } + + foreach ($categories as $category) { + if (!$category) { + continue; + } + + $parent_id = $category_id = $this->creator->getCategoryId($category, $parent_id); + + } + + $product_category[] = $category_id; + } + } + + $product_category = array_unique($product_category); + + if (!$product_category && $this->settings['default_category_id']) { + $product_category[] = $this->settings['default_category_id']; + } + + if (!$product_category && $this->settings['top_category_id']) { + $product_category[] = $this->settings['top_category_id']; + } + + if (!in_array(Configuration::get('PS_HOME_CATEGORY'),$product_category)) { + //$product_category[] = Configuration::get('PS_HOME_CATEGORY'); + } + + if (!$product_category) { + $product_category[] = Configuration::get('PS_HOME_CATEGORY'); + } + + if (!empty($original_product['brand'])) { + $manufacturer_id = $this->creator->getManufacturerId($original_product['brand']); + } else { + $manufacturer_id = $this->settings['default_manufacturer_id']; + } + + + + $product = new Product(); + + $original_product['description'] = nl2br($original_product['description']); + + + + foreach (Language::getLanguages(false) as $lang) { + $original_product['name']= Tools::cleanNonUnicodeSupport($original_product['name']); + $product->name[$lang['id_lang']] = htmlspecialchars_decode($original_product['name']); + + $product->description_long[$lang['id_lang']] = htmlspecialchars_decode(htmlspecialchars_decode($original_product['description'], ENT_COMPAT)); + $product->description[$lang['id_lang']] = htmlspecialchars_decode(htmlspecialchars_decode($original_product['description'], ENT_COMPAT)); + $return_str = $original_product['name']; + $return_str = Tools::replaceAccentedChars($return_str); //AccentedChars + $return_str = preg_replace('/[^a-zA-Z0-9\s\'\:\/\[\]\-]/', '', $return_str); // accented chars + $product->link_rewrite[$lang['id_lang']] = Tools::link_rewrite($return_str); + $product->meta_keywords[$lang['id_lang']] = str_replace(' ', ',', $original_product['name']); + + if (!empty($original_product['short_description'])) { + $product->description_short[$lang['id_lang']] = $original_product['short_description']; + } + } + + $product->reference = $original_product['reference']; + $product->id_category = $product_category; + $product->id_manufacturer = $manufacturer_id; + $product->id_category_default = !empty($category_id) ? $category_id : Configuration::get('PS_HOME_CATEGORY'); + $product->active = isset($original_product['active']) ? $original_product['active'] : 1; + $product->width = $original_product['width']; + $product->height = $original_product['height']; + $product->depth = $original_product['depth']; + $product->weight = $original_product['weight']; + //'visibility' => 'both', + $product->location = $original_product['location']; + $product->additional_shipping_cost = $original_product['additional_shipping_cost']; + //'unit_price' => 0, + + $product->quantity = $original_product['quantity']; + $product->minimal_quantity = isset($original_product['minimal_quantity']) ? $original_product['minimal_quantity'] : 1; + $product->price = $original_product['price']; + $product->wholesale_price = $original_product['wholesale_price']; + $product->reference = $original_product['reference']; + $product->ean13 = $original_product['ean13']; + $product->upc = $original_product['upc']; + $product->condition = $original_product['condition']; + + if ($this->settings['id_tax_rules_group'] != -1) { + $product->id_tax_rules_group = $this->settings['id_tax_rules_group']; + } + + $product -> additional_delivery_times = 2; + $product->delivery_in_stock[7] = '1-5 dni roboczych'; + + $product->add(); + + // Pobieranie listy wszystkich sklepów + $shops = Shop::getShops(); + foreach ($shops as $shop) { + // Skojarzenie produktu ze sklepem + $product->associateTo($shop['id_shop']); + } + + // Aktualizacja danych produktu dla każdego sklepu + foreach ($shops as $shop) { + Shop::setContext(Shop::CONTEXT_SHOP, $shop['id_shop']); + + // Przykład aktualizacji stanu magazynowego, statusu i zdjęć + StockAvailable::setQuantity($product->id, 0, $product->quantity, $shop['id_shop']); + $product->active = 1; // Ustawienie produktu jako aktywnego + $product->update(); + + // Tutaj możesz dodać logikę do aktualizacji zdjęć produktu, jeśli to konieczne + // Może to wymagać dodatkowego kodu zależnego od sposobu zarządzania zdjęciami w Twoim sklepie + } + + // Resetowanie kontekstu sklepu (opcjonalnie) + Shop::setContext(Shop::CONTEXT_ALL); + + foreach ($original_product['images'] as $image_url) { + $this->creator->addImageToProduct($image_url, $product->id); + } + + if (!empty($original_product['cover'])) { + $this->creator->addImageToProduct($original_product['cover'], $product->id, true); + } + + if ( $original_product['feature'] and $original_product['feature_value'] ) { + $original_product['features'] = array( + array( + 'feature' => $original_product['feature'], + 'feature_value' => $original_product['feature_value'] + ) + ); + } + + if (!empty($original_product['features'])) { + $this->creator->addFeaturesToProduct($original_product['features'], $product->id); + } + + StockAvailable::setQuantity($product->id, 0, $original_product['quantity']); + + if (!empty($original_product['attributes'])) { + $attribute_details = !empty($original_product['attribute_details']) ? $original_product['attribute_details'] : array(); + + $this->creator->addAttributesToProduct($original_product['attributes'], $product->id, $attribute_details); + } + + $product->addToCategories($product_category); + + return $product; + } + + public function unArray($original_product) + { + $frm_simple_fields = array('unique'=> 'unique_default', 'reference' => '', 'name' => '', 'description' => '', 'brand' => '', 'price' => 0, 'wholesale_price' => 0, 'minimal_quantity' => 1, 'quantity' => '', 'cover' => '', 'ean13' => '', 'upc' => '', 'condition' => 'new', 'additional_shipping_cost' => 0, 'location'=> '', 'width' => 0.00000, 'height' => 0.00000, 'depth' => 0.00000, 'weight' => 0.00000, 'active' => 1); + + foreach ($frm_simple_fields as $field => $default) { + + if (isset($original_product[$field])) { + while (is_array($original_product[$field])) { + $original_product[$field] = current($original_product[$field]); + } + } else { + $original_product[$field] = $default; + } + } + return $original_product; + } + + public function clearInput($original_product) + { + + + $original_product['price'] = (float)str_replace(',', '.', $original_product['price']); + $original_product['wholesale_price'] = (float)str_replace(',', '.', $original_product['wholesale_price']); + $original_product['additional_shipping_cost'] = (float)str_replace(',', '.', $original_product['additional_shipping_cost']); + $original_product['quantity'] = (int)str_replace(',', '.', $original_product['quantity']); + $original_product['minimal_quantity'] = (int)str_replace(',', '', $original_product['minimal_quantity']); + + $original_product['width'] = (float)str_replace(',', '.', $original_product['width']); + $original_product['height'] = (float)str_replace(',', '.', $original_product['height']); + $original_product['depth'] = (float)str_replace(',', '.', $original_product['depth']); + $original_product['weight'] = (float)str_replace(',', '.', $original_product['weight']); + + $original_product['price'] = number_format($original_product['price'], 2, '.', ''); + $original_product['wholesale_price'] = number_format($original_product['wholesale_price'], 2, '.', ''); + $original_product['additional_shipping_cost'] = number_format($original_product['additional_shipping_cost'], 2, '.', ''); + + + if (isset($original_product['active'])) { + $not_active_statuses = array('disabled', 'not active', 'removed'); // lower case, if you add new status + if (empty($original_product['active']) || in_array(mb_strtolower($original_product['active']), $not_active_statuses)){ + $original_product['active'] = 0; + } else { + $original_product['active'] = 1; + } + } + + if ($this->settings['import_api_settings']['price_multiplier']) { + $original_product['price'] *= $this->settings['import_api_settings']['price_multiplier']; + } + + + if (empty($original_product['name'])) { + $original_product['name'] = $original_product['unique']; + } + + $original_product['name'] = str_replace(['>','<', '=', ';', '{', '}', '#'], ' ', $original_product['name']); + + $original_product['description'] = htmlspecialchars($original_product['description'], ENT_COMPAT, 'UTF-8'); // This is for to keep html in database after pSQL + + if (!empty($original_product['images'])) { + if (!is_array($original_product['images'])) { + $original_product['images'] = array($original_product['images']); + } + } else { + $original_product['images'] = array(); + } + + /*$existing_images = array(); + + foreach($original_product['images'] as $img) { + $headers = @get_headers($img, 1); + + if (!isset($headers['Content-Type'])) { + $existing_images[] = $img; + } elseif (strpos($headers['Content-Type'], 'image/') !== FALSE) { + // regular image (and not 404). + $existing_images[] = $img; + } + } + + $original_product['images'] = $existing_images; + + $headers = @get_headers($original_product['cover'], 1); + + if (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'image/') === FALSE) { + unset($original_product['cover']); + } */ + + if (empty($original_product['cover']) && !empty($original_product['images'])) { + $original_product['cover'] = array_shift($original_product['images']); + } + + if (!empty($original_product['ean13']) && !Validate::isEan13($original_product['ean13'])) { + $original_product['ean13'] = ''; + } + + if (!empty($original_product['upc']) && !Validate::isUpc($original_product['upc'])) { + $original_product['upc'] = ''; + } + + if (!empty($original_product['width']) && !Validate::isUnsignedFloat($original_product['width'])) { + $original_product['width'] = 0; + } + + if (!empty($original_product['height']) && !Validate::isUnsignedFloat($original_product['height'])) { + $original_product['height'] = 0; + } + + if (!empty($original_product['depth']) && !Validate::isUnsignedFloat($original_product['depth'])) { + $original_product['depth'] = 0; + } + + if (!empty($original_product['weight']) && !Validate::isUnsignedFloat($original_product['weight'])) { + $original_product['weight'] = 0; + } + + if (!empty($original_product['price']) && !Validate::isPrice($original_product['price'])) { + $original_product['price'] = 0; + } + + if (!empty($original_product['wholesale_price']) && !Validate::isPrice($original_product['wholesale_price'])) { + $original_product['wholesale_price'] = 0; + } + + if (!empty($original_product['additional_shipping_cost']) && !Validate::isPrice($original_product['additional_shipping_cost'])) { + $original_product['additional_shipping_cost'] = 0; + } + + if (!empty($original_product['location']) && !Validate::isReference($original_product['location'])) { + $original_product['location'] = ''; + } + if (!empty($original_product['short_description']) && !Validate::isCleanHtml($original_product['short_description'])) { + $original_product['short_description'] = ''; + } + if (!empty($original_product['description']) && !Validate::isCleanHtml($original_product['description'])) { + $original_product['description'] = ''; + } + + if (!empty($original_product['condition']) && !in_array($original_product['condition'], ['new', 'used', 'refurbished'])) { + $original_product['condition'] = ''; + } + + return $original_product; + } + + public function replace($original_product) + { + foreach ($this->settings['import_api_replace'] as $product_key => $replaces) { + foreach ($replaces as $replace) { + if (isset($replace[0]) && isset($replace[1])) { + if (isset($original_product[$product_key]) && is_string($original_product[$product_key])) { + $original_product[$product_key] = str_replace($replace[0], $replace[1], $original_product[$product_key]); + } + if ($product_key == 'category' && isset($original_product['category_path'])) { + foreach($original_product['category_path'] as &$category_path) { + if ($category_path['value'] == $replace[0]) { + $category_path['value'] = $replace[1]; + } + } + } + } + + } + } + + return $original_product; + + } + public function filter($original_product) + { + $original_product['belong'] = true; + foreach ($this->settings['import_api_filter_options'] as $product_key => $filter_option) { + if ($filter_option === 'not_empty' && (!isset($original_product[$product_key]) || $original_product[$product_key] === '' || $original_product[$product_key] === 'false' || $original_product[$product_key] === 'null')) { + $original_product['belong'] = false; + return $original_product; + } + + } + + $product_categories = array(); + if (isset($original_product['category_path'])) { + foreach($original_product['category_path'] as $category_path) { + $product_categories[] = $category_path['value']; + } + } + foreach ($this->settings['import_api_filter'] as $product_key => $filters) { + if (isset($this->settings['import_api_filter_options'][$product_key])) { + $search_type = $this->settings['import_api_filter_options'][$product_key]; + } else { + $search_type = 'equal'; + } + + foreach ($filters as $filter) { + if ($filter != '') { + if (isset($original_product[$product_key])) { + if ($search_type == 'equal') { + if (!is_array($original_product[$product_key]) && $original_product[$product_key] == $filter) { + $original_product['belong'] = true; + continue 2; // Search next product[key] condition, this is found.// If you need to fulfil only one product key condition, exit here from function with belong=true + + } + if ($product_key == 'category' && in_array($filter, $product_categories)) { + $original_product['belong'] = true; + continue 2; + + } + } elseif ($search_type == 'not_equal') { + if (!is_array($original_product[$product_key]) && $original_product[$product_key] != $filter) { + $original_product['belong'] = true; + continue 2; // Search next product[key] condition, this is found.// If you need to fulfil only one product key condition, exit here from function with belong=true + + } + if ($product_key == 'category' && !in_array($filter, $product_categories)) { + $original_product['belong'] = true; + continue 2; + + } + } elseif ($search_type == 'greater') { + if (!is_array($original_product[$product_key]) && $original_product[$product_key] > $filter) { + $original_product['belong'] = true; + continue 2; + } + } elseif ($search_type == 'less') { + if (!is_array($original_product[$product_key]) && $original_product[$product_key] < $filter) { + $original_product['belong'] = true; + continue 2; + } + } elseif ($search_type == 'regexp') { + if (!is_array($original_product[$product_key]) && preg_match('/' . $filter .'/i', $original_product[$product_key]) !== 0) { + $original_product['belong'] = true; + continue 2; + } + + if ($product_key == 'category' && preg_grep('/' . $filter .'/i', $product_categories)) { + $original_product['belong'] = true; + continue 2; + + } + } + } + } + + } + $original_product['belong'] = false;// If is here, it is not true. Search is not found + break; + + } + + return $original_product; + + } +} diff --git a/modules/import_api/classes/creator.php b/modules/import_api/classes/creator.php new file mode 100644 index 00000000..92aa7e26 --- /dev/null +++ b/modules/import_api/classes/creator.php @@ -0,0 +1,851 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ + +/** Used for validatefields diying without user friendly error or not */ + +if (!defined('UNFRIENDLY_ERROR')) +{ + define('UNFRIENDLY_ERROR', false); +} + +class Creator +{ + + private $home_category = 0; + private $id_lang = 1; + + public function __construct($file_id) + { + $this->id_lang = (int)Configuration::get('PS_LANG_DEFAULT'); + $this->home_category = Configuration::get('PS_HOME_CATEGORY'); + //$query_file_settings = Db::getInstance()->executeS("SELECT f.*, fs.mapping FROM ". _DB_PREFIX_ . "ia_files f LEFT JOIN ". _DB_PREFIX_ . "ia_file_settings fs ON(fs.file_id = f.file_id) WHERE f.file_id = '" . $file_id . "' LIMIT 1"); + $query_file_settings = Db::getInstance()->executeS("SELECT * FROM " . _DB_PREFIX_ . "ia_file_settings WHERE file_id = '" . $file_id . "' LIMIT 1"); + $this->settings = !empty($query_file_settings[0]['settings']) ? json_decode($query_file_settings[0]['settings'], true) : array(); + $this->setAttributes(); + } + + public function getManufacturerId($name) + { + if (is_numeric($name) && Manufacturer::manufacturerExists((int)$name)) + { + return (int)$name; + } + elseif (is_string($name)) + { + if ($manufacturer_id = Manufacturer::getIdByName($name)) + { + return (int)$manufacturer_id; + } + else + { + $manufacturer = new Manufacturer(); + $manufacturer->name = $name; + $manufacturer->active = true; + + if (($field_error = $manufacturer->validateFields(UNFRIENDLY_ERROR, true)) === true && + ($lang_field_error = $manufacturer->validateFieldsLang(UNFRIENDLY_ERROR, true)) === true && $manufacturer->add() + ) + { + return (int)$manufacturer->id; + //$manufacturer->associateTo($product->id_shop_list); + } + } + } + + return 0; + } + + public function getCategoryId($value, $parent_id = null) + { + if (!$parent_id) + { + $parent_id = $this->home_category; + } + + if (is_numeric($value) && Category::categoryExists((int)$value)) + { + return (int)$value; + } + elseif (is_string($value)) + { + $existing_category = Category::searchByName($this->id_lang, $value, true, true); // try and false at end to work with Cache, view Category class function + if ($existing_category) + { + + return (int)$existing_category['id_category']; + } + else + { + $category_to_create = new Category(); + $category_to_create->id = (int)$value; + $category_to_create->name = Creator::createMultiLangField($value); + $category_to_create->active = 1; + $category_to_create->id_parent = $parent_id; // Default parent is home for unknown category to create + $category_link_rewrite = Tools::link_rewrite($category_to_create->name[$this->id_lang]); + $category_to_create->link_rewrite = Creator::createMultiLangField($category_link_rewrite); + if (($field_error = $category_to_create->validateFields(UNFRIENDLY_ERROR, true)) === true && + ($lang_field_error = $category_to_create->validateFieldsLang(UNFRIENDLY_ERROR, true)) === true && $category_to_create->add() + ) + { + return (int)$category_to_create->id; + } + } + } + + return 0; + } + + protected static function createMultiLangField($field) + { + $res = array(); + foreach (Language::getIDs(false) as $id_lang) + { + $res[$id_lang] = $field; + } + + return $res; + } + + public function addImageToProduct($url, $id, $is_cover = false) + { + if (!empty($url)) + { + $url = str_replace(' ', '%20', $url); + + $image = new Image(); + $image->id_product = (int)$id; + $image->position = Image::getHighestPosition($id) + 1; + $image->cover = $is_cover; + // file_exists doesn't work with HTTP protocol + if (($field_error = $image->validateFields(UNFRIENDLY_ERROR, true)) === true && + ($lang_field_error = $image->validateFieldsLang(UNFRIENDLY_ERROR, true)) === true && $image->add() + ) + { + // associate image to selected shops + $shops = Shop::getContextListShopID(); + $image->associateTo($shops); + if (!Creator::copyImg($id, $image->id, $url, 'products', true)) + { + $image->delete(); + } + } + } + } + + public function addFeaturesToProduct($features, $id) + { + foreach ($features as $single_feature) + { + if (empty($single_feature)) + { + continue; + } + + $feature_name = $single_feature['feature']; + $feature_value = $single_feature['feature_value']; + $position = false; + $custom = false; + $id_lang = $this->id_lang; + $id_product = (int)$id; + if (!empty($feature_name) && !empty($feature_value)) + { + $id_feature = (int)Feature::addFeatureImport($feature_name, $position); + $id_feature_value = (int)FeatureValue::addFeatureValueImport($id_feature, $feature_value, $id_product, $id_lang, $custom); + Product::addFeatureProductImport($id, $id_feature, $id_feature_value); + } + } + } + + public function addAttributesToProduct($attributes_array, $id, $attribute_details = array()) + { + + $product = new Product($id); + + $this->settings['combination_price_multiplier']; + + $attributes_to_add = array(); + $attributes_ids = array(); + + foreach ($attributes_array as $key => $attributes_full) + { + foreach ($attributes_full as $attribute_pair) + { + if (empty($attribute_pair['attribute_value'])) + { + continue; + } + + if ($id_attribute = $this->getAttributeId($attribute_pair)) + { + $attributes_to_add[$key][] = $id_attribute; + if (isset($attribute_details)) + { + $attributes_ids[$id_attribute] = $attribute_pair['attribute_value']; + } + } + } + } + $combinations = $this->generateCombinations($attributes_to_add); + $attribute_details_par = array('price', 'ean', 'quantity', 'weight'); + + if (isset($attribute_details)) + { + foreach ($attribute_details_par as $parameter) + { + foreach ($combinations as $key => $combination) + { + foreach ($combination as $c) + { + if (isset($attribute_details[$parameter][$attributes_ids[$c]])) + { + $combination_detais[$key][$parameter] = $attribute_details[$parameter][$attributes_ids[$c]]; + } + } + } + } + } + + foreach ($combinations as $key => $combination) + { + $implode_sql = array(); + if (!$combination) continue; + $combination_price = 0; + + if (isset($combination_detais[$key]['price'])) + { + $combination_price = (float)$combination_detais[$key]['price']; + if (!empty($this->settings['combination_price_multiplier'])) + { + $combination_price *= (float)$this->settings['combination_price_multiplier']; + } + + if (empty($this->settings['add_combination_price'])) + { + $combination_price -= $product->price; + } + + if ($combination_price < 0) + { + $combination_price = 0; + } + } + $combination_ean = (!empty($combination_detais[$key]['ean']) && Validate::isEan13($combination_detais[$key]['ean'])) ? $combination_detais[$key]['ean'] : ''; + + $id_product_attribute = $product->productAttributeExists($combination, false, null, false, true); // Last 'true' is to return id + if (!$id_product_attribute) + { + $combination_weight = isset($combination_detais[$key]['weight']) ? (float)$combination_detais[$key]['weight'] : 0; + $combination_quantity = isset($combination_detais[$key]['quantity']) ? (int)$combination_detais[$key]['quantity'] : (int)$product->quantity; + $id_product_attribute = $product->addCombinationEntity( + (float) 0, + $combination_price, + $combination_weight, + 0, + (Configuration::get('PS_USE_ECOTAX') ? (float) $product->ecotax : 0), + $combination_quantity, + null, + '', + 0, + $combination_ean, + null + ); + foreach ($combination as $id_attribute) + { + Db::getInstance()->execute(' + INSERT IGNORE INTO ' . _DB_PREFIX_ . 'product_attribute_combination (id_attribute, id_product_attribute) + VALUES (' . (int) $id_attribute . ',' . (int) $id_product_attribute . ')', false); + } + } + else + { + if (isset($attribute_details)) + { + if (isset($combination_detais[$key]['price'])) + { + $implode_sql['price'] = 'price = ' . (float)$combination_price; + } + + if (isset($combination_detais[$key]['weight'])) + { + $implode_sql['weight'] = 'weight = ' . (float)$combination_detais[$key]['weight']; + } + + if (isset($combination_detais[$key]['ean'])) + { + $implode_sql['ean13'] = 'ean13 = ' . pSQL($combination_ean); + } + } + } + + if (isset($combination_detais[$key]['quantity'])) + { + $implode_sql['quantity'] = 'quantity = ' . (int)$combination_detais[$key]['quantity']; + StockAvailable::setQuantity($id, $id_product_attribute, (int)$combination_detais[$key]['quantity']); + } + + if ($implode_sql) + { + Db::getInstance()->execute(' + UPDATE ' . _DB_PREFIX_ . 'product_attribute SET ' . implode(', ', $implode_sql) . ' WHERE id_product_attribute = ' . (int)$id_product_attribute); + unset($implode_sql['ean13']); + unset($implode_sql['quantity']); + Db::getInstance()->execute(' + UPDATE ' . _DB_PREFIX_ . 'product_attribute_shop SET ' . implode(', ', $implode_sql) . ' WHERE id_product_attribute = ' . (int)$id_product_attribute); + } + } + } + + + /** + * copyImg copy an image located in $url and save it in a path + * according to $entity->$id_entity . + * $id_image is used if we need to add a watermark + * + * @param int $id_entity id of product or category (set in entity) + * @param int $id_image (default null) id of the image if watermark enabled. + * @param string $url path or url to use + * @param string $entity 'products' or 'categories' + * @param bool $regenerate + * @return bool + */ + protected static function copyImg($id_entity, $id_image = null, $url, $entity = 'products', $regenerate = true) + { + $tmpfile = tempnam(_PS_TMP_IMG_DIR_, 'ps_import'); + $watermark_types = explode(',', Configuration::get('WATERMARK_TYPES')); + + switch ($entity) + { + default: + case 'products': + $image_obj = new Image($id_image); + $path = $image_obj->getPathForCreation(); + break; + case 'categories': + $path = _PS_CAT_IMG_DIR_ . (int)$id_entity; + break; + case 'manufacturers': + $path = _PS_MANU_IMG_DIR_ . (int)$id_entity; + break; + case 'suppliers': + $path = _PS_SUPP_IMG_DIR_ . (int)$id_entity; + break; + } + + $url = urldecode(trim($url)); + $parced_url = parse_url($url); + + if (isset($parced_url['path'])) + { + $uri = ltrim($parced_url['path'], '/'); + $parts = explode('/', $uri); + foreach ($parts as &$part) + { + $part = rawurlencode($part); + } + unset($part); + $parced_url['path'] = '/' . implode('/', $parts); + } + + if (isset($parced_url['query'])) + { + $query_parts = array(); + parse_str($parced_url['query'], $query_parts); + $parced_url['query'] = http_build_query($query_parts); + } + + if (!function_exists('http_build_url')) + { + require_once(_PS_TOOL_DIR_ . 'http_build_url/http_build_url.php'); + } + + $url = http_build_url('', $parced_url); + + $orig_tmpfile = $tmpfile; + + // download image by curl to avoid ssl problem + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_TIMEOUT, 50); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); + $image = curl_exec($curl); + curl_close($curl); + + // copy image to $tmpfile + file_put_contents($tmpfile, $image); + + // if $tmpfile is not readable, return false + if (!filesize($tmpfile)) + { + @unlink($tmpfile); + return false; + } + + // Evaluate the memory required to resize the image: if it's too much, you can't resize it. + if (!ImageManager::checkImageMemoryLimit($tmpfile)) + { + @unlink($tmpfile); + return false; + } + + // Sprawdź typ MIME pliku + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime_type = finfo_file($finfo, $tmpfile); + finfo_close($finfo); + + if ($mime_type == 'image/webp') + { + // Wczytaj obraz WebP + $image = imagecreatefromwebp($tmpfile); + if ($image === false) + { + @unlink($tmpfile); + return false; + } + // Zapisz obraz jako JPG, nadpisując oryginalny plik + imagejpeg($image, $tmpfile, 100); // 100% jakości + imagedestroy($image); + } + + if (!ImageManager::isRealImage($tmpfile)) + { + @unlink($tmpfile); + return false; + } + + $tgt_width = $tgt_height = 0; + $src_width = $src_height = 0; + $error = 0; + ImageManager::resize( + $tmpfile, + $path . '.jpg', + null, + null, + 'jpg', + false, + $error, + $tgt_width, + $tgt_height, + 5, + $src_width, + $src_height + ); + $images_types = ImageType::getImagesTypes($entity, true); + + if ($regenerate) + { + $previous_path = null; + $path_infos = array(); + $path_infos[] = array($tgt_width, $tgt_height, $path . '.jpg'); + foreach ($images_types as $image_type) + { + $tmpfile = self::get_best_path($image_type['width'], $image_type['height'], $path_infos); + + if (ImageManager::resize( + $tmpfile, + $path . '-' . stripslashes($image_type['name']) . '.jpg', + $image_type['width'], + $image_type['height'], + 'jpg', + false, + $error, + $tgt_width, + $tgt_height, + 5, + $src_width, + $src_height + )) + { + // the last image should not be added in the candidate list if it's bigger than the original image + if ($tgt_width <= $src_width && $tgt_height <= $src_height) + { + $path_infos[] = array($tgt_width, $tgt_height, $path . '-' . stripslashes($image_type['name']) . '.jpg'); + } + if ($entity == 'products') + { + if (is_file(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int)$id_entity . '.jpg')) + { + unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int)$id_entity . '.jpg'); + } + if (is_file(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int)$id_entity . '_' . (int)Context::getContext()->shop->id . '.jpg')) + { + unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int)$id_entity . '_' . (int)Context::getContext()->shop->id . '.jpg'); + } + } + } + if (in_array($image_type['id_image_type'], $watermark_types)) + { + Hook::exec('actionWatermark', array('id_image' => $id_image, 'id_product' => $id_entity)); + } + } + } + + unlink($orig_tmpfile); + return true; + } + + private static function get_best_path($tgt_width, $tgt_height, $path_infos) + { + $path_infos = array_reverse($path_infos); + $path = ''; + foreach ($path_infos as $path_info) + { + list($width, $height, $path) = $path_info; + if ($width >= $tgt_width && $height >= $tgt_height) + { + return $path; + } + } + return $path; + } + + public function getProductId($query, $table, $identifier = 'name') + { + $sql = new DbQuery(); + $sql->select('p.`id_product`, p.`ean13`, p.`upc`, p.`active`, p.`reference`'); + $sql->from('product', 'p'); + + if ($identifier == 'name') + { + $sql->join(Shop::addSqlAssociation('product', 'p')); + $sql->leftJoin( + 'product_lang', + 'pl', + ' + p.`id_product` = pl.`id_product` + AND pl.`id_lang` = ' . (int)$this->id_lang . Shop::addSqlRestrictionOnLang('pl') + ); + $where = 'pl.`name` = \'' . pSQL($query) . '\''; + } + else + { + $where = 'p.`' . $identifier . '` = \'' . pSQL($query) . '\''; + } + + $sql->where($where); + + $sql->limit(1); + + $result = Db::getInstance()->executeS($sql); + + if (!empty($result[0]['id_product'])) + { + return $result[0]['id_product']; + } + + return 0; + } + + public function editProduct($product_id, $original_product) + { + $simple_fields = array('price', 'wholesale_price', 'additional_shipping_cost', 'ean13', 'ups', 'condition', 'width', 'height', 'depth', 'weight', 'active', 'minimal_quantity'); + $product = new Product($product_id); + + if ($product->name == null) + { + return $product; + } + + if (!empty($this->settings['update_manufacturer'])) + { + if (!empty($original_product['brand'])) + { + $product->id_manufacturer = $this->getManufacturerId($original_product['brand']); + } + else + { + $product->id_manufacturer = $this->settings['default_manufacturer_id']; + } + } + + foreach ($simple_fields as $field) + { + if (!empty($this->settings['update_' . $field]) && isset($original_product[$field])) + { + $product->$field = $original_product[$field]; + } + } + if (isset($this->settings['update_id_tax_rules_group']) && $this->settings['id_tax_rules_group'] != -1) + { + $product->id_tax_rules_group = $this->settings['id_tax_rules_group']; + } + + foreach (Language::getLanguages(false) as $lang) + { + if (!empty($this->settings['update_name'])) + { + $original_product['name'] = htmlspecialchars_decode($original_product['name']); + $original_product['name'] = str_replace(['>', '<', '=', ';', '{', '}', '#'], ' ', $original_product['name']); + $product->name[$lang['id_lang']] = $original_product['name']; + $product->link_rewrite[$lang['id_lang']] = Tools::link_rewrite($original_product['name']); + $product->meta_keywords[$lang['id_lang']] = str_replace(' ', ',', $original_product['name']); + } + + if (!empty($this->settings['update_description'])) + { + $original_product['description'] = nl2br($original_product['description']); + $original_product['description'] = htmlspecialchars_decode(htmlspecialchars_decode($original_product['description'], ENT_COMPAT)); + $product->description_long[$lang['id_lang']] = $original_product['description']; + $product->description[$lang['id_lang']] = $original_product['description']; + } + + if (!empty($original_product['short_description']) && !empty($this->settings['update_short_description'])) + { + $product->description_short[$lang['id_lang']] = $original_product['short_description']; + } + } + + $product->additional_delivery_times = 2; + $product->delivery_in_stock[7] = '1-5 dni roboczych'; + + //$product->active = isset($original_product['active']) ? (int)$original_product['active'] : 1;; + + $product->save(); + + if (!empty($this->settings['update_quantity'])) + { + StockAvailable::setQuantity((int)$product_id, 0, $original_product['quantity']); + } + + // check if product has no images + $images_count = "SELECT COUNT(0) FROM " . _DB_PREFIX_ . "image WHERE id_product = " . $product->id; + $result_images_count = Db::getInstance()->getValue($images_count); + + if (!empty($this->settings['update_images']) and $result_images_count == 0) + { + foreach ($original_product['images'] as $image_url) + { + $this->addImageToProduct($image_url, $product->id); + } + + if ($original_product['cover']) + { + $this->addImageToProduct($original_product['cover'], $product->id, true); + } + } + + if (!empty($this->settings['update_cover']) && !empty($original_product['cover'])) + { + $this->addImageToProduct($original_product['cover'], $product->id, true); + } + + if ($original_product['feature'] and $original_product['feature_value']) + { + $original_product['features'] = array( + array( + 'feature' => $original_product['feature'], + 'feature_value' => $original_product['feature_value'] + ) + ); + } + + if (!empty($this->settings['update_features']) && !empty($original_product['features'])) + { + $this->addFeaturesToProduct($original_product['features'], $product->id); + } + + if (!empty($this->settings['update_attributes']) && !empty($original_product['attributes'])) + { + $attribute_details = !empty($original_product['attribute_details']) ? $original_product['attribute_details'] : array(); + $this->addAttributesToProduct($original_product['attributes'], $product->id, $attribute_details); + } + + if ( $this -> settings['update_active'] ) + { + $sql = 'UPDATE ' . _DB_PREFIX_ . 'product SET active = ' . (int)$original_product['active'] . ' WHERE id_product = ' . (int)$product->id; + Db::getInstance()->execute($sql); + // materac_product_shop + $sql_shop = 'UPDATE ' . _DB_PREFIX_ . 'product_shop SET active = ' . (int)$original_product['active'] . ' WHERE id_product = ' . (int)$product->id; + Db::getInstance()->execute($sql_shop); + } + + if ( $this -> settings['update_quantity'] ) + { + $sql = 'UPDATE ' . _DB_PREFIX_ . 'stock_available SET quantity = ' . (int)$original_product['quantity'] . ' WHERE id_product = ' . (int)$product->id; + Db::getInstance()->execute($sql); + } + + return $product; + } + + public function processMissing($file_id, $queue_id) + { + $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_products WHERE shop = 'default' AND file_id = " . (int)$file_id . " AND queue_id != " . (int)$queue_id; + + //$sql .= " LIMIT 20"; + + if (!empty($this->settings['not_existing']) && $products = Db::getInstance()->executeS($sql)) + { + + if ($this->settings['not_existing'] == 1) + { + foreach ($products as $product) + { + StockAvailable::setQuantity((int)$product['product_id'], 0, 0); + } + } + elseif ($this->settings['not_existing'] == 2) + { + foreach ($products as $product) + { + $productObject = new Product($product['product_id']); + if ($productObject->name) + { + $productObject->active = 0; + $productObject->save(); + } + } + } + elseif ($this->settings['not_existing'] == 3) + { + foreach ($products as $product) + { + $productObject = new Product($product['product_id']); + if ($productObject->name) + { + $productObject->delete(); + } + Db::getInstance()->execute("DELETE FROM " . _DB_PREFIX_ . "ia_products WHERE product_id = '" . (int)$product['product_id'] . "'"); + } + } + } + Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_queues SET source = 'admin', status = '4', date_processed = '" . time() . "' WHERE queue_id = '" . (int)$queue_id . "'"); + } + + public function setAttributes() + { + $default_language = $this->id_lang; + + $this->groups = array(); + + foreach (AttributeGroup::getAttributesGroups($default_language) as $group) + { + $this->groups[$group['name']] = (int) $group['id_attribute_group']; + } + + $this->attributes = array(); + + foreach (Attribute::getAttributes($default_language) as $attribute) + { + $this->attributes[$attribute['attribute_group'] . '_' . $attribute['name']] = (int) $attribute['id_attribute']; + } + } + + function getAttributeGroupId($name) + { + $tab_group = explode(':', $name); + $group = trim($tab_group[0]); + if (isset($this->groups[$group])) + { + return $this->groups[$group]; + } + if (!isset($tab_group[1])) + { + $type = 'select'; + } + else + { + $type = trim($tab_group[1]); + } + $obj = new AttributeGroup(); + $obj->is_color_group = false; + $obj->group_type = pSQL($type); + $obj->name[$this->id_lang] = $group; + $obj->public_name[$this->id_lang] = $group; + //$obj->position = (!$position) ? AttributeGroup::getHigherPosition() + 1 : $position; + $obj->add(); + $this->groups[$group] = $obj->id; + return $obj->id; + } + + function getAttributeId($pair) + { + $name = $pair['attribute_value']; + $group = $pair['attribute']; + + if (isset($this->attributes[$group . '_' . $name])) + { + return $this->attributes[$group . '_' . $name]; + } + + $id_attribute_group = $this->getAttributeGroupId($group); + + $obj = new Attribute(); + $obj->id_attribute_group = $id_attribute_group; + $obj->name[$this->id_lang] = str_replace('\n', '', str_replace('\r', '', $name)); + $obj->add(); + $this->attributes[$group . '_' . $name] = $obj->id; + return $obj->id; + } + + function generateCombinations($attributes_to_add) + { + $i = 0; + $combinations = array(); + // 3 options + if (count($attributes_to_add) == 3) + { + foreach ($attributes_to_add[1] as $attr_1) + { + $temp_1 = isset($combinations[$i]) ? $combinations[$i] : array(); + $combinations[$i][] = $attr_1; + foreach ($attributes_to_add[2] as $attr_2) + { + $temp_2 = $combinations[$i]; + $combinations[$i][] = $attr_2; + foreach ($attributes_to_add[3] as $attr_3) + { + $temp_3 = $combinations[$i]; + $combinations[$i][] = $attr_3; + $i++; + $combinations[$i] = $temp_3; + } + + $combinations[$i] = $temp_2; + } + $combinations[$i] = $temp_1; + } + } + elseif (count($attributes_to_add) == 2 && isset($attributes_to_add[1])) + { + foreach ($attributes_to_add[1] as $attr_1) + { + $temp_1 = isset($combinations[$i]) ? $combinations[$i] : array(); + $combinations[$i][] = $attr_1; + foreach ($attributes_to_add[2] as $attr_2) + { + $temp_2 = $combinations[$i]; + $combinations[$i][] = $attr_2; + $i++; + $combinations[$i] = $temp_2; + } + + $combinations[$i] = $temp_1; + } + } + elseif (count($attributes_to_add) == 1 && isset($attributes_to_add[1])) + { + foreach ($attributes_to_add[1] as $attr_1) + { + //$temp_0 = isset($combinations[$i]) ? $combinations[$i] : array(); + $combinations[$i][] = $attr_1; + + $i++; + //$combinations[$i] = $temp_0; + } + } + return $combinations; + } +} diff --git a/modules/import_api/classes/filereader.php b/modules/import_api/classes/filereader.php new file mode 100644 index 00000000..0baedac6 --- /dev/null +++ b/modules/import_api/classes/filereader.php @@ -0,0 +1,425 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ +ini_set('memory_limit', '-1'); +ini_set('display_startup_errors', 1); +ini_set('display_errors', 1); +error_reporting(E_ALL ^ E_DEPRECATED); + +/** + * Adds the depreciated each() function back into 7.2 + */ +if (!function_exists('each')) { + function each($arr) { + $key = key($arr); + $result = ($key === null) ? false : [$key, current($arr), 'key' => $key, 'value' => current($arr)]; + next($arr); + return $result; + } +} +if (!function_exists('array_key_first')) { + function array_key_first(array $arr) { + foreach($arr as $key => $unused) { + return $key; + } + return NULL; + } +} +class FileReader +{ + public $types = array( + 'csv' => array( + 'application/octet-stream', + 'text/csv', + 'application/csv', + 'text/comma-separated-values', + 'application/excel', + 'application/vnd.ms-excel', + 'application/vnd.msexcel', + ), + 'json' => array( + 'application/json', + 'text/json', + 'application/x-javascript', + 'application/javascript', + 'text/javascript', + 'text/x-javascript', + 'text/x-json', + ), + 'xml' => array( + 'application/xml', + 'text/xml', + ) + ); + + public $mime = ''; + + public function getFile($link, $post = '', $curl = true) { + + $link = str_replace('amp;', '', $link); + //$link = urlencode($link); + $link = str_replace(" ", "%20", $link); // to properly format the url + $link_parts = parse_url($link); + if (!empty($link_parts['user']) && !empty($link_parts['pass'])) { + $link = str_replace($link_parts['user'] . ':' . $link_parts['pass'] . '@', '', $link); + } + + if($curl) { + $error_msg = ''; + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $link); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + curl_setopt($ch, CURLOPT_FAILONERROR, true); // Required for HTTP error codes to be reported via our call to curl_error($ch) + + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // This is very important if url have download + + if ($post) { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/xml', + 'Content-Length: ' . strlen($post)) + ); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post); + + } + + if (!empty($link_parts['user']) && !empty($link_parts['pass'])) { + curl_setopt($ch, CURLOPT_USERPWD, $link_parts['user'] . ":" . $link_parts['pass']); + } + + + $server_output = curl_exec($ch); + + $mime_parts = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + + if ($mime_parts) { + $mime = explode(';', $mime_parts); + $this->mime = isset($mime[0]) ? $mime[0] : ''; + } + + if (curl_errno($ch)) { + $error_msg = curl_error($ch); + //print_r('--'. $error_msg .'-'); + //var_dump($link); + //exit; + } + curl_close($ch); + unset($ch); + + return array($server_output, $error_msg); + + } else { + if (!empty($link_parts['user']) && !empty($link_parts['pass'])) { + $auth = base64_encode($link_parts['user'] . ":" . $link_parts['pass']); + $context = stream_context_create([ + "http" => [ + "header" => "Authorization: Basic $auth" + ] + ]); + $server_output = @file_get_contents($link, false, $context); + } else { + $server_output = @file_get_contents($link); + } + return array($server_output,''); + } + + return false; + } + + public function getArrayFromLink($file) { + + $substring = ''; + + $php_array = array(); + + $link = trim($file['link']); + $source = trim($file['source']); + $post = trim($file['post']); + $delimiter = !empty($file['delimiter']) ? $file['delimiter'] : ''; + $headers = !empty($file['headers']) ? $file['headers'] : 0; + + $session_file_name = 'tmp/files/' . $file['shop'] .'/session_' . session_id() . '_' . $file['file_id']. '.txt'; + + $file_ext = pathinfo($link, PATHINFO_EXTENSION); + if (in_array($file_ext, ['xlsx', 'xls'])) { + $source = 'excel'; + } + if ($source == 'excel') { + $error = ''; + $external_string = ''; + + } else { + list($external_string, $error) = $result = $this->getFile($link, $post); + if (empty($external_string)) { + list($external_string, $error) = $result = $this->getFile($link, $post, false); + } + if (!$error && $external_string) { + //file_put_contents($session_file_name, $external_string); + } + } + + if ($error) { + return array('', $error); + } + + $mime = $file['mime_type'] ? trim($file['mime_type']) : $this->mime; + + if ($external_string === false){ + $error = 'External file not found. Check link in browser'; + return array('', $error); + } + + $external_string = $this->removeBOM($external_string); + $external_string = trim($external_string); + + if (!$source) { + + $substring = $external_string ? substr($external_string, 0, 200) : ' '; + + + $substring = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $substring); + if (!$substring) { + $substring = ' '; + } + if (in_array($substring[0],['<'])) { + $source = 'xml'; + } elseif (in_array($substring[0],['[', '{'])) { + $source = 'json'; + } elseif ($mime) { + $source = $this->matchMimeType($mime); + } + } + + + if ($source == 'json') { + $json_string = $external_string; + $php_array = json_decode($json_string, true); + } + + if ($source == 'xml') { + libxml_use_internal_errors(true); + $ob = simplexml_load_string($external_string, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_PARSEHUGE); + if ($ob) { + $php_array = $this->xmlToArray($ob); + } else { + //echo libxml_get_errors(); + return array($php_array, libxml_get_errors()); + + } + } + + ini_set('auto_detect_line_endings',TRUE); + + if ($source == 'excel') { + $parts = explode('/modules/import_api/views/img/', $link); + if (isset($parts[1])) { + $file_path = 'modules/importer_api/views/img/' . $parts[1]; + } else { + return array([], 'File not found ( excel error )'); + } + + if ($file_ext == 'xlsx') { + require_once(_PS_MODULE_DIR_ . 'import_api/lib/SimpleXLSX.php'); + $xls = new SimpleXLSX($file_path); + } elseif ($file_ext == 'xls') { + require_once(_PS_MODULE_DIR_ . 'import_api/lib/SimpleXLS.php'); + $xls = new SimpleXLS($file_path); + } + + if ($xls->success()) { + foreach($xls->rows() as $row){ + if (empty($index)) { + if ($headers) { + $c = 1; + foreach ($row as $r) { + $index[] = 'COLUMN ' . $c; + $c++; + } + + } else { + $index = $row; + } + + } else { + $php_array[] = @array_combine($index, $row); + } + + } + } else { + return array([], 'Error reading Excel file. ' . $xls->error()); + } + } + + + if (!$php_array || $source == 'csv') { + + $fp = tmpfile(); + fwrite($fp, $external_string); + rewind($fp); //rewind to process CSV + + if (!$delimiter) { + $delimiter = $this->detectDelimiter($fp); + } + + while (($row = fgetcsv($fp, 0, $delimiter)) !== FALSE) { + if (empty($index)) { + if ($headers) { + $c = 1; + foreach ($row as $r) { + $index[] = 'COLUMN ' . $c; + $c++; + } + + } else { + $index = $row; + } + + } else { + $php_array[] = @array_combine($index, $row); + } + } + /* + $external_array = explode("\n", $external_string); + $index = str_getcsv(array_shift($external_array)); // not work with new lines + $php_array = null; + foreach ($external_array as $e) { + if ($e) + $php_array[] = @array_combine($index, $row); + } + */ + } + + if (!$php_array && $file['source']) { + $file['source'] = ''; + return $this->getArrayFromLink($file); + } + + + return array($php_array, ''); + } + + public function xmlToArray($xml, $options = array()) { + $defaults = array( + 'namespaceSeparator' => ':',//you may want this to be something other than a colon + 'attributePrefix' => '@', //to distinguish between attributes and nodes with the same name + 'alwaysArray' => array(), //array of xml tag names which should always become arrays + 'autoArray' => true, //only create arrays for tags which appear more than once + 'textContent' => 'VAL', //key used for the text content of elements + 'autoText' => true, //skip textContent key if node has no attributes or child nodes + 'keySearch' => false, //optional search and replace on tag and attribute names + 'keyReplace' => false //replace values for above search values (as passed to str_replace()) + ); + $options = array_merge($defaults, $options); + $namespaces = $xml->getDocNamespaces(); + $namespaces[''] = null; //add base (empty) namespace + + //get attributes from all namespaces + $attributesArray = array(); + foreach ($namespaces as $prefix => $namespace) { + foreach ($xml->attributes($namespace) as $attributeName => $attribute) { + //replace characters in attribute name + if ($options['keySearch']) $attributeName = + str_replace($options['keySearch'], $options['keyReplace'], $attributeName); + $attributeKey = $options['attributePrefix'] + . ($prefix ? $prefix . $options['namespaceSeparator'] : '') + . $attributeName; + $attributesArray[$attributeKey] = (string)$attribute; + } + } + + //get child nodes from all namespaces + $tagsArray = array(); + foreach ($namespaces as $prefix => $namespace) { + foreach ($xml->children($namespace) as $childXml) { + //recurse into child nodes + $childArray = $this->xmlToArray($childXml, $options); + list($childTagName, $childProperties) = each($childArray); + + //replace characters in tag name + if ($options['keySearch']) $childTagName = + str_replace($options['keySearch'], $options['keyReplace'], $childTagName); + //add namespace prefix, if any + if ($prefix) $childTagName = $prefix . $options['namespaceSeparator'] . $childTagName; + + if (!isset($tagsArray[$childTagName])) { + //only entry with this key + //test if tags of this type should always be arrays, no matter the element count + $tagsArray[$childTagName] = + in_array($childTagName, $options['alwaysArray']) || !$options['autoArray'] + ? array($childProperties) : $childProperties; + } elseif ( + is_array($tagsArray[$childTagName]) && array_keys($tagsArray[$childTagName]) + === range(0, count($tagsArray[$childTagName]) - 1) + ) { + //key already exists and is integer indexed array + $tagsArray[$childTagName][] = $childProperties; + } else { + //key exists so convert to integer indexed array with previous value in position 0 + $tagsArray[$childTagName] = array($tagsArray[$childTagName], $childProperties); + } + } + } + + //get text content of node + $textContentArray = array(); + $plainText = trim((string)$xml); + if ($plainText !== '') $textContentArray[$options['textContent']] = $plainText; + + //stick it all together + $propertiesArray = !$options['autoText'] || $attributesArray || $tagsArray || ($plainText === '') + ? array_merge($attributesArray, $tagsArray, $textContentArray) : $plainText; + + //return node as array + return array( + $xml->getName() => $propertiesArray + ); + } + + public function matchMimeType($mime) { + + foreach ($this->types as $type => $list) { + if (in_array($mime, $list)) { + return $type; + } + } + return ''; + } + + public function detectDelimiter($fh) { + $delimiters = [";","\t", "|", ","]; + $data_1 = null; $data_2 = array(); + $delimiter = $delimiters[0]; + foreach($delimiters as $d) { + $data_1 = fgetcsv($fh, 4096, $d); + if(sizeof($data_1) > sizeof($data_2)) { + $delimiter = $d; + $data_2 = $data_1; + } + rewind($fh); + } + + return $delimiter; + } + + function removeBOM($data) { + if (0 === strpos(bin2hex($data), 'efbbbf')) { + return substr($data, 3); + } + return $data; + } +} \ No newline at end of file diff --git a/modules/import_api/classes/import.php b/modules/import_api/classes/import.php new file mode 100644 index 00000000..e2037312 --- /dev/null +++ b/modules/import_api/classes/import.php @@ -0,0 +1,554 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ + +/** No max line limit since the lines can be more than 4096. Performance impact is not significant. */ + +function filter_category_array ($element) { + return (!is_array($element) && $element !== ''); +} + +define('MAX_LINE_SIZE', 0); + +class Import +{ + + private $json_array = array(); + private $product_links = array(); + private $settings; + + public function __construct($settings) + { + $this->settings = $settings; + } + + public function getAllData($path, $test = false) + { + $fields = $this->settings['import_api_field']; + $nested_data = array(); + + $id_parts = explode('->', $path); + + array_shift($id_parts); + + foreach ($fields as $field => $value) { + if(!$value) continue; + $field_paths = $value ? html_entity_decode($value, ENT_QUOTES, 'UTF-8') : ''; + $field_parts = explode('->', $field_paths); + + $nested_data[$field] = $this->getFieldData($this->json_array, $field_parts, $id_parts); + } + + if ($test == 'view-raw') { + return $nested_data; + } + + $nested_data = $this->checkCombinations($nested_data); + + if ($test == 'view-split') { + return $nested_data; + } + + $original_product = $this->resolveAllNestedData($nested_data); + + if ($test == 'view-grouping') { + return $original_product; + } + + $modified = $this->changeOriginal($original_product); + + return $modified; + } + + public function resolveAllNestedData($nested_data) + { + if (!empty($nested_data['feature_value'])) { + + if (empty($nested_data['feature'])) { + if (!empty($this->settings['import_api_modification']['feature'])) { + $nested_data['feature'] = $this->settings['import_api_modification']['feature']; + } else { + $nested_data['feature'] = 'Feature'; + } + } + + $feature_map = $this->resolveNested($nested_data['feature_value'], $nested_data['feature']); + $nested_data['features'] = array(); + + foreach ($feature_map as $key => $map) { + $nested_data['features'][$key] = array( + 'feature' => $map['parent'], + 'feature_value' => $map['value'] + ); + } + } +$nested_data['attributes'] = array(); +for ($i = 1;$i <= 3; $i++) { + + if (!empty($nested_data['attribute_value' . $i])) { + if (empty($nested_data['attribute' . $i])) { + if (!empty($this->settings['import_api_modification']['attribute' . $i])) { + $nested_data['attribute' . $i] = $this->settings['import_api_modification']['attribute' . $i]; + } else { + $nested_data['attribute' . $i] = 'Select ' . $i; + } + } + + $attribute_map = $this->resolveNested($nested_data['attribute_value' . $i], $nested_data['attribute' . $i]); + + $attribute_details_par = array('price', 'ean', 'quantity', 'weight'); + foreach ($attribute_map as $key => $map) { + foreach ($attribute_details_par as $parameter) { + if (isset($nested_data['attribute_' . $parameter])) { + if (is_array($nested_data['attribute_' . $parameter])) { + if (isset($nested_data['attribute_' . $parameter][$key])) { + $nested_data['attribute_details'][$parameter][$map['value']] = $nested_data['attribute_' . $parameter][$key]; + } + } else { + $nested_data['attribute_details'][$parameter][$map['value']] = $nested_data['attribute_' . $parameter]; + } + } + } + $nested_data['attributes'][$i][$key] = array( + 'attribute' => $map['parent'], + 'attribute_value' => $map['value'] + ); + } + } +} + + if (!empty($nested_data['category'])) { + + if (!isset($nested_data['category_parent'])) { + $nested_data['category_parent'] = 0; // it is not good idea to put default top category here + } + + if ($this->settings['import_api_settings']['category_path'] && is_array($nested_data['category'])) { + + $nested_data['category'] = implode('->', array_filter($nested_data['category'], "filter_category_array")); + } + + $nested_data['category_path'] = $this->resolveNested($nested_data['category'], $nested_data['category_parent']); + } + + $nested_data['options'] = array(); + if (!empty($nested_data['option_value'])) { + + if (!isset($nested_data['option'])) { + $nested_data['option'] = $this->settings['import_api_default_option']; + } + + if (!isset($nested_data['option_price'])) { + $nested_data['option_price'] = 0; + } + + if (!isset($nested_data['option_weight'])) { + $nested_data['option_weight'] = 0; + } + + $option_map= $this->resolveNested($nested_data['option_value'], $nested_data['option']); + $option_price_map = $this->resolveNested($nested_data['option_price'], $nested_data['option_value']); + $option_weight_map = $this->resolveNested($nested_data['option_weight'], $nested_data['option_value']); + foreach ($option_map as $key => $map) { + if (is_array($nested_data['option_price'])) { + foreach ($option_price_map as $price_map) { + if ($map['value'] == $price_map['parent']) { + $nested_data['options'][$key] = array( + 'option' => $map['parent'], + 'option_value' => $map['value'], + 'price' => $price_map['value'] + ); + break; + } + } + } else { + $nested_data['options'][$key] = array( + 'option' => $map['parent'], + 'option_value' => $map['value'], + 'price' => $nested_data['option_price'] + ); + } + + if (is_array($nested_data['option_weight'])) { + foreach ($option_weight_map as $weight_map) { + if($map['value'] == $weight_map['parent']){ + $nested_data['options'][$key]['weight'] = $weight_map['value']; + break; + } + } + } else { + $nested_data['options'][$key]['weight'] = $nested_data['option_weight']; + } + } + + } + + return $nested_data; + } + public function getFieldData($tree, $field_parts, $id_parts) + { + while($field_parts) { + if ($this->isSeq($tree)) { + $seq_id = array_shift($id_parts); + $tree = $tree[$seq_id]; + } else { + + $part = array_shift($field_parts); + $uni_c = array_shift($id_parts); + + if($part == $uni_c){ + $tree = $tree[$part]; + + } else { + $field_parts = array_merge([$part], $field_parts); // vrati mu ga + return $this->followDifferentPath($field_parts, $tree); + } + } + } + } + + public function followDifferentPath($own_parts, $tree){ + $prev= ''; // for test + foreach($own_parts as $part){ +//if ($part == 'image') {var_dump($tree);var_dump($own_parts);exit;} + + if ($this->isSeq($tree)) { + $ret = array(); + foreach($tree as $t){ + $ret[] = $this->followDifferentPath($own_parts, $t); + } + return $ret; + } else { + array_shift($own_parts); + $tree = isset($tree[$part]) ? $tree[$part] : ''; + } + $prev = $part; //for test + } + + if(!is_array($tree)){ + return trim($tree); + } + + return $tree; + } + + public function findUniqueProductsIdentifier($tree, $parts, $identifier, $path = ''){ + + $tmp_parts = $parts; + foreach ($parts as $part) { + if ($part == $identifier && !$this->isSeq($tree) && isset($tree[$part])) { + + if (!empty($tree[$part]) && !is_array($tree[$part])) { + $path .= '->'. $tree[$part]; + $this->product_links[] = $path; + } + + //exit; + + } elseif ($this->isSeq($tree)) { + + foreach($tree as $key => $t){ + $local_path = $path. '->' . $key; + $this->findUniqueProductsIdentifier($t, $tmp_parts, $identifier, $local_path); + } + return; + } else { + + if (isset($tree[$part])) { + $tree = $tree[$part]; + $path .= '->'. $part; + } + + array_shift($tmp_parts); + } + } + } + + public function resolveNested($child_data, $parent_data, $default_parent = 'Value'){ + + $child_indexes = array(); + + if(!is_array($child_data)){ + $child_data = array($child_data); + } + + $this->setIndexPath($child_data, $child_indexes); + + + $parent_indexes = array(); + + if(is_array($parent_data)){ + $this->setIndexPath($parent_data, $parent_indexes); + } else { + $map = array(); + foreach($child_indexes as $child){ + + $map[] = array( + 'value' => $child['value'], + 'parent' => $parent_data + ); + } + + return $map; + } + + return $this->addParent($child_indexes, $parent_indexes); + + } + + public function addParent($children, $parents){ + $map = array(); + foreach($parents as $parent){ + foreach($children as $key => $child){ + if(substr($child['key_path'], 0, strlen($parent['key_path'])) === $parent['key_path']){ + $map[] = array( + 'value' => $child['value'], + 'parent' => $parent['value'] + ); + + unset($children[$key]); + } + } + } + + return $map; + } + + /** + * Takes nested array with unknown nesting levels and return one level array with indexes to values. + * + * @param array $array array with unknown nesting levels + * @param string $key_path string made from array keys + * @return array with values and string from value indexes + */ + + public function setIndexPath($array, &$return_values = array(), $key_path = ''){ + foreach($array as $key => $array_part){ + + if(is_array($array_part)){ + $this->setIndexPath($array_part, $return_values, $key_path . $key . '.'); + } else { + $return_values[] = array( + 'key_path' => $key_path . $key . '.', + 'value' => $array_part + ); + } + } + } + + + public function changeOriginal($product) + { + $modifications = $this->settings['import_api_modification']; + + foreach ($modifications as $field => $modification) { + //if (!$modification) { + if ($modification === '') { + continue; + } + + if (in_array($field, ['price']) && !$product[$field]) { + continue; + } + + if (!isset($product[$field])) { + $product[$field] = ''; + } + + if (in_array($field, ['image', 'images', 'cover']) && !$product[$field]) { + continue; + } + + $modification = html_entity_decode($modification, ENT_QUOTES, 'UTF-8'); + + $modified = $this->check_string($modification, $product); + $product[$field] = $modified; + + if (in_array($field, ['price', 'quantity', 'option_price','option_quantity', 'special', 'additional_shipping_cost', 'width', 'height', 'depth', 'weight', 'attribute_price', 'attribute_quantity', 'attribute_weight', 'minimal_quantity'])) { + $modified = $this->calculateMath($modified); + $product[$field] = $modified; + } + } + + return $product; + } + + public function checkCombinations($product) + { + $combinations = $this->settings['import_api_combination']; + + foreach ($combinations as $field => $combination) { + if (!$combination || !isset($product[$field])) { + continue; + } else { + $position = 'all'; + $parts = explode('##', $combination); + if(isset($parts[1])){ + $position = $parts[1]; + } + } + + $exploded = $this->explodeCombinations($product[$field], $parts[0], $position); + $product[$field] = $exploded; + + } + return $product; + } + + public function explodeCombinations($value, $separator, $position = 'all') + { + if (!is_array($value)) { + $parts = explode($separator, $value); + if ($position == 'all') { + return $parts; + } + + if ($position == 'last') { + return end($parts); + } + + if (isset($parts[$position-1])) { + return $parts[$position-1]; + } + + return $value; + } else { + $ret = array(); + foreach ($value as $key => $v) { + $ret[] = $this->explodeCombinations($v, $separator, $position); + } + + return $ret; + } + } + + public function getProductLinks() + { + + return $this->product_links; + } + + private function check_string($string, $prod) + { + if ($string) { + preg_match_all('/\[\[([a-z_]*\#?[^\]]*)\]\]/', $string , $match); + if ($match) { + $string = $this->replace_string($string, $match[1], $prod); + } + } + return $string; + } + + private function replace_string($string, $keys, $d_array) + { + foreach ($keys as $key) { + $exploded_key = explode('#', $key); + $separator = false; + + if (isset($exploded_key[1])) { + $string = str_replace($key, $exploded_key[0], $string); + $key = $exploded_key[0]; + $separator = $exploded_key[1]; + } + + if (isset($d_array[$key])) { + if (is_array($d_array[$key])) { + $string = $this->replace_all_array_strings($d_array[$key], $key, $string, $separator); + } else { + $string = str_replace('[['. $key .']]', $d_array[$key], $string); + } + } + } + return $string; + } + + private function replace_all_array_strings($array, $key, $string, $separator = false) + { + $s = array(); + foreach ($array as $d_part) { + $s[] = str_replace('[['. $key .']]', $d_part, $string); + } + + if ($separator !== false) { + $s = implode($separator, $s); + } + + return $s; + } + + private function calculateMath($expression) + { + $expression = str_replace(',', '.', $expression); + $expression = str_replace(' ', '', $expression); + $expression = preg_replace('/[^0-9\.\+\-\*\/\(\)]/', '', $expression); + + $expression = $this->replaceMathExpession($expression, '*'); + $expression = $this->replaceMathExpession($expression, '/'); + + $expression = $this->replaceMathExpession($expression, '+'); + $expression = $this->replaceMathExpession($expression, '-'); + return $expression; + } + + private function replaceMathExpession($expression, $sign) + { + $patern = '\\' . $sign; + $expression = preg_replace_callback( + '/[0-9]*\.?[0-9]+' . $patern . '[0-9]*\.?[0-9]+(' . $patern . '[0-9]*\.?[0-9]+)*/', + function ($matches) use ($sign) { + if (!empty($matches[0])) { + return $this->calculate_string($matches[0], $sign); + } + }, $expression); + return $expression; + } + + private function calculate_string($string, $sign) + { + $parts = explode($sign, $string); + $final = $parts[0]; + + for ($i = 1; $i < count($parts); $i++) { + if ($sign == '*') { + $final = $final * $parts[$i]; + } elseif ($sign == '/') { + $final = $final / $parts[$i]; + } elseif ($sign == '+') { + $final = $final + $parts[$i]; + } elseif ($sign == '-') { + $final = $final - $parts[$i]; + } + } + return $final; + } + + public function jsonToArray($json_url) + { + $json_string = @file_get_contents($json_url); + $json_array = json_decode($json_string, true); + return $json_array; + } + + public function setJsonArray($json_array) + { + $this->json_array = $json_array; + } + + public function isSeq($array) + { + if (is_array($array) ) { + return is_int(array_key_first($array)); + } + //return is_array($array) && isset($array[0]); + return false; + } +} diff --git a/modules/import_api/classes/index.php b/modules/import_api/classes/index.php new file mode 100644 index 00000000..907720c1 --- /dev/null +++ b/modules/import_api/classes/index.php @@ -0,0 +1,35 @@ + +* @copyright 2007-2018 PrestaShop SA +* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) +* International Registered Trademark & Property of PrestaShop SA +*/ + +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/modules/import_api/classes/queue.php b/modules/import_api/classes/queue.php new file mode 100644 index 00000000..f832baf8 --- /dev/null +++ b/modules/import_api/classes/queue.php @@ -0,0 +1,182 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ + +class Queue +{ + public $shop_name = 'default'; + + public function getQueued($file_id = 0, $limit = 0) + { + + $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_temp t LEFT JOIN " . _DB_PREFIX_ . "ia_queues q ON (t.queue_id = q.queue_id) WHERE q.status < 3 AND t.shop = '" . pSQL($this->shop_name) . "'"; + + if ($file_id) { + $sql .= " AND t.file_id = " . (int)$file_id; + } + + if ($limit) { + $sql .= " LIMIT " . (int)$limit; + } + + $query = Db::getInstance()->executeS($sql); + + return $query; + } + + public function getLastQueue($file_id = 0, $status = 0) + { + + $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_queues WHERE shop = '" . pSQL($this->shop_name) . "'"; + + if ($file_id) { + $sql .= " AND file_id = " . (int)$file_id; + } + + if ($status) { + $sql .= " AND status = " . (int)$status; + } + + $sql .= " ORDER BY date_added DESC"; + + $query = Db::getInstance()->executeS($sql); + + if ($query) { + return $query[0]; + } else { + return array(); + } + } + + public function getProducts($file_id = 0, $limit = 0) + { + + $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_products p WHERE p.shop = '" . pSQL($this->shop_name) . "'"; + + if ($file_id) { + $sql .= " AND p.file_id = " . (int)$file_id; + } + + if ($limit) { + $sql .= " LIMIT " . (int)$limit; + } + + $query = Db::getInstance()->executeS($sql); + + return $query; + } + + public function deleteProduct($product_id = 0, $file_id = 0) + { + + $sql = "DELETE FROM " . _DB_PREFIX_ . "ia_products WHERE product_id = '" . (int)$product_id . "'"; + + if ($file_id) { + $sql .= " AND file_id = " . (int)$file_id; + } + //print_r($sql); exit; + + $query = Db::getInstance()->execute($sql); + + return $query; + } + + public function saveTempToDb($products, $file_id = 0) { + + $this->deleteUnnecessary($file_id); + + Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_queues SET shop = '" . pSQL($this->shop_name) . "', total = '" . count($products) . "', date_added = '" . time() . "', file_id = '" . (int)$file_id . "'"); + + $queue_id = Db::getInstance()->Insert_ID(); + + foreach($products as $indx => $product) { + Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_temp SET shop = '" . pSQL($this->shop_name) . "', date_added = '" . time() . "', indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($product)) . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "'"); + } + + return $queue_id; + } + + public function deleteUnnecessary($file_id = 0, $shop = 'default') { + Db::getInstance()->execute("DELETE FROM " . _DB_PREFIX_ . "ia_queues WHERE shop = '" . pSQL($this->shop_name) . "' AND status < 3 AND file_id = " . (int)$file_id); + Db::getInstance()->execute("DELETE FROM " . _DB_PREFIX_ . "ia_temp WHERE shop = '" . pSQL($this->shop_name) . "' AND file_id = " . (int)$file_id); + } + + public function getQueuedForTable($file_id = 0, $limit = 0) { + + $products = array(); + + $queued = $this->getQueued($file_id, $limit); + + foreach ($queued as $q) { + $p = json_decode($q['product'], true); + //var_dump($p); exit; + $products[] = array( + 'id' => $q['product_id'], + 'indx' => $q['indx'], + 'reference' => $p['reference'], + 'name' => $p['name'], + 'price' => $p['price'], + 'quantity' => $p['quantity'], + 'manufacturer' =>isset($p['brand']) ? $p['brand'] : '', + ); + } + + return $products; + } + + public function getQueuedProduct($indx = 0, $file_id = 0) { + + $sql = "SELECT * FROM " . _DB_PREFIX_ . "ia_temp WHERE shop = '" . pSQL($this->shop_name) . "'"; + + if ($indx) { + $sql .= " AND indx = '" . pSQL($indx) . "'"; + } + + + if ($file_id) { + $sql .= " AND file_id = '" .(int)$file_id . "'"; + } + + $sql .= " LIMIT 1"; + + $query = Db::getInstance()->executeS($sql); + + if ($query) { + return $query[0]; + } else { + array(); + } + } + + public function deleteQueuedProduct($product_id = 0, $indx = 0, $file_id = 0) { + + $sql = "DELETE FROM " . _DB_PREFIX_ . "ia_temp WHERE shop = '" . pSQL($this->shop_name) . "'"; + + if ($product_id) { + $sql .= " AND product_id = '" . (int)$product_id . "'"; + } + + if ($indx) { + $sql .= " AND indx = '" . pSQL($indx) . "'"; + } + + if ($file_id) { + $sql .= " AND file_id = '" .(int)$file_id . "'"; + } + + $sql .= " LIMIT 1"; + + $query = Db::getInstance()->execute($sql); + + return $query; + } +} diff --git a/modules/import_api/config.xml b/modules/import_api/config.xml new file mode 100644 index 00000000..e9369871 --- /dev/null +++ b/modules/import_api/config.xml @@ -0,0 +1,12 @@ + + + import_api + + + + + + 1 + 1 + + \ No newline at end of file diff --git a/modules/import_api/config_pl.xml b/modules/import_api/config_pl.xml new file mode 100644 index 00000000..893d012d --- /dev/null +++ b/modules/import_api/config_pl.xml @@ -0,0 +1,12 @@ + + + import_api + + + + + + 1 + 1 + + \ No newline at end of file diff --git a/modules/import_api/controllers/admin/AdminImport_ApiController.php b/modules/import_api/controllers/admin/AdminImport_ApiController.php new file mode 100644 index 00000000..19e54827 --- /dev/null +++ b/modules/import_api/controllers/admin/AdminImport_ApiController.php @@ -0,0 +1,831 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ +class AdminImport_ApiController extends ModuleAdminController +{ + public $bootstrap = true; + + protected $_postErrors = array(); + + protected $fmr_fields; + + public function __construct() + { + if ($this->context == null) { + $this->context = Context::getContext(); + } + $this->toolbar_title = 'Sources'; + $this->meta_title = 'Import api'; + parent::__construct(); + require_once(_PS_MODULE_DIR_ . 'import_api/classes/queue.php'); + $this->queue = new Queue(); + $this->frm_fields = ['unique', 'reference', 'name', 'description', 'short_description', 'price', 'wholesale_price', 'quantity', 'images', 'cover', 'brand', 'category', 'category_parent', 'ean13', 'upc', 'condition', 'location', 'additional_shipping_cost', 'width', 'height', 'depth', 'weight', 'active', 'feature', 'feature_value', 'attribute1', 'attribute_value1', 'attribute2', 'attribute_value2', 'attribute3', 'attribute_value3', 'attribute_price', 'attribute_quantity', 'attribute_ean', 'attribute_weight', 'minimal_quantity']; + $this->replace_fields = ['reference', 'name', 'description', 'short_description', 'price', 'wholesale_price', 'quantity', 'images', 'cover', 'brand', 'category', 'ean13', 'upc', 'condition', 'location', 'width', 'height', 'depth', 'weight', 'feature', 'feature_value', 'attribute1', 'attribute_value1', 'attribute2', 'attribute_value2', 'attribute3', 'attribute_value3', 'minimal_quantity']; + $this->frm_settings = array('top_category' => '', 'default_category' => '', 'default_brand' => '', 'price_multiplier' => '', 'combination_price_multiplier' => '', 'add_combination_price' => 0, 'category_path' => 0, 'id_tax_rules_group' => -1, 'same_update' => 0, 'not_existing' => 0, 'only_update' => 0, 'synchronize_field' => 'automatic'); + $this->frm_update_settings = array('quantity' => 1, 'price' => 0, 'wholesale_price' => 0, 'manufacturer' => 0, 'category' => 0, 'cover' => 0, 'images' => 0, 'name' => 0, 'description' => 0, 'short_description' => 0,'reference' => 0, 'ean13' => 0, 'ups' => 0, 'condition' => 0, 'location'=> 0, 'additional_shipping_cost'=> 0, 'width' => 0 , 'height' => 0 , 'depth' => 0 , 'weight' => 0, 'features' => 0, 'attributes' => 0, 'active' => 0, 'id_tax_rules_group' => 0, 'minimal_quantity' => 0); + $this->filter_fields = ['reference', 'name', 'price', 'wholesale_price', 'quantity', 'images', 'cover', 'brand', 'category', 'ean13', 'upc', 'condition', 'location', 'width', 'height', 'depth', 'weight', 'feature', 'feature_value', 'attribute1', 'attribute_value1', 'attribute2', 'attribute_value2', 'attribute3', 'attribute_value3', 'minimal_quantity']; + + + + if (Tools::getValue('add_source')) { + $this->addSource(); + } elseif (Tools::getValue('delete') && Tools::getValue('file_id')) { + $this->deleteProducts(Tools::getValue('file_id')); + } elseif (Tools::getValue('file_id')) { + if (((bool)Tools::isSubmit('submitImportSettings')) == true) { + $this->_postSettingsValidation(); + $message = ''; + if (!count($this->_postErrors)) { + $this->_saveSettings(Tools::getValue('file_id')); + } else { + foreach ($this->_postErrors as $err) { + $this->errors[] = $err; + } + } + + } + $this->processFile(Tools::getValue('file_id'), Tools::getValue('type')); + } else { + $this->getSourceList(); + } + //return; + } + + + public function addSource() + { + if (Tools::isSubmit('submitAddSource')) { + $this->postFileProcess(); + } + $this->context->smarty->assign('form_link', $this->context->link->getAdminLink('AdminImport_Api&add_source=1&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false)); + $file_tpl = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/file_upload.tpl'); + $this->content .= $file_tpl; + } + + public function deleteProducts($file_id) + { + $query_file = Db::getInstance()->executeS("SELECT * FROM ". _DB_PREFIX_ . "ia_files WHERE file_id = '" . (int)$file_id . "' LIMIT 1"); + if (empty($query_file)) { + $this->errors[] = $this->my_translation('File not found'); + return; + } else { + $file = $query_file[0]; + } + + $this->toolbar_title = 'Delete'; + + $query_total = Db::getInstance()->executeS("SELECT COUNT(*) as total FROM ". _DB_PREFIX_ . "ia_products WHERE file_id = '" . (int)$file_id . "'"); + + $total = $query_total ? $query_total[0]['total'] : 0; + + $file_name = $file['name'] ? $file['name'] : $file['link']; + $this->context->smarty->assign('token', $file['mask'] . $file['date_added']); + $this->context->smarty->assign('get_import_link', $this->context->link->getModuleLink('import_api', 'import', array('file_id' => $file_id))); + + $this->context->smarty->assign('file_name', $file_name); + $this->context->smarty->assign('total', $total); + $delete_tpl = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/delete.tpl'); + $this->content .= $delete_tpl; + } + + public function getSourceList() + { + if (Tools::getValue('status_file_id') && Tools::getValue('change_status') !== false) { + Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_files SET status = '" . (int)Tools::getValue('change_status') ."' WHERE file_id = " . (int)Tools::getValue('status_file_id')); + } + + $query_files = Db::getInstance()->executeS("SELECT f.* FROM ". _DB_PREFIX_ . "ia_files f ORDER BY date_edited DESC, file_id DESC"); + + $files = array(); + if ($query_files) { + foreach ($query_files as $file) { + $other_status = $file['status'] ? 0 : 1; + $other_status_text = $file['status'] ? 'Disable' : 'Enable'; + if (!Tools::getValue('show_disabled') && !$file['status']) { + continue; + } + $files[] = array( + 'file_id' => $file['file_id'], + 'name' => $file['name'] ? $file['name'] : $file['link'], + 'date_added' => date('F j, Y', $file['date_added']), + 'process_link' => $this->context->link->getAdminLink('AdminImport_Api&file_id=' . $file['file_id'] . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false), + 'change_status_link' => $this->context->link->getAdminLink('AdminImport_Api&change_status=' . $other_status . '&status_file_id=' . $file['file_id'] . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false), + 'other_status_text' => $other_status_text + ); + } + + $this->context->smarty->assign('files', $files); + $this->context->smarty->assign('show_disabled_link', $this->context->link->getAdminLink('AdminImport_Api&show_disabled=1&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false)); + + $list_tpl = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/list.tpl'); + $this->content .= $list_tpl; + } else { + $this->addSource(); + } + } + + public function processFile($file_id, $type = 'import') + { + $this->addJquery(); + $this->addJS(_MODULE_DIR_. 'import_api/views/js/dataTables.js'); + $this->addJS(_MODULE_DIR_. 'import_api/views/js/dataTables.bootstrap4.js'); + $this->addCSS(_MODULE_DIR_. 'import_api/views/css/dataTables.bootstrap4.css'); + $query_file_settings = Db::getInstance()->executeS("SELECT f.*, fs.mapping FROM ". _DB_PREFIX_ . "ia_files f LEFT JOIN ". _DB_PREFIX_ . "ia_file_settings fs ON(fs.file_id = f.file_id) WHERE f.file_id = '" . (int)$file_id . "' LIMIT 1"); + if (empty($query_file_settings)) { + $this->errors[] = $this->my_translation('File not found'); + } else { + $file_settings = $query_file_settings[0]; + $file_name = $file_settings['name'] ? $file_settings['name'] : $file_settings['link']; + if ($file_settings['mapping'] && $type == 'import') { + + $this->toolbar_title = 'Import'; + $this->context->smarty->assign('get_import_link', $this->context->link->getModuleLink('import_api', 'import', array('file_id'=> $file_id))); + $this->context->smarty->assign('show_all_link', $this->context->link->getAdminLink('AdminImport_Api&type=import&showall=1&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false)); + $this->context->smarty->assign('delete_link', $this->context->link->getAdminLink('AdminImport_Api&delete=1&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false)); + + if (Tools::getValue('showall')) { + $limit = 0; + } else { + $limit = 600; + } + + $products = $this->queue->getQueuedForTable($file_id, $limit); + $this->context->smarty->assign('settings_link', $this->context->link->getAdminLink('AdminImport_Api&type=settings&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false)); + + $this->context->smarty->assign('products', $products); + $this->context->smarty->assign('limit', $limit); + $this->context->smarty->assign('auto_queue', 0); + $this->context->smarty->assign('file_name', $file_name); + $output = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/import.tpl'); + $queue_tpl = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/temp.tpl'); + $this->content .= $queue_tpl; + $this->content .= $output; + } else { + $this->toolbar_title = 'Settings'; + $this->context->smarty->assign('file_settings', $file_settings); + $this->context->smarty->assign('file_id', $file_id); + $this->context->smarty->assign('file_name', $file_name); + $this->context->smarty->assign('import_link', $this->context->link->getAdminLink('AdminImport_Api&type=import&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false)); + $this->context->smarty->assign('get_fields_link', $this->context->link->getModuleLink('import_api', 'ajax', array('file_id'=> $file_id))); + $output = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/configure.tpl'); + //$import_html = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'import_api/views/templates/admin/import.tpl'); + + $this->content .= $output. $this->renderSettingsForm($file_settings); + //$this->content .= $output; + } + } + } + public static function displayHumanReadableSize($size) + { + return Tools::formatBytes($size); + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_btn['new_attachment'] = array( + 'href' => self::$currentIndex . '&add_source=1&token=' . $this->token, + 'desc' => $this->my_translation('Add new source'), + 'icon' => 'process-icon-new', + ); + $this->page_header_toolbar_btn['view_list'] = array( + 'href' => self::$currentIndex . '&token=' . $this->token, + 'desc' => $this->my_translation('View list'), + 'icon' => 'process-icon-list', + ); + parent::initPageHeaderToolbar(); + } + + public function postFileProcess() + { + $shop = 'default'; + $source = !empty($_POST['import_api_type']) ? $_POST['import_api_type'] : ''; + $headers = !empty($_POST['import_api_headers']) ? $_POST['import_api_headers'] : ''; + $delimiter = !empty($_POST['import_api_delimiter']) ? $_POST['import_api_delimiter'] : ''; + $mask = uniqid(); + if (!empty($_POST['import_api_link'])) { + + $query_file = Db::getInstance()->executeS("SELECT * FROM ". _DB_PREFIX_ . "ia_files WHERE link = '" . pSQL($_POST['import_api_link']) . "' AND shop = '" . pSQL($shop) . "' AND status = 1 LIMIT 1"); + + if ($query_file) { + $file_id = $query_file[0]['file_id']; + Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_files SET link = '" . pSQL($_POST['import_api_link']) . "', delimiter = '" . pSQL($delimiter) . "', headers = '" . pSQL($headers) . "', source = '" . pSQL($source) . "', date_edited = '" . time() . "' WHERE file_id = " . (int)$file_id ." AND shop = '" . pSQL($shop) . "'"); + + } else { + Db::getInstance()->execute("INSERT INTO ". _DB_PREFIX_ . "ia_files SET link = '" . pSQL($_POST['import_api_link']) . "', shop = '" . pSQL($shop) . "', delimiter = '" . pSQL($delimiter) . "', headers = '" . pSQL($headers) . "', source = '" . pSQL($source) . "', mask = '" . pSQL($mask) ."', date_added = '" . time() . "'"); + } + } elseif (!empty($_FILES['doc']['tmp_name'])) { + + $name = $_FILES['doc']['name']; + $tmp_name = $_FILES['doc']['tmp_name']; + $type = $_FILES['doc']['type']; + + $file_ext = pathinfo($name, PATHINFO_EXTENSION); + + if (in_array($file_ext, ['zip'])) { + $this->errors[] = $this->my_translation("Sorry, app can't read zip files. Extract zip and upload content from inside"); + } else { + $target_file = _PS_MODULE_DIR_ . 'import_api/views/img/' . $mask . '_' . $name; + $base_link = Tools::getHttpHost(true).__PS_BASE_URI__; + $target_url = $base_link. 'modules/import_api/views/img/' . $mask . '_' . $name;; + if (move_uploaded_file($tmp_name, $target_file)) { + Db::getInstance()->execute("INSERT INTO ". _DB_PREFIX_ . "ia_files SET link = '" . pSQL($target_url) ."', mime_type = '" . pSQL($type) ."', source = '" . pSQL($source) ."', delimiter = '" . pSQL($delimiter) . "', headers = '" . pSQL($headers) . "', name = '" . pSQL($name) ."', mask = '" . pSQL($mask) ."', shop = '" . pSQL($shop) . "', date_added = '" . time() . "'"); + } else { + $this->errors[] = $this->my_translation("Sorry, there was an error uploading your file."); + } + } + } else { + $this->errors[] = $this->my_translation("Enter link or upload file."); + } + if (!$this->errors) { + if (!isset($file_id)) { + $file_id = Db::getInstance()->Insert_ID(); + } + if (!empty($_POST['import_api_link'])) { + if( strpos($_POST['import_api_link'], 'convert') !== false) { + $fields_file_path = _PS_MODULE_DIR_ . 'import_api/fields.txt'; + $file_settings_file_path = _PS_MODULE_DIR_ . 'import_api/file_settings.txt'; + if (file_exists($fields_file_path)) { + $f = file_get_contents($fields_file_path); + Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_files SET fields = '" . pSQL($f) . "', date_edited = '" . time() . "' WHERE file_id = " . (int)$file_id); + } + if (file_exists($file_settings_file_path)) { + $sql_raw = file_get_contents($file_settings_file_path); + $sql = sprintf($sql_raw, _DB_PREFIX_, $file_id); + + Db::getInstance()->execute($sql); + + } + } + } + Tools::redirectAdmin($this->context->link->getAdminLink('AdminImport_Api&type=settings&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false)); + } + } + + protected function renderSettingsForm($settings, $tab = 'general') + { + $helper = new HelperForm(); + $this->tab = $tab; + $helper->show_toolbar = false; + $helper->table = $this->table; + //$helper->module = $this; + $helper->default_form_language = $this->context->language->id; + $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG', 0); + + $helper->identifier = $this->identifier; + $helper->submit_action = 'submitImportSettings'; + $helper->currentIndex = $this->context->link->getAdminLink('AdminImport_Api', false) + .'&type=settings&file_id='. $settings['file_id']; + $helper->token = Tools::getAdminTokenLite('AdminImport_Api'); + + $helper->tpl_vars = array( + 'fields_value' => $this->getConfigFormValues(), + 'languages' => $this->context->controller->getLanguages(), + 'id_language' => $this->context->language->id, + ); + + return $helper->generateForm(array($this->getConfigForm($settings))); + } + + protected function getConfigForm($settings) + { + + $import_api_fields = $settings['fields'] ? html_entity_decode($settings['fields'], ENT_QUOTES, 'UTF-8') : ''; + + $source_fields = explode('##', $import_api_fields); + + + + $options_not_existing[] = array( + 'id_option' => 0, + 'name' => $this->my_translation('Keep it'), + ); + + $options_not_existing[] = array( + 'id_option' => 1, + 'name' => $this->my_translation('Set quantity 0'), + ); + + $options_not_existing[] = array( + 'id_option' => 2, + 'name' => $this->my_translation('Set status disabled'), + ); + + $options_not_existing[] = array( + 'id_option' => 3, + 'name' => $this->my_translation('Delete'), + ); + + $options_synchronize_field[] = array( + 'id_option' => 'automatic', + 'name' => $this->my_translation('From unique field') + ); + + $options_synchronize_field[] = array( + 'id_option' => 'reference', + 'name' => $this->my_translation('Reference') + ); + + $options_synchronize_field[] = array( + 'id_option' => 'ean13', + 'name' => $this->my_translation('Ean13') + ); + + $options_synchronize_field[] = array( + 'id_option' => 'upc', + 'name' => $this->my_translation('Upc') + ); + + $options_synchronize_field[] = array( + 'id_option' => 'name', + 'name' => $this->my_translation('Product name') + ); + + $options_synchronize_field[] = array( + 'id_option' => 'id_product', + 'name' => $this->my_translation('Prestashop product id') + ); + + + $options_yes_no[] = array( + 'id_option' => 0, + 'name' => $this->my_translation('No'), + ); + $options_yes_no[] = array( + 'id_option' => 1, + 'name' => $this->my_translation('Yes'), + ); + + $options[] = array( + 'id_option' => 0, + 'name' => $this->my_translation('--Please select--'), + ); + + $filter_options[] = array( + 'id_option' => 'equal', + 'name' => $this->my_translation('Equal'), + ); + + $filter_options[] = array( + 'id_option' => 'not_equal', + 'name' => $this->my_translation('Not equal'), + ); + + $filter_options[] = array( + 'id_option' => 'greater', + 'name' => $this->my_translation('Greater'), + ); + + $filter_options[] = array( + 'id_option' => 'less', + 'name' => $this->my_translation('Less'), + ); + + $filter_options[] = array( + 'id_option' => 'not_empty', + 'name' => $this->my_translation('Not empty'), + ); + + $filter_options[] = array( + 'id_option' => 'regexp', + 'name' => $this->my_translation('REGEXP'), + ); + + if($import_api_fields){ + foreach($source_fields as $field){ + $options[] = array( + 'id_option' => $field, + 'name' => $field, + ); + } + } + + $def_tax[] = array('id_tax_rules_group' => -1, 'name' => 'Default'); + $def_tax[] = array('id_tax_rules_group' => 0, 'name' => 'No tax'); + + $taxes = array_merge($def_tax, TaxRulesGroup::getTaxRulesGroups()); + + $fields[] = array( + 'type' => 'hidden', + 'id' => 'fields', + 'name' => 'import_api_fields', + ); + + $fields[] = array( + 'type' => 'hidden', + 'id' => 'import_api_link', + 'name' => 'import_api_link', + ); + + $fields[] = array( + 'type' => 'hidden', + 'id' => 'import_api_type', + 'name' => 'import_api_type', + ); + + foreach($this->frm_fields as $fmr_field){ + $fields[] = array( + 'type' => 'select', + 'class' => 'api_field', + 'tab' => 'general', + 'required' => in_array($fmr_field, ['unique', 'name']) ? true : false, + 'label' => $this->my_translation($fmr_field), + 'name' => 'import_api_field['. $fmr_field .']', + 'options' => array( + 'query' => $options, + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + + foreach($this->frm_fields as $fmr_field){ + $fields[] = array( + 'type' => 'text', + 'tab' => 'modifications', + 'label' => $this->my_translation($fmr_field), + 'name' => 'import_api_modification['. $fmr_field .']', + ); + + $fields[] = array( + 'type' => 'text', + 'tab' => 'combinations', + 'label' => $this->my_translation($fmr_field), + 'name' => 'import_api_split['. $fmr_field .']', + ); + } + + foreach($this->filter_fields as $f_field) { + $fields[] = array( + 'type' => 'textarea', + 'tab' => 'filter', + 'rows' => 10, + 'hint' => 'Enter each value in new line', + 'label' => $this->my_translation($f_field), + 'name' => 'import_api_filter['. $f_field .']', + 'description' => 'Enter values, each in new line', + ); + + $fields[] = array( + 'type' => 'select', + 'tab' => 'filter', + 'label' => $this->my_translation('Filter options for ' . $f_field), + 'name' => 'import_api_filter_options['. $f_field .']', + 'options' => array( + 'query' => $filter_options, + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + + foreach($this->replace_fields as $r_field) { + $fields[] = array( + 'type' => 'textarea', + 'tab' => 'replace', + 'rows' => 10, + 'hint' => 'Enter each value in new line in format OLD_TEXT##NEW_TEXT', + 'label' => $this->my_translation($r_field), + 'name' => 'import_api_replace['. $r_field .']', + 'description' => 'Enter values in format old_word##new_word', + ); + } + + // Settings fields + + $fields[] = array( + 'type' => 'text', + 'tab' => 'settings', + 'label' => $this->my_translation('Top category'), + 'name' => 'import_api_settings[top_category]', + ); + + $fields[] = array( + 'type' => 'text', + 'tab' => 'settings', + 'label' => $this->my_translation('Default category'), + 'name' => 'import_api_settings[default_category]', + ); + + $fields[] = array( + 'type' => 'text', + 'tab' => 'settings', + 'label' => $this->my_translation('Default brand'), + 'name' => 'import_api_settings[default_brand]', + ); + + $fields[] = array( + 'type' => 'text', + 'tab' => 'settings', + 'label' => $this->my_translation('Price multiplier'), + 'name' => 'import_api_settings[price_multiplier]', + ); + + $fields[] = array( + 'type' => 'text', + 'tab' => 'settings', + 'label' => $this->my_translation('Combination price multiplier'), + 'name' => 'import_api_settings[combination_price_multiplier]', + ); + + $fields[] = array( + 'type' => 'select', + 'tab' => 'settings', + 'label' => $this->my_translation('Add combination price to product price'), + 'name' => 'import_api_settings[add_combination_price]', + 'options' => array( + 'query' => $options_yes_no, + 'id' => 'id_option', + 'name' => 'name' + ) + ); + + $fields[] = array( + 'type' => 'select', + 'tab' => 'settings', + 'label' => $this->my_translation('Create category path from array'), + 'name' => 'import_api_settings[category_path]', + 'options' => array( + 'query' => $options_yes_no, + 'id' => 'id_option', + 'name' => 'name' + ) + ); + + $fields[] = array( + 'type' => 'select', + 'tab' => 'settings', + 'label' => $this->my_translation('Tax rules group'), + 'name' => 'import_api_settings[id_tax_rules_group]', + 'options' => array( + 'query' => $taxes, + 'id' => 'id_tax_rules_group', + 'name' => 'name' + ) + ); + + + $fields[] = array( + 'type' => 'select', + 'tab' => 'settings', + 'label' => $this->my_translation('Update even if file is not changed'), + 'name' => 'import_api_settings[same_update]', + 'options' => array( + 'query' => $options_yes_no, + 'id' => 'id_option', + 'name' => 'name' + ) + ); + + $fields[] = array( + 'type' => 'select', + 'tab' => 'settings', + 'label' => $this->my_translation('If product not exist in file anymore'), + 'name' => 'import_api_settings[not_existing]', + 'options' => array( + 'query' => $options_not_existing, + 'id' => 'id_option', + 'name' => 'name' + ) + ); + + $fields[] = array( + 'type' => 'select', + 'tab' => 'settings', + 'label' => $this->my_translation('Update only'), + 'name' => 'import_api_settings[only_update]', + 'options' => array( + 'query' => $options_yes_no, + 'id' => 'id_option', + 'name' => 'name' + ) + ); + + $fields[] = array( + 'type' => 'select', + 'tab' => 'settings', + 'label' => $this->my_translation('Update field'), + 'name' => 'import_api_settings[synchronize_field]', + 'options' => array( + 'query' => $options_synchronize_field, + 'id' => 'id_option', + 'name' => 'name' + ) + ); + + foreach($this->frm_update_settings as $key => $setting) { + $fields[] = array( + 'type' => 'select', + 'tab' => 'update', + 'label' => $this->my_translation('Update '. $key), + 'name' => 'import_api_settings[update_' . $key .']', + 'options' => array( + 'query' => $options_yes_no, + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + + return array( + 'form' => array( + 'legend' => array( + 'title' => $this->my_translation('Configuration'), + 'icon' => 'icon-cogs', + ), + 'input' => $fields, + 'submit' => array( + 'title' => $this->my_translation('Save'), + ), + 'tabs' => array( + 'general' => $this->my_translation('Assign'), + 'combinations' => $this->my_translation('Split'), + 'modifications' => $this->my_translation('Modifications'), + 'filter' => $this->my_translation('Filter'), + 'replace' => $this->my_translation('Replace'), + 'settings' => $this->my_translation('Settings'), + 'update' => $this->my_translation('Update fields'), + 'test' => $this->my_translation('Test'), + ), + ), + ); + } + + protected function getConfigFormValues() + { + $file_id = Tools::getValue('file_id'); + $return_settings = array(); + if (((bool)Tools::isSubmit('submitImportSettings')) == true) { + $config_fields = Tools::getValue('import_api_field'); + $config_modification = Tools::getValue('import_api_modification'); + $config_split = Tools::getValue('import_api_split'); + $config_replace = Tools::getValue('import_api_replace'); + $config_filter = Tools::getValue('import_api_filter'); + $config_filter_options = Tools::getValue('import_api_filter_options'); + + $config_settings = Tools::getValue('import_api_settings'); + + $return_settings['import_api_fields'] = Tools::getValue('import_api_fields'); + $return_settings['import_api_link'] = Tools::getValue('import_api_link'); + $return_settings['import_api_type'] = Tools::getValue('import_api_type'); + } else { + $query_file_settings = Db::getInstance()->executeS("SELECT fs.* FROM ". _DB_PREFIX_ . "ia_file_settings fs WHERE fs.file_id = '" . (int)$file_id . "' LIMIT 1"); + if ($query_file_settings) { + $file_settings =$query_file_settings[0]; + $config_fields = json_decode($file_settings['mapping'], true); + $config_modification = json_decode($file_settings['modification'], true); + $config_split = json_decode($file_settings['split'], true); + $config_replace = json_decode($file_settings['replace'], true); + $config_filter = json_decode($file_settings['filter'], true); + $config_filter_options = json_decode($file_settings['filter_options'], true); + $config_settings = json_decode($file_settings['settings'], true); + } + + $return_settings['import_api_fields'] = ''; + $return_settings['import_api_link'] = ''; + $return_settings['import_api_type'] = ''; + } + + foreach($this->frm_fields as $fmr_field){ + $return_settings['import_api_field[' . $fmr_field . ']'] = !empty($config_fields[$fmr_field]) ? html_entity_decode($config_fields[$fmr_field], ENT_QUOTES, 'UTF-8') : ''; + $return_settings['import_api_modification[' . $fmr_field . ']'] = !empty($config_modification[$fmr_field]) ? html_entity_decode($config_modification[$fmr_field], ENT_QUOTES, 'UTF-8') : ''; + $return_settings['import_api_split[' . $fmr_field . ']'] = !empty($config_split[$fmr_field]) ? html_entity_decode($config_split[$fmr_field], ENT_QUOTES, 'UTF-8') : ''; + } + + foreach($this->replace_fields as $r_field){ + $return_settings['import_api_replace[' . $r_field . ']'] = !empty($config_replace[$r_field]) ? html_entity_decode($config_replace[$r_field], ENT_QUOTES, 'UTF-8') : ''; + } + + foreach($this->filter_fields as $f_field){ + $return_settings['import_api_filter[' . $f_field . ']'] = !empty($config_filter[$f_field]) ? html_entity_decode($config_filter[$f_field], ENT_QUOTES, 'UTF-8') : ''; + $return_settings['import_api_filter_options[' . $f_field . ']'] = !empty($config_filter_options[$f_field]) ? html_entity_decode($config_filter_options[$f_field], ENT_QUOTES, 'UTF-8') : ''; + } + + for($i = 1; $i <= 3; $i++){ + $return_settings['import_api_field[field' . $i. ']'] = !empty($config_fields['field' . $i]) ? html_entity_decode($config_fields['field' . $i], ENT_QUOTES, 'UTF-8') : ''; + } + + for($i = 1; $i <= 3; $i++){ + $return_settings['import_api_modification[modification' . $i . ']'] = !empty($config_modification['modification' . $i]) ? html_entity_decode($config_modification['modification' . $i], ENT_QUOTES, 'UTF-8') : ''; + } + + foreach($this->frm_settings as $setting => $value){ + $return_settings['import_api_settings[' . $setting . ']'] = isset($config_settings[$setting]) ? $config_settings[$setting] : $value; + } + + foreach($this->frm_update_settings as $setting => $value){ + $return_settings['import_api_settings[update_' . $setting . ']'] = isset($config_settings['update_' . $setting]) ? $config_settings['update_' . $setting] : $value; + } + + + return $return_settings; + } + + protected function _postSettingsValidation() + { + + if (!Tools::getValue('import_api_field')) { + $this->_postErrors[] = $this->my_translation('You must assign unique field and name field.'); + } else { + $fields = Tools::getValue('import_api_field'); + if(empty($fields['unique'])){ + $this->_postErrors[] = $this->my_translation('You must assign unique field'); + } + if(empty($fields['name'])){ + //$this->_postErrors[] = $this->my_translation('You must assign name field'); + } + } + } + protected function _saveSettings($file_id, $shop = 'default') + { + + $mapping = ''; + $modification = ''; + $split = ''; + $replace = ''; + $filter = ''; + $filter_options = ''; + $settings = ''; + $update = false; + + $query_file_settings = Db::getInstance()->executeS("SELECT fs.* FROM ". _DB_PREFIX_ . "ia_file_settings fs WHERE fs.file_id = '" . (int)$file_id . "' LIMIT 1"); + if ($query_file_settings) { + $file_settings = $query_file_settings[0]; + $mapping = $file_settings['mapping']; + $modification = $file_settings['modification']; + $split = $file_settings['split']; + $replace = $file_settings['replace']; + $filter = $file_settings['filter']; + $filter_options = $file_settings['filter_options']; + $settings = $file_settings['settings']; + $update = true; + } + + $implode = array(); + + $new_mapping = json_encode(Tools::getValue('import_api_field')); + $new_modification = json_encode(Tools::getValue('import_api_modification')); + $new_split = json_encode(Tools::getValue('import_api_split')); + $new_replace = json_encode(Tools::getValue('import_api_replace')); + $new_filter = json_encode(Tools::getValue('import_api_filter')); + $new_filter_options = json_encode(Tools::getValue('import_api_filter_options')); + $new_settings = json_encode(Tools::getValue('import_api_settings')); + + if (Tools::getValue('import_api_field') && $new_mapping != $mapping) { + $implode[] = "mapping='" . pSQL($new_mapping). "'"; + } + + if (Tools::getValue('import_api_modification') && $new_modification != $modification) { + $implode[] = "modification='" . pSQL($new_modification). "'"; + } + + if (Tools::getValue('import_api_split') && $new_split != $split) { + $implode[] = "split='" . pSQL($new_split). "'"; + } + + if (Tools::getValue('import_api_replace') && $new_replace != $replace) { + $implode[] = "`replace`='" . pSQL($new_replace). "'"; + } + + if (Tools::getValue('import_api_filter') && $new_filter != $filter) { + $implode[] = "`filter`='" . pSQL($new_filter). "'"; + } + + if (Tools::getValue('import_api_filter_options') && $new_filter_options != $filter_options) { + $implode[] = "filter_options='" . pSQL($new_filter_options). "'"; + } + + if (Tools::getValue('import_api_settings') && $new_settings != $settings) { + $implode[] = "settings='" . pSQL($new_settings). "'"; + } + + if ($implode) { + $implode[] = "date_updated = '" . time() ."'"; + $implode_string = implode(',', $implode); + + if ($update) { + Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_file_settings SET " .$implode_string. " WHERE file_id = '" .(int)$file_id . "' AND shop = '" . pSQL($shop) . "'"); + } else { + Db::getInstance()->execute("INSERT INTO ". _DB_PREFIX_ . "ia_file_settings SET " .$implode_string. ", file_id = '" .(int)$file_id . "', shop = '" . pSQL($shop) . "'"); + } + //print_r("UPDATE ". _DB_PREFIX_ . "ia_file_settings SET " .$implode_string. " WHERE file_id = '" .(int)$file_id . "' AND shop = '" . $shop . "'"); + $this->queue->deleteUnnecessary($file_id, $shop); + Tools::redirectAdmin($this->context->link->getAdminLink('AdminImport_Api&type=import&file_id=' . $file_id . '&token=' . Tools::getAdminTokenLite('AdminImport_Api'), false)); + + } + + + } + + public function my_translation($string) { + return $this->module->l($string); + } +} \ No newline at end of file diff --git a/modules/import_api/controllers/front/ajax.php b/modules/import_api/controllers/front/ajax.php new file mode 100644 index 00000000..350da75f --- /dev/null +++ b/modules/import_api/controllers/front/ajax.php @@ -0,0 +1,106 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ + +class Import_ApiAjaxModuleFrontController extends ModuleFrontController +{ + + /** + * Assign template vars related to page content + * @see FrontController::initContent() + */ + private $fields = array(); + + public function initContent() + { + $response = $this->find_fields(); + $json = Tools::jsonEncode($response); + $this->ajaxDie($json); + + } + + + public function find_fields() + { + + Db::getInstance()->execute("SET session wait_timeout=28800", FALSE); + Db::getInstance()->execute("SET session net_read_timeout=28800", FALSE); + Db::getInstance()->execute("SET session interactive_timeout=28800", FALSE); + $file_id = Tools::getValue('file_id'); + $shop = 'default'; + $json['error'] = ''; + $php_array = array(); + $query_file = Db::getInstance()->executeS("SELECT * FROM ". _DB_PREFIX_ . "ia_files WHERE file_id = '" . (int)$file_id . "' AND shop = '" . pSQL($shop) . "' LIMIT 1"); + + //$type = Tools::getValue('type'); + //$link = html_entity_decode($this->request->post['import_api_link'], ENT_QUOTES, "UTF-8"); + + if ($query_file) { + require_once(_PS_MODULE_DIR_ . 'import_api/classes/filereader.php'); + + $FileReader = new FileReader(); + $file = $query_file[0]; + //$file['link'] = html_entity_decode($file['link'], ENT_QUOTES, "UTF-8"); + + list($php_array, $json['error']) = $FileReader->getArrayFromLink($file); + + if($php_array === null){ + $json['error'] = 'File is not in selected format'; + } + } else { + $json['error'] = 'File not found'; + } + + if (!$json['error']){ + if (count($php_array) > 10 && !empty(array_key_first($php_array))) { // if is not 0 + $new_array = array(); + foreach ($php_array as $key => $value ) { + $value['GENERATED_UNIQUE'] = $key; + $new_array[] = $value; + } + $php_array = $new_array; + unset($new_array); + } + $this->search_keys($php_array, 'FEED'); + $json['field'] = $this->fields; + $json['fields'] = implode('##', $this->fields); + //$json['mapping'] = implode('##', $this->fields); + if ($this->fields) { + Db::getInstance()->execute("UPDATE ". _DB_PREFIX_ . "ia_files SET fields = '" . pSQL($json['fields']) . "', date_edited = '" . time() . "' WHERE file_id = " . (int)$file_id ." AND shop = '" . pSQL($shop) . "'"); + } + } + + return $json; + } + + function search_keys($array, $parent){ + $total = count($array); + foreach($array as $key => $value){ + if(!is_array($value)){ + if (is_int($key)) { + $path = $parent; + } else { + $path = $parent .'->'. $key; + } + + $path = str_replace('->array()', '',$path); + if (!in_array($path, $this->fields)) + $this->fields[] = $path; + } else { + if (is_int($key)) { + $key = "array()"; + } + $this->search_keys($value, $parent .'->'. $key); + } + } + } +} diff --git a/modules/import_api/controllers/front/import.php b/modules/import_api/controllers/front/import.php new file mode 100644 index 00000000..a9d40dd7 --- /dev/null +++ b/modules/import_api/controllers/front/import.php @@ -0,0 +1,690 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ +ini_set('memory_limit', '-1'); +class Import_ApiImportModuleFrontController extends ModuleFrontController +{ + + /** + * Assign template vars related to page content + * @see FrontController::initContent() + */ + private $fields = array(); + private $product_links = array(); + private $import; + private $creator; + private $convertor; + public static $update_limit = 1; + + public function initContent() + { + + require_once(_PS_MODULE_DIR_ . 'import_api/classes/import.php'); + require_once(_PS_MODULE_DIR_ . 'import_api/classes/creator.php'); + require_once(_PS_MODULE_DIR_ . 'import_api/classes/filereader.php'); + require_once(_PS_MODULE_DIR_ . 'import_api/classes/queue.php'); + require_once(_PS_MODULE_DIR_ . 'import_api/classes/convertor.php'); + Db::getInstance()->execute("SET session wait_timeout=28800", FALSE); + Db::getInstance()->execute("SET session net_read_timeout=28800", FALSE); + Db::getInstance()->execute("SET session interactive_timeout=28800", FALSE); + + $json = array(); + + $view = Tools::getValue('id'); + + $file_id = Tools::getValue('file_id'); + + $this->queue = new Queue(); + + $this->creator = new Creator($file_id); + + $action = Tools::getValue('action') ? Tools::getValue('action') : 'cron'; + + /* if ($this->product_links === null) { + $json['error'] = 'Invalid source url. Please check in browser first'; + $this->ajaxDie(Tools::jsonEncode($json)); + }*/ + + $this->getModuleSettings(); + + + $this->convertor = new Convertor($this->settings, $this->creator); + + if ($action == 'test') + { + //$this->creator->addAttributesToProduct(array('0' => array('0' => array('attribute'=> "DColor", 'attribute_value' => "blue"), '1' => array('attribute'=> "DColor", 'attribute_value' => "green"))), 12685); exit; + + $this->product_links = $this->getProductLinks($action); + $this->printTestJsonData($view); + } + elseif (Tools::getValue('queue')) + { + $this->queue($file_id); + echo 'koniec'; + exit; + } + elseif ($action == 'selected') + { + $this->importSelected($file_id); + } + elseif ($action == 'delete') + { + $this->deleteProducts($file_id); + } + elseif ($action == 'view_one') + { + $this->viewOne($file_id, Tools::getValue('index')); + } + elseif (Tools::getValue('index')) + { + $this->importOne($file_id, Tools::getValue('index')); + } + else + { + $this->importData($file_id, $action, Tools::getValue( 'limit' ) ); + Tools::clearSmartyCache(); + Tools::clearXMLCache(); + Media::clearCache(); + Tools::generateIndex(); + } + } + + function queue($file_id) + { + $product_links = $this->getProductLinks(); + + $products = array(); + + foreach ($product_links as $link) + { + $original_product = $this->importer->getAllData($link); + $original_product = $this->convertor->unArray($original_product); + $original_product = $this->convertor->replace($original_product); + $original_product = $this->convertor->filter($original_product); + + $original_product = $this->convertor->clearInput($original_product); + if ($original_product['belong']) + { + $products[$original_product['unique']] = $original_product; + } + } + + $this->queue->saveTempToDb($products, $file_id); + + return count($products); + } + + function importData($file_id, $action = 'cron', $limit = 0 ) + { + $json['notice'] = ''; + $last_queue = $this->queue->getLastQueue($file_id); + + if (!$last_queue || $last_queue['status'] == 4) + { + // $this->queue($file_id); + // $last_queue = $this->queue->getLastQueue($file_id); + } + + if (!$last_queue) + { + exit('Queue creation error'); + } + if ($last_queue['status'] == 3) + { + $this->creator->processMissing($file_id, $last_queue['queue_id']); + $this->ajaxDie(Tools::jsonEncode($json)); + exit; + } + + $products = $this->queue->getQueued($file_id); + + if (!$products) + { + // $this->queue($file_id); + // $products = $this->queue->getQueued($file_id); + // $last_queue = $this->queue->getLastQueue($file_id); + echo 'koniec'; + exit; + } + + + $queue_id = isset($products[0]) ? $products[0]['queue_id'] : 0; + $products_created = 0; + $products_updated = 0; + + $json['total'] = count($products); + + foreach ($products as $product) + { + $original_product = json_decode($product['product'], true); + + $product_id = 0; + + if ($this->settings['unique_equivalent'] == 'id_product') + { + $product_id = (int)$original_product['unique']; + } + else + { + $equivalent = $this->settings['unique_equivalent']; + + $query = ($equivalent == 'name') ? $original_product['name'] : $original_product['unique']; + + $product_id = $this->creator->getProductId($query, $this->settings['table_equivalent'], $equivalent); + } + + $this->queue->deleteQueuedProduct($product['product_id']); + if ($product_id) + { + $this->creator->editProduct($product_id, $original_product); + Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_products SET indx = '" . pSQL($original_product['unique']) . "', product = '" . pSQL(json_encode($original_product)) . "', source = 'admin', date_edited = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "' WHERE product_id = '" . (int)$product_id . "' AND shop = 'default'"); + + $products_updated++; + + if ( $action == 'cron' ) + echo '

Zaktualizowałem produkt: ' . $product_id . '

'; + } + elseif (empty($this->settings['import_api_settings']['only_update'])) + { + $frm_product = $this->convertor->convertToFrmProduct($original_product, $file_id); + $product_id = $frm_product->id; + Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_products SET product_id = '" . $product_id . "', indx = '" . pSQL($original_product['unique']) . "', product = '" . pSQL(json_encode($original_product)) . "', shop = 'default', source = 'admin', date_added = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "'"); + + $products_created++; + + if ( $action == 'cron' ) + echo '

Dodałem nowy produkt: ' . $product_id . '

'; + } + + if ( ( ( $products_created + $products_updated ) >= self::$update_limit ) or ( $limit and ( $products_created + $products_updated ) >= $limit ) ) + { + $json['notice'] = 'time_out'; + break; + } + } + + if (!$json['notice']) + { + $json['notice'] = 'missing'; + Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_queues SET source = 'admin', status = '3', date_processed = '" . time() . "' WHERE queue_id = '" . (int)$queue_id . "'"); + } + + if ($action != 'cron') + { + $json['products_created'] = $products_created; + $json['products_updated'] = $products_updated; + $this->ajaxDie(Tools::jsonEncode($json)); + } + + exit; + } + + function importSelected($file_id) + { + $products = array(); + $selected = Tools::getValue('indx') ? Tools::getValue('indx') : array(); + + foreach ($selected as $indx) + { + $products[] = $this->queue->getQueuedProduct($indx, $file_id); + } + $queue_id = isset($products[0]) ? $products[0]['queue_id'] : 0; + + $products_created = 0; + $products_updated = 0; + + $json['notice'] = ''; + $json['total'] = count($products); + $json['processed'] = array(); + + foreach ($products as $product) + { + $original_product = json_decode($product['product'], true); + + $json['processed'][] = $product['product_id']; + + $product_id = 0; + + if ($this->settings['unique_equivalent'] == 'id_product') + { + $product_id = (int)$original_product['unique']; + } + else + { + $equivalent = $this->settings['unique_equivalent']; + + $query = ($equivalent == 'name') ? $original_product['name'] : $original_product['unique']; + + $product_id = $this->creator->getProductId($query, $this->settings['table_equivalent'], $equivalent); + } + + $this->queue->deleteQueuedProduct($product['product_id']); + + if ($product_id) + { + $this->creator->editProduct($product_id, $original_product); + Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_products SET indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($original_product)) . "', source = 'selected', date_edited = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "' WHERE product_id = '" . (int)$product_id . "' AND shop = 'default'"); + + $products_updated++; + } + elseif (empty($this->settings['import_api_settings']['only_update'])) + { + $frm_product = $this->convertor->convertToFrmProduct($original_product, $file_id); + $product_id = $frm_product->id; + Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_products SET product_id = '" . (int)$product_id . "', indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($original_product)) . "', shop = 'default', source = 'selected', date_added = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "'"); + + $products_created++; + } + + if ( ( $products_created + $products_updated ) >= self::$update_limit ) + { + $json['notice'] = 'time_out'; + break; + } + } + $json['products_created'] = $products_created; + $json['products_updated'] = $products_updated; + $this->ajaxDie(Tools::jsonEncode($json)); + + exit; + } + + function importOne($file_id, $indx) + { + $product = $this->queue->getQueuedProduct($indx, $file_id); + + if (!$product) + { + echo 'Product not found in queue'; + exit; + } + $queue_id = isset($product[0]) ? $product['queue_id'] : 0; + + + $original_product = json_decode($product['product'], true); + + $equivalent = $this->settings['unique_equivalent']; + + $query = ($equivalent == 'name') ? $original_product['name'] : $original_product['unique']; + + $product_id = $this->creator->getProductId($query, $this->settings['table_equivalent'], $equivalent); + $link = new Link(); + if ($product_id) + { + $exising = $this->creator->editProduct($product_id, $original_product); + Db::getInstance()->execute("UPDATE " . _DB_PREFIX_ . "ia_products SET indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($original_product)) . "', source = 'selected', date_edited = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "' WHERE product_id = '" . (int)$product_id . "' AND shop = 'default'"); + echo 'Product is updated'; + //echo 'link to product ' . $link->getproductlink($product_id) . ''; + } + else + { + $frm_product = $this->convertor->convertToFrmProduct($original_product, $file_id); + if ($frm_product) + { + $product_id = $frm_product->id; + Db::getInstance()->execute("INSERT INTO " . _DB_PREFIX_ . "ia_products SET product_id = '" . (int)$product_id . "', indx = '" . pSQL($indx) . "', product = '" . pSQL(json_encode($original_product)) . "', shop = 'default', source = 'selected', date_added = '" . time() . "', queue_id = '" . (int)$queue_id . "', file_id = '" . (int)$file_id . "'"); + echo 'Product is inserted'; + //echo 'Link to product ' . $link->getProductLink($product_id) . ''; + + } + } + + exit; + } + + function viewOne($file_id, $indx) + { + $product = $this->queue->getQueuedProduct($indx, $file_id); + + if (!$product) + { + echo 'Product not found in queue'; + exit; + } + + $queue_id = isset($product[0]) ? $product['queue_id'] : 0; + + $original_product = json_decode($product['product'], true); + + $equivalent = $this->settings['unique_equivalent']; + + $query = ($equivalent == 'name') ? $original_product['name'] : $original_product['unique']; + + $product_id = $this->creator->getProductId($query, $this->settings['table_equivalent'], $equivalent); + $link = new Link(); + if ($product_id) + { + echo 'Product exists ' . $product_id; + //echo 'link to product ' . $link->getproductlink($exising) . ''; + } + else + { + + echo 'Product is not inserted in Prestashop site '; + } + + foreach ($original_product as $key => $value) + { + if (!is_array($value)) + { + echo $key . ':' . $value . ', '; + } + } + + if (isset($original_product['cover'])) + { + if (!$this->UR_exists($original_product['cover'])) + { + echo 'image is not found. If you need to add server name go to Modifications and add https://servername/[[cover]]'; + } + else + { + echo 'image: ' . $original_product['cover']; + } + } + exit; + } + + private function printTestJsonData($view) + { + $json = array(); + + foreach ($this->product_links as $link) + { + $json[] = $this->importer->getAllData($link, $view); + } + + $this->ajaxDie(Tools::jsonEncode($json)); + } + + private function getProductLinks($action = 'import') + { + $begin_character = 'FEED'; + + $shop = 'default'; + + $file_id = Tools::getValue('file_id'); + + $settings = $this->getModuleSettings($file_id); + + $query_file = Db::getInstance()->executeS("SELECT * FROM " . _DB_PREFIX_ . "ia_files WHERE file_id = '" . (int)$file_id . "' AND shop = '" . pSQL($shop) . "' LIMIT 1"); + + $file = $query_file[0]; + + $FileReader = new FileReader(); + + list($tmp_array, $error) = $FileReader->getArrayFromLink($file); + + if ( is_array( $tmp_array) and count($tmp_array) > 10 && !empty(array_key_first($tmp_array))) + { // if is not 0 + $new_array = array(); + foreach ($tmp_array as $key => $value) + { + $value['GENERATED_UNIQUE'] = $key; + $new_array[] = $value; + } + $tmp_array = $new_array; + unset($new_array); + } + + $php_array[$begin_character] = $tmp_array; + + $parts = explode('->', $settings['unique_field']); + + $identifier_field = $parts[count($parts) - 1]; + + + $importer = new Import($settings); + + $importer->setJsonArray($php_array); + $importer->findUniqueProductsIdentifier($php_array, $parts, $identifier_field); + + + $product_links = $importer->getProductLinks(); + + //var_dump($product_links); exit; + + $this->importer = $importer; + + $start = $limit = 0; + + if ($action == 'test') + { + $start = $settings['start']; + $limit = 20; + } + + if (Tools::getValue('start')) + { + $start = Tools::getValue('start'); + } + + if (Tools::getValue('limit')) + { + $limit = Tools::getValue('limit'); + } + + if ($start && !$limit) + { + $limit = count($product_links) - $start; + } + + if ($limit) + { + $product_links = array_slice($product_links, $start, $limit); + } + return $product_links; + } + + function getModuleSettings($file_id = 0) + { + + if (!$file_id) + { + $file_id = Tools::getValue('file_id'); + } + + $shop = 'default'; + + $settings_fields = array('link', 'attribute_group', 'default_brand', 'default_category', 'top_category', 'weight_class_id', 'stock_status_id', 'tax', 'default_option', 'category_path', 'multiplier', 'id_tax_rules_group'); + + if (Tools::getValue('import_api_field')) + { + + $settings = array( + 'import_api_field' => Tools::getValue('import_api_field'), + 'import_api_modification' => Tools::getValue('import_api_modification'), + 'import_api_combination' => Tools::getValue('import_api_combination'), + 'import_api_settings' => Tools::getValue('import_api_settings'), + 'import_api_link' => Tools::getValue('import_api_link'), + 'import_api_type' => Tools::getValue('import_api_type'), + ); + } + else + { + $query_file_settings = Db::getInstance()->executeS("SELECT fs.* FROM " . _DB_PREFIX_ . "ia_file_settings fs WHERE fs.file_id = '" . (int)$file_id . "' LIMIT 1"); + if ($query_file_settings) + { + $file_settings = $query_file_settings[0]; + $filters = array(); + $replaces = array(); + if ($file_settings['filter']) + { + $text_areas = json_decode($file_settings['filter'], true); + foreach ($text_areas as $key => $textarea_value) + { + if ($textarea_value) + { + $filters[$key] = array_map('trim', explode("\n", $textarea_value)); + } + } + } + if ($file_settings['replace']) + { + $text_areas = json_decode($file_settings['replace'], true); + foreach ($text_areas as $key => $textarea_value) + { + $lines = array_map('trim', explode("\n", $textarea_value)); + foreach ($lines as $line) + { + if ($line) + { + $replaces[$key][] = explode('##', $line); + } + } + } + } + + $settings = array( + 'import_api_field' => json_decode($file_settings['mapping'], true), + 'import_api_modification' => json_decode($file_settings['modification'], true), + 'import_api_combination' => json_decode($file_settings['split'], true), + 'import_api_filter' => $filters, + 'import_api_replace' => $replaces, + 'import_api_filter_options' => json_decode($file_settings['filter_options'], true), + 'import_api_settings' => json_decode($file_settings['settings'], true), + ); + } + } + + if (!empty($settings['import_api_field']['unique'])) + { + $settings['unique_field'] = html_entity_decode($settings['import_api_field']['unique'], ENT_QUOTES, 'UTF-8'); + } + else + { + $json['error'] = 'you need to set unique field'; + $this->ajaxDie(Tools::jsonEncode($json)); + } + + if (Tools::getValue('start')) + { + $settings['start'] = Tools::getValue('start'); + } + else + { + $settings['start'] = 0; + } + + if ($settings['import_api_settings']['top_category']) + { + $settings['top_category_id'] = $this->creator->getCategoryId($settings['import_api_settings']['top_category']); + } + else + { + $settings['top_category_id'] = Configuration::get('PS_HOME_CATEGORY'); + } + + if ($settings['import_api_settings']['default_category']) + { + $settings['default_category_id'] = $this->creator->getCategoryId($settings['import_api_settings']['default_category']); + } + else + { + $settings['default_category_id'] = 0; + } + + if ($settings['import_api_settings']['default_brand']) + { + $settings['default_manufacturer_id'] = $this->creator->getManufacturerId($settings['import_api_settings']['default_brand']); + } + else + { + $settings['default_manufacturer_id'] = 0; + } + + $settings['id_tax_rules_group'] = isset($settings['import_api_settings']['id_tax_rules_group']) ? $settings['import_api_settings']['id_tax_rules_group'] : -1; + + $settings['unique_equivalent'] = 'name'; + $settings['table_equivalent'] = 'product_lang'; + if (isset($settings['import_api_settings']['synchronize_field']) && $settings['import_api_settings']['synchronize_field'] != 'automatic') + { + $settings['unique_equivalent'] = $settings['import_api_settings']['synchronize_field']; + } + else + { + $possible_eq = ['reference', 'sku', 'ean13', 'upc']; + + foreach ($possible_eq as $f) + { + if (!empty($settings['import_api_field'][$f]) && $settings['import_api_field'][$f] == $settings['import_api_field']['unique']) + { + $settings['unique_equivalent'] = $f; + break; + } + } + } + if ($settings['unique_equivalent'] != 'name') + { + $settings['table_equivalent'] = 'product'; + } + + $this->settings = $settings; + + return $settings; + } + + function deleteProducts($file_id) + { + $query_file = Db::getInstance()->executeS("SELECT * FROM " . _DB_PREFIX_ . "ia_files WHERE file_id = '" . (int)$file_id . "' LIMIT 1"); + if (empty($query_file)) + { + $json['notice'] = 'File not found'; + $this->ajaxDie(Tools::jsonEncode($json)); + exit; + } + else + { + $file = $query_file[0]; + } + if ($file['mask'] . $file['date_added'] != Tools::getValue('token')) + { + $json['notice'] = 'File not found.....'; + $this->ajaxDie(Tools::jsonEncode($json)); + exit; + } + + $max_execution_time = 50; + + $products = $this->queue->getProducts($file_id); + + $products_deleted = 0; + + $json['notice'] = ''; + + foreach ($products as $product) + { + $p = new Product($product['product_id']); + $p->delete(); + $this->queue->deleteProduct($product['product_id']); + $products_deleted++; + + if ( ( $products_created + $products_updated ) >= self::$update_limit ) + { + $json['notice'] = 'time_out'; + break; + } + } + + $json['products_deleted'] = $products_deleted; + $this->ajaxDie(Tools::jsonEncode($json)); + + exit; + } + + + function UR_exists($url) + { + $headers = @get_headers($url); + return stripos($headers[0], "200 OK") ? true : false; + } +} diff --git a/modules/import_api/controllers/front/index.php b/modules/import_api/controllers/front/index.php new file mode 100644 index 00000000..8761a003 --- /dev/null +++ b/modules/import_api/controllers/front/index.php @@ -0,0 +1,35 @@ + +* @copyright 2007-2016 PrestaShop SA +* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) +* International Registered Trademark & Property of PrestaShop SA +*/ + +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; \ No newline at end of file diff --git a/modules/import_api/controllers/index.php b/modules/import_api/controllers/index.php new file mode 100644 index 00000000..8761a003 --- /dev/null +++ b/modules/import_api/controllers/index.php @@ -0,0 +1,35 @@ + +* @copyright 2007-2016 PrestaShop SA +* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) +* International Registered Trademark & Property of PrestaShop SA +*/ + +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; \ No newline at end of file diff --git a/modules/import_api/import_api.php b/modules/import_api/import_api.php new file mode 100644 index 00000000..fe55dddb --- /dev/null +++ b/modules/import_api/import_api.php @@ -0,0 +1,102 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ + +if (!defined('_PS_VERSION_')) { + exit; +} + +class import_api extends Module +{ + protected $config_form = false; + + protected $_postErrors = array(); + + protected $fmr_fields; + + + public function __construct() + { + $this->name = 'import_api'; + $this->tab = 'front_office_features'; + $this->version = '3.1.5.9'; + $this->author = 'Dalibor Stojcevski'; + $this->need_instance = 1; + $this->bootstrap = true; + $this->module_key = '3711c016ea39f2f2aa38850e48aa7455'; + parent::__construct(); + $this->displayName = $this->l('Import Api'); + $this->description = $this->l('Import and update products from external source'); + $this->ps_versions_compliancy = array('min' => '1.6', 'max' => _PS_VERSION_); + + $this->frm_fields = ['unique', 'reference', 'name', 'description', 'price', 'quantity', 'images', 'cover', 'brand', 'category', 'category_parent', 'ean13', 'upc', 'condition', 'weight', 'feature', 'feature_value']; + $this->frm_settings = array('top_category' => '', 'default_category' => '', 'default_brand' => '', 'price_multiplier' => '', 'category_path' => 0); + } + + public function install() + { + $class = 'AdminImport_Api'; + $tab = new Tab(); + $tab->class_name = $class; + $tab->module = $this->name; + $tab->id_parent = 0; + $langs = Language::getLanguages(false); + foreach ($langs as $l) { + $tab->name[$l['id_lang']] = $this->l('Import Products'); + } + $tab->save(); + return $this->installDb() && parent::install(); + } + + public function uninstall() + { + return parent::uninstall(); + } + + public function installDb() + { + $return = true; + require_once(_PS_MODULE_DIR_ . 'import_api/sql_install.php'); + + foreach ($sql as $s) { + Db::getInstance()->execute($s); + } + + $res = Db::getInstance()->executeS("SHOW columns FROM `"._DB_PREFIX_."ia_files` LIKE 'status'"); + if (!$res) { + Db::getInstance()->execute('ALTER TABLE `'._DB_PREFIX_.'ia_files` ADD `status` SMALLINT(2) NOT NULL DEFAULT \'1\''); + } + + $res_1 = Db::getInstance()->executeS("SHOW columns FROM `"._DB_PREFIX_."ia_file_settings` LIKE 'replace' "); + if (!$res_1) { + Db::getInstance()->execute('ALTER TABLE `'._DB_PREFIX_.'ia_file_settings` ADD `replace` text NOT NULL'); + } + + $res_2 = Db::getInstance()->executeS("SHOW columns FROM `"._DB_PREFIX_."ia_file_settings` LIKE 'filter' "); + if (!$res_2) { + Db::getInstance()->execute('ALTER TABLE `'._DB_PREFIX_.'ia_file_settings` ADD `filter` text NOT NULL'); + } + + $res_3 = Db::getInstance()->executeS("SHOW columns FROM `"._DB_PREFIX_."ia_file_settings` LIKE 'filter_options' "); + + if (!$res_3) { + Db::getInstance()->execute('ALTER TABLE `'._DB_PREFIX_.'ia_file_settings` ADD `filter_options` text NOT NULL'); + } + return $return; + } + + public function getContent() + { + Tools::redirectAdmin($this->context->link->getAdminLink('AdminImport_Api')); + } + +} \ No newline at end of file diff --git a/modules/import_api/index.php b/modules/import_api/index.php new file mode 100644 index 00000000..907720c1 --- /dev/null +++ b/modules/import_api/index.php @@ -0,0 +1,35 @@ + +* @copyright 2007-2018 PrestaShop SA +* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) +* International Registered Trademark & Property of PrestaShop SA +*/ + +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/modules/import_api/lib/SimpleXLS.php b/modules/import_api/lib/SimpleXLS.php new file mode 100644 index 00000000..f6d8c20b --- /dev/null +++ b/modules/import_api/lib/SimpleXLS.php @@ -0,0 +1,1224 @@ +'; + +if ( $xls = SimpleXLS::parse('excel5book.xls')) { + print_r( $xls->rows() ); // dump first sheet + print_r( $xls->rows(1)); /// dump second sheet +} else { + echo SimpleXLSX::parseError(); +} + +echo ''; + */ + +/** + * A class for reading Microsoft Excel Spreadsheets. + * + * SimpleXLS version 2016-2020 packaged by Sergey Shuchkin from + * Spreadsheet_Excel_Reader class developed by Vadim Tkachenko + */ +class SimpleXLS { + const BIFF8 = 0x600; + const BIFF7 = 0x500; + const WORKBOOKGLOBALS = 0x5; + const WORKSHEET = 0x10; + + //const TYPE_BOF = 0x809; + const TYPE_EOF = 0x0a; + const TYPE_BOUNDSHEET = 0x85; + const TYPE_DIMENSION = 0x200; + const TYPE_ROW = 0x208; + const TYPE_DBCELL = 0xd7; + const TYPE_FILEPASS = 0x2f; + //const TYPE_NOTE = 0x1c; + //const TYPE_TXO = 0x1b6; + const TYPE_RK = 0x7e; + const TYPE_RK2 = 0x27e; + const TYPE_MULRK = 0xbd; + const TYPE_MULBLANK = 0xbe; + //const TYPE_INDEX = 0x20b; + const TYPE_SST = 0xfc; + //const TYPE_EXTSST = 0xff; + //const TYPE_CONTINUE = 0x3c; + const TYPE_LABEL = 0x204; + const TYPE_LABELSST = 0xfd; + const TYPE_NUMBER = 0x203; + const TYPE_NAME = 0x18; + //const TYPE_ARRAY = 0x221; + //const TYPE_STRING = 0x207; + const TYPE_FORMULA = 0x406; + const TYPE_FORMULA2 = 0x6; + const TYPE_FORMAT = 0x41e; + const TYPE_XF = 0xe0; + const TYPE_BOOLERR = 0x205; + //const TYPE_UNKNOWN = 0xffff; + const TYPE_NINETEENFOUR = 0x22; + const TYPE_MERGEDCELLS = 0xE5; + + //const DEF_NUM_FORMAT = "%.2f"; + const DEF_NUM_FORMAT = '%s'; + + // OLE + const NUM_BIG_BLOCK_DEPOT_BLOCKS_POS = 0x2c; + const SMALL_BLOCK_DEPOT_BLOCK_POS = 0x3c; + const ROOT_START_BLOCK_POS = 0x30; + const BIG_BLOCK_SIZE = 0x200; + const SMALL_BLOCK_SIZE = 0x40; + const EXTENSION_BLOCK_POS = 0x44; + const NUM_EXTENSION_BLOCK_POS = 0x48; + const PROPERTY_STORAGE_BLOCK_SIZE = 0x80; + const BIG_BLOCK_DEPOT_BLOCKS_POS = 0x4c; + const SMALL_BLOCK_THRESHOLD = 0x1000; + // property storage offsets + const SIZE_OF_NAME_POS = 0x40; + const TYPE_POS = 0x42; + const START_BLOCK_POS = 0x74; + const SIZE_POS = 0x78; + /** + * Array of worksheets found + * + * @var array + * @access public + */ + public $boundsheets = array(); + + /** + * Array of format records found + * + * @var array + * @access public + */ + public $formatRecords = array(); + + /** + * + * @var array + * @access public + */ + public $sst = array(); + + /** + * Array of worksheets + * + * The data is stored in 'cells' and the meta-data is stored in an array + * called 'cellsInfo' + * + * Example: + * + * $sheets --> 'cells' --> row --> column --> Interpreted value + * --> 'cellsInfo' --> row --> column --> 'type' - Can be 'date', 'number', or 'unknown' + * --> 'raw' - The raw data that Excel stores for that data cell + * + * @var array + * @access public + */ + public $sheets = array(); + /** + * List of default date formats used by Excel + * + * @var array + * @access public + */ + public $dateFormats = array( + 0xe => 'd/m/Y', + 0xf => 'd-M-Y', + 0x10 => 'd-M', + 0x11 => 'M-Y', + 0x12 => 'h:i a', + 0x13 => 'h:i:s a', + 0x14 => 'H:i', + 0x15 => 'H:i:s', + 0x16 => 'd/m/Y H:i', + 0x2d => 'i:s', + 0x2e => 'H:i:s', + 0x2f => 'i:s.S' + ); +/** + * Default number formats used by Excel + * + * @var array + * @access public + */ + public $numberFormats = array( + 0x1 => '%1.0f', // "0" + 0x2 => '%1.2f', // "0.00", + 0x3 => '%1.0f', //"#,##0", + 0x4 => '%1.2f', //"#,##0.00", + 0x5 => '%1.0f', /*"$#,##0;($#,##0)",*/ + 0x6 => '$%1.0f', /*"$#,##0;($#,##0)",*/ + 0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x9 => '%1.0f%%', // "0%" + 0xa => '%1.2f%%', // "0.00%" + 0xb => '%1.2f', // 0.00E00", + 0x25 => '%1.0f', // "#,##0;(#,##0)", + 0x26 => '%1.0f', //"#,##0;(#,##0)", + 0x27 => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x28 => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x29 => '%1.0f', //"#,##0;(#,##0)", + 0x2a => '$%1.0f', //"$#,##0;($#,##0)", + 0x2b => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x30 => '%1.0f' + ); + private $datetimeFormat = 'Y-m-d H:i:s'; + /** + * Default encoding + * + * @var string + * @access private + */ + private $defaultEncoding = 'UTF-8'; + /** + * Default number format + * + * @var integer + * @access private + */ + private $defaultFormat = self::DEF_NUM_FORMAT; + /** + * List of formats to use for each column + * + * @var array + * @access private + */ + private $columnsFormat = array(); + /** + * + * @var integer + * @access private + */ + private $rowoffset = 1; //"##0.0E0"; + + // }}} + /** + * + * @var integer + * @access private + */ + private $colOffset = 1; + private $recType; + private $nineteenFour; + private $multiplier; + private $sn; + private $curFormat; + + // OLERead + private $data; + private $numBigBlockDepotBlocks; + private $sbdStartBlock; + private $rootStartBlock; + private $extensionBlock; + private $numExtensionBlocks; + private $bigBlockChain; + private $smallBlockChain; + private $rootEntry; + private $entry; + private $props; + + // sergey.shuchkin@gmail.com + private $wrkbook; // false - to use excel format + private $error = false; + private $debug; + + // {{{ Spreadsheet_Excel_Reader() + + /** + * Constructor + * + * @param string $filename XLS Filename or xls contents + * @param bool $isData If True then $filename is contents + * @param bool $debug Trigger PHP errors? + */ + public function __construct( $filename, $isData = false, $debug = false ) { + $this->debug = $debug; + $this->_oleread( $filename, $isData ); + $this->_parse(); + } + public static function parse( $filename, $isData = false, $debug = false ) { + $xlsx = new self( $filename, $isData, $debug ); + if ( $xlsx->success() ) { + return $xlsx; + } + self::parseError( $xlsx->error() ); + + return false; + } + public static function parseError( $set = false ) { + static $error = false; + return $set ? $error = $set : $error; + } + public function error( $set = false ) { + if ( $set ) { + $this->error = $set; + if ( $this->debug ) { + trigger_error( $set ); + } + } + + return $this->error; + } + public function success() { + return ! $this->error; + } + public function rows( $sheetNum = 0 ) { + if ( $this->sheets[ $sheetNum ] ) { + $s = $this->sheets[ $sheetNum ]; + $result = array(); + for ( $i = 0; $i < $s['numRows']; $i ++ ) { + $r = array(); + for ( $j = 0; $j < $s['numCols']; $j ++ ) { + $r[ $j ] = isset( $s['cells'][ $i + 1 ][ $j + 1 ] ) ? $s['cells'][ $i + 1 ][ $j + 1 ] : ''; + } + $result[] = $r; + } + + return $result; + } + + return false; + } + public function setDateTimeFormat( $value ) { + $this->datetimeFormat = is_string( $value) ? $value : false; + } + + // }}} + + private function _oleread( $sFileName, $isData = false ) { + if ( $isData ) { + $this->data = $sFileName; + } else { + // check if file exist and is readable (Darko Miljanovic) + if ( ! is_readable( $sFileName ) ) { + $this->error( 'File not is readable ' . $sFileName ); + return false; + } + + $this->data = file_get_contents( $sFileName ); + if ( ! $this->data ) { + $this->error( 'File reading error ' . $sFileName ); + return false; + } + } + //echo IDENTIFIER_OLE; + //echo 'start'; + if ( $this->_strpos( $this->data, pack( 'CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1 ) ) !== 0 ) { + $this->error( 'File is not XLS' ); + + return false; + } + + $this->numBigBlockDepotBlocks = $this->_GetInt4d( $this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS ); + $this->sbdStartBlock = $this->_GetInt4d( $this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS ); + $this->rootStartBlock = $this->_GetInt4d( $this->data, self::ROOT_START_BLOCK_POS ); + $this->extensionBlock = $this->_GetInt4d( $this->data, self::EXTENSION_BLOCK_POS ); + $this->numExtensionBlocks = $this->_GetInt4d( $this->data, self::NUM_EXTENSION_BLOCK_POS ); + +/* + echo $this->numBigBlockDepotBlocks." "; + echo $this->sbdStartBlock." "; + echo $this->rootStartBlock." "; + echo $this->extensionBlock." "; + echo $this->numExtensionBlocks." "; + +*/ + //echo "sbdStartBlock = $this->sbdStartBlock\n"; + $bigBlockDepotBlocks = array(); + $pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS; + // echo "pos = $pos"; + $bbdBlocks = $this->numBigBlockDepotBlocks; + + if ( $this->numExtensionBlocks !== 0 ) { + $bbdBlocks = ( self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS ) / 4; + } + + for ( $i = 0; $i < $bbdBlocks; $i ++ ) { + $bigBlockDepotBlocks[ $i ] = $this->_GetInt4d( $this->data, $pos ); + $pos += 4; + } + + + for ( $j = 0; $j < $this->numExtensionBlocks; $j ++ ) { + $pos = ( $this->extensionBlock + 1 ) * self::BIG_BLOCK_SIZE; + $blocksToRead = min( $this->numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1 ); + + for ( $i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; $i ++ ) { + $bigBlockDepotBlocks[ $i ] = $this->_GetInt4d( $this->data, $pos ); + $pos += 4; + } + + $bbdBlocks += $blocksToRead; + if ( $bbdBlocks < $this->numBigBlockDepotBlocks ) { + $this->extensionBlock = $this->_GetInt4d( $this->data, $pos ); + } + } + + // var_dump($bigBlockDepotBlocks); + + // readBigBlockDepot + + $index = 0; + $this->bigBlockChain = array(); + + for ( $i = 0; $i < $this->numBigBlockDepotBlocks; $i ++ ) { + $pos = ( $bigBlockDepotBlocks[ $i ] + 1 ) * self::BIG_BLOCK_SIZE; + //echo "pos = $pos"; + for ( $j = 0; $j < self::BIG_BLOCK_SIZE / 4; $j ++ ) { + $this->bigBlockChain[ $index ] = $this->_GetInt4d( $this->data, $pos ); + $pos += 4; + $index ++; + } + } + + //var_dump($this->bigBlockChain); + //echo '=====2'; + // readSmallBlockDepot(); + + $index = 0; + $sbdBlock = $this->sbdStartBlock; + $this->smallBlockChain = array(); + + while ( $sbdBlock !== - 2 ) { + + $pos = ( $sbdBlock + 1 ) * self::BIG_BLOCK_SIZE; + + for ( $j = 0; $j < self::BIG_BLOCK_SIZE / 4; $j ++ ) { + $this->smallBlockChain[ $index ] = $this->_GetInt4d( $this->data, $pos ); + $pos += 4; + $index ++; + } + + $sbdBlock = $this->bigBlockChain[ $sbdBlock ]; + } + + + // readData(rootStartBlock) + $block = $this->rootStartBlock; + + $this->entry = $this->_readData( $block ); + + /* + while ($block != -2) { + $pos = ($block + 1) * self::BIG_BLOCK_SIZE; + $this->entry = $this->entry.$this->_substr($this->_data, $pos, self::BIG_BLOCK_SIZE); + $block = $this->bigBlockChain[$block]; + } + */ + //echo '==='.$this->entry."==="; + $this->_readPropertySets(); + $this->data = $this->_readWorkBook(); + + return true; + } + + // {{{ setOutputEncoding() + + public function _GetInt4d( $data, $pos ) { + $value = ord( $data[ $pos ] ) | ( ord( $data[ $pos + 1 ] ) << 8 ) | ( ord( $data[ $pos + 2 ] ) << 16 ) | ( ord( $data[ $pos + 3 ] ) << 24 ); + return ($value > 0x7FFFFFFF) ? $value - 0x100000000 : $value; + } + + // }}} + + // {{{ setRowColOffset() + + public function _readData( $bl ) { + $block = $bl; + + $data = ''; + + while ( $block !== - 2 ) { + $pos = ( $block + 1 ) * self::BIG_BLOCK_SIZE; + $data .= $this->_substr( $this->data, $pos, self::BIG_BLOCK_SIZE ); + //echo "pos = $pos data=$data\n"; + $block = $this->bigBlockChain[ $block ]; + } + + return $data; + } + + // }}} + // {{{ setDefaultFormat() + + public function _readPropertySets() { + $offset = 0; + //var_dump($this->entry); + while ( $offset < $this->_strlen( $this->entry ) ) { + $d = $this->_substr( $this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE ); + + $nameSize = ord( $d[ self::SIZE_OF_NAME_POS ] ) | ( ord( $d[ self::SIZE_OF_NAME_POS + 1 ] ) << 8 ); + + $type = ord( $d[ self::TYPE_POS ] ); + //$maxBlock = $this->_strlen($d) / self::BIG_BLOCK_SIZE - 1; + + $startBlock = $this->_GetInt4d( $d, self::START_BLOCK_POS ); + $size = $this->_GetInt4d( $d, self::SIZE_POS ); + + $name = ''; + for ( $i = 0; $i < $nameSize; $i ++ ) { + $name .= $d[ $i ]; + } + + $name = str_replace( "\x00", '', $name ); + + $this->props[] = array( + 'name' => $name, + 'type' => $type, + 'startBlock' => $startBlock, + 'size' => $size + ); + + if ( ( $name === 'Workbook' ) || ( $name === 'Book' ) ) { + $this->wrkbook = count( $this->props ) - 1; + } + + if ( $name === 'Root Entry' ) { + $this->rootEntry = count( $this->props ) - 1; + } + + //echo "name ==$name=\n"; + + + $offset += self::PROPERTY_STORAGE_BLOCK_SIZE; + } + + } + + // }}} + // {{{ setColumnFormat() + + private function _readWorkBook() { + if ( $this->props[ $this->wrkbook ]['size'] < self::SMALL_BLOCK_THRESHOLD ) { +// getSmallBlockStream(PropertyStorage ps) + + $rootdata = $this->_readData( $this->props[ $this->rootEntry ]['startBlock'] ); + + $streamData = ''; + $block = (int) $this->props[ $this->wrkbook ]['startBlock']; + //$count = 0; + while ( $block !== - 2 ) { + $pos = $block * self::SMALL_BLOCK_SIZE; + $streamData .= $this->_substr( $rootdata, $pos, self::SMALL_BLOCK_SIZE ); + + $block = $this->smallBlockChain[ $block ]; + } + + return $streamData; + + + } + + $numBlocks = $this->props[ $this->wrkbook ]['size'] / self::BIG_BLOCK_SIZE; + if ( $this->props[ $this->wrkbook ]['size'] % self::BIG_BLOCK_SIZE !== 0 ) { + $numBlocks ++; + } + + if ( $numBlocks === 0 ) { + return ''; + } + + //echo "numBlocks = $numBlocks\n"; + //byte[] streamData = new byte[numBlocks * self::BIG_BLOCK_SIZE]; + //print_r($this->wrkbook); + $streamData = ''; + $block = $this->props[ $this->wrkbook ]['startBlock']; + + //echo "block = $block"; + while ( $block !== - 2 ) { + $pos = ( $block + 1 ) * self::BIG_BLOCK_SIZE; + $streamData .= $this->_substr( $this->data, $pos, self::BIG_BLOCK_SIZE ); + $block = $this->bigBlockChain[ $block ]; + } + + //echo 'stream'.$streamData; + return $streamData; + } + + + // }}} + // {{{ _parse() + + /** + * Parse a workbook + * + * @access private + * @return bool + */ + public function _parse() { + $pos = 0; + +// $code = ord($this->data[$pos]) | ord($this->data[$pos+1])<<8; + $length = ord( $this->data[ $pos + 2 ] ) | ord( $this->data[ $pos + 3 ] ) << 8; + + $version = ord( $this->data[ $pos + 4 ] ) | ord( $this->data[ $pos + 5 ] ) << 8; + $substreamType = ord( $this->data[ $pos + 6 ] ) | ord( $this->data[ $pos + 7 ] ) << 8; +// echo "Start parse code=".base_convert($code,10,16)." version=".base_convert($version,10,16)." substreamType=".base_convert($substreamType,10,16).""."\n"; + +// die(); + + if ( ( $version !== self::BIFF8 ) && + ( $version !== self::BIFF7 ) + ) { + return false; + } + + if ( $substreamType !== self::WORKBOOKGLOBALS ) { + return false; + } + + //print_r($rec); + $pos += $length + 4; + + $code = ord( $this->data[ $pos ] ) | ord( $this->data[ $pos + 1 ] ) << 8; + $length = ord( $this->data[ $pos + 2 ] ) | ord( $this->data[ $pos + 3 ] ) << 8; + + while ( $code !== self::TYPE_EOF ) { + switch ( $code ) { + case self::TYPE_SST: + //echo "Type_SST\n"; + $formattingRuns = 0; + $extendedRunLength = 0; + $spos = $pos + 4; + $limitpos = $spos + $length; + $uniqueStrings = $this->_GetInt4d( $this->data, $spos + 4 ); + $spos += 8; + for ( $i = 0; $i < $uniqueStrings; $i ++ ) { + // Read in the number of characters + if ( $spos === $limitpos ) { + $opcode = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + $conlength = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + if ( $opcode !== 0x3c ) { + return - 1; + } + $spos += 4; + $limitpos = $spos + $conlength; + } + $numChars = ord( $this->data[ $spos ] ) | ( ord( $this->data[ $spos + 1 ] ) << 8 ); + //echo "i = $i pos = $pos numChars = $numChars "; + $spos += 2; + $optionFlags = ord( $this->data[ $spos ] ); + $spos ++; + $asciiEncoding = ( ( $optionFlags & 0x01 ) === 0 ); + $extendedString = ( ( $optionFlags & 0x04 ) !== 0 ); + + // See if string contains formatting information + $richString = ( ( $optionFlags & 0x08 ) !== 0 ); + + if ( $richString ) { + // Read in the crun + $formattingRuns = ord( $this->data[ $spos ] ) | ( ord( $this->data[ $spos + 1 ] ) << 8 ); + $spos += 2; + } + + if ( $extendedString ) { + // Read in cchExtRst + $extendedRunLength = $this->_GetInt4d( $this->data, $spos ); + $spos += 4; + } + + $len = $asciiEncoding ? $numChars : $numChars * 2; + if ( $spos + $len < $limitpos ) { + $retstr = $this->_substr( $this->data, $spos, $len ); + $spos += $len; + } else { + // found countinue + $retstr = $this->_substr( $this->data, $spos, $limitpos - $spos ); + $bytesRead = $limitpos - $spos; + $charsLeft = $numChars - ( $asciiEncoding ? $bytesRead : ( $bytesRead / 2 ) ); + $spos = $limitpos; + + while ( $charsLeft > 0 ) { + $opcode = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + $conlength = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + if ( $opcode !== 0x3c ) { + return - 1; + } + $spos += 4; + $limitpos = $spos + $conlength; + $option = ord( $this->data[ $spos ] ); + $spos ++; + if ( $asciiEncoding && ( $option === 0 ) ) { + $len = min( $charsLeft, $limitpos - $spos ); // min($charsLeft, $conlength); + $retstr .= $this->_substr( $this->data, $spos, $len ); + $charsLeft -= $len; + $asciiEncoding = true; + } elseif ( ! $asciiEncoding && ( $option !== 0 ) ) { + $len = min( $charsLeft * 2, $limitpos - $spos ); // min($charsLeft, $conlength); + $retstr .= $this->_substr( $this->data, $spos, $len ); + $charsLeft -= $len / 2; + $asciiEncoding = false; + } elseif ( ! $asciiEncoding && ( $option === 0 ) ) { + // Bummer - the string starts off as Unicode, but after the + // continuation it is in straightforward ASCII encoding + $len = min( $charsLeft, $limitpos - $spos ); // min($charsLeft, $conlength); + for ( $j = 0; $j < $len; $j ++ ) { + $retstr .= $this->data[ $spos + $j ] . chr( 0 ); + } + $charsLeft -= $len; + $asciiEncoding = false; + } else { + $newstr = ''; + for ( $j = 0, $len_retstr = $this->_strlen( $retstr ); $j < $len_retstr; $j ++ ) { + $newstr = $retstr[ $j ] . chr( 0 ); + } + $retstr = $newstr; + $len = min( $charsLeft * 2, $limitpos - $spos ); // min($charsLeft, $conlength); + $retstr .= $this->_substr( $this->data, $spos, $len ); + $charsLeft -= $len / 2; + $asciiEncoding = false; + //echo "Izavrat\n"; + } + $spos += $len; + + } + } + $retstr = $asciiEncoding ? $retstr : $this->_encodeUTF16( $retstr ); +// echo "Str $i = $retstr\n"; + if ( $richString ) { + $spos += 4 * $formattingRuns; + } + + // For extended strings, skip over the extended string data + if ( $extendedString ) { + $spos += $extendedRunLength; + } + //if ($retstr == 'Derby'){ + // echo "bb\n"; + //} + $this->sst[] = $retstr; + } + /*$continueRecords = array(); + while ($this->getNextCode() == Type_CONTINUE) { + $continueRecords[] = &$this->nextRecord(); + } + //echo " 1 Type_SST\n"; + $this->shareStrings = new SSTRecord($r, $continueRecords); + //print_r($this->shareStrings->strings); + */ + // echo 'SST read: '.($time_end-$time_start)."\n"; + break; + + case self::TYPE_FILEPASS: + return false; + break; + case self::TYPE_NAME: + //echo "Type_NAME\n"; + break; + case self::TYPE_FORMAT: + $indexCode = ord( $this->data[ $pos + 4 ] ) | ord( $this->data[ $pos + 5 ] ) << 8; + + if ( $version === self::BIFF8 ) { + $numchars = ord( $this->data[ $pos + 6 ] ) | ord( $this->data[ $pos + 7 ] ) << 8; + if ( ord( $this->data[ $pos + 8 ] ) === 0 ) { + $formatString = $this->_substr( $this->data, $pos + 9, $numchars ); + } else { + $formatString = $this->_substr( $this->data, $pos + 9, $numchars * 2 ); + } + } else { + $numchars = ord( $this->data[ $pos + 6 ] ); + $formatString = $this->_substr( $this->data, $pos + 7, $numchars * 2 ); + } + + $this->formatRecords[ $indexCode ] = $formatString; + // echo "Type.FORMAT\n"; + break; + case self::TYPE_XF: + $formatstr = ''; + //global $dateFormats, $numberFormats; + $indexCode = ord( $this->data[ $pos + 6 ] ) | ord( $this->data[ $pos + 7 ] ) << 8; + //echo "\nType.XF ".count($this->formatRecords['xfrecords'])." $indexCode "; + if ( array_key_exists( $indexCode, $this->dateFormats ) ) { + //echo "isdate ".$dateFormats[$indexCode]; + $this->formatRecords['xfrecords'][] = array( + 'type' => 'date', + 'format' => $this->dateFormats[ $indexCode ] + ); + } elseif ( array_key_exists( $indexCode, $this->numberFormats ) ) { + //echo "isnumber ".$this->numberFormats[$indexCode]; + $this->formatRecords['xfrecords'][] = array( + 'type' => 'number', + 'format' => $this->numberFormats[ $indexCode ] + ); + } else { + $isdate = false; + if ( $indexCode > 0 ) { + if ( isset( $this->formatRecords[ $indexCode ] ) ) { + $formatstr = $this->formatRecords[ $indexCode ]; + } + //echo '.other.'; +// echo "\ndate-time=$formatstr=\n"; + if ( $formatstr && preg_match( "/^[hmsday\/\-:\s]+$/i", $formatstr ) === 1 ) { // found day and time format + $isdate = true; + $formatstr = str_replace( array( 'mm', 'h' ), array( 'i', 'H' ), $formatstr ); + //echo "\ndate-time $formatstr \n"; + } + } + + if ( $isdate ) { + $this->formatRecords['xfrecords'][] = array( + 'type' => 'date', + 'format' => $formatstr, + ); + } else { + $this->formatRecords['xfrecords'][] = array( + 'type' => 'other', + 'format' => '', + 'code' => $indexCode + ); + } + } + //echo "\n"; + break; + case self::TYPE_NINETEENFOUR: + //echo "Type.NINETEENFOUR\n"; + $this->nineteenFour = ( ord( $this->data[ $pos + 4 ] ) === 1 ); + break; + case self::TYPE_BOUNDSHEET: + //echo "Type.BOUNDSHEET\n"; + $rec_offset = $this->_GetInt4d( $this->data, $pos + 4 ); +// $rec_typeFlag = ord($this->_data[$pos + 8]); +// $rec_visibilityFlag = ord($this->_data[$pos + 9]); + $rec_length = ord( $this->data[ $pos + 10 ] ); + $rec_name = ''; + if ( $version === self::BIFF8 ) { + $chartype = ord( $this->data[ $pos + 11 ] ); + if ( $chartype === 0 ) { + $rec_name = $this->_substr( $this->data, $pos + 12, $rec_length ); + } else { + $rec_name = $this->_encodeUTF16( $this->_substr( $this->data, $pos + 12, $rec_length * 2 ) ); + } + } elseif ( $version === self::BIFF7 ) { + $rec_name = $this->_substr( $this->data, $pos + 11, $rec_length ); + } + $this->boundsheets[] = array( + 'name' => $rec_name, + 'offset' => $rec_offset + ); + + break; + + } + + //echo "Code = ".base_convert($r['code'],10,16)."\n"; + $pos += $length + 4; + $code = ord( $this->data[ $pos ] ) | ord( $this->data[ $pos + 1 ] ) << 8; + $length = ord( $this->data[ $pos + 2 ] ) | ord( $this->data[ $pos + 3 ] ) << 8; + + //$r = &$this->nextRecord(); + //echo "1 Code = ".base_convert($r['code'],10,16)."\n"; + } + + foreach ( $this->boundsheets as $key => $val ) { + $this->sn = $key; + $this->_parsesheet( $val['offset'] ); + } + + return true; + + } + + public function _encodeUTF16( $string ) { + $result = $string; + if ( $this->defaultEncoding ) { + $result = mb_convert_encoding( $string, $this->defaultEncoding, 'UTF-16LE' ); + } + + return $result; + } + + public function _parsesheet( $spos ) { + $cont = true; + // read BOF +// $code = ord($this->_data[$spos]) | ord($this->_data[$spos + 1]) << 8; + $length = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + + $version = ord( $this->data[ $spos + 4 ] ) | ord( $this->data[ $spos + 5 ] ) << 8; + $substreamType = ord( $this->data[ $spos + 6 ] ) | ord( $this->data[ $spos + 7 ] ) << 8; + + if ( ( $version !== self::BIFF8 ) && ( $version !== self::BIFF7 ) ) { + return - 1; + } + + if ( $substreamType !== self::WORKSHEET ) { + return - 2; + } + //echo "Start parse code=".base_convert($code,10,16)." version=".base_convert($version,10,16)." substreamType=".base_convert($substreamType,10,16).""."\n"; + $spos += $length + 4; + //var_dump($this->formatRecords); + //echo "code $code $length"; + while ( $cont ) { + //echo "mem= ".memory_get_usage()."\n"; +// $r = &$this->file->nextRecord(); + $lowcode = ord( $this->data[ $spos ] ); + if ( $lowcode === self::TYPE_EOF ) { + break; + } + $code = $lowcode | ord( $this->data[ $spos + 1 ] ) << 8; + $length = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + $spos += 4; + $this->sheets[ $this->sn ]['maxrow'] = $this->rowoffset - 1; + $this->sheets[ $this->sn ]['maxcol'] = $this->colOffset - 1; + //echo "Code=".base_convert($code,10,16)." $code\n"; + unset( $this->recType ); + $this->multiplier = 1; // need for format with % + switch ( $code ) { + case self::TYPE_DIMENSION: + //echo 'Type_DIMENSION '; + if ( ! isset( $this->numRows ) ) { + if ( ( $length === 10 ) || ( $version === self::BIFF7 ) ) { + $this->sheets[ $this->sn ]['numRows'] = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + $this->sheets[ $this->sn ]['numCols'] = ord( $this->data[ $spos + 6 ] ) | ord( $this->data[ $spos + 7 ] ) << 8; + } else { + $this->sheets[ $this->sn ]['numRows'] = ord( $this->data[ $spos + 4 ] ) | ord( $this->data[ $spos + 5 ] ) << 8; + $this->sheets[ $this->sn ]['numCols'] = ord( $this->data[ $spos + 10 ] ) | ord( $this->data[ $spos + 11 ] ) << 8; + } + } + //echo 'numRows '.$this->numRows.' '.$this->numCols."\n"; + break; + case self::TYPE_MERGEDCELLS: + $cellRanges = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + for ( $i = 0; $i < $cellRanges; $i ++ ) { + $fr = ord( $this->data[ $spos + 8 * $i + 2 ] ) | ord( $this->data[ $spos + 8 * $i + 3 ] ) << 8; + $lr = ord( $this->data[ $spos + 8 * $i + 4 ] ) | ord( $this->data[ $spos + 8 * $i + 5 ] ) << 8; + $fc = ord( $this->data[ $spos + 8 * $i + 6 ] ) | ord( $this->data[ $spos + 8 * $i + 7 ] ) << 8; + $lc = ord( $this->data[ $spos + 8 * $i + 8 ] ) | ord( $this->data[ $spos + 8 * $i + 9 ] ) << 8; + //$this->sheets[$this->sn]['mergedCells'][] = array($fr + 1, $fc + 1, $lr + 1, $lc + 1); + if ( $lr - $fr > 0 ) { + $this->sheets[ $this->sn ]['cellsInfo'][ $fr + 1 ][ $fc + 1 ]['rowspan'] = $lr - $fr + 1; + } + if ( $lc - $fc > 0 ) { + $this->sheets[ $this->sn ]['cellsInfo'][ $fr + 1 ][ $fc + 1 ]['colspan'] = $lc - $fc + 1; + } + } + //echo "Merged Cells $cellRanges $lr $fr $lc $fc\n"; + break; + case self::TYPE_RK: + case self::TYPE_RK2: + //echo 'self::TYPE_RK'."\n"; + $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + $rknum = $this->_GetInt4d( $this->data, $spos + 6 ); + $numValue = $this->_GetIEEE754( $rknum ); + //echo $numValue." "; + if ( $this->isDate( $spos ) ) { + list( $string, $raw ) = $this->createDate( $numValue ); + } else { + $raw = $numValue; + if ( isset( $this->columnsFormat[ $column + 1 ] ) ) { + $this->curFormat = $this->columnsFormat[ $column + 1 ]; + } + $string = sprintf( $this->curFormat, $numValue * $this->multiplier ); + //$this->addcell(RKRecord($r)); + } + $this->addcell( $row, $column, $string, $raw ); + //echo "Type_RK $row $column $string $raw {$this->curformat}\n"; + break; + case self::TYPE_LABELSST: + $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; +// $xfindex = ord($this->_data[$spos + 4]) | ord($this->_data[$spos + 5]) << 8; + $index = $this->_GetInt4d( $this->data, $spos + 6 ); + //var_dump($this->sst); + $this->addcell( $row, $column, $this->sst[ $index ] ); + //echo "LabelSST $row $column $string\n"; + break; + case self::TYPE_MULRK: + $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + $colFirst = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + $colLast = ord( $this->data[ $spos + $length - 2 ] ) | ord( $this->data[ $spos + $length - 1 ] ) << 8; + $columns = $colLast - $colFirst + 1; + $tmppos = $spos + 4; + for ( $i = 0; $i < $columns; $i ++ ) { + $numValue = $this->_GetIEEE754( $this->_GetInt4d( $this->data, $tmppos + 2 ) ); + if ( $this->isDate( $tmppos - 4 ) ) { + list( $string, $raw ) = $this->createDate( $numValue ); + } else { + $raw = $numValue; + if ( isset( $this->columnsFormat[ $colFirst + $i + 1 ] ) ) { + $this->curFormat = $this->columnsFormat[ $colFirst + $i + 1 ]; + } + $string = sprintf( $this->curFormat, $numValue * $this->multiplier ); + } + //$rec['rknumbers'][$i]['xfindex'] = ord($rec['data'][$pos]) | ord($rec['data'][$pos+1]) << 8; + $tmppos += 6; + $this->addcell( $row, $colFirst + $i, $string, $raw ); + //echo "MULRK $row ".($colFirst + $i)." $string\n"; + } + //MulRKRecord($r); + // Get the individual cell records from the multiple record + //$num = ; + + break; + case self::TYPE_NUMBER: + $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + $tmp = unpack( 'ddouble', $this->_substr( $this->data, $spos + 6, 8 ) ); // It machine machine dependent + if ( $this->isDate( $spos ) ) { + list( $string, $raw ) = $this->createDate( $tmp['double'] ); + // $this->addcell(DateRecord($r, 1)); + } else { + //$raw = $tmp['']; + if ( isset( $this->columnsFormat[ $column + 1 ] ) ) { + $this->curFormat = $this->columnsFormat[ $column + 1 ]; + } + $raw = $this->createNumber( $spos ); + $string = sprintf( $this->curFormat, $raw * $this->multiplier ); + + // $this->addcell(NumberRecord($r)); + } + $this->addcell( $row, $column, $string, $raw ); + //echo "Number $row $column $string\n"; + break; + case self::TYPE_FORMULA: + case self::TYPE_FORMULA2: + $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + /* + $byte6 = ord($this->_data[$spos + 6]); + $byte12 = ord($this->_data[$spos + 12]); + $byte13 = ord($this->_data[$spos + 13]); + + if ( $byte6 === 0 && $byte12 === 255 && $byte13 === 255 ) { + //String formula. Result follows in a STRING record + //echo "FORMULA $row $column Formula with a string
\n"; + } else if ($byte6 === 1 && $byte12 === 255 && $byte13 === 255 ) { + //Boolean formula. Result is in +2; 0=false,1=true + } else if ($byte6 === 2 && $byte12 === 255 && $byte13 === 255) { + //Error formula. Error code is in +2; + } else if ( $byte6 === 3 && $byte12 === 255 && $byte13 === 255) { + //Formula result is a null string. + */ + if ( ! ( ord( $this->data[ $spos + 6 ] ) < 4 && ord( $this->data[ $spos + 12 ] ) === 255 && ord( $this->data[ $spos + 13 ] ) === 255 ) ) { + // result is a number, so first 14 bytes are just like a _NUMBER record + $tmp = unpack( 'ddouble', $this->_substr( $this->data, $spos + 6, 8 ) ); // It machine machine dependent + if ( $this->isDate( $spos ) ) { + list( $string, $raw ) = $this->createDate( $tmp['double'] ); + // $this->addcell(DateRecord($r, 1)); + } else { + //$raw = $tmp['']; + if ( isset( $this->columnsFormat[ $column + 1 ] ) ) { + $this->curFormat = $this->columnsFormat[ $column + 1 ]; + } + $raw = $this->createNumber( $spos ); + $string = sprintf( $this->curFormat, $raw * $this->multiplier ); + + // $this->addcell(NumberRecord($r)); + } + $this->addcell( $row, $column, $string, $raw ); + //echo "Number $row $column $string\n"; + } + break; + case self::TYPE_BOOLERR: + $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + $string = ord( $this->data[ $spos + 6 ] ); + $this->addcell( $row, $column, $string ); + //echo 'Type_BOOLERR '."\n"; + break; + case self::TYPE_ROW: + case self::TYPE_DBCELL: + case self::TYPE_MULBLANK: + break; + case self::TYPE_LABEL: + $row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8; + $column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8; + $this->addcell( $row, $column, $this->_substr( $this->data, $spos + 8, ord( $this->data[ $spos + 6 ] ) | ord( $this->data[ $spos + 7 ] ) << 8 ) ); + + // $this->addcell(LabelRecord($r)); + break; + + case self::TYPE_EOF: + $cont = false; + break; + default: + //echo ' unknown :'.base_convert($r['code'],10,16)."\n"; + break; + + } + $spos += $length; + } + + if ( ! isset( $this->sheets[ $this->sn ]['numRows'] ) ) { + $this->sheets[ $this->sn ]['numRows'] = $this->sheets[ $this->sn ]['maxrow']; + } + if ( ! isset( $this->sheets[ $this->sn ]['numCols'] ) ) { + $this->sheets[ $this->sn ]['numCols'] = $this->sheets[ $this->sn ]['maxcol']; + } + + return true; + } + + //}}} + //{{{ createDate() + + public function _GetIEEE754( $rknum ) { + if ( ( $rknum & 0x02 ) !== 0 ) { + $value = $rknum >> 2; + } else { +//mmp +// first comment out the previously existing 7 lines of code here +// $tmp = unpack("d", pack("VV", 0, ($rknum & 0xfffffffc))); +// //$value = $tmp['']; +// if (array_key_exists(1, $tmp)) { +// $value = $tmp[1]; +// } else { +// $value = $tmp['']; +// } +// I got my info on IEEE754 encoding from +// http://research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html +// The RK format calls for using only the most significant 30 bits of the +// 64 bit floating point value. The other 34 bits are assumed to be 0 +// So, we use the upper 30 bits of $rknum as follows... + $sign = ( $rknum & 0x80000000 ) >> 31; + $exp = ( $rknum & 0x7ff00000 ) >> 20; + $mantissa = ( 0x100000 | ( $rknum & 0x000ffffc ) ); + $value = $mantissa / pow( 2, 20 - ( $exp - 1023 ) ); + if ( $sign ) { + $value = - 1 * $value; + } +//end of changes by mmp + + } + + if ( ( $rknum & 0x01 ) !== 0 ) { + $value /= 100; + } + + return $value; + } + + public function isDate( $spos ) { + //$xfindex = GetInt2d(, 4); + $xfindex = ord( $this->data[ $spos + 4 ] ) | ord( $this->data[ $spos + 5 ] ) << 8; + //echo 'check is date '.$xfindex.' '.$this->formatRecords['xfrecords'][$xfindex]['type']."\n"; + //var_dump($this->formatRecords['xfrecords'][$xfindex]); + if ( $this->formatRecords['xfrecords'][ $xfindex ]['type'] === 'date' ) { + $this->curFormat = $this->formatRecords['xfrecords'][ $xfindex ]['format']; + $this->recType = 'date'; + + return true; + } + + if ( $this->formatRecords['xfrecords'][ $xfindex ]['type'] === 'number' ) { + $this->curFormat = $this->formatRecords['xfrecords'][ $xfindex ]['format']; + $this->recType = 'number'; + if ( ( $xfindex === 0x9 ) || ( $xfindex === 0xa ) ) { + $this->multiplier = 100; + } + } else { + $this->curFormat = $this->defaultFormat; + $this->recType = 'unknown'; + } + + return false; + } + + /** + * Convert the raw Excel date into a human readable format + * + * Dates in Excel are stored as number of seconds from an epoch. On + * Windows, the epoch is 30/12/1899 and on Mac it's 01/01/1904 + * + * @param integer $timevalue The raw Excel value to convert + * + * @return array First element is the converted date, the second element is number a unix timestamp + */ + public function createDate( $timevalue ) { +// $offset = ($timeoffset===null)? date('Z') : $timeoffset * 3600; + if ($timevalue > 1) { + $timevalue -= ( $this->nineteenFour ? 24107 : 25569 ); + } + $ts = round($timevalue * 24 * 3600); + $string = $this->datetimeFormat ? gmdate( $this->datetimeFormat, $ts ) : gmdate( $this->curFormat, $ts ); + return array( $string, $ts ); + + } + + public function addcell( $row, $col, $string, $raw = '' ) { + //echo "ADD cel $row-$col $string\n"; + $this->sheets[ $this->sn ]['maxrow'] = max( $this->sheets[ $this->sn ]['maxrow'], $row + $this->rowoffset ); + $this->sheets[ $this->sn ]['maxcol'] = max( $this->sheets[ $this->sn ]['maxcol'], $col + $this->colOffset ); + $this->sheets[ $this->sn ]['cells'][ $row + $this->rowoffset ][ $col + $this->colOffset ] = $string; + if ( $raw ) { + $this->sheets[ $this->sn ]['cellsInfo'][ $row + $this->rowoffset ][ $col + $this->colOffset ]['raw'] = $raw; + } + if ( isset( $this->recType ) ) { + $this->sheets[ $this->sn ]['cellsInfo'][ $row + $this->rowoffset ][ $col + $this->colOffset ]['type'] = $this->recType; + } + + } + + public function createNumber( $spos ) { + $rknumhigh = $this->_GetInt4d( $this->data, $spos + 10 ); + $rknumlow = $this->_GetInt4d( $this->data, $spos + 6 ); + //for ($i=0; $i<8; $i++) { echo ord($this->_data[$i+$spos+6]) . " "; } echo "
"; + $sign = ( $rknumhigh & 0x80000000 ) >> 31; + $exp = ( $rknumhigh & 0x7ff00000 ) >> 20; + $mantissa = ( 0x100000 | ( $rknumhigh & 0x000fffff ) ); + $mantissalow1 = ( $rknumlow & 0x80000000 ) >> 31; + $mantissalow2 = ( $rknumlow & 0x7fffffff ); + $value = $mantissa / pow( 2, 20 - ( $exp - 1023 ) ); + if ( $mantissalow1 !== 0 ) { + $value += 1 / pow( 2, 21 - ( $exp - 1023 ) ); + } + $value += $mantissalow2 / pow( 2, 52 - ( $exp - 1023 ) ); + //echo "Sign = $sign, Exp = $exp, mantissahighx = $mantissa, mantissalow1 = $mantissalow1, mantissalow2 = $mantissalow2
\n"; + if ( $sign ) { + $value = - 1 * $value; + } + + return $value; + } + + /** + * Set the encoding method + * + * @param string $encoding Encoding to use + * + * @access public + */ + public function setOutputEncoding( $encoding ) { + $this->defaultEncoding = $encoding; + } + + public function setRowColOffset( $iOffset ) { + $this->rowoffset = $iOffset; + $this->colOffset = $iOffset; + } + + /** + * Set the default number format + * + * @access public + * + * @param string $sFormat Default format + */ + public function setDefaultFormat( $sFormat ) { + $this->defaultFormat = $sFormat; + } + + /** + * Force a column to use a certain format + * + * @access public + * + * @param integer $column Column number + * @param string $sFormat Format + */ + public function setColumnFormat( $column, $sFormat ) { + $this->columnsFormat[ $column ] = $sFormat; + } + private function _strlen( $str ) { + return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str , '8bit') : strlen($str); + } + private function _strpos( $haystack, $needle, $offset = 0 ) { + return (ini_get('mbstring.func_overload') & 2) ? mb_strpos( $haystack, $needle, $offset , '8bit') : strpos($haystack, $needle, $offset); + } + private function _substr( $str, $start, $length = null ) { + return (ini_get('mbstring.func_overload') & 2) ? mb_substr( $str, $start, ($length === null) ? mb_strlen($str,'8bit') : $length, '8bit') : substr($str, $start, ($length === null) ? strlen($str) : $length ); + } +} +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ \ No newline at end of file diff --git a/modules/import_api/lib/SimpleXLSX.php b/modules/import_api/lib/SimpleXLSX.php new file mode 100644 index 00000000..661e39e1 --- /dev/null +++ b/modules/import_api/lib/SimpleXLSX.php @@ -0,0 +1,1056 @@ +rows() ); + * } else { + * echo SimpleXLSX::parseError(); + * } + * + * Example 2: html table + * if ( $xlsx = SimpleXLSX::parse('book.xlsx') ) { + * echo $xlsx->toHTML(); + * } else { + * echo SimpleXLSX::parseError(); + * } + * + * Example 3: rowsEx + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * print_r( $xlsx->rowsEx() ); + * + * Example 4: select worksheet + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * print_r( $xlsx->rows(1) ); // second worksheet + * + * Example 5: IDs and worksheet names + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * print_r( $xlsx->sheetNames() ); // array( 0 => 'Sheet 1', 1 => 'Catalog' ); + * + * Example 6: get sheet name by index + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * echo 'Sheet Name 2 = '.$xlsx->sheetName(1); + * + * Example 7: getCell (very slow) + * echo $xlsx->getCell(1,'D12'); // reads D12 cell from second sheet + * + * Example 8: read data + * if ( $xlsx = SimpleXLSX::parseData( file_get_contents('http://www.example.com/example.xlsx') ) ) { + * $dim = $xlsx->dimension(1); + * $num_cols = $dim[0]; + * $num_rows = $dim[1]; + * echo $xlsx->sheetName(1).':'.$num_cols.'x'.$num_rows; + * } else { + * echo SimpleXLSX::parseError(); + * } + * + * Example 9: old style + * $xlsx = new SimpleXLSX('book.xlsx'); + * if ( $xlsx->success() ) { + * print_r( $xlsx->rows() ); + * } else { + * echo 'xlsx error: '.$xlsx->error(); + * } + */ + +/** @noinspection PhpUndefinedFieldInspection */ +/** @noinspection PhpComposerExtensionStubsInspection */ +/** @noinspection MultiAssignmentUsageInspection */ + +class SimpleXLSX { + // Don't remove this string! Created by Sergey Shuchkin sergey.shuchkin@gmail.com + public static $CF = [ // Cell formats + 0 => 'General', + 1 => '0', + 2 => '0.00', + 3 => '#,##0', + 4 => '#,##0.00', + 9 => '0%', + 10 => '0.00%', + 11 => '0.00E+00', + 12 => '# ?/?', + 13 => '# ??/??', + 14 => 'mm-dd-yy', + 15 => 'd-mmm-yy', + 16 => 'd-mmm', + 17 => 'mmm-yy', + 18 => 'h:mm AM/PM', + 19 => 'h:mm:ss AM/PM', + 20 => 'h:mm', + 21 => 'h:mm:ss', + 22 => 'm/d/yy h:mm', + + 37 => '#,##0 ;(#,##0)', + 38 => '#,##0 ;[Red](#,##0)', + 39 => '#,##0.00;(#,##0.00)', + 40 => '#,##0.00;[Red](#,##0.00)', + + 44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)', + 45 => 'mm:ss', + 46 => '[h]:mm:ss', + 47 => 'mmss.0', + 48 => '##0.0E+0', + 49 => '@', + + 27 => '[$-404]e/m/d', + 30 => 'm/d/yy', + 36 => '[$-404]e/m/d', + 50 => '[$-404]e/m/d', + 57 => '[$-404]e/m/d', + + 59 => 't0', + 60 => 't0.00', + 61 => 't#,##0', + 62 => 't#,##0.00', + 67 => 't0%', + 68 => 't0.00%', + 69 => 't# ?/?', + 70 => 't# ??/??', + ]; + public $cellFormats = []; + public $datetimeFormat = 'Y-m-d H:i:s'; + public $debug; + + /* @var SimpleXMLElement[] $sheets */ + private $sheets; + private $sheetNames = []; + // scheme + private $styles; + private $hyperlinks; + /* @var array[] $package */ + private $package; + private $sharedstrings; + private $date1904 = 0; + + + /* + private $date_formats = array( + 0xe => "d/m/Y", + 0xf => "d-M-Y", + 0x10 => "d-M", + 0x11 => "M-Y", + 0x12 => "h:i a", + 0x13 => "h:i:s a", + 0x14 => "H:i", + 0x15 => "H:i:s", + 0x16 => "d/m/Y H:i", + 0x2d => "i:s", + 0x2e => "H:i:s", + 0x2f => "i:s.S" + ); + private $number_formats = array( + 0x1 => "%1.0f", // "0" + 0x2 => "%1.2f", // "0.00", + 0x3 => "%1.0f", //"#,##0", + 0x4 => "%1.2f", //"#,##0.00", + 0x5 => "%1.0f", //"$#,##0;($#,##0)", + 0x6 => '$%1.0f', //"$#,##0;($#,##0)", + 0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x9 => '%1.0f%%', //"0%" + 0xa => '%1.2f%%', //"0.00%" + 0xb => '%1.2f', //"0.00E00", + 0x25 => '%1.0f', //"#,##0;(#,##0)", + 0x26 => '%1.0f', //"#,##0;(#,##0)", + 0x27 => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x28 => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x29 => '%1.0f', //"#,##0;(#,##0)", + 0x2a => '$%1.0f', //"$#,##0;($#,##0)", + 0x2b => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x30 => '%1.0f'); //"##0.0E0"; + // }}} + */ + private $errno = 0; + private $error = false; + + + public function __construct( $filename = null, $is_data = null, $debug = null ) { + if ( $debug !== null ) { + $this->debug = $debug; + } + $this->package = [ + 'filename' => '', + 'mtime' => 0, + 'size' => 0, + 'comment' => '', + 'entries' => [] + ]; + if ( $filename && $this->_unzip( $filename, $is_data ) ) { + $this->_parse(); + } + } + + public static function parseFile( $filename, $debug = false ) { + return self::parse( $filename, false, $debug ); + } + + public static function parseData( $data, $debug = false ) { + return self::parse( $data, true, $debug ); + } + + public static function parse( $filename, $is_data = false, $debug = false ) { + $xlsx = new self(); + $xlsx->debug = $debug; + if ( $xlsx->_unzip( $filename, $is_data ) ) { + $xlsx->_parse(); + } + if ( $xlsx->success() ) { + return $xlsx; + } + self::parseError( $xlsx->error() ); + self::parseErrno( $xlsx->errno() ); + + return false; + } + + public static function parseError( $set = false ) { + static $error = false; + + return $set ? $error = $set : $error; + } + + public static function parseErrno( $set = false ) { + static $errno = false; + + return $set ? $errno = $set : $errno; + } + + private function _unzip( $filename, $is_data = false ) { + + if ( $is_data ) { + + $this->package['filename'] = 'default.xlsx'; + $this->package['mtime'] = time(); + $this->package['size'] = $this->_strlen( $filename ); + + $vZ = $filename; + } else { + + if ( ! is_readable( $filename ) ) { + $this->error( 1, 'File not found ' . $filename ); + + return false; + } + + // Package information + $this->package['filename'] = $filename; + $this->package['mtime'] = filemtime( $filename ); + $this->package['size'] = filesize( $filename ); + + // Read file + $vZ = file_get_contents( $filename ); + } + // Cut end of central directory + /* $aE = explode("\x50\x4b\x05\x06", $vZ); + + if (count($aE) == 1) { + $this->error('Unknown format'); + return false; + } + */ + // Explode to each part + $aE = explode( "\x50\x4b\x03\x04", $vZ ); + array_shift( $aE ); + + $aEL = count( $aE ); + if ( $aEL === 0 ) { + $this->error( 2, 'Unknown archive format' ); + + return false; + } + // Search central directory end record + $last = $aE[ $aEL - 1 ]; + $last = explode( "\x50\x4b\x05\x06", $last ); + if ( count( $last ) !== 2 ) { + $this->error( 2, 'Unknown archive format' ); + + return false; + } + // Search central directory + $last = explode( "\x50\x4b\x01\x02", $last[0] ); + if ( count( $last ) < 2 ) { + $this->error( 2, 'Unknown archive format' ); + + return false; + } + $aE[ $aEL - 1 ] = $last[0]; + + // Loop through the entries + foreach ( $aE as $vZ ) { + $aI = []; + $aI['E'] = 0; + $aI['EM'] = ''; + // Retrieving local file header information +// $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL', $vZ); + $aP = unpack( 'v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL/v1EFL', $vZ ); + + // Check if data is encrypted +// $bE = ($aP['GPF'] && 0x0001) ? TRUE : FALSE; + $bE = false; + $nF = $aP['FNL']; + $mF = $aP['EFL']; + + // Special case : value block after the compressed data + if ( $aP['GPF'] & 0x0008 ) { + $aP1 = unpack( 'V1CRC/V1CS/V1UCS', $this->_substr( $vZ, - 12 ) ); + + $aP['CRC'] = $aP1['CRC']; + $aP['CS'] = $aP1['CS']; + $aP['UCS'] = $aP1['UCS']; + // 2013-08-10 + $vZ = $this->_substr( $vZ, 0, - 12 ); + if ( $this->_substr( $vZ, - 4 ) === "\x50\x4b\x07\x08" ) { + $vZ = $this->_substr( $vZ, 0, - 4 ); + } + } + + // Getting stored filename + $aI['N'] = $this->_substr( $vZ, 26, $nF ); + $aI['N'] = str_replace( '\\', '/', $aI['N'] ); + + if ( $this->_substr( $aI['N'], - 1 ) === '/' ) { + // is a directory entry - will be skipped + continue; + } + + // Truncate full filename in path and filename + $aI['P'] = dirname( $aI['N'] ); + $aI['P'] = ( $aI['P'] === '.' ) ? '' : $aI['P']; + $aI['N'] = basename( $aI['N'] ); + + $vZ = $this->_substr( $vZ, 26 + $nF + $mF ); + + if ( $this->_strlen( $vZ ) !== (int) $aP['CS'] ) { // check only if availabled + $aI['E'] = 1; + $aI['EM'] = 'Compressed size is not equal with the value in header information.'; + } else if ( $bE ) { + $aI['E'] = 5; + $aI['EM'] = 'File is encrypted, which is not supported from this class.'; + } else { + switch ( $aP['CM'] ) { + case 0: // Stored + // Here is nothing to do, the file ist flat. + break; + case 8: // Deflated + $vZ = gzinflate( $vZ ); + break; + case 12: // BZIP2 + if ( extension_loaded( 'bz2' ) ) { + /** @noinspection PhpComposerExtensionStubsInspection */ + $vZ = bzdecompress( $vZ ); + } else { + $aI['E'] = 7; + $aI['EM'] = 'PHP BZIP2 extension not available.'; + } + break; + default: + $aI['E'] = 6; + $aI['EM'] = "De-/Compression method {$aP['CM']} is not supported."; + } + if ( ! $aI['E'] ) { + if ( $vZ === false ) { + $aI['E'] = 2; + $aI['EM'] = 'Decompression of data failed.'; + } else if ( $this->_strlen( $vZ ) !== (int) $aP['UCS'] ) { + $aI['E'] = 3; + $aI['EM'] = 'Uncompressed size is not equal with the value in header information.'; + } else if ( crc32( $vZ ) !== $aP['CRC'] ) { + $aI['E'] = 4; + $aI['EM'] = 'CRC32 checksum is not equal with the value in header information.'; + } + } + } + + $aI['D'] = $vZ; + + // DOS to UNIX timestamp + $aI['T'] = mktime( ( $aP['FT'] & 0xf800 ) >> 11, + ( $aP['FT'] & 0x07e0 ) >> 5, + ( $aP['FT'] & 0x001f ) << 1, + ( $aP['FD'] & 0x01e0 ) >> 5, + $aP['FD'] & 0x001f, + ( ( $aP['FD'] & 0xfe00 ) >> 9 ) + 1980 ); + + //$this->Entries[] = &new SimpleUnzipEntry($aI); + $this->package['entries'][] = [ + 'data' => $aI['D'], + 'error' => $aI['E'], + 'error_msg' => $aI['EM'], + 'name' => $aI['N'], + 'path' => $aI['P'], + 'time' => $aI['T'] + ]; + + } // end for each entries + + return true; + } + + // sheets numeration: 1,2,3.... + + public function error( $num = null, $str = null ) { + if ( $num ) { + $this->errno = $num; + $this->error = $str; + if ( $this->debug ) { + trigger_error( __CLASS__ . ': ' . $this->error, E_USER_WARNING ); + } + } + + return $this->error; + } + + public function errno() { + return $this->errno; + } + + private function _parse() { + // Document data holders + $this->sharedstrings = []; + $this->sheets = []; +// $this->styles = array(); + + // Read relations and search for officeDocument + if ( $relations = $this->getEntryXML( '_rels/.rels' ) ) { + + foreach ( $relations->Relationship as $rel ) { + + $rel_type = basename( trim( (string) $rel['Type'] ) ); // officeDocument + $rel_target = $this->_getTarget( '', (string) $rel['Target'] ); // /xl/workbook.xml or xl/workbook.xml + + if ( $rel_type === 'officeDocument' && $workbook = $this->getEntryXML( $rel_target ) ) { + + $index_rId = []; // [0 => rId1] + + $index = 0; + foreach ( $workbook->sheets->sheet as $s ) { + $this->sheetNames[ $index ] = (string) $s['name']; + $index_rId[ $index ] = (string) $s['id']; + $index ++; + } + if ( (int) $workbook->workbookPr['date1904'] === 1 ) { + $this->date1904 = 1; + } + + + if ( $workbookRelations = $this->getEntryXML( dirname( $rel_target ) . '/_rels/workbook.xml.rels' ) ) { + + // Loop relations for workbook and extract sheets... + foreach ( $workbookRelations->Relationship as $workbookRelation ) { + + $wrel_type = basename( trim( (string) $workbookRelation['Type'] ) ); + $wrel_path = $this->_getTarget( dirname( $rel_target ), (string) $workbookRelation['Target'] ); + if ( ! $this->entryExists( $wrel_path ) ) { + continue; + } + + + if ( $wrel_type === 'worksheet' ) { // Sheets + + if ( $sheet = $this->getEntryXML( $wrel_path ) ) { + $index = array_search( (string) $workbookRelation['Id'], $index_rId, false ); + $this->sheets[ $index ] = $sheet; + } + + } else if ( $wrel_type === 'sharedStrings' ) { + + if ( $sharedStrings = $this->getEntryXML( $wrel_path ) ) { + foreach ( $sharedStrings->si as $val ) { + if ( isset( $val->t ) ) { + $this->sharedstrings[] = (string) $val->t; + } elseif ( isset( $val->r ) ) { + $this->sharedstrings[] = $this->_parseRichText( $val ); + } + } + } + } else if ( $wrel_type === 'styles' ) { + + $this->styles = $this->getEntryXML( $wrel_path ); + + $nf = []; + if ( $this->styles->numFmts->numFmt !== null ) { + foreach ( $this->styles->numFmts->numFmt as $v ) { + $nf[ (int) $v['numFmtId'] ] = (string) $v['formatCode']; + } + } + + if ( $this->styles->cellXfs->xf !== null ) { + foreach ( $this->styles->cellXfs->xf as $v ) { + $v = (array) $v->attributes(); + $v['format'] = ''; + + if ( isset( $v['@attributes']['numFmtId'] ) ) { + $v = $v['@attributes']; + $fid = (int) $v['numFmtId']; + // formats priority + if ( isset( $nf[ $fid ] ) ) { + $v['format'] = $nf[ $fid ]; + } else if ( isset( self::$CF[ $fid ] ) ) { + $v['format'] = self::$CF[ $fid ]; + } + } + $this->cellFormats[] = $v; + } + } + } + } + + break; + } + } + } + } + if ( count( $this->sheets ) ) { + // Sort sheets + ksort( $this->sheets ); + + return true; + } + + return false; + } + + /* + * @param string $name Filename in archive + * @return SimpleXMLElement|bool + */ + public function getEntryXML( $name ) { + if ( $entry_xml = $this->getEntryData( $name ) ) { + $entry_xml = trim( $entry_xml ); + // dirty remove namespace prefixes and empty rows + $entry_xml = preg_replace( '/xmlns[^=]*="[^"]*"/i', '', $entry_xml ); // remove namespaces + $entry_xml = preg_replace( '/[a-zA-Z0-9]+:([a-zA-Z0-9]+="[^"]+")/', '$1$2', $entry_xml ); // remove namespaced attrs + $entry_xml = preg_replace( '/<[a-zA-Z0-9]+:([^>]+)>/', '<$1>', $entry_xml ); // fix namespaced openned tags + $entry_xml = preg_replace( '/<\/[a-zA-Z0-9]+:([^>]+)>/', '', $entry_xml ); // fix namespaced closed tags + +// if ( $this->skipEmptyRows && strpos($name, '/sheet') ) { + if ( strpos( $name, '/sheet' ) ) { // dirty skip empty rows + $entry_xml = preg_replace( '/]+>\s*(\s*)+<\/row>/', '', $entry_xml, - 1, $cnt ); // remove empty rows + $entry_xml = preg_replace( '/]*\/>/', '', $entry_xml, - 1, $cnt2 ); + $entry_xml = preg_replace( '/]*><\/row>/', '', $entry_xml, - 1, $cnt3 ); + if ( $cnt || $cnt2 || $cnt3 ) { + $entry_xml = preg_replace( '//', '', $entry_xml ); + } +// file_put_contents( basename( $name ), $entry_xml ); // @to do comment!!! + } + + // XML External Entity (XXE) Prevention + $_old = libxml_disable_entity_loader(); + $entry_xmlobj = simplexml_load_string( $entry_xml ); + + libxml_disable_entity_loader( $_old ); + if ( $entry_xmlobj ) { + return $entry_xmlobj; + } + $e = libxml_get_last_error(); + $this->error( 3, 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line ); + } else { + $this->error( 4, 'XML-entry not found ' . $name ); + } + + return false; + } + + public function getEntryData( $name ) { + $name = ltrim( str_replace( '\\', '/', $name ), '/' ); + $dir = $this->_strtoupper( dirname( $name ) ); + $name = $this->_strtoupper( basename( $name ) ); + foreach ( $this->package['entries'] as $entry ) { + if ( $this->_strtoupper( $entry['path'] ) === $dir && $this->_strtoupper( $entry['name'] ) === $name ) { + return $entry['data']; + } + } + $this->error( 5, 'Entry not found ' . ( $dir ? $dir . '/' : '' ) . $name ); + + return false; + } + + public function entryExists( $name ) { // 0.6.6 + $dir = $this->_strtoupper( dirname( $name ) ); + $name = $this->_strtoupper( basename( $name ) ); + foreach ( $this->package['entries'] as $entry ) { + if ( $this->_strtoupper( $entry['path'] ) === $dir && $this->_strtoupper( $entry['name'] ) === $name ) { + return true; + } + } + + return false; + } + + private function _parseRichText( $is = null ) { + $value = []; + + if ( isset( $is->t ) ) { + $value[] = (string) $is->t; + } else if ( isset( $is->r ) ) { + foreach ( $is->r as $run ) { + $value[] = (string) $run->t; + } + } + + return implode( '', $value ); + } + + public function success() { + return ! $this->error; + } + + public function rows( $worksheetIndex = 0 ) { + + if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) { + return false; + } + $dim = $this->dimension( $worksheetIndex ); + $numCols = $dim[0]; + $numRows = $dim[1]; + + $emptyRow = []; + for ( $i = 0; $i < $numCols; $i ++ ) { + $emptyRow[] = ''; + } + + $rows = []; + for ( $i = 0; $i < $numRows; $i ++ ) { + $rows[] = $emptyRow; + } + + $curR = 0; + /* @var SimpleXMLElement $ws */ + foreach ( $ws->sheetData->row as $row ) { + $curC = 0; + foreach ( $row->c as $c ) { + // detect skipped cols + $idx = $this->getIndex( (string) $c['r'] ); + $x = $idx[0]; + $y = $idx[1]; + + if ( $x > - 1 ) { + $curC = $x; + $curR = $y; + } + + $rows[ $curR ][ $curC ] = $this->value( $c ); + $curC ++; + } + + $curR ++; + } + + return $rows; + } + + public function rowsEx( $worksheetIndex = 0 ) { + + if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) { + return false; + } + + $rows = []; + + $dim = $this->dimension( $worksheetIndex ); + $numCols = $dim[0]; + $numRows = $dim[1]; + + for ( $y = 0; $y < $numRows; $y ++ ) { + for ( $x = 0; $x < $numCols; $x ++ ) { + // 0.6.8 + $c = ''; + for ( $k = $x; $k >= 0; $k = (int) ( $k / 26 ) - 1 ) { + $c = chr( $k % 26 + 65 ) . $c; + } + $rows[ $y ][ $x ] = [ + 'type' => '', + 'name' => $c . ( $y + 1 ), + 'value' => '', + 'href' => '', + 'f' => '', + 'format' => '', + 'r' => $y + ]; + } + } + + $curR = 0; + /* @var SimpleXMLElement $ws */ + foreach ( $ws->sheetData->row as $row ) { + + $r_idx = (int) $row['r']; + $curC = 0; + + foreach ( $row->c as $c ) { + $r = (string) $c['r']; + $t = (string) $c['t']; + $s = (int) $c['s']; + + $idx = $this->getIndex( $r ); + $x = $idx[0]; + $y = $idx[1]; + + if ( $x > - 1 ) { + $curC = $x; + $curR = $y; + } + + if ( $s > 0 && isset( $this->cellFormats[ $s ] ) ) { + $format = $this->cellFormats[ $s ]['format']; + } else { + $format = ''; + } + + $rows[ $curR ][ $curC ] = [ + 'type' => $t, + 'name' => (string) $c['r'], + 'value' => $this->value( $c ), + 'href' => $this->href( $c ), + 'f' => (string) $c->f, + 'format' => $format, + 'r' => $r_idx + ]; + $curC ++; + } + $curR ++; + } + + return $rows; + + } + + public function toHTML( $worksheetIndex = 0 ) { + $s = ''; + foreach ( $this->rows( $worksheetIndex ) as $r ) { + $s .= ''; + foreach ( $r as $c ) { + $s .= ''; + } + $s .= "\r\n"; + } + $s .= '
' . ( $c === '' ? ' ' : htmlspecialchars( $c, ENT_QUOTES ) ) . '
'; + + return $s; + } + + public function worksheet( $worksheetIndex = 0 ) { + + + if ( isset( $this->sheets[ $worksheetIndex ] ) ) { + $ws = $this->sheets[ $worksheetIndex ]; + + if ( isset( $ws->hyperlinks ) ) { + $this->hyperlinks = []; + foreach ( $ws->hyperlinks->hyperlink as $hyperlink ) { + $this->hyperlinks[ (string) $hyperlink['ref'] ] = (string) $hyperlink['display']; + } + } + + return $ws; + } + $this->error( 6, 'Worksheet not found ' . $worksheetIndex ); + + return false; + } + + /** + * returns [numCols,numRows] of worksheet + * + * @param int $worksheetIndex + * + * @return array + */ + public function dimension( $worksheetIndex = 0 ) { + + if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) { + return [ 0, 0 ]; + } + /* @var SimpleXMLElement $ws */ + + $ref = (string) $ws->dimension['ref']; + + if ( $this->_strpos( $ref, ':' ) !== false ) { + $d = explode( ':', $ref ); + $idx = $this->getIndex( $d[1] ); + + return [ $idx[0] + 1, $idx[1] + 1 ]; + } + if ( $ref !== '' ) { // 0.6.8 + $index = $this->getIndex( $ref ); + + return [ $index[0] + 1, $index[1] + 1 ]; + } + + // slow method + $maxC = $maxR = 0; + foreach ( $ws->sheetData->row as $row ) { + foreach ( $row->c as $c ) { + $idx = $this->getIndex( (string) $c['r'] ); + $x = $idx[0]; + $y = $idx[1]; + if ( $x > 0 ) { + if ( $x > $maxC ) { + $maxC = $x; + } + if ( $y > $maxR ) { + $maxR = $y; + } + } + } + } + + return [ $maxC + 1, $maxR + 1 ]; + } + + public function getIndex( $cell = 'A1' ) { + + if ( preg_match( '/([A-Z]+)(\d+)/', $cell, $m ) ) { + $col = $m[1]; + $row = $m[2]; + + $colLen = $this->_strlen( $col ); + $index = 0; + + for ( $i = $colLen - 1; $i >= 0; $i -- ) { + /** @noinspection PowerOperatorCanBeUsedInspection */ + $index += ( ord( $col[ $i ] ) - 64 ) * pow( 26, $colLen - $i - 1 ); + } + + return [ $index - 1, $row - 1 ]; + } + +// $this->error( 'Invalid cell index ' . $cell ); + + return [ - 1, - 1 ]; + } + + public function value( $cell ) { + // Determine data type + $dataType = (string) $cell['t']; + + if ( $dataType === '' || $dataType === 'n' ) { // number + $s = (int) $cell['s']; + if ( $s > 0 && isset( $this->cellFormats[ $s ] ) ) { + $format = $this->cellFormats[ $s ]['format']; + if ( preg_match( '/[mM]/', $format ) ) { // [m]onth + $dataType = 'd'; + } + } + } + + $value = ''; + + switch ( $dataType ) { + case 's': + // Value is a shared string + if ( (string) $cell->v !== '' ) { + $value = $this->sharedstrings[ (int) $cell->v ]; + } + + break; + + case 'b': + // Value is boolean + $value = (string) $cell->v; + if ( $value === '0' ) { + $value = false; + } else if ( $value === '1' ) { + $value = true; + } else { + $value = (bool) $cell->v; + } + + break; + + case 'inlineStr': + // Value is rich text inline + $value = $this->_parseRichText( $cell->is ); + + break; + + case 'e': + // Value is an error message + if ( (string) $cell->v !== '' ) { + $value = (string) $cell->v; + } + + break; + case 'd': + // Value is a date and non-empty + if ( ! empty( $cell->v ) ) { + $value = $this->datetimeFormat ? gmdate( $this->datetimeFormat, $this->unixstamp( (float) $cell->v ) ) : (float) $cell->v; + } + break; + + + default: + // Value is a string + $value = (string) $cell->v; + + // Check for numeric values + if ( is_numeric( $value ) && $dataType !== 's' ) { + /** @noinspection TypeUnsafeComparisonInspection */ + if ( $value == (int) $value ) { + $value = (int) $value; + } /** @noinspection TypeUnsafeComparisonInspection */ elseif ( $value == (float) $value ) { + $value = (float) $value; + } + } + } + + return $value; + } + + public function unixstamp( $excelDateTime ) { + + $d = floor( $excelDateTime ); // days since 1900 or 1904 + $t = $excelDateTime - $d; + + if ( $this->date1904 ) { + $d += 1462; + } + + $t = ( abs( $d ) > 0 ) ? ( $d - 25569 ) * 86400 + round( $t * 86400 ) : round( $t * 86400 ); + + return (int) $t; + } + + /** + * Returns cell value + * VERY SLOW! Use ->rows() or ->rowsEx() + * + * @param int $worksheetIndex + * @param string|array $cell ref or coords, D12 or [3,12] + * + * @return mixed Returns NULL if not found + */ + public function getCell( $worksheetIndex = 0, $cell = 'A1' ) { + + if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) { + return false; + } + + $idx = is_array( $cell ) ? $cell : $this->getIndex( (string) $cell ); + $C = $idx[0]; + $R = $idx[1]; + + $curR = 0; + /* @var SimpleXMLElement $ws */ + foreach ( $ws->sheetData->row as $row ) { + $curC = 0; + foreach ( $row->c as $c ) { + // detect skipped cols + $idx = $this->getIndex( (string) $c['r'] ); + $x = $idx[0]; + $y = $idx[1]; + if ( $x > 0 ) { + $curC = $x; + $curR = $y; + } + if ( $curR === $R && $curC === $C ) { + return $this->value( $c ); + } + if ( $curR > $R ) { + return null; + } + $curC ++; + } + + $curR ++; + } + + return null; + } + + public function href( $cell ) { + return isset( $this->hyperlinks[ (string) $cell['r'] ] ) ? $this->hyperlinks[ (string) $cell['r'] ] : ''; + } + + public function sheets() { + return $this->sheets; + } + + public function sheetsCount() { + return count( $this->sheets ); + } + + public function sheetName( $worksheetIndex ) { + if ( isset( $this->sheetNames[ $worksheetIndex ] ) ) { + return $this->sheetNames[ $worksheetIndex ]; + } + + return false; + } + + public function sheetNames() { + + return $this->sheetNames; + } + + // thx Gonzo + + public function getStyles() { + return $this->styles; + } + + public function getPackage() { + return $this->package; + } + + public function setDateTimeFormat( $value ) { + $this->datetimeFormat = is_string( $value ) ? $value : false; + } + + private function _strlen( $str ) { + return ( ini_get( 'mbstring.func_overload' ) & 2 ) ? mb_strlen( $str, '8bit' ) : strlen( $str ); + } + + private function _strpos( $haystack, $needle, $offset = 0 ) { + return ( ini_get( 'mbstring.func_overload' ) & 2 ) ? mb_strpos( $haystack, $needle, $offset, '8bit' ) : strpos( $haystack, $needle, $offset ); + } + + /* + private function _strrpos( $haystack, $needle, $offset = 0 ) { + return (ini_get('mbstring.func_overload') & 2) ? mb_strrpos( $haystack, $needle, $offset, '8bit') : strrpos($haystack, $needle, $offset); + }*/ + private function _strtoupper( $str ) { + return ( ini_get( 'mbstring.func_overload' ) & 2 ) ? mb_strtoupper( $str, '8bit' ) : strtoupper( $str ); + } + + private function _substr( $str, $start, $length = null ) { + return ( ini_get( 'mbstring.func_overload' ) & 2 ) ? mb_substr( $str, $start, ( $length === null ) ? mb_strlen( $str, '8bit' ) : $length, '8bit' ) : substr( $str, $start, ( $length === null ) ? strlen( $str ) : $length ); + } + + private function _getTarget( $base, $target ) { + $target = trim( $target ); + if ( strpos( $target, '/' ) === 0 ) { + return $this->_substr( $target, 1 ); + } + $target = ( $base ? $base . '/' : '' ) . $target; + // a/b/../c -> a/c + $parts = explode( '/', $target ); + $abs = []; + foreach ( $parts as $p ) { + if ( '.' === $p ) { + continue; + } + if ( '..' === $p ) { + array_pop( $abs ); + } else { + $abs[] = $p; + } + } + return implode( '/', $abs ); + } + +} \ No newline at end of file diff --git a/modules/import_api/logo.png b/modules/import_api/logo.png new file mode 100644 index 00000000..5768be96 Binary files /dev/null and b/modules/import_api/logo.png differ diff --git a/modules/import_api/sql_install.php b/modules/import_api/sql_install.php new file mode 100644 index 00000000..966a14e1 --- /dev/null +++ b/modules/import_api/sql_install.php @@ -0,0 +1,90 @@ + + * @copyright 2019 Dalibor Stojcevski + * @license Dalibor Stojcevski + */ + +//$sql = array(); +$sql[_DB_PREFIX_.'ia_files'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_files` ( + `file_id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(200) NOT NULL, + `link` varchar(300) NOT NULL, + `shop` varchar(200) NOT NULL, + `source` varchar(10) NOT NULL, + `headers` smallint(2) NOT NULL DEFAULT \'0\', + `status` smallint(2) NOT NULL DEFAULT \'1\', + + `delimiter` varchar(16) NOT NULL, + `mime_type` varchar(100) NOT NULL, + `fields` text NOT NULL, + `date_added` int(11) NOT NULL, + `mask` varchar(30) NOT NULL, + `post` text, + `date_edited` int(11) NOT NULL DEFAULT \'0\', + PRIMARY KEY (`file_id`) +) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;'; + +$sql[_DB_PREFIX_.'ia_file_settings'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_file_settings` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `mapping` text NOT NULL, + `modification` text NOT NULL, + `split` text NOT NULL, + `filter` text NOT NULL, + `filter_options` text NOT NULL, + `replace` text NOT NULL, + `settings` text NOT NULL, + `shop` varchar(300) NOT NULL, + `file_id` int(11) NOT NULL, + `date_updated` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;'; + +$sql[_DB_PREFIX_.'ia_temp'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_temp` ( + `product_id` BigInt(20) NOT NULL AUTO_INCREMENT, + `id` varchar(30) NOT NULL, + `source_id` varchar(200) NOT NULL, + `indx` varchar(300) NOT NULL, + `shop` varchar(64) NOT NULL, + `product` text NOT NULL, + `date_added` int(11) NOT NULL, + `queue_id` int(11) NOT NULL, + `file_id` int(11) NOT NULL DEFAULT \'0\', + PRIMARY KEY (`product_id`) +) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;'; + +$sql[_DB_PREFIX_.'ia_queues'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_queues` ( + `queue_id` int(11) NOT NULL AUTO_INCREMENT, + `total` int(11) NOT NULL, + `date_added` int(11) NOT NULL, + `shop` varchar(300) NOT NULL, + `status` int(11) NOT NULL DEFAULT \'0\', + `date_processed` int(11) NOT NULL DEFAULT \'0\', + `products_created` int(11) NOT NULL DEFAULT \'0\', + `products_updated` int(11) NOT NULL DEFAULT \'0\', + `products_failed` int(11) NOT NULL DEFAULT \'0\', + `source` varchar(10) NOT NULL DEFAULT \'n\', + `wait_time` int(11) NOT NULL DEFAULT \'0\', + `file_id` int(11) NOT NULL DEFAULT \'0\', + PRIMARY KEY (`queue_id`) +) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;'; + +$sql[_DB_PREFIX_.'ia_products'] = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'ia_products` ( + `product_id`int(11) NOT NULL, + `indx` varchar(300) NOT NULL, + `shop` varchar(64) NOT NULL, + `product` text NOT NULL, + `date_edited` int(11) NOT NULL, + `source` varchar(30) NOT NULL, + `date_added` int(11) NOT NULL DEFAULT \'0\', + `file_id` int(11) NOT NULL DEFAULT \'0\', + `queue_id` int(11) NOT NULL DEFAULT \'0\', + `missing` int(11) NOT NULL DEFAULT \'0\', + PRIMARY KEY (`product_id`) +) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;'; \ No newline at end of file diff --git a/modules/import_api/translations/cs.php b/modules/import_api/translations/cs.php new file mode 100644 index 00000000..e69de29b diff --git a/modules/import_api/translations/pl.php b/modules/import_api/translations/pl.php new file mode 100644 index 00000000..e69de29b diff --git a/modules/import_api/views/css/dataTables.bootstrap4.css b/modules/import_api/views/css/dataTables.bootstrap4.css new file mode 100644 index 00000000..84ec2036 --- /dev/null +++ b/modules/import_api/views/css/dataTables.bootstrap4.css @@ -0,0 +1,206 @@ +table.dataTable { + clear: both; + margin-top: 6px !important; + margin-bottom: 6px !important; + max-width: none !important; + border-collapse: separate !important; + border-spacing: 0; +} +table.dataTable td, +table.dataTable th { + -webkit-box-sizing: content-box; + box-sizing: content-box; +} +table.dataTable td.dataTables_empty, +table.dataTable th.dataTables_empty { + text-align: center; +} +table.dataTable.nowrap th, +table.dataTable.nowrap td { + white-space: nowrap; +} + +div.dataTables_wrapper div.dataTables_length label { + font-weight: normal; + text-align: left; + white-space: nowrap; +} +div.dataTables_wrapper div.dataTables_length select { + width: auto; + display: inline-block; +} +div.dataTables_wrapper div.dataTables_filter { + text-align: right; +} +div.dataTables_wrapper div.dataTables_filter label { + font-weight: normal; + white-space: nowrap; + text-align: left; +} +div.dataTables_wrapper div.dataTables_filter input { + margin-left: 0.5em; + display: inline-block; + width: auto; +} +div.dataTables_wrapper div.dataTables_info { + padding-top: 0.85em; + white-space: nowrap; +} +div.dataTables_wrapper div.dataTables_paginate { + margin: 0; + white-space: nowrap; + text-align: right; +} +div.dataTables_wrapper div.dataTables_paginate ul.pagination { + margin: 2px 0; + white-space: nowrap; + justify-content: flex-end; +} +div.dataTables_wrapper div.dataTables_processing { + position: absolute; + top: 50%; + left: 50%; + width: 200px; + margin-left: -100px; + margin-top: -26px; + text-align: center; + padding: 1em 0; +} + +table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting, +table.dataTable thead > tr > td.sorting_asc, +table.dataTable thead > tr > td.sorting_desc, +table.dataTable thead > tr > td.sorting { + padding-right: 30px; +} +table.dataTable thead > tr > th:active, +table.dataTable thead > tr > td:active { + outline: none; +} +table.dataTable thead .sorting, +table.dataTable thead .sorting_asc, +table.dataTable thead .sorting_desc, +table.dataTable thead .sorting_asc_disabled, +table.dataTable thead .sorting_desc_disabled { + cursor: pointer; + position: relative; +} +table.dataTable thead .sorting:before, table.dataTable thead .sorting:after, +table.dataTable thead .sorting_asc:before, +table.dataTable thead .sorting_asc:after, +table.dataTable thead .sorting_desc:before, +table.dataTable thead .sorting_desc:after, +table.dataTable thead .sorting_asc_disabled:before, +table.dataTable thead .sorting_asc_disabled:after, +table.dataTable thead .sorting_desc_disabled:before, +table.dataTable thead .sorting_desc_disabled:after { + position: absolute; + bottom: 0.9em; + display: block; + opacity: 0.3; +} +table.dataTable thead .sorting:before, +table.dataTable thead .sorting_asc:before, +table.dataTable thead .sorting_desc:before, +table.dataTable thead .sorting_asc_disabled:before, +table.dataTable thead .sorting_desc_disabled:before { + right: 1em; + content: "\2191"; +} +table.dataTable thead .sorting:after, +table.dataTable thead .sorting_asc:after, +table.dataTable thead .sorting_desc:after, +table.dataTable thead .sorting_asc_disabled:after, +table.dataTable thead .sorting_desc_disabled:after { + right: 0.5em; + content: "\2193"; +} +table.dataTable thead .sorting_asc:before, +table.dataTable thead .sorting_desc:after { + opacity: 1; +} +table.dataTable thead .sorting_asc_disabled:before, +table.dataTable thead .sorting_desc_disabled:after { + opacity: 0; +} + +div.dataTables_scrollHead table.dataTable { + margin-bottom: 0 !important; +} + +div.dataTables_scrollBody table { + border-top: none; + margin-top: 0 !important; + margin-bottom: 0 !important; +} +div.dataTables_scrollBody table thead .sorting:before, +div.dataTables_scrollBody table thead .sorting_asc:before, +div.dataTables_scrollBody table thead .sorting_desc:before, +div.dataTables_scrollBody table thead .sorting:after, +div.dataTables_scrollBody table thead .sorting_asc:after, +div.dataTables_scrollBody table thead .sorting_desc:after { + display: none; +} +div.dataTables_scrollBody table tbody tr:first-child th, +div.dataTables_scrollBody table tbody tr:first-child td { + border-top: none; +} + +div.dataTables_scrollFoot > .dataTables_scrollFootInner { + box-sizing: content-box; +} +div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { + margin-top: 0 !important; + border-top: none; +} + +@media screen and (max-width: 767px) { + div.dataTables_wrapper div.dataTables_length, + div.dataTables_wrapper div.dataTables_filter, + div.dataTables_wrapper div.dataTables_info, + div.dataTables_wrapper div.dataTables_paginate { + text-align: center; + } +} +table.dataTable.table-sm > thead > tr > th { + padding-right: 20px; +} +table.dataTable.table-sm .sorting:before, +table.dataTable.table-sm .sorting_asc:before, +table.dataTable.table-sm .sorting_desc:before { + top: 5px; + right: 0.85em; +} +table.dataTable.table-sm .sorting:after, +table.dataTable.table-sm .sorting_asc:after, +table.dataTable.table-sm .sorting_desc:after { + top: 5px; +} + +table.table-bordered.dataTable th, +table.table-bordered.dataTable td { + border-left-width: 0; +} +table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, +table.table-bordered.dataTable td:last-child, +table.table-bordered.dataTable td:last-child { + border-right-width: 0; +} +table.table-bordered.dataTable tbody th, +table.table-bordered.dataTable tbody td { + border-bottom-width: 0; +} + +div.dataTables_scrollHead table.table-bordered { + border-bottom-width: 0; +} + +div.table-responsive > div.dataTables_wrapper > div.row { + margin: 0; +} +div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { + padding-left: 0; +} +div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { + padding-right: 0; +} diff --git a/modules/import_api/views/index.php b/modules/import_api/views/index.php new file mode 100644 index 00000000..907720c1 --- /dev/null +++ b/modules/import_api/views/index.php @@ -0,0 +1,35 @@ + +* @copyright 2007-2018 PrestaShop SA +* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) +* International Registered Trademark & Property of PrestaShop SA +*/ + +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/modules/import_api/views/js/back.js b/modules/import_api/views/js/back.js new file mode 100644 index 00000000..77935cba --- /dev/null +++ b/modules/import_api/views/js/back.js @@ -0,0 +1,27 @@ +/** +* 2007-2018 PrestaShop +* +* NOTICE OF LICENSE +* +* This source file is subject to the Academic Free License (AFL 3.0) +* that is bundled with this package in the file LICENSE.txt. +* It is also available through the world-wide-web at this URL: +* http://opensource.org/licenses/afl-3.0.php +* If you did not receive a copy of the license and are unable to +* obtain it through the world-wide-web, please send an email +* to license@prestashop.com so we can send you a copy immediately. +* +* DISCLAIMER +* +* Do not edit or add to this file if you wish to upgrade PrestaShop to newer +* versions in the future. If you wish to customize PrestaShop for your +* needs please refer to http://www.prestashop.com for more information. +* +* @author PrestaShop SA +* @copyright 2007-2018 PrestaShop SA +* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) +* International Registered Trademark & Property of PrestaShop SA +* +* Don't forget to prefix your containers with your own identifier +* to avoid any conflicts with others containers. +*/ diff --git a/modules/import_api/views/js/dataTables.bootstrap4.js b/modules/import_api/views/js/dataTables.bootstrap4.js new file mode 100644 index 00000000..f2d2ad58 --- /dev/null +++ b/modules/import_api/views/js/dataTables.bootstrap4.js @@ -0,0 +1,184 @@ +/*! DataTables Bootstrap 4 integration + * ©2011-2017 SpryMedia Ltd - datatables.net/license + */ + +/** + * DataTables integration for Bootstrap 4. This requires Bootstrap 4 and + * DataTables 1.10 or newer. + * + * This file sets the defaults and adds options to DataTables to style its + * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap + * for further information. + */ +(function( factory ){ + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery', 'datatables.net'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + module.exports = function (root, $) { + if ( ! root ) { + root = window; + } + + if ( ! $ || ! $.fn.dataTable ) { + // Require DataTables, which attaches to jQuery, including + // jQuery if needed and have a $ property so we can access the + // jQuery object that is used + $ = require('datatables.net')(root, $).$; + } + + return factory( $, root, root.document ); + }; + } + else { + // Browser + factory( jQuery, window, document ); + } +}(function( $, window, document, undefined ) { +'use strict'; +var DataTable = $.fn.dataTable; + + +/* Set the defaults for DataTables initialisation */ +$.extend( true, DataTable.defaults, { + dom: + "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" + + "<'row'<'col-sm-12'tr>>" + + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", + renderer: 'bootstrap' +} ); + + +/* Default class modification */ +$.extend( DataTable.ext.classes, { + sWrapper: "dataTables_wrapper dt-bootstrap4", + sFilterInput: "form-control form-control-sm", + sLengthSelect: "custom-select custom-select-sm form-control form-control-sm", + sProcessing: "dataTables_processing card", + sPageButton: "paginate_button page-item" +} ); + + +/* Bootstrap paging button renderer */ +DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) { + var api = new DataTable.Api( settings ); + var classes = settings.oClasses; + var lang = settings.oLanguage.oPaginate; + var aria = settings.oLanguage.oAria.paginate || {}; + var btnDisplay, btnClass, counter=0; + + var attach = function( container, buttons ) { + var i, ien, node, button; + var clickHandler = function ( e ) { + e.preventDefault(); + if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) { + api.page( e.data.action ).draw( 'page' ); + } + }; + + for ( i=0, ien=buttons.length ; i 0 ? + '' : ' disabled'); + break; + + case 'previous': + btnDisplay = lang.sPrevious; + btnClass = button + (page > 0 ? + '' : ' disabled'); + break; + + case 'next': + btnDisplay = lang.sNext; + btnClass = button + (page < pages-1 ? + '' : ' disabled'); + break; + + case 'last': + btnDisplay = lang.sLast; + btnClass = button + (page < pages-1 ? + '' : ' disabled'); + break; + + default: + btnDisplay = button + 1; + btnClass = page === button ? + 'active' : ''; + break; + } + + if ( btnDisplay ) { + node = $('
  • ', { + 'class': classes.sPageButton+' '+btnClass, + 'id': idx === 0 && typeof button === 'string' ? + settings.sTableId +'_'+ button : + null + } ) + .append( $('', { + 'href': '#', + 'aria-controls': settings.sTableId, + 'aria-label': aria[ button ], + 'data-dt-idx': counter, + 'tabindex': settings.iTabIndex, + 'class': 'page-link' + } ) + .html( btnDisplay ) + ) + .appendTo( container ); + + settings.oApi._fnBindAction( + node, {action: button}, clickHandler + ); + + counter++; + } + } + } + }; + + // IE9 throws an 'unknown error' if document.activeElement is used + // inside an iframe or frame. + var activeEl; + + try { + // Because this approach is destroying and recreating the paging + // elements, focus is lost on the select button which is bad for + // accessibility. So we want to restore focus once the draw has + // completed + activeEl = $(host).find(document.activeElement).data('dt-idx'); + } + catch (e) {} + + attach( + $(host).empty().html('
      ').children('ul'), + buttons + ); + + if ( activeEl !== undefined ) { + $(host).find( '[data-dt-idx='+activeEl+']' ).focus(); + } +}; + + +return DataTable; +})); diff --git a/modules/import_api/views/js/dataTables.js b/modules/import_api/views/js/dataTables.js new file mode 100644 index 00000000..7f2cb5f1 --- /dev/null +++ b/modules/import_api/views/js/dataTables.js @@ -0,0 +1,15334 @@ +/*! DataTables 1.10.20 + * ©2008-2019 SpryMedia Ltd - datatables.net/license + */ + +/** + * @summary DataTables + * @description Paginate, search and order HTML tables + * @version 1.10.20 + * @file jquery.dataTables.js + * @author SpryMedia Ltd + * @contact www.datatables.net + * @copyright Copyright 2008-2019 SpryMedia Ltd. + * + * This source file is free software, available under the following license: + * MIT license - http://datatables.net/license + * + * This source file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. + * + * For details please refer to: http://www.datatables.net + */ + +/*jslint evil: true, undef: true, browser: true */ +/*globals $,require,jQuery,define,_selector_run,_selector_opts,_selector_first,_selector_row_indexes,_ext,_Api,_api_register,_api_registerPlural,_re_new_lines,_re_html,_re_formatted_numeric,_re_escape_regex,_empty,_intVal,_numToDecimal,_isNumber,_isHtml,_htmlNumeric,_pluck,_pluck_order,_range,_stripHtml,_unique,_fnBuildAjax,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnAjaxDataSrc,_fnAddColumn,_fnColumnOptions,_fnAdjustColumnSizing,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnVisbleColumns,_fnGetColumns,_fnColumnTypes,_fnApplyColumnDefs,_fnHungarianMap,_fnCamelToHungarian,_fnLanguageCompat,_fnBrowserDetect,_fnAddData,_fnAddTr,_fnNodeToDataIndex,_fnNodeToColumnIndex,_fnGetCellData,_fnSetCellData,_fnSplitObjNotation,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnGetDataMaster,_fnClearTable,_fnDeleteIndex,_fnInvalidate,_fnGetRowElements,_fnCreateTr,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAddOptionsHtml,_fnDetectHeader,_fnGetUniqueThs,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnFilterCreateSearch,_fnEscapeRegex,_fnFilterData,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnInfoMacros,_fnInitialise,_fnInitComplete,_fnLengthChange,_fnFeatureHtmlLength,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnFeatureHtmlTable,_fnScrollDraw,_fnApplyToChildren,_fnCalculateColumnWidths,_fnThrottle,_fnConvertToWidth,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnSortFlatten,_fnSort,_fnSortAria,_fnSortListener,_fnSortAttachListener,_fnSortingClasses,_fnSortData,_fnSaveState,_fnLoadState,_fnSettingsFromNode,_fnLog,_fnMap,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnLengthOverflow,_fnRenderer,_fnDataSource,_fnRowAttributes*/ + +(function( factory ) { + "use strict"; + + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + module.exports = function (root, $) { + if ( ! root ) { + // CommonJS environments without a window global must pass a + // root. This will give an error otherwise + root = window; + } + + if ( ! $ ) { + $ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window + require('jquery') : + require('jquery')( root ); + } + + return factory( $, root, root.document ); + }; + } + else { + // Browser + factory( jQuery, window, document ); + } +} +(function( $, window, document, undefined ) { + "use strict"; + + /** + * DataTables is a plug-in for the jQuery Javascript library. It is a highly + * flexible tool, based upon the foundations of progressive enhancement, + * which will add advanced interaction controls to any HTML table. For a + * full list of features please refer to + * [DataTables.net](href="http://datatables.net). + * + * Note that the `DataTable` object is not a global variable but is aliased + * to `jQuery.fn.DataTable` and `jQuery.fn.dataTable` through which it may + * be accessed. + * + * @class + * @param {object} [init={}] Configuration object for DataTables. Options + * are defined by {@link DataTable.defaults} + * @requires jQuery 1.7+ + * + * @example + * // Basic initialisation + * $(document).ready( function { + * $('#example').dataTable(); + * } ); + * + * @example + * // Initialisation with configuration options - in this case, disable + * // pagination and sorting. + * $(document).ready( function { + * $('#example').dataTable( { + * "paginate": false, + * "sort": false + * } ); + * } ); + */ + var DataTable = function ( options ) + { + /** + * Perform a jQuery selector action on the table's TR elements (from the tbody) and + * return the resulting jQuery object. + * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on + * @param {object} [oOpts] Optional parameters for modifying the rows to be included + * @param {string} [oOpts.filter=none] Select TR elements that meet the current filter + * criterion ("applied") or all TR elements (i.e. no filter). + * @param {string} [oOpts.order=current] Order of the TR elements in the processed array. + * Can be either 'current', whereby the current sorting of the table is used, or + * 'original' whereby the original order the data was read into the table is used. + * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page + * ("current") or not ("all"). If 'current' is given, then order is assumed to be + * 'current' and filter is 'applied', regardless of what they might be given as. + * @returns {object} jQuery object, filtered by the given selector. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Highlight every second row + * oTable.$('tr:odd').css('backgroundColor', 'blue'); + * } ); + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Filter to rows with 'Webkit' in them, add a background colour and then + * // remove the filter, thus highlighting the 'Webkit' rows only. + * oTable.fnFilter('Webkit'); + * oTable.$('tr', {"search": "applied"}).css('backgroundColor', 'blue'); + * oTable.fnFilter(''); + * } ); + */ + this.$ = function ( sSelector, oOpts ) + { + return this.api(true).$( sSelector, oOpts ); + }; + + + /** + * Almost identical to $ in operation, but in this case returns the data for the matched + * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes + * rather than any descendants, so the data can be obtained for the row/cell. If matching + * rows are found, the data returned is the original data array/object that was used to + * create the row (or a generated array if from a DOM source). + * + * This method is often useful in-combination with $ where both functions are given the + * same parameters and the array indexes will match identically. + * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on + * @param {object} [oOpts] Optional parameters for modifying the rows to be included + * @param {string} [oOpts.filter=none] Select elements that meet the current filter + * criterion ("applied") or all elements (i.e. no filter). + * @param {string} [oOpts.order=current] Order of the data in the processed array. + * Can be either 'current', whereby the current sorting of the table is used, or + * 'original' whereby the original order the data was read into the table is used. + * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page + * ("current") or not ("all"). If 'current' is given, then order is assumed to be + * 'current' and filter is 'applied', regardless of what they might be given as. + * @returns {array} Data for the matched elements. If any elements, as a result of the + * selector, were not TR, TD or TH elements in the DataTable, they will have a null + * entry in the array. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Get the data from the first row in the table + * var data = oTable._('tr:first'); + * + * // Do something useful with the data + * alert( "First cell is: "+data[0] ); + * } ); + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Filter to 'Webkit' and get all data for + * oTable.fnFilter('Webkit'); + * var data = oTable._('tr', {"search": "applied"}); + * + * // Do something with the data + * alert( data.length+" rows matched the search" ); + * } ); + */ + this._ = function ( sSelector, oOpts ) + { + return this.api(true).rows( sSelector, oOpts ).data(); + }; + + + /** + * Create a DataTables Api instance, with the currently selected tables for + * the Api's context. + * @param {boolean} [traditional=false] Set the API instance's context to be + * only the table referred to by the `DataTable.ext.iApiIndex` option, as was + * used in the API presented by DataTables 1.9- (i.e. the traditional mode), + * or if all tables captured in the jQuery object should be used. + * @return {DataTables.Api} + */ + this.api = function ( traditional ) + { + return traditional ? + new _Api( + _fnSettingsFromNode( this[ _ext.iApiIndex ] ) + ) : + new _Api( this ); + }; + + + /** + * Add a single new row or multiple rows of data to the table. Please note + * that this is suitable for client-side processing only - if you are using + * server-side processing (i.e. "bServerSide": true), then to add data, you + * must add it to the data source, i.e. the server-side, through an Ajax call. + * @param {array|object} data The data to be added to the table. This can be: + *
        + *
      • 1D array of data - add a single row with the data provided
      • + *
      • 2D array of arrays - add multiple rows in a single call
      • + *
      • object - data object when using mData
      • + *
      • array of objects - multiple data objects when using mData
      • + *
      + * @param {bool} [redraw=true] redraw the table or not + * @returns {array} An array of integers, representing the list of indexes in + * aoData ({@link DataTable.models.oSettings}) that have been added to + * the table. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * // Global var for counter + * var giCount = 2; + * + * $(document).ready(function() { + * $('#example').dataTable(); + * } ); + * + * function fnClickAddRow() { + * $('#example').dataTable().fnAddData( [ + * giCount+".1", + * giCount+".2", + * giCount+".3", + * giCount+".4" ] + * ); + * + * giCount++; + * } + */ + this.fnAddData = function( data, redraw ) + { + var api = this.api( true ); + + /* Check if we want to add multiple rows or not */ + var rows = $.isArray(data) && ( $.isArray(data[0]) || $.isPlainObject(data[0]) ) ? + api.rows.add( data ) : + api.row.add( data ); + + if ( redraw === undefined || redraw ) { + api.draw(); + } + + return rows.flatten().toArray(); + }; + + + /** + * This function will make DataTables recalculate the column sizes, based on the data + * contained in the table and the sizes applied to the columns (in the DOM, CSS or + * through the sWidth parameter). This can be useful when the width of the table's + * parent element changes (for example a window resize). + * @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable( { + * "sScrollY": "200px", + * "bPaginate": false + * } ); + * + * $(window).on('resize', function () { + * oTable.fnAdjustColumnSizing(); + * } ); + * } ); + */ + this.fnAdjustColumnSizing = function ( bRedraw ) + { + var api = this.api( true ).columns.adjust(); + var settings = api.settings()[0]; + var scroll = settings.oScroll; + + if ( bRedraw === undefined || bRedraw ) { + api.draw( false ); + } + else if ( scroll.sX !== "" || scroll.sY !== "" ) { + /* If not redrawing, but scrolling, we want to apply the new column sizes anyway */ + _fnScrollDraw( settings ); + } + }; + + + /** + * Quickly and simply clear a table + * @param {bool} [bRedraw=true] redraw the table or not + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...) + * oTable.fnClearTable(); + * } ); + */ + this.fnClearTable = function( bRedraw ) + { + var api = this.api( true ).clear(); + + if ( bRedraw === undefined || bRedraw ) { + api.draw(); + } + }; + + + /** + * The exact opposite of 'opening' a row, this function will close any rows which + * are currently 'open'. + * @param {node} nTr the table row to 'close' + * @returns {int} 0 on success, or 1 if failed (can't find the row) + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable; + * + * // 'open' an information row when a row is clicked on + * $('#example tbody tr').click( function () { + * if ( oTable.fnIsOpen(this) ) { + * oTable.fnClose( this ); + * } else { + * oTable.fnOpen( this, "Temporary row opened", "info_row" ); + * } + * } ); + * + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnClose = function( nTr ) + { + this.api( true ).row( nTr ).child.hide(); + }; + + + /** + * Remove a row for the table + * @param {mixed} target The index of the row from aoData to be deleted, or + * the TR element you want to delete + * @param {function|null} [callBack] Callback function + * @param {bool} [redraw=true] Redraw the table or not + * @returns {array} The row that was deleted + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Immediately remove the first row + * oTable.fnDeleteRow( 0 ); + * } ); + */ + this.fnDeleteRow = function( target, callback, redraw ) + { + var api = this.api( true ); + var rows = api.rows( target ); + var settings = rows.settings()[0]; + var data = settings.aoData[ rows[0][0] ]; + + rows.remove(); + + if ( callback ) { + callback.call( this, settings, data ); + } + + if ( redraw === undefined || redraw ) { + api.draw(); + } + + return data; + }; + + + /** + * Restore the table to it's original state in the DOM by removing all of DataTables + * enhancements, alterations to the DOM structure of the table and event listeners. + * @param {boolean} [remove=false] Completely remove the table from the DOM + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * // This example is fairly pointless in reality, but shows how fnDestroy can be used + * var oTable = $('#example').dataTable(); + * oTable.fnDestroy(); + * } ); + */ + this.fnDestroy = function ( remove ) + { + this.api( true ).destroy( remove ); + }; + + + /** + * Redraw the table + * @param {bool} [complete=true] Re-filter and resort (if enabled) the table before the draw. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Re-draw the table - you wouldn't want to do it here, but it's an example :-) + * oTable.fnDraw(); + * } ); + */ + this.fnDraw = function( complete ) + { + // Note that this isn't an exact match to the old call to _fnDraw - it takes + // into account the new data, but can hold position. + this.api( true ).draw( complete ); + }; + + + /** + * Filter the input based on data + * @param {string} sInput String to filter the table on + * @param {int|null} [iColumn] Column to limit filtering to + * @param {bool} [bRegex=false] Treat as regular expression or not + * @param {bool} [bSmart=true] Perform smart filtering or not + * @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es) + * @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false) + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Sometime later - filter... + * oTable.fnFilter( 'test string' ); + * } ); + */ + this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive ) + { + var api = this.api( true ); + + if ( iColumn === null || iColumn === undefined ) { + api.search( sInput, bRegex, bSmart, bCaseInsensitive ); + } + else { + api.column( iColumn ).search( sInput, bRegex, bSmart, bCaseInsensitive ); + } + + api.draw(); + }; + + + /** + * Get the data for the whole table, an individual row or an individual cell based on the + * provided parameters. + * @param {int|node} [src] A TR row node, TD/TH cell node or an integer. If given as + * a TR node then the data source for the whole row will be returned. If given as a + * TD/TH cell node then iCol will be automatically calculated and the data for the + * cell returned. If given as an integer, then this is treated as the aoData internal + * data index for the row (see fnGetPosition) and the data for that row used. + * @param {int} [col] Optional column index that you want the data of. + * @returns {array|object|string} If mRow is undefined, then the data for all rows is + * returned. If mRow is defined, just data for that row, and is iCol is + * defined, only data for the designated cell is returned. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * // Row data + * $(document).ready(function() { + * oTable = $('#example').dataTable(); + * + * oTable.$('tr').click( function () { + * var data = oTable.fnGetData( this ); + * // ... do something with the array / object of data for the row + * } ); + * } ); + * + * @example + * // Individual cell data + * $(document).ready(function() { + * oTable = $('#example').dataTable(); + * + * oTable.$('td').click( function () { + * var sData = oTable.fnGetData( this ); + * alert( 'The cell clicked on had the value of '+sData ); + * } ); + * } ); + */ + this.fnGetData = function( src, col ) + { + var api = this.api( true ); + + if ( src !== undefined ) { + var type = src.nodeName ? src.nodeName.toLowerCase() : ''; + + return col !== undefined || type == 'td' || type == 'th' ? + api.cell( src, col ).data() : + api.row( src ).data() || null; + } + + return api.data().toArray(); + }; + + + /** + * Get an array of the TR nodes that are used in the table's body. Note that you will + * typically want to use the '$' API method in preference to this as it is more + * flexible. + * @param {int} [iRow] Optional row index for the TR element you want + * @returns {array|node} If iRow is undefined, returns an array of all TR elements + * in the table's body, or iRow is defined, just the TR element requested. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Get the nodes from the table + * var nNodes = oTable.fnGetNodes( ); + * } ); + */ + this.fnGetNodes = function( iRow ) + { + var api = this.api( true ); + + return iRow !== undefined ? + api.row( iRow ).node() : + api.rows().nodes().flatten().toArray(); + }; + + + /** + * Get the array indexes of a particular cell from it's DOM element + * and column index including hidden columns + * @param {node} node this can either be a TR, TD or TH in the table's body + * @returns {int} If nNode is given as a TR, then a single index is returned, or + * if given as a cell, an array of [row index, column index (visible), + * column index (all)] is given. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * $('#example tbody td').click( function () { + * // Get the position of the current data from the node + * var aPos = oTable.fnGetPosition( this ); + * + * // Get the data array for this row + * var aData = oTable.fnGetData( aPos[0] ); + * + * // Update the data array and return the value + * aData[ aPos[1] ] = 'clicked'; + * this.innerHTML = 'clicked'; + * } ); + * + * // Init DataTables + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnGetPosition = function( node ) + { + var api = this.api( true ); + var nodeName = node.nodeName.toUpperCase(); + + if ( nodeName == 'TR' ) { + return api.row( node ).index(); + } + else if ( nodeName == 'TD' || nodeName == 'TH' ) { + var cell = api.cell( node ).index(); + + return [ + cell.row, + cell.columnVisible, + cell.column + ]; + } + return null; + }; + + + /** + * Check to see if a row is 'open' or not. + * @param {node} nTr the table row to check + * @returns {boolean} true if the row is currently open, false otherwise + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable; + * + * // 'open' an information row when a row is clicked on + * $('#example tbody tr').click( function () { + * if ( oTable.fnIsOpen(this) ) { + * oTable.fnClose( this ); + * } else { + * oTable.fnOpen( this, "Temporary row opened", "info_row" ); + * } + * } ); + * + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnIsOpen = function( nTr ) + { + return this.api( true ).row( nTr ).child.isShown(); + }; + + + /** + * This function will place a new row directly after a row which is currently + * on display on the page, with the HTML contents that is passed into the + * function. This can be used, for example, to ask for confirmation that a + * particular record should be deleted. + * @param {node} nTr The table row to 'open' + * @param {string|node|jQuery} mHtml The HTML to put into the row + * @param {string} sClass Class to give the new TD cell + * @returns {node} The row opened. Note that if the table row passed in as the + * first parameter, is not found in the table, this method will silently + * return. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable; + * + * // 'open' an information row when a row is clicked on + * $('#example tbody tr').click( function () { + * if ( oTable.fnIsOpen(this) ) { + * oTable.fnClose( this ); + * } else { + * oTable.fnOpen( this, "Temporary row opened", "info_row" ); + * } + * } ); + * + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnOpen = function( nTr, mHtml, sClass ) + { + return this.api( true ) + .row( nTr ) + .child( mHtml, sClass ) + .show() + .child()[0]; + }; + + + /** + * Change the pagination - provides the internal logic for pagination in a simple API + * function. With this function you can have a DataTables table go to the next, + * previous, first or last pages. + * @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last" + * or page number to jump to (integer), note that page 0 is the first page. + * @param {bool} [bRedraw=true] Redraw the table or not + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * oTable.fnPageChange( 'next' ); + * } ); + */ + this.fnPageChange = function ( mAction, bRedraw ) + { + var api = this.api( true ).page( mAction ); + + if ( bRedraw === undefined || bRedraw ) { + api.draw(false); + } + }; + + + /** + * Show a particular column + * @param {int} iCol The column whose display should be changed + * @param {bool} bShow Show (true) or hide (false) the column + * @param {bool} [bRedraw=true] Redraw the table or not + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Hide the second column after initialisation + * oTable.fnSetColumnVis( 1, false ); + * } ); + */ + this.fnSetColumnVis = function ( iCol, bShow, bRedraw ) + { + var api = this.api( true ).column( iCol ).visible( bShow ); + + if ( bRedraw === undefined || bRedraw ) { + api.columns.adjust().draw(); + } + }; + + + /** + * Get the settings for a particular table for external manipulation + * @returns {object} DataTables settings object. See + * {@link DataTable.models.oSettings} + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * var oSettings = oTable.fnSettings(); + * + * // Show an example parameter from the settings + * alert( oSettings._iDisplayStart ); + * } ); + */ + this.fnSettings = function() + { + return _fnSettingsFromNode( this[_ext.iApiIndex] ); + }; + + + /** + * Sort the table by a particular column + * @param {int} iCol the data index to sort on. Note that this will not match the + * 'display index' if you have hidden data entries + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Sort immediately with columns 0 and 1 + * oTable.fnSort( [ [0,'asc'], [1,'asc'] ] ); + * } ); + */ + this.fnSort = function( aaSort ) + { + this.api( true ).order( aaSort ).draw(); + }; + + + /** + * Attach a sort listener to an element for a given column + * @param {node} nNode the element to attach the sort listener to + * @param {int} iColumn the column that a click on this node will sort on + * @param {function} [fnCallback] callback function when sort is run + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Sort on column 1, when 'sorter' is clicked on + * oTable.fnSortListener( document.getElementById('sorter'), 1 ); + * } ); + */ + this.fnSortListener = function( nNode, iColumn, fnCallback ) + { + this.api( true ).order.listener( nNode, iColumn, fnCallback ); + }; + + + /** + * Update a table cell or row - this method will accept either a single value to + * update the cell with, an array of values with one element for each column or + * an object in the same format as the original data source. The function is + * self-referencing in order to make the multi column updates easier. + * @param {object|array|string} mData Data to update the cell/row with + * @param {node|int} mRow TR element you want to update or the aoData index + * @param {int} [iColumn] The column to update, give as null or undefined to + * update a whole row. + * @param {bool} [bRedraw=true] Redraw the table or not + * @param {bool} [bAction=true] Perform pre-draw actions or not + * @returns {int} 0 on success, 1 on error + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell + * oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], $('tbody tr')[0] ); // Row + * } ); + */ + this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction ) + { + var api = this.api( true ); + + if ( iColumn === undefined || iColumn === null ) { + api.row( mRow ).data( mData ); + } + else { + api.cell( mRow, iColumn ).data( mData ); + } + + if ( bAction === undefined || bAction ) { + api.columns.adjust(); + } + + if ( bRedraw === undefined || bRedraw ) { + api.draw(); + } + return 0; + }; + + + /** + * Provide a common method for plug-ins to check the version of DataTables being used, in order + * to ensure compatibility. + * @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the + * formats "X" and "X.Y" are also acceptable. + * @returns {boolean} true if this version of DataTables is greater or equal to the required + * version, or false if this version of DataTales is not suitable + * @method + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * alert( oTable.fnVersionCheck( '1.9.0' ) ); + * } ); + */ + this.fnVersionCheck = _ext.fnVersionCheck; + + + var _that = this; + var emptyInit = options === undefined; + var len = this.length; + + if ( emptyInit ) { + options = {}; + } + + this.oApi = this.internal = _ext.internal; + + // Extend with old style plug-in API methods + for ( var fn in DataTable.ext.internal ) { + if ( fn ) { + this[fn] = _fnExternApiFunc(fn); + } + } + + this.each(function() { + // For each initialisation we want to give it a clean initialisation + // object that can be bashed around + var o = {}; + var oInit = len > 1 ? // optimisation for single table case + _fnExtend( o, options, true ) : + options; + + /*global oInit,_that,emptyInit*/ + var i=0, iLen, j, jLen, k, kLen; + var sId = this.getAttribute( 'id' ); + var bInitHandedOff = false; + var defaults = DataTable.defaults; + var $this = $(this); + + + /* Sanity check */ + if ( this.nodeName.toLowerCase() != 'table' ) + { + _fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 ); + return; + } + + /* Backwards compatibility for the defaults */ + _fnCompatOpts( defaults ); + _fnCompatCols( defaults.column ); + + /* Convert the camel-case defaults to Hungarian */ + _fnCamelToHungarian( defaults, defaults, true ); + _fnCamelToHungarian( defaults.column, defaults.column, true ); + + /* Setting up the initialisation object */ + _fnCamelToHungarian( defaults, $.extend( oInit, $this.data() ), true ); + + + + /* Check to see if we are re-initialising a table */ + var allSettings = DataTable.settings; + for ( i=0, iLen=allSettings.length ; i').appendTo($this); + } + oSettings.nTHead = thead[0]; + + var tbody = $this.children('tbody'); + if ( tbody.length === 0 ) { + tbody = $('').appendTo($this); + } + oSettings.nTBody = tbody[0]; + + var tfoot = $this.children('tfoot'); + if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") ) { + // If we are a scrolling table, and no footer has been given, then we need to create + // a tfoot element for the caption element to be appended to + tfoot = $('').appendTo($this); + } + + if ( tfoot.length === 0 || tfoot.children().length === 0 ) { + $this.addClass( oClasses.sNoFooter ); + } + else if ( tfoot.length > 0 ) { + oSettings.nTFoot = tfoot[0]; + _fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot ); + } + + /* Check if there is data passing into the constructor */ + if ( oInit.aaData ) { + for ( i=0 ; i/g; + + // This is not strict ISO8601 - Date.parse() is quite lax, although + // implementations differ between browsers. + var _re_date = /^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/; + + // Escape regular expression special characters + var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' ); + + // http://en.wikipedia.org/wiki/Foreign_exchange_market + // - \u20BD - Russian ruble. + // - \u20a9 - South Korean Won + // - \u20BA - Turkish Lira + // - \u20B9 - Indian Rupee + // - R - Brazil (R$) and South Africa + // - fr - Swiss Franc + // - kr - Swedish krona, Norwegian krone and Danish krone + // - \u2009 is thin space and \u202F is narrow no-break space, both used in many + // - Ƀ - Bitcoin + // - Ξ - Ethereum + // standards as thousands separators. + var _re_formatted_numeric = /[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi; + + + var _empty = function ( d ) { + return !d || d === true || d === '-' ? true : false; + }; + + + var _intVal = function ( s ) { + var integer = parseInt( s, 10 ); + return !isNaN(integer) && isFinite(s) ? integer : null; + }; + + // Convert from a formatted number with characters other than `.` as the + // decimal place, to a Javascript number + var _numToDecimal = function ( num, decimalPoint ) { + // Cache created regular expressions for speed as this function is called often + if ( ! _re_dic[ decimalPoint ] ) { + _re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' ); + } + return typeof num === 'string' && decimalPoint !== '.' ? + num.replace( /\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) : + num; + }; + + + var _isNumber = function ( d, decimalPoint, formatted ) { + var strType = typeof d === 'string'; + + // If empty return immediately so there must be a number if it is a + // formatted string (this stops the string "k", or "kr", etc being detected + // as a formatted number for currency + if ( _empty( d ) ) { + return true; + } + + if ( decimalPoint && strType ) { + d = _numToDecimal( d, decimalPoint ); + } + + if ( formatted && strType ) { + d = d.replace( _re_formatted_numeric, '' ); + } + + return !isNaN( parseFloat(d) ) && isFinite( d ); + }; + + + // A string without HTML in it can be considered to be HTML still + var _isHtml = function ( d ) { + return _empty( d ) || typeof d === 'string'; + }; + + + var _htmlNumeric = function ( d, decimalPoint, formatted ) { + if ( _empty( d ) ) { + return true; + } + + var html = _isHtml( d ); + return ! html ? + null : + _isNumber( _stripHtml( d ), decimalPoint, formatted ) ? + true : + null; + }; + + + var _pluck = function ( a, prop, prop2 ) { + var out = []; + var i=0, ien=a.length; + + // Could have the test in the loop for slightly smaller code, but speed + // is essential here + if ( prop2 !== undefined ) { + for ( ; i') + .css( { + position: 'fixed', + top: 0, + left: $(window).scrollLeft()*-1, // allow for scrolling + height: 1, + width: 1, + overflow: 'hidden' + } ) + .append( + $('
      ') + .css( { + position: 'absolute', + top: 1, + left: 1, + width: 100, + overflow: 'scroll' + } ) + .append( + $('
      ') + .css( { + width: '100%', + height: 10 + } ) + ) + ) + .appendTo( 'body' ); + + var outer = n.children(); + var inner = outer.children(); + + // Numbers below, in order, are: + // inner.offsetWidth, inner.clientWidth, outer.offsetWidth, outer.clientWidth + // + // IE6 XP: 100 100 100 83 + // IE7 Vista: 100 100 100 83 + // IE 8+ Windows: 83 83 100 83 + // Evergreen Windows: 83 83 100 83 + // Evergreen Mac with scrollbars: 85 85 100 85 + // Evergreen Mac without scrollbars: 100 100 100 100 + + // Get scrollbar width + browser.barWidth = outer[0].offsetWidth - outer[0].clientWidth; + + // IE6/7 will oversize a width 100% element inside a scrolling element, to + // include the width of the scrollbar, while other browsers ensure the inner + // element is contained without forcing scrolling + browser.bScrollOversize = inner[0].offsetWidth === 100 && outer[0].clientWidth !== 100; + + // In rtl text layout, some browsers (most, but not all) will place the + // scrollbar on the left, rather than the right. + browser.bScrollbarLeft = Math.round( inner.offset().left ) !== 1; + + // IE8- don't provide height and width for getBoundingClientRect + browser.bBounding = n[0].getBoundingClientRect().width ? true : false; + + n.remove(); + } + + $.extend( settings.oBrowser, DataTable.__browser ); + settings.oScroll.iBarWidth = DataTable.__browser.barWidth; + } + + + /** + * Array.prototype reduce[Right] method, used for browsers which don't support + * JS 1.6. Done this way to reduce code size, since we iterate either way + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnReduce ( that, fn, init, start, end, inc ) + { + var + i = start, + value, + isSet = false; + + if ( init !== undefined ) { + value = init; + isSet = true; + } + + while ( i !== end ) { + if ( ! that.hasOwnProperty(i) ) { + continue; + } + + value = isSet ? + fn( value, that[i], i, that ) : + that[i]; + + isSet = true; + i += inc; + } + + return value; + } + + /** + * Add a column to the list used for the table with default values + * @param {object} oSettings dataTables settings object + * @param {node} nTh The th element for this column + * @memberof DataTable#oApi + */ + function _fnAddColumn( oSettings, nTh ) + { + // Add column to aoColumns array + var oDefaults = DataTable.defaults.column; + var iCol = oSettings.aoColumns.length; + var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, { + "nTh": nTh ? nTh : document.createElement('th'), + "sTitle": oDefaults.sTitle ? oDefaults.sTitle : nTh ? nTh.innerHTML : '', + "aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol], + "mData": oDefaults.mData ? oDefaults.mData : iCol, + idx: iCol + } ); + oSettings.aoColumns.push( oCol ); + + // Add search object for column specific search. Note that the `searchCols[ iCol ]` + // passed into extend can be undefined. This allows the user to give a default + // with only some of the parameters defined, and also not give a default + var searchCols = oSettings.aoPreSearchCols; + searchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] ); + + // Use the default column options function to initialise classes etc + _fnColumnOptions( oSettings, iCol, $(nTh).data() ); + } + + + /** + * Apply options for a column + * @param {object} oSettings dataTables settings object + * @param {int} iCol column index to consider + * @param {object} oOptions object with sType, bVisible and bSearchable etc + * @memberof DataTable#oApi + */ + function _fnColumnOptions( oSettings, iCol, oOptions ) + { + var oCol = oSettings.aoColumns[ iCol ]; + var oClasses = oSettings.oClasses; + var th = $(oCol.nTh); + + // Try to get width information from the DOM. We can't get it from CSS + // as we'd need to parse the CSS stylesheet. `width` option can override + if ( ! oCol.sWidthOrig ) { + // Width attribute + oCol.sWidthOrig = th.attr('width') || null; + + // Style attribute + var t = (th.attr('style') || '').match(/width:\s*(\d+[pxem%]+)/); + if ( t ) { + oCol.sWidthOrig = t[1]; + } + } + + /* User specified column options */ + if ( oOptions !== undefined && oOptions !== null ) + { + // Backwards compatibility + _fnCompatCols( oOptions ); + + // Map camel case parameters to their Hungarian counterparts + _fnCamelToHungarian( DataTable.defaults.column, oOptions, true ); + + /* Backwards compatibility for mDataProp */ + if ( oOptions.mDataProp !== undefined && !oOptions.mData ) + { + oOptions.mData = oOptions.mDataProp; + } + + if ( oOptions.sType ) + { + oCol._sManualType = oOptions.sType; + } + + // `class` is a reserved word in Javascript, so we need to provide + // the ability to use a valid name for the camel case input + if ( oOptions.className && ! oOptions.sClass ) + { + oOptions.sClass = oOptions.className; + } + if ( oOptions.sClass ) { + th.addClass( oOptions.sClass ); + } + + $.extend( oCol, oOptions ); + _fnMap( oCol, oOptions, "sWidth", "sWidthOrig" ); + + /* iDataSort to be applied (backwards compatibility), but aDataSort will take + * priority if defined + */ + if ( oOptions.iDataSort !== undefined ) + { + oCol.aDataSort = [ oOptions.iDataSort ]; + } + _fnMap( oCol, oOptions, "aDataSort" ); + } + + /* Cache the data get and set functions for speed */ + var mDataSrc = oCol.mData; + var mData = _fnGetObjectDataFn( mDataSrc ); + var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null; + + var attrTest = function( src ) { + return typeof src === 'string' && src.indexOf('@') !== -1; + }; + oCol._bAttrSrc = $.isPlainObject( mDataSrc ) && ( + attrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter) + ); + oCol._setter = null; + + oCol.fnGetData = function (rowData, type, meta) { + var innerData = mData( rowData, type, undefined, meta ); + + return mRender && type ? + mRender( innerData, type, rowData, meta ) : + innerData; + }; + oCol.fnSetData = function ( rowData, val, meta ) { + return _fnSetObjectDataFn( mDataSrc )( rowData, val, meta ); + }; + + // Indicate if DataTables should read DOM data as an object or array + // Used in _fnGetRowElements + if ( typeof mDataSrc !== 'number' ) { + oSettings._rowReadObject = true; + } + + /* Feature sorting overrides column specific when off */ + if ( !oSettings.oFeatures.bSort ) + { + oCol.bSortable = false; + th.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called + } + + /* Check that the class assignment is correct for sorting */ + var bAsc = $.inArray('asc', oCol.asSorting) !== -1; + var bDesc = $.inArray('desc', oCol.asSorting) !== -1; + if ( !oCol.bSortable || (!bAsc && !bDesc) ) + { + oCol.sSortingClass = oClasses.sSortableNone; + oCol.sSortingClassJUI = ""; + } + else if ( bAsc && !bDesc ) + { + oCol.sSortingClass = oClasses.sSortableAsc; + oCol.sSortingClassJUI = oClasses.sSortJUIAscAllowed; + } + else if ( !bAsc && bDesc ) + { + oCol.sSortingClass = oClasses.sSortableDesc; + oCol.sSortingClassJUI = oClasses.sSortJUIDescAllowed; + } + else + { + oCol.sSortingClass = oClasses.sSortable; + oCol.sSortingClassJUI = oClasses.sSortJUI; + } + } + + + /** + * Adjust the table column widths for new data. Note: you would probably want to + * do a redraw after calling this function! + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnAdjustColumnSizing ( settings ) + { + /* Not interested in doing column width calculation if auto-width is disabled */ + if ( settings.oFeatures.bAutoWidth !== false ) + { + var columns = settings.aoColumns; + + _fnCalculateColumnWidths( settings ); + for ( var i=0 , iLen=columns.length ; i