AntonLV 2 лет назад
Родитель
Сommit
083bd3fdff

+ 38 - 9
modules/boonex/ads/classes/BxAdsConfig.php

@@ -17,11 +17,14 @@ class BxAdsConfig extends BxBaseModTextConfig
 
     protected $_bAuction;
 
-    protected $_sSource;
+    protected $_bSources;
 
     protected $_bPromotion;
     protected $_fPromotionCpm;
 
+    protected $_sCacheEngineShopify;
+    protected $_iCacheLifetimeShopify;
+
     public function __construct($aModule)
     {
         parent::__construct($aModule);
@@ -42,6 +45,9 @@ class BxAdsConfig extends BxBaseModTextConfig
             'ICON' => 'ad col-green2',
 
             // database tables
+            'TABLE_SOURCES' => $aModule['db_prefix'] . 'sources',
+            'TABLE_SOURCES_OPTIONS' => $aModule['db_prefix'] . 'sources_options',
+            'TABLE_SOURCES_OPTIONS_VALUES' => $aModule['db_prefix'] . 'sources_options_values',
             'TABLE_ENTRIES' => $aModule['db_prefix'] . 'entries',
             'TABLE_ENTRIES_FULLTEXT' => 'title_text',
             'TABLE_CATEGORIES' => $aModule['db_prefix'] . 'categories',
@@ -63,8 +69,10 @@ class BxAdsConfig extends BxBaseModTextConfig
             'FIELD_SOLD' => 'sold',
             'FIELD_SHIPPED' => 'shipped',
             'FIELD_RECEIVED' => 'received',
+            'FIELD_SOURCE_TYPE' => 'source_type',
             'FIELD_SOURCE' => 'source',
             'FIELD_TITLE' => 'title',
+            'FIELD_URL' => 'url',
             'FIELD_NAME' => 'name',
             'FIELD_TEXT' => 'text',
             'FIELD_TEXT_ID' => 'ad-text',
@@ -125,6 +133,7 @@ class BxAdsConfig extends BxBaseModTextConfig
             'URL_CATEGORIES' => 'page.php?i=ads-categories',
             'URL_MANAGE_COMMON' => 'page.php?i=ads-manage',
             'URL_MANAGE_ADMINISTRATION' => 'page.php?i=ads-administration',
+            'URL_SOURCES' => 'page.php?i=ads-sources',
             'URI_FAVORITES_LIST' => 'ads-favorites',
             'URL_LICENSES_COMMON' => 'page.php?i=ads-licenses',
             'URL_LICENSES_ADMINISTRATION' => 'page.php?i=ads-licenses-administration',
@@ -133,7 +142,6 @@ class BxAdsConfig extends BxBaseModTextConfig
 
             // some params
             'PARAM_AUTO_APPROVE' => 'bx_ads_enable_auto_approve',
-            'PARAM_SOURCE' => 'bx_ads_enable_source',
             'PARAM_CHARS_SUMMARY' => 'bx_ads_summary_chars',
             'PARAM_CHARS_SUMMARY_PLAIN' => 'bx_ads_plain_summary_chars',
             'PARAM_NUM_RSS' => 'bx_ads_rss_num',
@@ -145,6 +153,7 @@ class BxAdsConfig extends BxBaseModTextConfig
             'PARAM_USE_IIN' => 'bx_ads_internal_interested_notification',
             'PARAM_CATEGORY_LEVEL_MAX' => 1,
             'PARAM_USE_AUCTION' => 'bx_ads_enable_auction',
+            'PARAM_USE_SOURCES' => 'bx_ads_enable_sources',
             'PARAM_USE_PROMOTION' => 'bx_ads_enable_promotion',
             'PARAM_PROMOTION_CPM' => 'bx_ads_promotion_cpm',
 
@@ -182,6 +191,8 @@ class BxAdsConfig extends BxBaseModTextConfig
             'OBJECT_CATEGORY' => '',
             'OBJECT_PRIVACY_VIEW' => 'bx_ads_allow_view_to',
             'OBJECT_PRIVACY_LIST_VIEW' => 'bx_ads_allow_view_favorite_list',
+            'OBJECT_FORM_SOURCES_DETAILS' => 'bx_ads_form_sources_details',
+            'OBJECT_FORM_SOURCES_DETAILS_DISPLAY_EDIT' => 'bx_ads_form_sources_details_edit',
             'OBJECT_FORM_CATEGORY' => 'bx_ads_category',
             'OBJECT_FORM_CATEGORY_DISPLAY_ADD' => 'bx_ads_category_add',
             'OBJECT_FORM_CATEGORY_DISPLAY_EDIT' => 'bx_ads_category_edit',
@@ -345,15 +356,18 @@ class BxAdsConfig extends BxBaseModTextConfig
 
         $this->_bPromotion = false;
         $this->_fPromotionCpm = 0;
+
+        $this->_sCacheEngineShopify = 'File';
+        $this->_iCacheLifetimeShopify = 2419200; //1 month
     }
 
     public function init(&$oDb)
     {
         $this->_oDb = &$oDb;
 
-        $this->_bAuction = getParam($this->CNF['PARAM_USE_AUCTION']) == 'on';
+        $this->_bSources = getParam($this->CNF['PARAM_USE_SOURCES']) == 'on';
 
-        $this->_sSource = getParam($this->CNF['PARAM_SOURCE']);
+        $this->_bAuction = getParam($this->CNF['PARAM_USE_AUCTION']) == 'on';
 
         $this->_bPromotion = getParam($this->CNF['PARAM_USE_PROMOTION']) == 'on';
         if($this->_bPromotion) {
@@ -371,6 +385,11 @@ class BxAdsConfig extends BxBaseModTextConfig
         return [BX_BASE_MOD_TEXT_STATUS_ACTIVE];
     }
 
+    public function isSources()
+    {
+        return $this->_bSources;
+    }
+
     public function isAuction()
     {
         return $this->_bAuction;
@@ -381,11 +400,6 @@ class BxAdsConfig extends BxBaseModTextConfig
         return $this->_bPromotion;
     }
 
-    public function getSource()
-    {
-        return $this->_sSource;
-    }
-
     public function getEntryName($sName)
     {
         return uriGenerate($sName, $this->CNF['TABLE_ENTRIES'], $this->CNF['FIELD_NAME'], ['lowercase' => false]);
@@ -400,6 +414,21 @@ class BxAdsConfig extends BxBaseModTextConfig
     {
         return $this->_fPromotionCpm;
     }
+    
+    public function getCacheEngineShopify()
+    {
+        return $this->_sCacheEngineShopify;
+    }
+
+    public function getCacheLifetimeShopify()
+    {
+        return $this->_iCacheLifetimeShopify;
+    }
+
+    public function getCacheKeyShopify()
+    {
+        return 'bx_ads_shopify_' . bx_site_hash() . '.php';
+    }
 }
 
 /** @} */

+ 130 - 1
modules/boonex/ads/classes/BxAdsDb.php

@@ -14,11 +14,140 @@
  */
 class BxAdsDb extends BxBaseModTextDb
 {
-    function __construct(&$oConfig)
+    public function __construct(&$oConfig)
     {
         parent::__construct($oConfig);
     }
 
+    public function getSourcesDetailsForm()
+    {
+        $sQuery = "SELECT
+                `ts`.`id` AS `source_id`,
+                `ts`.`name` AS `source_name`,
+                `ts`.`caption` AS `source_caption`,
+                `ts`.`description` AS `source_description`,
+                `ts`.`option_prefix` AS `source_option_prefix`,
+                `tso`.`id` AS `id`,
+                `tso`.`name` AS `name`,
+                `tso`.`type` AS `type`,
+                `tso`.`caption` AS `caption`,
+                `tso`.`description` AS `description`,
+                `tso`.`extra` AS `extra`,
+                `tso`.`check_type` AS `check_type`,
+                `tso`.`check_params` AS `check_params`,
+                `tso`.`check_error` AS `check_error`
+            FROM `" . $this->_sPrefix . "sources` AS `ts`
+            LEFT JOIN `" . $this->_sPrefix . "sources_options` AS `tso` ON `ts`.`id`=`tso`.`source_id`
+            WHERE `ts`.`active`='1' 
+            ORDER BY `ts`.`order` ASC, `tso`.`order` ASC";
+
+        return $this->getAll($sQuery);
+    }
+    
+    public function getSources($aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aMethod = array('name' => 'getAll', 'params' => [0 => 'query']);
+
+        $sWhereClause = "";
+        switch($aParams['sample']) {
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'name' => $aParams['name']
+                );
+
+                $sWhereClause = " AND `ts`.`name`=:name";
+                break;
+
+            case 'all':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'name';
+
+                if(!empty($aParams['active'])) 
+                    $sWhereClause = " AND `ts`.`active`='1'";
+                break;
+        }          
+
+        $aMethod['params'][0] = "SELECT
+                `ts`.`id` AS `id`,
+                `ts`.`name` AS `name`,
+                `ts`.`caption` AS `caption`,
+                `ts`.`description` AS `description`,
+                `ts`.`option_prefix` AS `option_prefix`,
+                `ts`.`class_name` AS `class_name`,
+                `ts`.`class_file` AS `class_file`
+            FROM `" . $CNF['TABLE_SOURCES'] . "` AS `ts`
+            WHERE 1" . $sWhereClause;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function getOption($aParams)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+
+        $sSelectClause = "`tso`.*";
+        $sJoinClause = $sWhereClause = "";
+        switch($aParams['sample']) {
+            case 'by_pid_and_name':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = [
+                    'source_id' => $aParams['source_id'],
+                    'name' => $aParams['name'],
+                ];
+
+                $sWhereClause = " AND `tso`.`source_id`=:source_id AND `tso`.`name`=:name";
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_SOURCES_OPTIONS'] . "` AS `tso` " . $sJoinClause . " WHERE 1" . $sWhereClause;
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+
+    public function getSourcesOptions($iProfileId = 0, $iSourceId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aBinding = [
+            'profile_id' => $iProfileId
+    	];
+
+        if(empty($iProfileId) && empty($iSourceId))
+           return $this->getAll("SELECT `id`, `name`, `type` FROM `" . $CNF['TABLE_SOURCES_OPTIONS'] . "`");
+
+        $sWhereAddon = "";
+        if(!empty($iSourceId)) {
+            $aBinding['source_id'] = $iSourceId;
+
+            $sWhereAddon = " AND `tso`.`source_id`=:source_id";
+        }
+
+        $sQuery = "SELECT
+               `tso`.`name` AS `name`,
+               `tsov`.`value` AS `value`
+            FROM `" . $CNF['TABLE_SOURCES_OPTIONS'] . "` AS `tso`
+            LEFT JOIN `" . $CNF['TABLE_SOURCES_OPTIONS_VALUES'] . "` AS `tsov` ON `tso`.`id`=`tsov`.`option_id`
+            WHERE 1" . $sWhereAddon . " AND `tsov`.`profile_id`=:profile_id ORDER BY `tso`.`order`";
+
+        return $this->getAllWithKey($sQuery, 'name', $aBinding);
+    }
+
+    public function updateSourceOption($iProfileId, $iOptionId, $sValue)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return $this->query("INSERT INTO `" . $CNF['TABLE_SOURCES_OPTIONS_VALUES'] . "` SET `profile_id`=:profile_id, `option_id`=:option_id, `value`=:value ON DUPLICATE KEY UPDATE `value`=:value", [
+            'profile_id' => $iProfileId, 
+            'option_id' => $iOptionId, 
+            'value' => $sValue
+        ]);
+    }
+
     function getEntriesBy($aParams = array())
     {
         $CNF = &$this->_oConfig->CNF;

+ 39 - 9
modules/boonex/ads/classes/BxAdsFormEntry.php

@@ -14,6 +14,7 @@
  */
 class BxAdsFormEntry extends BxBaseModTextFormEntry
 {
+    protected $_bSources;
     protected $_iCategory;
     protected $_sGhostTemplateCover = 'form_ghost_template_cover.html';
     protected $_bDisplayEditBudget;
@@ -25,16 +26,29 @@ class BxAdsFormEntry extends BxBaseModTextFormEntry
 
         $CNF = &$this->_oModule->_oConfig->CNF;
 
+        $sJsObject = $this->_oModule->_oConfig->getJsObject('form');
+
+        $this->_bSources = $this->_oModule->_oConfig->isSources();
         $this->_bDisplayEditBudget = $this->aParams['display'] == $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT_BUDGET'];
 
         $this->_initCategoryFields((int)BxDolSession::getInstance()->getValue($this->aParams['display'] . '_category'));
 
-        if(isset($this->aInputs[$CNF['FIELD_SOURCE']]) && !$this->_oModule->_oConfig->getSource())
-            unset($this->aInputs[$CNF['FIELD_SOURCE']]);
+        if(!$this->_bSources) {
+            if(isset($this->aInputs[$CNF['FIELD_SOURCE_TYPE']]))
+                unset($this->aInputs[$CNF['FIELD_SOURCE_TYPE']]);
 
-        if(isset($this->aInputs[$CNF['FIELD_TITLE']], $this->aInputs[$CNF['FIELD_NAME']])) {
-            $sJsObject = $this->_oModule->_oConfig->getJsObject('form');
+            if(isset($this->aInputs[$CNF['FIELD_SOURCE']]))
+                unset($this->aInputs[$CNF['FIELD_SOURCE']]);
+        }
 
+        if(isset($this->aInputs[$CNF['FIELD_SOURCE_TYPE']], $this->aInputs[$CNF['FIELD_SOURCE']])) {
+            $aMask = ["javascript:%s.loadFromSource(this, '%s', '%s');", $sJsObject, $CNF['FIELD_SOURCE_TYPE'], $CNF['FIELD_SOURCE']];
+            $sOnBlur = call_user_func_array('sprintf', $aMask);
+
+            $this->aInputs[$CNF['FIELD_SOURCE']]['attrs']['onblur'] = $sOnBlur;
+        }
+
+        if(isset($this->aInputs[$CNF['FIELD_TITLE']], $this->aInputs[$CNF['FIELD_NAME']])) {
             $aMask = array('mask' => "javascript:%s.checkName(this, '%s', '%s');", $sJsObject, $CNF['FIELD_TITLE'], $CNF['FIELD_NAME']);
             if($this->aParams['display'] == $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT'] && bx_get('id') !== false) {
                 $aMask['mask'] = "javascript:%s.checkName(this, '%s', '%s', %d);";
@@ -112,16 +126,13 @@ class BxAdsFormEntry extends BxBaseModTextFormEntry
             $this->aInputs[$CNF['FIELD_POLL']]['tr_attrs'] = array('class'=> 'bx-base-text-attachment-item');
         }
 
-        if($this->aParams['display'] == $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'] && isset($this->aInputs['do_submit'])) {
-            $sJsObject = $this->_oModule->_oConfig->getJsObject('form');
-
+        if($this->aParams['display'] == $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'] && isset($this->aInputs['do_submit']))
             $this->aInputs['do_submit'] = array_merge($this->aInputs['do_submit'], [
                 'type' => 'button',
                 'attrs' => [
                     'onclick' => $sJsObject . '.selectCategory(this);'
                 ]
             ]);
-        }
     }
 
     function getCode($bDynamicMode = false)
@@ -149,8 +160,27 @@ class BxAdsFormEntry extends BxBaseModTextFormEntry
             $aContentInfo = $this->_oModule->_oDb->getContentInfoById($aValues['id']);
         $bContentInfo = !empty($aContentInfo) && is_array($aContentInfo);
 
-        if($bContentInfo)
+        $iContentAuthorId = bx_get_logged_profile_id();
+        if($bContentInfo) {
+            $iContentAuthorId = (int)$aContentInfo[$CNF['FIELD_AUTHOR']];
+
             $this->_initCategoryFields($aContentInfo[$CNF['FIELD_CATEGORY']]);
+        }
+
+        if($this->_bSources) {
+            if(isset($this->aInputs[$CNF['FIELD_SOURCE_TYPE']]) && !empty($iContentAuthorId)) {
+                $aSource = $this->_oModule->serviceGetSource($iContentAuthorId);
+                if(!empty($aSource) && is_array($aSource))
+                    $this->aInputs[$CNF['FIELD_SOURCE_TYPE']]['value'] = $aSource['name'];
+            }
+
+            if(isset($this->aInputs[$CNF['FIELD_SOURCE']]) && $bContentInfo) {
+                if(!isset($this->aInputs[$CNF['FIELD_SOURCE']]['attrs']))
+                    $this->aInputs[$CNF['FIELD_SOURCE']]['attrs'] = [];
+
+                $this->aInputs[$CNF['FIELD_SOURCE']]['attrs']['readonly'] = 'readonly';
+            }
+        }
 
         if(isset($CNF['FIELD_COVER']) && isset($this->aInputs[$CNF['FIELD_COVER']])) {
             if($bContentInfo)

+ 143 - 0
modules/boonex/ads/classes/BxAdsFormSourcesDetails.php

@@ -0,0 +1,143 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Ads Ads
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxAdsSourcesDetailsFormCheckerHelper extends BxDolFormCheckerHelper
+{
+    function checkHttps ($s)
+    {
+        return empty($s) || substr(BX_DOL_URL_ROOT, 0, 5) == 'https';
+    }
+}
+
+class BxAdsFormSourcesDetails extends BxTemplFormView
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    protected $_bCollapseFirst;
+    
+    protected $_iProfileId;
+
+    function __construct($aInfo, $oTemplate = false)
+    {
+        parent::__construct($aInfo, $oTemplate);
+
+        $this->_sModule = 'bx_ads';
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        $this->_bCollapseFirst = false;
+        
+        $this->_iProfileId = $this->_oModule->getProfileId();
+
+        $this->init();
+    }
+
+    public function setProfileId($iProfileId = 0)
+    {
+        if(!empty($iProfileId))
+            $this->_iProfileId = $iProfileId;
+    }
+
+    public function init()
+    {
+        $aInputs = $this->_oModule->_oDb->getSourcesDetailsForm();
+        if(empty($aInputs))
+            return false;
+
+        $bCollapsed = $this->_bCollapseFirst;
+        $iSource = 0;
+        $sSource = "";
+        $oSource = null;
+        foreach($aInputs as $aInput) {
+            if($iSource != $aInput['source_id']) {
+                $this->aInputs['source_' . $aInput['source_id'] . '_begin'] = [
+                    'type' => 'block_header',
+                    'caption' => _t($aInput['source_caption']),
+                    'info' => _t($aInput['source_description']),
+                    'collapsable' => true,
+                    'collapsed' => $bCollapsed
+                ];
+
+                $iSource = $aInput['source_id'];
+                $sSource = $aInput['source_name'];
+                $oSource = $this->_oModule->getObjectSource($sSource, $this->_iProfileId);
+                $bCollapsed = true;
+            }
+
+            $this->aInputs[$aInput['name']] = [
+                'type' => $aInput['type'],
+                'name' => $aInput['name'],
+                'caption' => _t($aInput['caption']),
+                'value' => $oSource->getOption($aInput['name']),
+                'info' => _t($aInput['description']),
+            	'attrs' => [
+                    'bx-data-source' => $iSource
+            	],
+                'checker' => [
+                    'func' => $aInput['check_type'],
+                    'params' => $aInput['check_params'],
+                    'error' => _t($aInput['check_error']),
+                ]
+            ];
+
+            //--- Make some field dependent actions ---//
+            switch($aInput['type']) {
+                case 'select':
+                    if(empty($aInput['extra']))
+                       break;
+
+                    $aAddon = ['values' => []];
+
+                    if(BxDolService::isSerializedService($aInput['extra']))
+                        $aAddon['values'] = BxDolService::callSerialized($aInput['extra']);
+                    else {
+                        $aPairs = explode(',', $aInput['extra']);
+                        foreach($aPairs as $sPair) {
+                            $aPair = explode('|', $sPair);
+                            $aAddon['values'][] = ['key' => $aPair[0], 'value' => _t($aPair[1])];
+                        }
+                    }
+                    break;
+
+                case 'checkbox':
+                    $this->aInputs[$aInput['name']]['value'] = 'on';
+                    $aAddon = array('checked' => $oSource->getOption($aInput['name']) == 'on');
+                    break;
+
+                case 'value':
+                       $sName = str_replace($aInput['source_option_prefix'], '', $aInput['name']);
+                       if(!in_array($sName, array('return_data_url', 'notify_url')))
+                            break;
+
+                       $sMethod = 'get' . bx_gen_method_name($sName);
+                       if(method_exists($oSource, $sMethod))
+                            $this->aInputs[$aInput['name']]['value'] = $oSource->$sMethod($this->_iProfileId);
+                       break;
+            }
+
+            if(!empty($aAddon) && is_array($aAddon))
+                $this->aInputs[$aInput['name']] = array_merge($this->aInputs[$aInput['name']], $aAddon);
+        }
+
+        $this->aInputs['source_' . $iSource . '_end'] = [
+            'type' => 'block_end'
+        ];
+        $this->aInputs['submit'] = [
+            'type' => 'submit',
+            'name' => 'submit',
+            'value' => _t('_bx_ads_form_sources_details_input_do_submit'),
+        ];
+
+        return true;
+    }
+}
+
+/** @} */

+ 153 - 9
modules/boonex/ads/classes/BxAdsModule.php

@@ -63,6 +63,34 @@ class BxAdsModule extends BxBaseModTextModule
         return $aResult;
     }
 
+    public function actionShopifyCall()
+    {
+        $iProfileId = bx_get_logged_profile_id();
+
+        bx_import('SourceShopifyAdmin', $this->_aModule);
+        $oShopify = new BxAdsSourceShopifyAdmin($iProfileId, $this);
+        
+        $aResponse = $oShopify->getProduct('7433957113996');
+        print_r($aResponse); exit;
+    }
+
+    public function actionLoadEntryFromSource()
+    {
+        $sSourceType = bx_process_url_param(bx_process_input(bx_get('source_type')));
+        $sSource = bx_process_url_param(bx_process_input(bx_get('source')));
+        if(empty($sSourceType) || empty($sSource))
+            return echoJson(['code' => 1]);
+
+        $aEntry = $this->serviceLoadEntryFromSource($sSourceType, $sSource);
+        if(empty($aEntry) || !is_array($aEntry))
+            return echoJson(['code' => 2]);
+
+        return echoJson([
+            'code' => 0,
+            'fields' => $aEntry
+        ]);
+    }
+
     public function actionGetCategoryForm()
     {
         if(($iCategory = bx_get('category')) === false || (int)$iCategory == 0)
@@ -299,6 +327,8 @@ class BxAdsModule extends BxBaseModTextModule
     public function serviceGetSafeServices()
     {
         return array_merge(parent::serviceGetSafeServices(), [
+            'IsSourcesAvaliable' => '',
+            'LoadEntryFromSource' => '',
             'EntityReviews' => '',
             'EntityReviewsRating' => '',
             'CategoriesList' => '',
@@ -308,14 +338,6 @@ class BxAdsModule extends BxBaseModTextModule
         ]);
     }
 
-    public function serviceGetOptionsSource()
-    {
-        return [
-            ['key' => '', 'value' => _t('_Select_one')],
-            ['key' => 'shopify', 'value' => _t('_bx_ads_option_source_shopify')],
-        ];
-    }
-
     public function serviceRegisterImpression($iId)
     {
         return ['code' => $this->_oDb->updatePromotionTracker($iId, 'impressions') ? 0 : 1];
@@ -323,12 +345,23 @@ class BxAdsModule extends BxBaseModTextModule
     
     public function serviceRegisterClick($iId)
     {
+        $CNF = &$this->_oConfig->CNF;
+
         if(!$this->_oDb->updatePromotionTracker($iId, 'clicks'))
             return ['code' => 1];
 
+        $sUrl = '';
+        if($this->_oConfig->isSources())  {
+            $aEntry = $this->_oDb->getContentInfoById($iId);
+            if(!empty($aEntry) && is_array($aEntry) && !empty($aEntry[$CNF['FIELD_URL']]))
+                $sUrl = $aEntry[$CNF['FIELD_URL']];
+        }
+        else
+            $sUrl = $this->_oConfig->getViewEntryUrl($iId);
+
         return [
             'code' => 0,
-            'redirect' => $this->_oConfig->getViewEntryUrl($iId)
+            'redirect' => $sUrl
         ];
     }
 
@@ -1623,6 +1656,98 @@ class BxAdsModule extends BxBaseModTextModule
         return $this->_oTemplate->entryOfferAccepted($iUserId, $aContent, $aOffer);
     }
 
+    public function serviceIsSourcesAvaliable()
+    {
+        return $this->_oConfig->isSources();
+    }
+    
+    public function serviceGetSources($iAuthorId)
+    {
+        $aSources = $this->_oDb->getSources(['sample' => 'all']);
+        $aOptions = $this->_oDb->getSourcesOptions($iAuthorId);
+
+        $aResult = [];
+        foreach($aSources as $sSource => $aSource) {
+            if(!isset($aOptions[$aSource['option_prefix'] . 'active']) || $aOptions[$aSource['option_prefix'] . 'active']['value'] != 'on') 
+                continue;
+
+            foreach($aOptions as $sName => $aOption)
+                if(strpos($sName, $aSource['option_prefix']) !== false)
+                    $aSource['options'][$sName] = $aOption;
+            $aResult[$sSource] = $aSource;
+        }
+
+        return $aResult; 
+    }
+
+    public function serviceGetSource($iAuthorId, $sAuthorSource = '')
+    {
+    	$aSourcesOptions = $this->serviceGetSources($iAuthorId);
+        if(empty($aSourcesOptions) || !is_array($aSourcesOptions))
+            return false;
+
+        $mixedResult = false;
+        if(!empty($sAuthorSource)) {
+            if(!empty($aSourcesOptions[$sAuthorSource]) && is_array($aSourcesOptions[$sAuthorSource]))
+                $mixedResult = $aSourcesOptions[$sAuthorSource];
+        }
+        else 
+            $mixedResult = array_shift($aSourcesOptions);
+
+    	return $mixedResult;
+    }
+
+    public function serviceLoadEntryFromSource($sSourceType, $sSource)
+    {
+        $iProfileId = bx_get_logged_profile_id();
+
+        $oSource = $this->getObjectSource($sSourceType, $iProfileId);
+        if(!$oSource)
+            return [];
+
+        return $oSource->getEntry($sSource);
+    }
+
+    public function serviceBlockSourcesDetails($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isLogged())
+            return [
+                'content' => MsgBox(_t('_Access denied'))
+            ];
+
+        $iProfileId = !empty($iProfileId) ? $iProfileId : bx_get_logged_profile_id();
+
+        $oForm = BxTemplFormView::getObjectInstance($CNF['OBJECT_FORM_SOURCES_DETAILS'], $CNF['OBJECT_FORM_SOURCES_DETAILS_DISPLAY_EDIT']);
+        $oForm->setProfileId($iProfileId);
+        $oForm->initChecker();
+
+        if($oForm->isSubmitted()) {
+            if($oForm->isValid()) {
+                $aOptions = $this->_oDb->getSourcesOptions();
+                foreach($aOptions as $aOption) {
+                    $sValue = bx_get($aOption['name']) !== false ? bx_get($aOption['name']) : '';
+                    $this->_oDb->updateSourceOption($iProfileId, $aOption['id'], bx_process_input($sValue));
+                }
+
+                header('Location: ' . bx_absolute_url(BxDolPermalinks::getInstance()->permalink($CNF['URL_SOURCES'])));
+                return;
+            }
+            else
+                foreach($oForm->aInputs as $aInput)
+                    if(!empty($aInput['error']) && !empty($aInput['attrs']['bx-data-source'])) {
+                        $sSourceBlock = 'source_' . (int)$aInput['attrs']['bx-data-source'] . '_begin';
+                        if(!empty($oForm->aInputs[$sSourceBlock]))
+                            $oForm->aInputs[$sSourceBlock]['collapsed'] = false;
+                    }
+        }
+
+        return [
+            'content' => $oForm->getCode()
+        ];
+    }
+
     public function isEntryActive($aContentInfo)
     {
         $CNF = &$this->_oConfig->CNF;
@@ -1998,6 +2123,25 @@ class BxAdsModule extends BxBaseModTextModule
         bx_alert($this->getName(), 'received', $iContentId, false, $aParams);
     }
 
+    public function getObjectSource($sSource, $iProfileId = 0)
+    {
+        $aSource = $this->_oDb->getSources(array('sample' => 'by_name', 'name' => $sSource));
+        if(empty($aSource) || !is_array($aSource) || empty($aSource['class_name']))
+            return false;
+
+        if(!empty($iProfileId)) {
+            $aSource['author'] = (int)$iProfileId;
+            $aSource['options'] = $this->_oDb->getSourcesOptions((int)$iProfileId, $aSource['id']);
+        }
+
+        $sClassPath = !empty($aSource['class_file']) ? BX_DIRECTORY_PATH_ROOT . $aSource['class_file'] : $this->_oConfig->getClassPath() . $aSource['class_name'] . '.php';
+        if(!file_exists($sClassPath))
+            return false;
+
+        require_once($sClassPath);
+        return new $aSource['class_name']($this->_iProfileId, $aSource, $this);
+    }
+
     public function offerAccept($iId) 
     {
         $CNF = &$this->_oConfig->CNF;

+ 86 - 0
modules/boonex/ads/classes/BxAdsSource.php

@@ -0,0 +1,86 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Ads Ads
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxAdsSource extends BxDol
+{
+    protected $_oModule;
+    protected $_iProfile;
+
+    protected $_iId;
+    protected $_sName;
+    protected $_sCaption;
+    protected $_sPrefix;
+    protected $_aOptions;
+
+    public function __construct($iProfile, $aSource, &$oModule)
+    {
+        parent::__construct();
+
+        $this->_oModule = $oModule;
+        $this->_iProfile = $iProfile;
+
+        if(empty($aSource) || !is_array($aSource) || strcmp($aSource['name'], $this->_sName) != 0)
+            $this->_log("Unexpected value provided for the credentials");
+
+        $this->_iId = (int)$aSource['id'];
+        $this->_sName = $aSource['name'];
+        $this->_sCaption = _t($aSource['caption']);
+        $this->_sPrefix = $aSource['option_prefix'];
+
+        $this->_aOptions = [];
+        if(!empty($aSource['options']) && is_array($aSource['options']))
+            $this->initOptions($aSource['options']);
+    }
+
+    public function initOptions($aOptions)
+    {
+    	$this->_aOptions = $aOptions;
+    }
+
+    public function initOptionsByAuthor($iProfileId)
+    {
+        $this->_iProfileId = (int)$iProfileId;
+
+        $aOptions = $this->_oModule->_oDb->getSourcesOptions($this->_iProfileId, $this->_iId);
+        if(!empty($aOptions) && is_array($aOptions))
+            $this->initOptions($aOptions);
+    }
+
+    public function getOption($sName)
+    {
+    	if(substr($sName, 0, strlen($this->_sPrefix)) != $this->_sPrefix)
+            $sName = $this->_sPrefix . $sName;
+
+        return isset($this->_aOptions[$sName]) ? $this->_aOptions[$sName]['value'] : '';
+    }
+
+    public function getEntry($sId)
+    {
+        return [];
+    }
+
+    /**
+     * Internal methods.
+     */
+    protected function _call($sRequest, $aParams, $sMethod = 'post-json', $aHeaders = [])
+    {}
+
+    protected function _log($sMessage, $bUseLog = false)
+    {
+        if($bUseLog) {
+            //TODO: Use bx_log here.
+        }
+        else
+            throw new Exception($sMessage);
+
+        return false;
+    }    
+}

+ 104 - 0
modules/boonex/ads/classes/BxAdsSourceShopifyAdmin.php

@@ -0,0 +1,104 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Ads Ads
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+require_once('BxAdsSource.php');
+
+/*
+ * Login for Owner/Collaborator account:
+ * https://admin.shopify.com/store/your_shop/
+ * 
+ * Integration:
+ * 0. Custom apps creation guide (for a shop Owner/Collaborator, not a Partner):
+ * https://help.shopify.com/en/manual/apps/app-types/custom-apps
+ * 
+ * 1. Make authenticated requests to the REST Admin API:
+ * https://shopify.dev/docs/apps/auth/admin-app-access-tokens
+ * 
+ * 2. REST Admin API reference:
+ * https://shopify.dev/docs/api/admin-rest
+ * 
+ * 3. REST Admin API -> Product
+ * https://shopify.dev/docs/api/admin-rest/2023-10/resources/product
+ * 
+ */
+class BxAdsSourceShopifyAdmin extends BxAdsSource
+{
+    public static $NAME = 'shopify_admin';
+
+    protected $_sShopDomain;
+    protected $_sAccessToken;
+
+    protected $_sEndpoint;
+    protected $_sStorefront;
+
+    public function __construct($iProfile, $aSource, &$oModule)
+    {
+        $this->_sName = self::$NAME;
+
+        parent::__construct($iProfile, $aSource, $oModule);
+
+        $this->_sShopDomain = $this->getOption('shop_domain');
+        $this->_sAccessToken = $this->getOption('access_token');
+
+        $this->_sEndpoint = "https://{$this->_sShopDomain}/admin/api/2023-10/";
+        $this->_sStorefront = "https://{$this->_sShopDomain}/";
+    }
+
+    public function getEntry($sId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sProduct = $this->_call('products/' . $sId . '.json', [
+            'fields' => 'id,title,handle,body_html,variants',
+        ], 'get');
+        
+        if(empty($sProduct))
+            return [];
+
+        $aProduct = json_decode($sProduct, true);
+        if(empty($aProduct) || !is_array($aProduct) || empty($aProduct['product']))
+            return [];
+
+        $aProduct = $aProduct['product'];
+
+        $aResult = [
+            $CNF['FIELD_TITLE'] => ['type' => 'text', 'value' => $aProduct['title']],
+            $CNF['FIELD_URL'] => ['type' => 'hidden', 'value' => $this->_sStorefront . 'products/' . $aProduct['handle']],
+            $CNF['FIELD_TEXT'] => ['type' => 'text_html', 'value' => $aProduct['body_html']],
+        ];
+
+        if(!empty($aProduct['variants']) && is_array($aProduct['variants'])) {
+            $aVariant = array_shift($aProduct['variants']);
+
+            if(isset($aVariant['price']))
+                $aResult[$CNF['FIELD_PRICE']] = ['type' => 'text', 'value' => (float)$aVariant['price']];
+
+            if(isset($aVariant['inventory_quantity']))
+                $aResult[$CNF['FIELD_QUANTITY']] = ['type' => 'text', 'value' => (int)$aVariant['inventory_quantity']];
+        }
+
+        return $aResult;
+    }
+
+    /**
+     * Internal methods.
+     */
+    protected function _call($sRequest, $aParams, $sMethod = 'post-json', $aHeaders = [])
+    {
+        $aHeaders[] = 'Content-Type: application/json';
+        if(!empty($this->_sAccessToken))
+            $aHeaders[] = 'X-Shopify-Access-Token: ' . $this->_sAccessToken;
+        else
+            $aHeaders[] = 'Authorization: Basic ' . base64_encode($this->_sApiKey . ':' . $this->_sApiSecretKey);
+
+        return bx_file_get_contents($this->_sEndpoint . $sRequest, $aParams, $sMethod, $aHeaders);
+    }
+}

+ 199 - 0
modules/boonex/ads/classes/BxAdsSourceShopifyPartner.php

@@ -0,0 +1,199 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Ads Ads
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+require_once('BxAdsSource.php');
+
+/*
+ * Note. A custom app created via Partner Dashboard should use the OAuth authentication flow.
+ * Methods makeAuthorization and authorizeApplication are needed for this purpose.
+ * 
+ * Login for Partner account:
+ * https://accounts.shopify.com/
+ * 
+ * 
+ * Integration:
+ * 0. Create custom app in Partner dashboard.
+ * 
+ * 1. Authorization flow:
+ * https://shopify.dev/docs/apps/auth/oauth
+ * 
+ */
+
+class BxAdsSourceShopifyPartner extends BxAdsSource
+{
+    public static $NAME = 'shopify_partner';
+
+    protected $_sShopDomain;
+    protected $_sApiKey;
+    protected $_sApiSecretKey;
+    protected $_aScopes;
+
+    protected $_sEndpoint;
+
+    protected $_sToken;
+    protected $_aLastResponseHeaders;   
+
+    protected $_sCacheKey;
+    protected $_iCacheLifetime;
+    protected $_oCache;
+
+    public function __construct($iProfile, $aSource, $oModule)
+    {
+        $this->_sName = self::$NAME;
+
+        parent::__construct($iProfile, $aSource, &$oModule);
+
+        $this->_sShopDomain = $this->getOption('shop_domain');
+        $this->_sApiKey = $this->getOption('api_key');
+        $this->_sApiSecretKey = $this->getOption('api_secret_key');
+        $this->_sToken = $this->getOption('token');
+        $this->_aScopes = [];
+
+        $this->_sEndpoint = "https://{$this->_sShopDomain}";
+
+        $this->_sCacheKey = $this->_oModule->_oConfig->getCacheKeyShopify();
+        $this->_iCacheLifetime = $this->_oModule->_oConfig->getCacheLifetimeShopify();
+
+        $sCacheEngine = $this->_oModule->_oConfig->getCacheEngineShopify();
+        $this->_oCache = bx_instance('BxDolCache' . $sCacheEngine);
+        if(!$this->_oCache->isAvailable())
+            $this->_oCache = bx_instance('BxDolCacheFile');
+    }
+
+    public function makeAuthorization($aScopes = [])
+    {
+        $sNonce = bin2hex(random_bytes(10));
+
+        if(empty($aScopes))
+            $aScopes = $this->_aScopes;
+
+        $aCached = $this->_oCache->getData($this->_sCacheKey, $this->_iCacheLifetime);
+
+        if(empty($aCached))
+            $aCached = [];
+        $aCached[$this->_sShopDomain] = ['nonce' => $sNonce, 'scopes' => $aScopes];
+
+        $this->_oCache->setData($this->_sCacheKey, $aCached, $this->_iCacheLifetime);
+
+        return $this->_getAuthorizeUrl($aScopes, BX_DOL_URL_ROOT . $this->_oModule->_oConfig->getBaseUri() . 'shopify_handle_callback', $sNonce);
+    }
+    
+    public function authorizeApplication($aRequestData)
+    {
+        $aRequiredKeys = ['code', 'hmac', 'state', 'shop'];
+        foreach($aRequiredKeys as $sRk)
+            if(!in_array($sRk, array_keys($aRequestData)))
+                return $this->_log("The provided request data is missing one of the following keys: " . implode(', ', $aRequiredKeys));
+
+        $aCached = $this->_oCache->getData($this->_sCacheKey, $this->_iCacheLifetime);
+        if(!isset($aCached[$this->_sShopDomain]))
+            return $this->_log("Nonce cannot be found: " . $this->_sShopDomain);
+
+        $sNonce = $aCached[$this->_sShopDomain]['nonce'];
+        if($aRequestData['state'] !== $sNonce)
+            return $this->_log("The provided nonce ($sNonce) did not match the nonce provided by Shopify ({$aRequestData['state']})");
+
+        if(!filter_var($aRequestData['shop'], FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME))
+            return $this->_log("The shop provided by Shopify ({$aRequestData['shop']}) is an invalid hostname.");
+
+        if($aRequestData['shop'] !== $this->_sShopDomain)
+            return $this->_log("The shop provided by Shopify ({$aRequestData['shop']}) does not match the shop provided to this API ({$this->_sShopDomain})");
+
+        // Check HMAC signature. See https://help.shopify.com/api/getting-started/authentication/oauth#verification
+        $aHmacSource = [];
+        foreach($aRequestData as $sKey => $sValue) {
+            if($sKey === 'hmac')
+                continue;
+
+            // Replace the characters as specified by Shopify in the keys and values
+            $aPatterns = ['&' => '%26', '%' => '%25'];
+            $aPatternsKey = array_merge($aPatterns, ['=' => '%3D']);
+            $sKey = str_replace(array_keys($aPatternsKey), array_values($aPatternsKey), $sKey);
+            $sValue = str_replace(array_keys($aPatterns), array_values($aPatterns), $sValue);
+
+            $aHmacSource[] = $sKey . '=' . $sValue;
+        }
+
+        // Sort the key value pairs lexographically and then generate the HMAC signature of the provided data
+        sort($aHmacSource);
+        $sHmacBase = implode('&', $aHmacSource);
+        $sHmacString = hash_hmac('sha256', $sHmacBase, $this->_sApiSecretKey);
+
+        // Verify that the signatures match
+        if($sHmacString !== $aRequestData['hmac'])
+            return $this->_log("The HMAC provided by Shopify ({$requestData['hmac']}) doesn't match the HMAC verification ($sHmacString).");
+
+        // Make the access token request to Shopify
+        $aResponse = $this->_callAuthorize(['code' => $requestData['code']]);
+
+        /**
+         * TODO: Decode data and retrieve Access token.
+         * 
+        // Decode the response from Shopify
+        $data = json_decode($response->getBody());
+
+        // Set the access token
+        $this->setToken($data->access_token);
+         */
+
+        return $aResponse;
+    }
+
+    /**
+     * Internal methods.
+     */
+    protected function _getAuthorizeUrl($aScopes, $sRedirectUrl, $sNonce, $bOnlineAccessMode = false)
+    {
+        $aParams = [
+            'client_id' => $this->_sApiKey,
+            'scope' => implode(',', $aScopes),
+            'redirect_uri' => $sRedirectUrl,
+            'state' => $sNonce,
+        ];
+
+        if ($bOnlineAccessMode)
+            $args['grant_options[]'] = 'per-user';
+
+        return bx_append_url_params("https://" . $this->_sShopDomain . "/admin/oauth/authorize", $aParams);
+    }
+
+    protected function _callAuthorize($aParams)
+    {
+        $aParams = array_merge([
+            'client_id'     => $this->_sApiKey,
+            'client_secret' => $this->_sApiSecretKey
+        ], $aParams);
+
+        return $this->_call('admin/oauth/access_token', $aParams, 'post-json', ['Content-Type' => 'application/json']);
+    }
+    
+    protected function _call($sRequest, $aParams, $sMethod = 'post-json', $aHeaders = [])
+    {
+        $aHeaders[] = 'Content-Type: application/json';
+        if(!empty($this->_sToken))
+            $aHeaders[] = 'X-Shopify-Access-Token: ' . $this->_sToken;
+        else
+            $aHeaders[] = 'Authorization: Basic ' . base64_encode($this->_sApiKey . ':' . $this->_sApiSecretKey);
+
+        return bx_file_get_contents($this->_sEndpoint . $sRequest, $aParams, $sMethod, $aHeaders);
+    }
+
+    protected function _log($sMessage, $bUseLog = false)
+    {
+        if($bUseLog) {
+            //TODO: Use bx_log here.
+        }
+        else
+            throw new Exception($sMessage);
+
+        return false;
+    }    
+}

+ 5 - 0
modules/boonex/ads/classes/BxAdsTemplate.php

@@ -205,6 +205,11 @@ class BxAdsTemplate extends BxBaseModTextTemplate
         $CNF = &$this->_oConfig->CNF;
 
         $aResult = parent::getUnit($aData, $aParams);
+        
+        if($this->_oConfig->isSources() && !empty($aData[$CNF['FIELD_URL']])) 
+            $aResult = array_merge($aResult, [
+                'content_url' => $aData[$CNF['FIELD_URL']],
+            ]);
 
         if($this->_oConfig->isPromotion()) {
             $sJsObject = $this->_oConfig->getJsObject('main');

+ 23 - 2
modules/boonex/ads/install/langs/en.xml

@@ -16,11 +16,24 @@
     <string name="_bx_ads_option_offer_lifetime"><![CDATA[Decline offers automatically after (in hours)]]></string>
     <string name="_bx_ads_option_internal_interested_notification"><![CDATA[Use internal notification about Interested action]]></string>
     <string name="_bx_ads_option_enable_auction"><![CDATA[Enable Auction mode]]></string>
-    <string name="_bx_ads_option_enable_source"><![CDATA[External source]]></string>
-    <string name="_bx_ads_option_source_shopify"><![CDATA[Shopify API]]></string>
+    <string name="_bx_ads_option_enable_sources"><![CDATA[Enable external sources]]></string>
     <string name="_bx_ads_option_enable_promotion"><![CDATA[Enable paid promotion]]></string>
     <string name="_bx_ads_option_promotion_cpm"><![CDATA[Promotion price (CPM)]]></string>
 
+    <string name="_bx_ads_src_cpt_shopify_admin"><![CDATA[Shopify (Admin API)]]></string>
+    <string name="_bx_ads_src_dsc_shopify_admin"><![CDATA[For a custom app created directly in the Shopify admin.]]></string>
+    <string name="_bx_ads_src_opt_cpt_active"><![CDATA[Active]]></string>
+    <string name="_bx_ads_src_opt_dsc_active"><![CDATA[Activate(Deactivate) source]]></string>
+    <string name="_bx_ads_src_opt_cpt_shop_domain"><![CDATA[Shop domain]]></string>
+    <string name="_bx_ads_src_opt_dsc_shop_domain"><![CDATA[]]></string>
+    <string name="_bx_ads_src_opt_cpt_access_token"><![CDATA[Access token]]></string>
+    <string name="_bx_ads_src_opt_dsc_access_token"><![CDATA[]]></string>
+
+
+    <string name="_bx_ads_form_sources_details"><![CDATA[Sources Details]]></string>
+    <string name="_bx_ads_form_sources_details_display_edit"><![CDATA[Edit Sources Details]]></string>
+    <string name="_bx_ads_form_sources_details_input_do_submit"><![CDATA[Submit]]></string>
+
     <string name="_bx_ads_form_category"><![CDATA[Category]]></string>
     <string name="_bx_ads_form_category_display_add"><![CDATA[Add Category]]></string>
     <string name="_bx_ads_form_category_display_edit"><![CDATA[Edit Category]]></string>
@@ -286,6 +299,11 @@
     <string name="_bx_ads_page_title_sys_offers_all"><![CDATA[All Offers]]></string>
     <string name="_bx_ads_page_title_offers_all"><![CDATA[All Offers]]></string>
     <string name="_bx_ads_page_block_title_offers_all"><![CDATA[All Offers]]></string>
+    <string name="_bx_ads_page_title_sys_sources"><![CDATA[Sources]]></string>
+    <string name="_bx_ads_page_title_sources"><![CDATA[Sources]]></string>
+    <string name="_bx_ads_page_block_title_system_sources_details"><![CDATA[Sources Details]]></string>
+    <string name="_bx_ads_page_block_title_sources_details"><![CDATA[Sources Details]]></string>
+    
 
     <string name="_bx_ads_menu_title_entry_attachments"><![CDATA[Form Attachments]]></string>
     <string name="_bx_ads_menu_set_title_entry_attachments"><![CDATA[Form Attachments]]></string>
@@ -382,6 +400,9 @@
     <string name="_bx_ads_menu_item_title_licenses"><![CDATA[Ads Licenses]]></string>
     <string name="_bx_ads_menu_item_title_system_offers_all"><![CDATA[Ads Offers]]></string>
     <string name="_bx_ads_menu_item_title_offers_all"><![CDATA[Offers]]></string>
+    <string name="_bx_ads_menu_item_title_system_sources"><![CDATA[Sources]]></string>
+    <string name="_bx_ads_menu_item_title_sources"><![CDATA[Sources]]></string>
+    
 
     <string name="_bx_ads_grid_column_title_icon"><![CDATA[Icon]]></string>
     <string name="_bx_ads_grid_column_title_title"><![CDATA[Title]]></string>

+ 15 - 7
modules/boonex/ads/install/sql/enable.sql

@@ -11,7 +11,7 @@ SET @iCategId = LAST_INSERT_ID();
 
 INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `check`, `check_error`, `extra`, `order`) VALUES
 ('bx_ads_enable_auto_approve', 'on', @iCategId, '_bx_ads_option_enable_auto_approve', 'checkbox', '', '', '', 0),
-('bx_ads_enable_source', '', @iCategId, '_bx_ads_option_enable_source', 'select', '', '', 'a:2:{s:6:"module";s:6:"bx_ads";s:6:"method";s:18:"get_options_source";}', 1),
+('bx_ads_enable_sources', '', @iCategId, '_bx_ads_option_enable_sources', 'checkbox', '', '', '', 1),
 ('bx_ads_enable_auction', '', @iCategId, '_bx_ads_option_enable_auction', 'checkbox', '', '', '', 2),
 ('bx_ads_internal_interested_notification', '', @iCategId, '_bx_ads_option_internal_interested_notification', 'checkbox', '', '', '', 4),
 
@@ -185,6 +185,13 @@ INSERT INTO `sys_objects_page`(`object`, `title_system`, `title`, `module`, `lay
 INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title_system`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`) VALUES 
 ('bx_ads_administration', 1, 'bx_ads', '_bx_ads_page_block_title_system_manage_administration', '_bx_ads_page_block_title_manage', 11, 192, 'service', 'a:3:{s:6:"module";s:6:"bx_ads";s:6:"method";s:12:"manage_tools";s:6:"params";a:1:{i:0;s:14:"administration";}}', 0, 1, 0);
 
+-- PAGE: module sources
+INSERT INTO `sys_objects_page`(`object`, `title_system`, `title`, `module`, `layout_id`, `visible_for_levels`, `visible_for_levels_editable`, `uri`, `url`, `meta_description`, `meta_keywords`, `meta_robots`, `cache_lifetime`, `cache_editable`, `deletable`, `override_class_name`, `override_class_file`) VALUES 
+('bx_ads_sources', '_bx_ads_page_title_sys_sources', '_bx_ads_page_title_sources', 'bx_ads', 5, 2147483646, 1, 'ads-sources', 'page.php?i=ads-sources', '', '', '', 0, 1, 0, 'BxAdsPageBrowse', 'modules/boonex/ads/classes/BxAdsPageBrowse.php');
+
+INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title_system`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`) VALUES 
+('bx_ads_sources', 1, 'bx_ads', '_bx_ads_page_block_title_system_sources_details', '_bx_ads_page_block_title_sources_details', 11, 2147483646, 'service', 'a:2:{s:6:"module";s:6:"bx_ads";s:6:"method";s:21:"block_sources_details";}}', 0, 1, 0);
+
 -- PAGE: manage own licenses
 INSERT INTO `sys_objects_page`(`object`, `title_system`, `title`, `module`, `layout_id`, `visible_for_levels`, `visible_for_levels_editable`, `uri`, `url`, `meta_description`, `meta_keywords`, `meta_robots`, `cache_lifetime`, `cache_editable`, `deletable`, `override_class_name`, `override_class_file`) VALUES 
 ('bx_ads_licenses', '_bx_ads_page_title_sys_licenses', '_bx_ads_page_title_licenses', 'bx_ads', 5, 2147483647, 1, 'ads-licenses', '', '', '', '', 0, 1, 0, 'BxAdsPageLicenses', 'modules/boonex/ads/classes/BxAdsPageLicenses.php');
@@ -342,12 +349,13 @@ INSERT INTO `sys_objects_menu`(`object`, `title`, `set_name`, `module`, `templat
 INSERT INTO `sys_menu_sets`(`set_name`, `module`, `title`, `deletable`) VALUES 
 ('bx_ads_submenu', 'bx_ads', '_bx_ads_menu_set_title_submenu', 0);
 
-INSERT INTO `sys_menu_items`(`set_name`, `module`, `name`, `title_system`, `title`, `link`, `onclick`, `target`, `icon`, `submenu_object`, `visible_for_levels`, `active`, `copyable`, `order`) VALUES 
-('bx_ads_submenu', 'bx_ads', 'ads-home', '_bx_ads_menu_item_title_system_entries_public', '_bx_ads_menu_item_title_entries_public', 'page.php?i=ads-home', '', '', '', '', 2147483647, 1, 1, 1),
-('bx_ads_submenu', 'bx_ads', 'ads-popular', '_bx_ads_menu_item_title_system_entries_popular', '_bx_ads_menu_item_title_entries_popular', 'page.php?i=ads-popular', '', '', '', '', 2147483647, 1, 1, 2),
-('bx_ads_submenu', 'bx_ads', 'ads-categories', '_bx_ads_menu_item_title_system_entries_categories', '_bx_ads_menu_item_title_entries_categories', 'page.php?i=ads-categories', '', '', '', '', 2147483647, 1, 1, 3),
-('bx_ads_submenu', 'bx_ads', 'ads-search', '_bx_ads_menu_item_title_system_entries_search', '_bx_ads_menu_item_title_entries_search', 'page.php?i=ads-search', '', '', '', '', 2147483647, 1, 1, 4),
-('bx_ads_submenu', 'bx_ads', 'ads-manage', '_bx_ads_menu_item_title_system_entries_manage', '_bx_ads_menu_item_title_entries_manage', 'page.php?i=ads-manage', '', '', '', '', 2147483646, 1, 1, 5);
+INSERT INTO `sys_menu_items`(`set_name`, `module`, `name`, `title_system`, `title`, `link`, `onclick`, `target`, `icon`, `submenu_object`, `visible_for_levels`, `visibility_custom`, `active`, `copyable`, `order`) VALUES 
+('bx_ads_submenu', 'bx_ads', 'ads-home', '_bx_ads_menu_item_title_system_entries_public', '_bx_ads_menu_item_title_entries_public', 'page.php?i=ads-home', '', '', '', '', 2147483647, '', 1, 1, 1),
+('bx_ads_submenu', 'bx_ads', 'ads-popular', '_bx_ads_menu_item_title_system_entries_popular', '_bx_ads_menu_item_title_entries_popular', 'page.php?i=ads-popular', '', '', '', '', 2147483647, '', 1, 1, 2),
+('bx_ads_submenu', 'bx_ads', 'ads-categories', '_bx_ads_menu_item_title_system_entries_categories', '_bx_ads_menu_item_title_entries_categories', 'page.php?i=ads-categories', '', '', '', '', 2147483647, '', 1, 1, 3),
+('bx_ads_submenu', 'bx_ads', 'ads-search', '_bx_ads_menu_item_title_system_entries_search', '_bx_ads_menu_item_title_entries_search', 'page.php?i=ads-search', '', '', '', '', 2147483647, '', 1, 1, 4),
+('bx_ads_submenu', 'bx_ads', 'ads-manage', '_bx_ads_menu_item_title_system_entries_manage', '_bx_ads_menu_item_title_entries_manage', 'page.php?i=ads-manage', '', '', '', '', 2147483646, '', 1, 1, 5),
+('bx_ads_submenu', 'bx_ads', 'ads-sources', '_bx_ads_menu_item_title_system_sources', '_bx_ads_menu_item_title_sources', 'page.php?i=ads-sources', '', '', '', '', 2147483646, 'a:2:{s:6:"module";s:6:"bx_ads";s:6:"method";s:20:"is_sources_avaliable";}', 1, 1, 6);
 
 -- MENU: sub-menu for view entry
 INSERT INTO `sys_objects_menu`(`object`, `title`, `set_name`, `module`, `template_id`, `deletable`, `active`, `override_class_name`, `override_class_file`) VALUES 

+ 153 - 86
modules/boonex/ads/install/sql/install.sql

@@ -7,11 +7,13 @@ CREATE TABLE IF NOT EXISTS `bx_ads_entries` (
   `sold` int(11) NOT NULL,
   `shipped` int(11) NOT NULL,
   `received` int(11) NOT NULL,
+  `source_type` varchar(32) NOT NULL DEFAULT '',
   `source` varchar(255) NOT NULL DEFAULT '',
   `category` int(11) NOT NULL,
   `thumb` int(11) NOT NULL,
   `name` varchar(255) NOT NULL,
   `title` varchar(255) NOT NULL,
+  `url` varchar(255) NOT NULL,
   `price` float NOT NULL,
   `auction` tinyint(4) NOT NULL DEFAULT '0',
   `quantity` int(11) NOT NULL default '1',
@@ -168,6 +170,56 @@ CREATE TABLE IF NOT EXISTS `bx_ads_interested_track` (
   KEY `interested` (`entry_id`, `profile_id`)
 );
 
+-- TABLE: sources
+CREATE TABLE IF NOT EXISTS `bx_ads_sources` (
+  `id` int(11) NOT NULL auto_increment,
+  `name` varchar(64) NOT NULL default '',
+  `caption` varchar(128) NOT NULL default '',
+  `description` varchar(128) NOT NULL default '',
+  `option_prefix` varchar(32) NOT NULL default '',
+  `active` tinyint(4) NOT NULL default '0',
+  `order` tinyint(4) NOT NULL default '0',
+  `class_name` varchar(128) NOT NULL default '',
+  `class_file` varchar(255) NOT NULL  default '',
+  PRIMARY KEY(`id`)
+);
+
+CREATE TABLE IF NOT EXISTS `bx_ads_sources_options` (
+  `id` int(11) NOT NULL auto_increment,
+  `source_id` varchar(64) NOT NULL default '',
+  `name` varchar(64) NOT NULL default '',
+  `type` varchar(64) NOT NULL default 'text',
+  `caption` varchar(255) NOT NULL default '',
+  `description` text NOT NULL default '',
+  `extra` varchar(255) NOT NULL default '',
+  `check_type` varchar(64) NOT NULL default '',
+  `check_params` varchar(128) NOT NULL default '',
+  `check_error` varchar(128) NOT NULL default '',
+  `order` tinyint(4) NOT NULL default '0',
+  PRIMARY KEY(`id`),
+  UNIQUE KEY `name`(`name`)
+);
+
+CREATE TABLE IF NOT EXISTS `bx_ads_sources_options_values` (
+  `id` int(11) NOT NULL auto_increment,
+  `profile_id` int(11) NOT NULL default '0',
+  `option_id` int(11) NOT NULL default '0',  
+  `value` varchar(255) NOT NULL default '',
+  PRIMARY KEY(`id`),
+  UNIQUE KEY `value`(`profile_id`, `option_id`)
+);
+
+-- Shopify (Admin API) source
+INSERT INTO `bx_ads_sources`(`name`, `caption`, `description`, `option_prefix`, `active`, `order`, `class_name`) VALUES
+('shopify_admin', '_bx_ads_src_cpt_shopify_admin', '_bx_ads_src_dsc_shopify_admin', 'shf_adm_', 1, 1, 'BxAdsSourceShopifyAdmin');
+SET @iSourceId = LAST_INSERT_ID();
+
+INSERT INTO `bx_ads_sources_options`(`source_id`, `name`, `type`, `caption`, `description`, `extra`, `check_type`, `check_params`, `check_error`, `order`) VALUES
+(@iSourceId, 'shf_adm_active', 'checkbox', '_bx_ads_src_opt_cpt_active', '_bx_ads_src_opt_dsc_active', '', '', '', '', 1),
+(@iSourceId, 'shf_adm_shop_domain', 'text', '_bx_ads_src_opt_cpt_shop_domain', '_bx_ads_src_opt_dsc_shop_domain', '', '', '', '', 2),
+(@iSourceId, 'shf_adm_access_token', 'text', '_bx_ads_src_opt_cpt_access_token', '_bx_ads_src_opt_dsc_access_token', '', '', '', '', 3);
+
+
 -- TABLE: types of categories
 CREATE TABLE IF NOT EXISTS `bx_ads_commodities` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
@@ -665,6 +717,13 @@ INSERT INTO `sys_transcoder_filters` (`transcoder_object`, `filter`, `filter_par
 ('bx_ads_gallery_files', 'Resize', 'a:4:{s:1:"w";s:3:"600";s:1:"h";s:3:"600";s:13:"square_resize";s:1:"1";s:10:"force_type";s:3:"jpg";}', '0');
 
 
+-- FORMS: sources details
+INSERT INTO `sys_objects_form` (`object`, `module`, `title`, `action`, `form_attrs`, `submit_name`, `table`, `key`, `uri`, `uri_title`, `params`, `deletable`, `active`, `override_class_name`, `override_class_file`) VALUES
+('bx_ads_form_sources_details', 'bx_ads', '_bx_ads_form_sources_details', '', '', 'submit', '', 'id', '', '', 'a:1:{s:14:"checker_helper";s:36:"BxAdsSourcesDetailsFormCheckerHelper";}', 0, 1, 'BxAdsFormSourcesDetails', 'modules/boonex/ads/classes/BxAdsFormSourcesDetails.php');
+
+INSERT INTO `sys_form_displays` (`display_name`, `module`, `object`, `title`, `view_mode`) VALUES
+('bx_ads_form_sources_details_edit', 'bx_ads', 'bx_ads_form_sources_details', '_bx_ads_form_sources_details_display_edit', 0);
+
 -- FORMS: category
 INSERT INTO `sys_objects_form`(`object`, `module`, `title`, `action`, `form_attrs`, `table`, `key`, `uri`, `uri_title`, `submit_name`, `params`, `deletable`, `active`, `override_class_name`, `override_class_file`) VALUES 
 ('bx_ads_category', 'bx_ads', '_bx_ads_form_category', '', 'a:1:{s:7:"enctype";s:19:"multipart/form-data";}', 'bx_ads_categories', 'id', '', '', 'a:1:{i:0;s:9:"do_submit";}', '', 0, 1, 'BxAdsFormCategory', 'modules/boonex/ads/classes/BxAdsFormCategory.php');
@@ -754,7 +813,9 @@ INSERT INTO `sys_form_inputs`(`object`, `module`, `name`, `value`, `values`, `ch
 ('bx_ads', 'bx_ads', 'changed', '', '', 0, 'datetime', '_bx_ads_form_entry_input_sys_date_changed', '_bx_ads_form_entry_input_date_changed', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0),
 ('bx_ads', 'bx_ads', 'attachments', '', '', 0, 'custom', '_bx_ads_form_entry_input_sys_attachments', '', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0),
 ('bx_ads', 'bx_ads', 'labels', '', '', 0, 'custom', '_sys_form_input_sys_labels', '_sys_form_input_labels', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0),
+('bx_ads', 'bx_ads', 'source_type', '', '', 0, 'hidden', '_bx_ads_form_entry_input_sys_source_type', '_bx_ads_form_entry_input_source_type', '', 0, 0, 0, '', '', '', '', '', '', 'Xss', '', 1, 0),
 ('bx_ads', 'bx_ads', 'source', '', '', 0, 'text', '_bx_ads_form_entry_input_sys_source', '_bx_ads_form_entry_input_source', '', 0, 0, 0, '', '', '', '', '', '', 'Xss', '', 1, 0),
+('bx_ads', 'bx_ads', 'url', '', '', 0, 'hidden', '_bx_ads_form_entry_input_sys_url', '', '', 0, 0, 0, '', '', '', '', '', '', 'Xss', '', 1, 0),
 ('bx_ads', 'bx_ads', 'budget_total', '', '', 0, 'text', '_bx_ads_form_entry_input_sys_budget_total', '_bx_ads_form_entry_input_budget_total', '', 0, 0, 0, '', '', '', '', '', '', 'Float', '', 1, 0),
 ('bx_ads', 'bx_ads', 'budget_daily', '', '', 0, 'text', '_bx_ads_form_entry_input_sys_budget_daily', '_bx_ads_form_entry_input_budget_daily', '', 0, 0, 0, '', '', '', '', '', '', 'Float', '', 1, 0);
 
@@ -769,98 +830,104 @@ INSERT INTO `sys_form_display_inputs`(`display_name`, `input_name`, `visible_for
 ('bx_ads_entry_delete', 'delete_confirm', 2147483647, 1, 1),
 ('bx_ads_entry_delete', 'do_submit', 2147483647, 1, 2),
 
-('bx_ads_entry_price_add', 'source', 2147483647, 1, 0),
-('bx_ads_entry_price_add', 'category', 2147483647, 1, 1),
-('bx_ads_entry_price_add', 'category_view', 2147483647, 1, 2),
-('bx_ads_entry_price_add', 'title', 2147483647, 1, 3),
-('bx_ads_entry_price_add', 'name', 2147483647, 1, 4),
-('bx_ads_entry_price_add', 'price', 2147483647, 1, 5),
-('bx_ads_entry_price_add', 'auction', 2147483647, 1, 6),
-('bx_ads_entry_price_add', 'quantity', 2147483647, 1, 7),
-('bx_ads_entry_price_add', 'text', 2147483647, 1, 8),
-('bx_ads_entry_price_add', 'attachments', 2147483647, 1, 9),
-('bx_ads_entry_price_add', 'pictures', 2147483647, 1, 10),
-('bx_ads_entry_price_add', 'videos', 2147483647, 1, 11),
-('bx_ads_entry_price_add', 'files', 2147483647, 1, 12),
-('bx_ads_entry_price_add', 'polls', 2147483647, 1, 13),
-('bx_ads_entry_price_add', 'covers', 2147483647, 1, 14),
-('bx_ads_entry_price_add', 'budget_total', 2147483647, 1, 15),
-('bx_ads_entry_price_add', 'budget_daily', 2147483647, 1, 16),
-('bx_ads_entry_price_add', 'allow_view_to', 2147483647, 1, 17),
-('bx_ads_entry_price_add', 'cf', 2147483647, 1, 18),
-('bx_ads_entry_price_add', 'notes_purchased', 2147483647, 1, 19),
-('bx_ads_entry_price_add', 'location', 2147483647, 1, 20),
-('bx_ads_entry_price_add', 'do_submit', 2147483647, 1, 21),
-
-('bx_ads_entry_price_edit', 'source', 2147483647, 1, 0),
-('bx_ads_entry_price_edit', 'category_view', 2147483647, 1, 1),
-('bx_ads_entry_price_edit', 'title', 2147483647, 1, 2),
-('bx_ads_entry_price_edit', 'name', 2147483647, 1, 3),
-('bx_ads_entry_price_edit', 'price', 2147483647, 1, 4),
-('bx_ads_entry_price_edit', 'auction', 2147483647, 1, 5),
-('bx_ads_entry_price_edit', 'quantity', 2147483647, 1, 6),
-('bx_ads_entry_price_edit', 'text', 2147483647, 1, 7),
-('bx_ads_entry_price_edit', 'attachments', 2147483647, 1, 8),
-('bx_ads_entry_price_edit', 'pictures', 2147483647, 1, 9),
-('bx_ads_entry_price_edit', 'videos', 2147483647, 1, 10),
-('bx_ads_entry_price_edit', 'files', 2147483647, 1, 11),
-('bx_ads_entry_price_edit', 'polls', 2147483647, 1, 12),
-('bx_ads_entry_price_edit', 'covers', 2147483647, 1, 13),
-('bx_ads_entry_price_edit', 'allow_view_to', 2147483647, 1, 14),
-('bx_ads_entry_price_edit', 'cf', 2147483647, 1, 15),
-('bx_ads_entry_price_edit', 'notes_purchased', 2147483647, 1, 16),
-('bx_ads_entry_price_edit', 'location', 2147483647, 1, 17),
-('bx_ads_entry_price_edit', 'do_submit', 2147483647, 1, 18),
+('bx_ads_entry_price_add', 'url', 2147483647, 1, 0),
+('bx_ads_entry_price_add', 'source_type', 2147483647, 1, 1),
+('bx_ads_entry_price_add', 'source', 2147483647, 1, 2),
+('bx_ads_entry_price_add', 'category', 2147483647, 1, 3),
+('bx_ads_entry_price_add', 'category_view', 2147483647, 1, 4),
+('bx_ads_entry_price_add', 'title', 2147483647, 1, 5),
+('bx_ads_entry_price_add', 'name', 2147483647, 1, 6),
+('bx_ads_entry_price_add', 'price', 2147483647, 1, 7),
+('bx_ads_entry_price_add', 'auction', 2147483647, 1, 8),
+('bx_ads_entry_price_add', 'quantity', 2147483647, 1, 9),
+('bx_ads_entry_price_add', 'text', 2147483647, 1, 10),
+('bx_ads_entry_price_add', 'attachments', 2147483647, 1, 11),
+('bx_ads_entry_price_add', 'pictures', 2147483647, 1, 12),
+('bx_ads_entry_price_add', 'videos', 2147483647, 1, 13),
+('bx_ads_entry_price_add', 'files', 2147483647, 1, 14),
+('bx_ads_entry_price_add', 'polls', 2147483647, 1, 15),
+('bx_ads_entry_price_add', 'covers', 2147483647, 1, 16),
+('bx_ads_entry_price_add', 'budget_total', 2147483647, 1, 17),
+('bx_ads_entry_price_add', 'budget_daily', 2147483647, 1, 18),
+('bx_ads_entry_price_add', 'allow_view_to', 2147483647, 1, 19),
+('bx_ads_entry_price_add', 'cf', 2147483647, 1, 20),
+('bx_ads_entry_price_add', 'notes_purchased', 2147483647, 1, 21),
+('bx_ads_entry_price_add', 'location', 2147483647, 1, 22),
+('bx_ads_entry_price_add', 'do_submit', 2147483647, 1, 23),
+
+('bx_ads_entry_price_edit', 'source_type', 2147483647, 1, 1),
+('bx_ads_entry_price_edit', 'source', 2147483647, 1, 2),
+('bx_ads_entry_price_edit', 'category_view', 2147483647, 1, 3),
+('bx_ads_entry_price_edit', 'title', 2147483647, 1, 4),
+('bx_ads_entry_price_edit', 'name', 2147483647, 1, 5),
+('bx_ads_entry_price_edit', 'price', 2147483647, 1, 6),
+('bx_ads_entry_price_edit', 'auction', 2147483647, 1, 7),
+('bx_ads_entry_price_edit', 'quantity', 2147483647, 1, 8),
+('bx_ads_entry_price_edit', 'text', 2147483647, 1, 9),
+('bx_ads_entry_price_edit', 'attachments', 2147483647, 1, 10),
+('bx_ads_entry_price_edit', 'pictures', 2147483647, 1, 11),
+('bx_ads_entry_price_edit', 'videos', 2147483647, 1, 12),
+('bx_ads_entry_price_edit', 'files', 2147483647, 1, 13),
+('bx_ads_entry_price_edit', 'polls', 2147483647, 1, 14),
+('bx_ads_entry_price_edit', 'covers', 2147483647, 1, 15),
+('bx_ads_entry_price_edit', 'allow_view_to', 2147483647, 1, 16),
+('bx_ads_entry_price_edit', 'cf', 2147483647, 1, 17),
+('bx_ads_entry_price_edit', 'notes_purchased', 2147483647, 1, 18),
+('bx_ads_entry_price_edit', 'location', 2147483647, 1, 19),
+('bx_ads_entry_price_edit', 'do_submit', 2147483647, 1, 20),
 
 ('bx_ads_entry_price_view', 'category_view', 2147483647, 1, 1),
 ('bx_ads_entry_price_view', 'price', 2147483647, 1, 2),
 ('bx_ads_entry_price_view', 'added', 2147483647, 1, 3),
 ('bx_ads_entry_price_view', 'changed', 2147483647, 1, 4),
 
-('bx_ads_entry_price_year_add', 'source', 2147483647, 1, 0),
-('bx_ads_entry_price_year_add', 'category', 2147483647, 1, 1),
-('bx_ads_entry_price_year_add', 'category_view', 2147483647, 1, 2),
-('bx_ads_entry_price_year_add', 'title', 2147483647, 1, 3),
-('bx_ads_entry_price_year_add', 'name', 2147483647, 1, 4),
-('bx_ads_entry_price_year_add', 'price', 2147483647, 1, 5),
-('bx_ads_entry_price_year_add', 'auction', 2147483647, 1, 6),
-('bx_ads_entry_price_year_add', 'quantity', 2147483647, 1, 7),
-('bx_ads_entry_price_year_add', 'year', 2147483647, 1, 8),
-('bx_ads_entry_price_year_add', 'text', 2147483647, 1, 9),
-('bx_ads_entry_price_year_add', 'attachments', 2147483647, 1, 10),
-('bx_ads_entry_price_year_add', 'pictures', 2147483647, 1, 11),
-('bx_ads_entry_price_year_add', 'videos', 2147483647, 1, 12),
-('bx_ads_entry_price_year_add', 'files', 2147483647, 1, 13),
-('bx_ads_entry_price_year_add', 'polls', 2147483647, 1, 14),
-('bx_ads_entry_price_year_add', 'covers', 2147483647, 1, 15),
-('bx_ads_entry_price_year_add', 'budget_total', 2147483647, 1, 16),
-('bx_ads_entry_price_year_add', 'budget_daily', 2147483647, 1, 17),
-('bx_ads_entry_price_year_add', 'allow_view_to', 2147483647, 1, 18),
-('bx_ads_entry_price_year_add', 'cf', 2147483647, 1, 19),
-('bx_ads_entry_price_year_add', 'notes_purchased', 2147483647, 1, 20),
-('bx_ads_entry_price_year_add', 'location', 2147483647, 1, 21),
-('bx_ads_entry_price_year_add', 'do_submit', 2147483647, 1, 22),
-
-('bx_ads_entry_price_year_edit', 'source', 2147483647, 1, 0),
-('bx_ads_entry_price_year_edit', 'category_view', 2147483647, 1, 1),
-('bx_ads_entry_price_year_edit', 'title', 2147483647, 1, 2),
-('bx_ads_entry_price_year_edit', 'name', 2147483647, 1, 3),
-('bx_ads_entry_price_year_edit', 'price', 2147483647, 1, 4),
-('bx_ads_entry_price_year_edit', 'auction', 2147483647, 1, 5),
-('bx_ads_entry_price_year_edit', 'quantity', 2147483647, 1, 6),
-('bx_ads_entry_price_year_edit', 'year', 2147483647, 1, 7),
-('bx_ads_entry_price_year_edit', 'text', 2147483647, 1, 8),
-('bx_ads_entry_price_year_edit', 'attachments', 2147483647, 1, 9),
-('bx_ads_entry_price_year_edit', 'pictures', 2147483647, 1, 10),
-('bx_ads_entry_price_year_edit', 'videos', 2147483647, 1, 11),
-('bx_ads_entry_price_year_edit', 'files', 2147483647, 1, 12),
-('bx_ads_entry_price_year_edit', 'polls', 2147483647, 1, 13),
-('bx_ads_entry_price_year_edit', 'covers', 2147483647, 1, 14),
-('bx_ads_entry_price_year_edit', 'allow_view_to', 2147483647, 1, 15),
-('bx_ads_entry_price_year_edit', 'cf', 2147483647, 1, 16),
-('bx_ads_entry_price_year_edit', 'notes_purchased', 2147483647, 1, 17),
-('bx_ads_entry_price_year_edit', 'location', 2147483647, 1, 18),
-('bx_ads_entry_price_year_edit', 'do_submit', 2147483647, 1, 19),
+('bx_ads_entry_price_year_add', 'url', 2147483647, 1, 0),
+('bx_ads_entry_price_year_add', 'source_type', 2147483647, 1, 1),
+('bx_ads_entry_price_year_add', 'source', 2147483647, 1, 2),
+('bx_ads_entry_price_year_add', 'category', 2147483647, 1, 3),
+('bx_ads_entry_price_year_add', 'category_view', 2147483647, 1, 4),
+('bx_ads_entry_price_year_add', 'title', 2147483647, 1, 5),
+('bx_ads_entry_price_year_add', 'name', 2147483647, 1, 6),
+('bx_ads_entry_price_year_add', 'price', 2147483647, 1, 7),
+('bx_ads_entry_price_year_add', 'auction', 2147483647, 1, 8),
+('bx_ads_entry_price_year_add', 'quantity', 2147483647, 1, 9),
+('bx_ads_entry_price_year_add', 'year', 2147483647, 1, 10),
+('bx_ads_entry_price_year_add', 'text', 2147483647, 1, 11),
+('bx_ads_entry_price_year_add', 'attachments', 2147483647, 1, 12),
+('bx_ads_entry_price_year_add', 'pictures', 2147483647, 1, 13),
+('bx_ads_entry_price_year_add', 'videos', 2147483647, 1, 14),
+('bx_ads_entry_price_year_add', 'files', 2147483647, 1, 15),
+('bx_ads_entry_price_year_add', 'polls', 2147483647, 1, 16),
+('bx_ads_entry_price_year_add', 'covers', 2147483647, 1, 17),
+('bx_ads_entry_price_year_add', 'budget_total', 2147483647, 1, 18),
+('bx_ads_entry_price_year_add', 'budget_daily', 2147483647, 1, 19),
+('bx_ads_entry_price_year_add', 'allow_view_to', 2147483647, 1, 20),
+('bx_ads_entry_price_year_add', 'cf', 2147483647, 1, 21),
+('bx_ads_entry_price_year_add', 'notes_purchased', 2147483647, 1, 22),
+('bx_ads_entry_price_year_add', 'location', 2147483647, 1, 23),
+('bx_ads_entry_price_year_add', 'do_submit', 2147483647, 1, 24),
+
+('bx_ads_entry_price_year_edit', 'source_type', 2147483647, 1, 1),
+('bx_ads_entry_price_year_edit', 'source', 2147483647, 1, 2),
+('bx_ads_entry_price_year_edit', 'category_view', 2147483647, 1, 3),
+('bx_ads_entry_price_year_edit', 'title', 2147483647, 1, 4),
+('bx_ads_entry_price_year_edit', 'name', 2147483647, 1, 5),
+('bx_ads_entry_price_year_edit', 'price', 2147483647, 1, 6),
+('bx_ads_entry_price_year_edit', 'auction', 2147483647, 1, 7),
+('bx_ads_entry_price_year_edit', 'quantity', 2147483647, 1, 8),
+('bx_ads_entry_price_year_edit', 'year', 2147483647, 1, 9),
+('bx_ads_entry_price_year_edit', 'text', 2147483647, 1, 10),
+('bx_ads_entry_price_year_edit', 'attachments', 2147483647, 1, 11),
+('bx_ads_entry_price_year_edit', 'pictures', 2147483647, 1, 12),
+('bx_ads_entry_price_year_edit', 'videos', 2147483647, 1, 13),
+('bx_ads_entry_price_year_edit', 'files', 2147483647, 1, 14),
+('bx_ads_entry_price_year_edit', 'polls', 2147483647, 1, 15),
+('bx_ads_entry_price_year_edit', 'covers', 2147483647, 1, 16),
+('bx_ads_entry_price_year_edit', 'allow_view_to', 2147483647, 1, 17),
+('bx_ads_entry_price_year_edit', 'cf', 2147483647, 1, 18),
+('bx_ads_entry_price_year_edit', 'notes_purchased', 2147483647, 1, 19),
+('bx_ads_entry_price_year_edit', 'location', 2147483647, 1, 20),
+('bx_ads_entry_price_year_edit', 'do_submit', 2147483647, 1, 21),
 
 ('bx_ads_entry_price_year_view', 'category_view', 2147483647, 1, 1),
 ('bx_ads_entry_price_year_view', 'price', 2147483647, 1, 2),

+ 1 - 1
modules/boonex/ads/install/sql/uninstall.sql

@@ -1,6 +1,6 @@
 
 -- TABLES
-DROP TABLE IF EXISTS `bx_ads_entries`, `bx_ads_categories_types`, `bx_ads_categories`, `bx_ads_interested_track`, `bx_ads_commodities`, `bx_ads_licenses`, `bx_ads_licenses_deleted`, `bx_ads_promo_licenses`, `bx_ads_promo_licenses_deleted`, `bx_ads_promo_tracker`, `bx_ads_covers`, `bx_ads_files`, `bx_ads_photos`, `bx_ads_photos_resized`, `bx_ads_videos`, `bx_ads_videos_resized`, `bx_ads_cmts`, `bx_ads_cmts_notes`, `bx_ads_reviews`, `bx_ads_votes`, `bx_ads_votes_track`, `bx_ads_reactions`, `bx_ads_reactions_track`, `bx_ads_views_track`, `bx_ads_meta_keywords`, `bx_ads_meta_locations`, `bx_ads_meta_mentions`, `bx_ads_reports`, `bx_ads_reports_track`, `bx_ads_favorites_track`, `bx_ads_favorites_lists`, `bx_ads_scores`, `bx_ads_scores_track`, `bx_ads_polls`, `bx_ads_polls_answers`, `bx_ads_polls_answers_votes`, `bx_ads_polls_answers_votes_track`, `bx_ads_offers`;
+DROP TABLE IF EXISTS `bx_ads_entries`, `bx_ads_categories_types`, `bx_ads_categories`, `bx_ads_interested_track`, `bx_ads_sources`, `bx_ads_sources_options`, `bx_ads_sources_options_values`, `bx_ads_commodities`, `bx_ads_licenses`, `bx_ads_licenses_deleted`, `bx_ads_promo_licenses`, `bx_ads_promo_licenses_deleted`, `bx_ads_promo_tracker`, `bx_ads_covers`, `bx_ads_files`, `bx_ads_photos`, `bx_ads_photos_resized`, `bx_ads_videos`, `bx_ads_videos_resized`, `bx_ads_cmts`, `bx_ads_cmts_notes`, `bx_ads_reviews`, `bx_ads_votes`, `bx_ads_votes_track`, `bx_ads_reactions`, `bx_ads_reactions_track`, `bx_ads_views_track`, `bx_ads_meta_keywords`, `bx_ads_meta_locations`, `bx_ads_meta_mentions`, `bx_ads_reports`, `bx_ads_reports_track`, `bx_ads_favorites_track`, `bx_ads_favorites_lists`, `bx_ads_scores`, `bx_ads_scores_track`, `bx_ads_polls`, `bx_ads_polls_answers`, `bx_ads_polls_answers_votes`, `bx_ads_polls_answers_votes_track`, `bx_ads_offers`;
 
 -- STORAGES & TRANSCODERS
 DELETE FROM `sys_objects_storage` WHERE `object` LIKE 'bx_ads_%';

+ 58 - 0
modules/boonex/ads/js/form.js

@@ -59,6 +59,59 @@ BxAdsForm.prototype.onSelectCategory = function(oData) {
         window[this._sObjNameForm].resetChanged();
 };
 
+BxAdsForm.prototype.loadFromSource = function(oSource, sSourceTypeId, sSourceId) {
+    var $this = this;
+
+    var oDate = new Date();
+    var oForm = jQuery(oSource).parents('.bx-form-advanced:first');
+
+    var oSourceType = oForm.find("[name='" + sSourceTypeId + "']");
+    var sSourceType = oSourceType.val();
+    var bSourceType = sSourceType.length != 0;
+
+    var oSource = oForm.find("[name='" + sSourceId + "']");
+    var sSource = oSource.val();
+    var bSource = sSource.length != 0;
+
+    if(!bSourceType || !bSource)
+        return;
+
+    this.loadingInField(oSource, true);
+
+    jQuery.get(
+        this._sActionsUrl + 'load_entry_from_source',
+        {
+            source_type: sSourceType,
+            source: sSource,
+            _t: oDate.getTime()
+        },
+        function(oData) {
+            $this.loadingInField(oSource, false);
+
+            if(!oData || oData.code == undefined || parseInt(oData.code) != 0)
+                return;
+
+            Object.keys(oData.fields).forEach(sKey => {
+                var oField = oForm.find("[name='" + sKey + "']");
+                if(oField.lenght == 0)
+                    return;
+
+                switch (oData.fields[sKey].type) {
+                    case 'hidden':
+                    case 'text':
+                        oField.val(oData.fields[sKey].value);
+                        break;
+
+                    case 'text_html':
+                        bx_editor_insert_html(oField.attr('id'), '', oData.fields[sKey].value);
+                        break;
+                }
+            });
+        },
+        'json'
+    );
+};
+
 BxAdsForm.prototype.checkName = function(oSource, sTitleId, sNameId, iId) {
     var oDate = new Date();
     var oForm = jQuery(oSource).parents('.bx-form-advanced:first');
@@ -107,6 +160,11 @@ BxAdsForm.prototype.loadingInBlock = function(e, bShow) {
     bx_loading(oParent, bShow);
 };
 
+BxAdsForm.prototype.loadingInField = function(e, bShow) {
+    var oParent = $(e).length ? $(e).parents('.bx-form-element-wrapper:first') : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
 BxAdsForm.prototype._getDefaultData = function() {
     var oDate = new Date();
     return jQuery.extend({}, this._oRequestParams, {_t:oDate.getTime()});

+ 5 - 0
modules/boonex/artificer/data/template/system/css/common.css

@@ -306,6 +306,11 @@ button.bx-btn:focus .bx-loading-ajax-btn {
     outline: 0;	
 }
 
+.bx-form-element-wrapper .bx-loading-ajax {
+    width: 4rem;
+    height: 4rem;
+}
+
 /*-- Show More --*/
 .bx-overflow-ready {
     max-height: 16rem;

+ 20 - 3
modules/boonex/russian/data/langs/bx_ads/ru.xml

@@ -16,10 +16,22 @@
     <string name="_bx_ads_option_offer_lifetime"><![CDATA[Автоматически отклонять предложения по истечении (в часах)]]></string>
     <string name="_bx_ads_option_internal_interested_notification"><![CDATA[Использовать встроенную нотификацию о действии 'Интересно']]></string>
     <string name="_bx_ads_option_enable_auction"><![CDATA[Включить режим аукциона]]></string>
-    <string name="_bx_ads_option_enable_source"><![CDATA[Внешний источник]]></string>
-    <string name="_bx_ads_option_source_shopify"><![CDATA[Shopify API]]></string>
+    <string name="_bx_ads_option_enable_sources"><![CDATA[Включить внешние источники]]></string>
     <string name="_bx_ads_option_enable_promotion"><![CDATA[Включить платную рекламу]]></string>
     <string name="_bx_ads_option_promotion_cpm"><![CDATA[Цена за рекламу (цена за 1000 показов)]]></string>
+    
+    <string name="_bx_ads_src_cpt_shopify_admin"><![CDATA[Shopify (Admin API)]]></string>
+    <string name="_bx_ads_src_dsc_shopify_admin"><![CDATA[Для пользовательского приложения, созданного непосредственно в админ панеле Shopify.]]></string>
+    <string name="_bx_ads_src_opt_cpt_active"><![CDATA[Активировать]]></string>
+    <string name="_bx_ads_src_opt_dsc_active"><![CDATA[Активировать(деактивировать) источник.]]></string>
+    <string name="_bx_ads_src_opt_cpt_shop_domain"><![CDATA[Домен магазина]]></string>
+    <string name="_bx_ads_src_opt_dsc_shop_domain"><![CDATA[]]></string>
+    <string name="_bx_ads_src_opt_cpt_access_token"><![CDATA[Токен доступа]]></string>
+    <string name="_bx_ads_src_opt_dsc_access_token"><![CDATA[]]></string>
+
+    <string name="_bx_ads_form_sources_details"><![CDATA[Детали источников]]></string>
+    <string name="_bx_ads_form_sources_details_display_edit"><![CDATA[Редактировать детали источников]]></string>
+    <string name="_bx_ads_form_sources_details_input_do_submit"><![CDATA[Выполнить]]></string>
 
     <string name="_bx_ads_form_category"><![CDATA[Категория]]></string>
     <string name="_bx_ads_form_category_display_add"><![CDATA[Добавить категорию]]></string>
@@ -286,6 +298,10 @@
     <string name="_bx_ads_page_title_sys_offers_all"><![CDATA[Все предложения]]></string>
     <string name="_bx_ads_page_title_offers_all"><![CDATA[Все предложения]]></string>
     <string name="_bx_ads_page_block_title_offers_all"><![CDATA[Все предложения]]></string>
+    <string name="_bx_ads_page_title_sys_sources"><![CDATA[Источники]]></string>
+    <string name="_bx_ads_page_title_sources"><![CDATA[Источники]]></string>
+    <string name="_bx_ads_page_block_title_system_sources_details"><![CDATA[Детали источников]]></string>
+    <string name="_bx_ads_page_block_title_sources_details"><![CDATA[Детали источников]]></string>
 
     <string name="_bx_ads_menu_title_entry_attachments"><![CDATA[Форма - Прикрепления]]></string>
     <string name="_bx_ads_menu_set_title_entry_attachments"><![CDATA[Форма - Прикрепления]]></string>
@@ -305,7 +321,6 @@
     <string name="_bx_ads_menu_item_title_cpa_file_html5"><![CDATA[Добавить файл]]></string>
     <string name="_bx_ads_menu_item_title_system_cpa_poll"><![CDATA[Добавить голосование]]></string>
     <string name="_bx_ads_menu_item_title_cpa_poll"><![CDATA[Добавить голосование]]></string>
-
     <string name="_bx_ads_menu_set_title_view_entry"><![CDATA[Объявление - Действия]]></string>
     <string name="_bx_ads_menu_title_view_entry"><![CDATA[Объявление - Действия]]></string>
     <string name="_bx_ads_menu_title_submenu"><![CDATA[Объявления - Подменю]]></string>
@@ -382,6 +397,8 @@
     <string name="_bx_ads_menu_item_title_licenses"><![CDATA[Лицензии к объявлениям]]></string>
     <string name="_bx_ads_menu_item_title_system_offers_all"><![CDATA[Предложения к объявлениям]]></string>
     <string name="_bx_ads_menu_item_title_offers_all"><![CDATA[Предложения]]></string>
+    <string name="_bx_ads_menu_item_title_system_sources"><![CDATA[Источники]]></string>
+    <string name="_bx_ads_menu_item_title_sources"><![CDATA[Источники]]></string>
 
     <string name="_bx_ads_grid_column_title_icon"><![CDATA[Иконка]]></string>
     <string name="_bx_ads_grid_column_title_title"><![CDATA[Заголовок]]></string>

+ 8 - 3
template/css/common.css

@@ -718,9 +718,9 @@ div.bx-popup-chain-item .bx-loading-ajax {
 }
 
 @-moz-document url-prefix() {
-	button.bx-btn .bx-loading-ajax-btn {
-		top: -1px;
-	}
+    button.bx-btn .bx-loading-ajax-btn {
+        top: -1px;
+    }
 }
 
 button.bx-btn:focus,
@@ -728,6 +728,11 @@ button.bx-btn:focus .bx-loading-ajax-btn {
     outline: 0;	
 }
 
+.bx-form-element-wrapper .bx-loading-ajax {
+    width: 4rem;
+    height: 4rem;
+}
+
 
 /* --------------- SVG loading indicator */
 svg .outer,