Doofinder_Feed - Version 1.6.5

Version Notes

Correctly handle custom boost fields.

Download this release

Release Info

Developer Carlos Escribano Rey
Extension Doofinder_Feed
Version 1.6.5
Comparing to
See all releases


Version 1.6.5

Files changed (76) hide show
  1. app/code/community/Doofinder/Feed/Block/Adminhtml/Log/View.php +76 -0
  2. app/code/community/Doofinder/Feed/Block/Adminhtml/Map/Additional.php +161 -0
  3. app/code/community/Doofinder/Feed/Block/Integration.php +34 -0
  4. app/code/community/Doofinder/Feed/Block/Settings/Buttons/Generate.php +44 -0
  5. app/code/community/Doofinder/Feed/Block/Settings/Buttons/ViewLog.php +34 -0
  6. app/code/community/Doofinder/Feed/Block/Settings/Panel/Cron.php +52 -0
  7. app/code/community/Doofinder/Feed/Block/Settings/Panel/Crondescription.php +15 -0
  8. app/code/community/Doofinder/Feed/Block/Settings/Panel/Datetime.php +49 -0
  9. app/code/community/Doofinder/Feed/Block/Settings/Panel/Description.php +54 -0
  10. app/code/community/Doofinder/Feed/Block/Settings/Panel/File.php +68 -0
  11. app/code/community/Doofinder/Feed/Block/Settings/Panel/Hashdescription.php +6 -0
  12. app/code/community/Doofinder/Feed/Block/Settings/Panel/Layerdescription.php +16 -0
  13. app/code/community/Doofinder/Feed/Block/Settings/Panel/Message.php +72 -0
  14. app/code/community/Doofinder/Feed/Helper/Data.php +552 -0
  15. app/code/community/Doofinder/Feed/Helper/Log.php +55 -0
  16. app/code/community/Doofinder/Feed/Helper/Search.php +80 -0
  17. app/code/community/Doofinder/Feed/Helper/Tax.php +24 -0
  18. app/code/community/Doofinder/Feed/Model/Adminhtml/System/Config/Backend/Cron.php +54 -0
  19. app/code/community/Doofinder/Feed/Model/Adminhtml/System/Config/Validation/Hashid.php +19 -0
  20. app/code/community/Doofinder/Feed/Model/CatalogSearch/Resource/Fulltext.php +137 -0
  21. app/code/community/Doofinder/Feed/Model/Config.php +200 -0
  22. app/code/community/Doofinder/Feed/Model/Cron.php +40 -0
  23. app/code/community/Doofinder/Feed/Model/Generator.php +837 -0
  24. app/code/community/Doofinder/Feed/Model/Log.php +19 -0
  25. app/code/community/Doofinder/Feed/Model/Map/Product/Abstract.php +645 -0
  26. app/code/community/Doofinder/Feed/Model/Map/Product/Associated.php +172 -0
  27. app/code/community/Doofinder/Feed/Model/Map/Product/Bundle.php +75 -0
  28. app/code/community/Doofinder/Feed/Model/Map/Product/Configurable.php +200 -0
  29. app/code/community/Doofinder/Feed/Model/Map/Product/Downloadable.php +20 -0
  30. app/code/community/Doofinder/Feed/Model/Map/Product/Grouped.php +62 -0
  31. app/code/community/Doofinder/Feed/Model/Map/Product/Simple.php +20 -0
  32. app/code/community/Doofinder/Feed/Model/Map/Product/Virtual.php +20 -0
  33. app/code/community/Doofinder/Feed/Model/Mysql4/Cron.php +17 -0
  34. app/code/community/Doofinder/Feed/Model/Mysql4/Cron/Collection.php +17 -0
  35. app/code/community/Doofinder/Feed/Model/Mysql4/Log.php +17 -0
  36. app/code/community/Doofinder/Feed/Model/Mysql4/Log/Collection.php +18 -0
  37. app/code/community/Doofinder/Feed/Model/Observers/Feed.php +350 -0
  38. app/code/community/Doofinder/Feed/Model/Observers/Logs.php +45 -0
  39. app/code/community/Doofinder/Feed/Model/Observers/Schedule.php +278 -0
  40. app/code/community/Doofinder/Feed/Model/Resource/Mysql4/Setup.php +14 -0
  41. app/code/community/Doofinder/Feed/Model/System/Config/Backend/Map/Additional.php +24 -0
  42. app/code/community/Doofinder/Feed/Model/System/Config/Backend/Total/Limit.php +17 -0
  43. app/code/community/Doofinder/Feed/Model/System/Config/Reset.php +18 -0
  44. app/code/community/Doofinder/Feed/Model/System/Config/Source/Product/Attributes.php +48 -0
  45. app/code/community/Doofinder/Feed/Model/Tools.php +383 -0
  46. app/code/community/Doofinder/Feed/Test/Controller/Index.php +47 -0
  47. app/code/community/Doofinder/Feed/Test/Controller/Index/fixtures/testConfig.yaml +1 -0
  48. app/code/community/Doofinder/Feed/Test/Controller/Index/fixtures/testFeed.yaml +6 -0
  49. app/code/community/Doofinder/Feed/Test/Controller/Index/fixtures/testIndex.yaml +1 -0
  50. app/code/community/Doofinder/Feed/Test/Controller/Index/providers/testConfig.yaml +1 -0
  51. app/code/community/Doofinder/Feed/Test/Controller/Index/providers/testFeed.yaml +2 -0
  52. app/code/community/Doofinder/Feed/Test/Controller/Index/providers/testIndex.yaml +1 -0
  53. app/code/community/Doofinder/Feed/Test/Model/Product.php +59 -0
  54. app/code/community/Doofinder/Feed/Test/Model/Product/expectations/testGenerator.yaml +12 -0
  55. app/code/community/Doofinder/Feed/Test/Model/Product/fixtures/testGenerator.yaml +118 -0
  56. app/code/community/Doofinder/Feed/Test/Model/Product/providers/testGenerator.yaml +12 -0
  57. app/code/community/Doofinder/Feed/controllers/DoofinderFeedFeedController.php +43 -0
  58. app/code/community/Doofinder/Feed/controllers/DoofinderFeedLogController.php +22 -0
  59. app/code/community/Doofinder/Feed/controllers/FeedController.php +311 -0
  60. app/code/community/Doofinder/Feed/controllers/IndexController.php +32 -0
  61. app/code/community/Doofinder/Feed/etc/config.xml +337 -0
  62. app/code/community/Doofinder/Feed/etc/system.xml +515 -0
  63. app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-install-1.5.4.php +59 -0
  64. app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-install-1.5.7.php +163 -0
  65. app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-upgrade-1.5.4-1.5.5.php +18 -0
  66. app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-upgrade-1.5.5-1.5.6.php +101 -0
  67. app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-upgrade-1.5.6-1.5.7.php +19 -0
  68. app/design/adminhtml/default/default/layout/doofinder.xml +16 -0
  69. app/design/frontend/base/default/layout/doofinder.xml +7 -0
  70. app/etc/modules/Doofinder_Feed.xml +9 -0
  71. js/doofinder/admin.js +41 -0
  72. lib/Doofinder/doofinder_api.php +804 -0
  73. lib/Doofinder/doofinder_management_api.php +408 -0
  74. lib/Doofinder/errors.php +48 -0
  75. package.xml +58 -0
  76. skin/adminhtml/default/default/doofinder/styles.css +50 -0
app/code/community/Doofinder/Feed/Block/Adminhtml/Log/View.php ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Adminhtml_Log_View extends Mage_Adminhtml_Block_Widget_Grid
13
+ {
14
+ protected $_defaultSort = 'id';
15
+ protected $_defaultDir = 'desc';
16
+
17
+ protected $_processId = null;
18
+
19
+ public function __construct()
20
+ {
21
+ parent::__construct();
22
+
23
+ $this->_processId = Mage::app()->getRequest()->getParam('processId', false);
24
+ }
25
+
26
+ protected function _prepareCollection()
27
+ {
28
+ $collection = Mage::getResourceModel('doofinder_feed/log_collection');
29
+
30
+ if ($this->_processId) {
31
+ $collection->getSelect()->where("process_id = $this->_processId");
32
+ }
33
+
34
+ $this->setCollection($collection);
35
+
36
+ return parent::_prepareCollection();
37
+ }
38
+
39
+ protected function _prepareColumns()
40
+ {
41
+ $this->addColumn('id', array(
42
+ 'header' => Mage::helper('doofinder_feed')->__('ID'),
43
+ 'index' => 'id',
44
+ 'type' => 'number',
45
+ ));
46
+
47
+ if (!$this->_processId) {
48
+ $this->addColumn('process_id', array(
49
+ 'header' => Mage::helper('doofinder_feed')->__('Process ID'),
50
+ 'index' => 'process_id',
51
+ 'type' => 'number',
52
+ ));
53
+ }
54
+
55
+ $this->addColumn('time', array(
56
+ 'header' => Mage::helper('doofinder_feed')->__('Time'),
57
+ 'index' => 'time',
58
+ 'type' => 'datetime',
59
+ ));
60
+
61
+ $this->addColumn('type', array(
62
+ 'header' => Mage::helper('doofinder_feed')->__('Type'),
63
+ 'index' => 'type',
64
+ 'type' => 'options',
65
+ 'options' => Mage::helper('doofinder_feed/log')->listLogTypes(),
66
+ ));
67
+
68
+ $this->addColumn('message', array(
69
+ 'header' => Mage::helper('doofinder_feed')->__('Message'),
70
+ 'index' => 'message',
71
+ 'type' => 'text',
72
+ ));
73
+
74
+ return parent::_prepareColumns();
75
+ }
76
+ }
app/code/community/Doofinder/Feed/Block/Adminhtml/Map/Additional.php ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Adminhtml_Map_Additional extends Mage_Adminhtml_Block_System_Config_Form_Field
13
+ {
14
+ protected $_addRowButtonHtml = array();
15
+ protected $_removeRowButtonHtml = array();
16
+
17
+ protected $_rows = 0;
18
+
19
+ /**
20
+ * Returns html part of the setting
21
+ *
22
+ * @param Varien_Data_Form_Element_Abstract $element
23
+ * @return string
24
+ */
25
+ protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
26
+ {
27
+ $this->setElement($element);
28
+
29
+ $html = '<table style="display:none"><tbody id="doofinder_feed_additional_mapping_template">';
30
+ $html .= $this->_getRowTemplateHtml(-1);
31
+ $html .= '</tbody></table>';
32
+
33
+ $html .= '<table>';
34
+ $html .= '<thead><tr>';
35
+ $html .= '<th>' . $this->__('Label') . '</th><th>' . $this->__('Field') . '</th><th>' . $this->__('Attribute') . '</th>';
36
+ $html .= '</tr></thead>';
37
+ $html .= '<tbody id="doofinder_feed_additional_mapping_container">';
38
+
39
+ $count = 0;
40
+ if ($this->_getValue('additional_mapping')) {
41
+ foreach ($this->_getValue('additional_mapping') as $i => $f) {
42
+ $html .= $this->_getRowTemplateHtml($count++);
43
+ }
44
+ }
45
+
46
+ $html .= '</tbody></table>';
47
+ $html .= $this->_getAddRowButtonHtml();
48
+
49
+ $html .= '<script type="text/javascript">';
50
+ ob_start();
51
+ ?>
52
+ var DoofinderFeedMapAdditionalRowGenerator = function() {
53
+ this.count = <?php print $count; ?>;
54
+ };
55
+
56
+ DoofinderFeedMapAdditionalRowGenerator.prototype.add = function() {
57
+ var html = $('doofinder_feed_additional_mapping_template').innerHTML;
58
+ html = html.replace(/\[additional_mapping\]\[-1\]/g, '[additional_mapping][' + (this.count++) + ']');
59
+ Element.insert($('doofinder_feed_additional_mapping_container'), {bottom: html});
60
+ };
61
+
62
+ var doofinderFeedMapAdditionalRowGenerator = new DoofinderFeedMapAdditionalRowGenerator();
63
+ <?php
64
+ $html .= ob_get_clean();
65
+ $html .= '</script>';
66
+
67
+ return $html;
68
+ }
69
+
70
+ /**
71
+ * Retrieve html template for setting
72
+ *
73
+ * @param int $rowIndex
74
+ * @return string
75
+ */
76
+ protected function _getRowTemplateHtml($rowIndex = null)
77
+ {
78
+ $value = $rowIndex !== null ? (array) $this->_getValue('additional_mapping/' . $rowIndex) : array();
79
+ $value += array('field' => '', 'label' => '', 'attribute' => '');
80
+ $html = '<tr>';
81
+
82
+ $html .= '<td>';
83
+ $html .= '<input name="'
84
+ . $this->getElement()->getName() . '[additional_mapping][' . $rowIndex . '][label]" value="'
85
+ . $value['label'] . '" ' . $this->_getDisabled() . '/> ';
86
+ $html .= '</td><td>';
87
+ $html .= '<input name="'
88
+ . $this->getElement()->getName() . '[additional_mapping][' . $rowIndex . '][field]" value="'
89
+ . $value['field'] . '" ' . $this->_getDisabled() . '/> ';
90
+ $html .= '</td><td>';
91
+ $html .= '<select name="'
92
+ . $this->getElement()->getName() . '[additional_mapping][' . $rowIndex . '][attribute]" ' . $this->_getDisabled() . '>';
93
+ foreach (Mage::getSingleton('doofinder_feed/system_config_source_product_attributes')->toOptionArray() as $key => $label) {
94
+ $html .= '<option value="' . $key . '"'. ($value['attribute'] == $key ? 'selected="selected"' : '') . '>' . $label . '</option>';
95
+ }
96
+ $html .= '</select> ';
97
+ $html .= '</td><td>';
98
+ $html .= $this->_getRemoveRowButtonHtml();
99
+ $html .= '</td>';
100
+ $html .= '</tr>';
101
+
102
+ return $html;
103
+ }
104
+
105
+ protected function _getDisabled()
106
+ {
107
+ return $this->getElement()->getDisabled() ? 'disabled' : '';
108
+ }
109
+
110
+ protected function _getValue($key)
111
+ {
112
+ return $this->getElement()->getData('value/' . $key);
113
+ }
114
+
115
+ protected function _getSelected($key, $value)
116
+ {
117
+ return $this->getElement()->getData('value/' . $key) == $value ? 'selected="selected"' : '';
118
+ }
119
+
120
+ protected function _getAddRowButtonHtml()
121
+ {
122
+ $container = isset($container) ? $container : null;
123
+
124
+ if (!isset($this->_addRowButtonHtml[$container])) {
125
+ $_cssClass = 'add';
126
+
127
+ if (version_compare(Mage::getVersion(), '1.6', '<')) {
128
+ $_cssClass .= ' ' . $this->_getDisabled();
129
+ }
130
+
131
+ $this->_addRowButtonHtml[$container] = $this->getLayout()->createBlock('adminhtml/widget_button')
132
+ ->setType('button')
133
+ ->setClass($_cssClass)
134
+ ->setLabel($this->__('Add'))
135
+ ->setOnClick("doofinderFeedMapAdditionalRowGenerator.add()")
136
+ ->setDisabled($this->_getDisabled())
137
+ ->toHtml();
138
+ }
139
+ return $this->_addRowButtonHtml[$container];
140
+ }
141
+
142
+ protected function _getRemoveRowButtonHtml()
143
+ {
144
+ if (!$this->_removeRowButtonHtml) {
145
+ $_cssClass = 'delete v-middle';
146
+
147
+ if (version_compare(Mage::getVersion(), '1.6', '<')) {
148
+ $_cssClass .= ' ' . $this->_getDisabled();
149
+ }
150
+
151
+ $this->_removeRowButtonHtml = $this->getLayout()->createBlock('adminhtml/widget_button')
152
+ ->setType('button')
153
+ ->setClass($_cssClass)
154
+ ->setLabel($this->__('Delete'))
155
+ ->setOnClick("Element.remove($(this).up('tr'))")
156
+ ->setDisabled($this->_getDisabled())
157
+ ->toHtml();
158
+ }
159
+ return $this->_removeRowButtonHtml;
160
+ }
161
+ }
app/code/community/Doofinder/Feed/Block/Integration.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Integration extends Mage_Core_Block_Abstract
13
+ {
14
+ /**
15
+ * Produce the integration script
16
+ *
17
+ * @return string
18
+ */
19
+ protected function _toHtml()
20
+ {
21
+ $enabled = Mage::getStoreConfig('doofinder_search/layer_settings/enabled', Mage::app()->getStore());
22
+ $script = Mage::getStoreConfig('doofinder_search/layer_settings/script', Mage::app()->getStore());
23
+
24
+ if ($enabled) {
25
+ $script .= '<script type="text/javascript">';
26
+ $script .= "if (typeof Varien.searchForm !== 'undefined') Varien.searchForm.prototype.initAutocomplete = function() { $('search_autocomplete').hide(); };";
27
+ $script .= '</script>';
28
+
29
+ return $script;
30
+ } else {
31
+ return '';
32
+ }
33
+ }
34
+ }
app/code/community/Doofinder/Feed/Block/Settings/Buttons/Generate.php ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Settings_Buttons_Generate extends Mage_Adminhtml_Block_System_Config_Form_Field
13
+ {
14
+ protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
15
+ {
16
+ $this->setElement($element);
17
+ $element->setScopeLabel('');
18
+
19
+ $storeCode = Mage::app()->getRequest()->getParam('store');
20
+ $url = Mage::helper("adminhtml")->getUrl('adminhtml/doofinderFeedFeed/generate', array('store' => $storeCode));
21
+
22
+ $script = "<script type=\"text/javascript\">
23
+ function generateFeed() {
24
+ var call = new Ajax.Request('" . $url . "', {
25
+ method: 'get',
26
+ onComplete: function(transport) {
27
+ alert(transport.responseText);
28
+ window.location.reload();
29
+ }
30
+ });
31
+ }
32
+ </script>";
33
+
34
+ $html = $this->getLayout()->createBlock('adminhtml/widget_button')
35
+ ->setType('button')
36
+ ->setClass('generate-feed')
37
+ ->setLabel('Start Feed Generation Now')
38
+ ->setOnClick("confirm('No changes will be saved, feed will be rescheduled (if there\'s a process running it will be stopped and the feed will be reset). Do you want to proceed?') && generateFeed()")
39
+ ->setAfterHtml($script)
40
+ ->toHtml();
41
+ return $html;
42
+ }
43
+
44
+ }
app/code/community/Doofinder/Feed/Block/Settings/Buttons/ViewLog.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Settings_Buttons_ViewLog extends Mage_Adminhtml_Block_System_Config_Form_Field
13
+ {
14
+
15
+ protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
16
+ {
17
+ $this->setElement($element);
18
+ $element->setScopeLabel('');
19
+
20
+ $process = Mage::getModel('doofinder_feed/cron')->load(Mage::app()->getRequest()->getParam('store'), 'store_code');
21
+
22
+ $url = Mage::helper("adminhtml")->getUrl('adminhtml/doofinderFeedLog/view/processId/' . $process->getId());
23
+
24
+ $html = $this->getLayout()->createBlock('adminhtml/widget_button')
25
+ ->setType('button')
26
+ ->setClass('view-log')
27
+ ->setLabel('View log')
28
+ ->setOnClick("setLocation('$url')")
29
+ ->toHtml();
30
+
31
+ return $html;
32
+ }
33
+
34
+ }
app/code/community/Doofinder/Feed/Block/Settings/Panel/Cron.php ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ class Doofinder_Feed_Block_Settings_Panel_Cron extends Mage_Adminhtml_Block_System_Config_Form_Field
3
+ {
4
+ // 12 Hours in seconds
5
+ const ALLOWED_TIME = 43200;
6
+
7
+ protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
8
+ {
9
+ $lastSchedule = Mage::getModel('cron/schedule')->getCollection()
10
+ ->setOrder('finished_at', 'desc')
11
+ ->getFirstItem();
12
+
13
+ $message = '';
14
+ if ($lastSchedule && count($lastSchedule->getData()) > 0) {
15
+ $scheduleTime = strtotime($lastSchedule->getFinishedAt());
16
+ $currentTime = time();
17
+
18
+ // Difference in seconds
19
+ $dif = ($currentTime - $scheduleTime);
20
+
21
+ // If difference is bigger than allowed, display message
22
+ if ($dif > self::ALLOWED_TIME) {
23
+
24
+ $message = sprintf('Cron was run for the last time at %s. Taking into account the settings of the step delay option, there might be problems with the cron\'s configuration.', $lastSchedule->getFinishedAt());
25
+ Mage::helper('doofinder_feed')->__($message);
26
+ }
27
+ } else {
28
+ $message = Mage::helper('doofinder_feed')->__('There are no registered cron tasks. Please, check your system\'s crontab configuration.');
29
+ }
30
+
31
+ return '<p class="error">' . $message . '</p>';
32
+ }
33
+
34
+ public function render(Varien_Data_Form_Element_Abstract $element)
35
+ {
36
+ $html = '<td class="label"></td>' .
37
+ '<td class="value" colspan="3">' . $this->_getElementHtml($element) . '</td>';
38
+ return $this->_decorateRowHtml($element, $html);
39
+ }
40
+
41
+ /**
42
+ * Decorate field row html
43
+ *
44
+ * @param Varien_Data_Form_Element_Abstract $element
45
+ * @param string $html
46
+ * @return string
47
+ */
48
+ protected function _decorateRowHtml($element, $html)
49
+ {
50
+ return '<tr id="row_' . $element->getHtmlId() . '">' . $html . '</tr>';
51
+ }
52
+ }
app/code/community/Doofinder/Feed/Block/Settings/Panel/Crondescription.php ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Settings_Panel_CronDescription extends Doofinder_Feed_Block_Settings_Panel_Description
13
+ {
14
+ protected $description = 'THIS FEATURE IS CURRENTLY IN BETA.<br>Feeds can be generated directly in your server to save computer resources. See <a href="http://www.doofinder.com/support/topics/plugins/magento">this article</a> for more information.';
15
+ }
app/code/community/Doofinder/Feed/Block/Settings/Panel/Datetime.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Settings_Panel_Datetime extends Mage_Adminhtml_Block_System_Config_Form_Field
13
+ {
14
+ protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
15
+ {
16
+ $this->setElement($element);
17
+ $name = $element->getName();
18
+ $element->setScopeLabel('');
19
+ $code = Mage::app()->getRequest()->getParam('store');
20
+ $field = $this->_getField($name);
21
+ $html = '';
22
+ if ($field && $code) {
23
+ $datetime = Mage::getModel('doofinder_feed/cron')->load($code, 'store_code')->getData($field);
24
+ if ($datetime) {
25
+ $msg = $datetime;
26
+
27
+ try {
28
+ $dateTimestamp = Mage::getModel('core/date')->timestamp(strtotime($datetime));
29
+ $msg = Mage::helper('core')->formatDate(date('Y-m-d H:i:s', $dateTimestamp), null, true);
30
+ } catch (Exception $e) {}
31
+
32
+ $class = 'feed-datetime';
33
+ $html = "<p class='{$class}'>{$msg}</p>";
34
+ }
35
+ }
36
+ return $html;
37
+ }
38
+
39
+ private function _getField($name = null)
40
+ {
41
+ $pattern = '/groups\[panel\]\[fields\]\[([a-z_-]*)\]\[value\]/';
42
+ $preg = preg_match($pattern, $name, $match);
43
+ if ($preg && isset($match[1])) {
44
+ return $match[1];
45
+ } else {
46
+ return false;
47
+ }
48
+ }
49
+ }
app/code/community/Doofinder/Feed/Block/Settings/Panel/Description.php ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Settings_Panel_Description extends Mage_Adminhtml_Block_System_Config_Form_Field
13
+ {
14
+ const INFO = 'info';
15
+ const WARNING = 'warning';
16
+
17
+ protected $level = self::INFO;
18
+ protected $description = 'You can set the rest of the options for each store separately by modifying the Current Configuration Scope.';
19
+
20
+ protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
21
+ {
22
+ $text = '';
23
+
24
+ if (!Mage::app()->getRequest()->getParam('store'))
25
+ {
26
+ $text = $this->description;
27
+ }
28
+
29
+ $this->setElement($element);
30
+ $name = $element->getName();
31
+ $element->setScopeLabel('');
32
+
33
+ return '<p class="doofinder-' . $this->level . '">' . $text . '</p>';
34
+ }
35
+
36
+ public function render(Varien_Data_Form_Element_Abstract $element)
37
+ {
38
+ $html = '<td class="label"></td>' .
39
+ '<td class="value" colspan="3">' . $this->_getElementHtml($element) . '</td>';
40
+ return $this->_decorateRowHtml($element, $html);
41
+ }
42
+
43
+ /**
44
+ * Decorate field row html
45
+ *
46
+ * @param Varien_Data_Form_Element_Abstract $element
47
+ * @param string $html
48
+ * @return string
49
+ */
50
+ protected function _decorateRowHtml($element, $html)
51
+ {
52
+ return '<tr id="row_' . $element->getHtmlId() . '">' . $html . '</tr>';
53
+ }
54
+ }
app/code/community/Doofinder/Feed/Block/Settings/Panel/File.php ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Settings_Panel_File extends Mage_Adminhtml_Block_System_Config_Form_Field
13
+ {
14
+ /**
15
+ * Error prefix
16
+ * @var string
17
+ */
18
+ const ERROR_PREFIX = "#error#";
19
+
20
+ protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
21
+ {
22
+ $this->setElement($element);
23
+ $name = $element->getName();
24
+ $element->setScopeLabel('');
25
+ $store_code = Mage::app()->getRequest()->getParam('store');
26
+
27
+ $stores = array();
28
+
29
+ if ($store_code) {
30
+ $stores[$store_code] = Mage::getModel('core/store')->load($store_code);
31
+ } else {
32
+ foreach (Mage::app()->getStores() as $store) {
33
+ if ($store->getIsActive()) {
34
+ $stores[$store->getCode()] = $store;
35
+ }
36
+ }
37
+ }
38
+
39
+ $files = array();
40
+
41
+ foreach ($stores as $store) {
42
+ $process = Mage::getModel('doofinder_feed/cron')->load($store->getCode(), 'store_code');
43
+ $lastGeneratedName = $process->getLastFeedName();
44
+
45
+ $fileUrl = Mage::getBaseUrl('media').'doofinder'.DS.$lastGeneratedName;
46
+ $fileDir = Mage::getBaseDir('media').DS.'doofinder'.DS.$lastGeneratedName;
47
+ if ($lastGeneratedName && file_exists($fileDir)) {
48
+ $files[$store->getCode()] = "<a href='{$fileUrl}' target='_blank'>" . (count($stores) > 1 ? $fileUrl : "Get {$lastGeneratedName}") . "</a>";
49
+ } else {
50
+ $files[$store->getCode()] = "Currently there is no file to preview.";
51
+ }
52
+ }
53
+
54
+ $html = '';
55
+
56
+ if (count($files) > 1) {
57
+ $html .= '<ul>';
58
+ foreach ($files as $code => $file) {
59
+ $html .= '<li><b>' . $stores[$code]->getName() . ':</b><div>' . $file . '</div></li>';
60
+ }
61
+ $html .= '</ul>';
62
+ } else {
63
+ $html .= reset($files);
64
+ }
65
+
66
+ return $html;
67
+ }
68
+ }
app/code/community/Doofinder/Feed/Block/Settings/Panel/Hashdescription.php ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php
2
+ class Doofinder_Feed_Block_Settings_Panel_HashDescription extends Doofinder_Feed_Block_Settings_Panel_Description
3
+ {
4
+ protected $level = self::WARNING;
5
+ protected $description = '<b>IMPORTANT:</b> You must configure a "hashid" for each store view. Use the "Current Configuration Scope" selector at the top left side of the page to choose a store view.';
6
+ }
app/code/community/Doofinder/Feed/Block/Settings/Panel/Layerdescription.php ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Settings_Panel_LayerDescription extends Doofinder_Feed_Block_Settings_Panel_Description
13
+ {
14
+ protected $level = self::WARNING;
15
+ protected $description = '<b>IMPORTANT:</b> You must configure a different Layer script for each store view. Use the "Current Configuration Scope" selector at the top left side of the page to choose a store view.';
16
+ }
app/code/community/Doofinder/Feed/Block/Settings/Panel/Message.php ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category blocks
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Block_Settings_Panel_Message extends Mage_Adminhtml_Block_System_Config_Form_Field
13
+ {
14
+ /**
15
+ * Error prefix
16
+ * @var string
17
+ */
18
+ const ERROR_PREFIX = "#error#";
19
+
20
+ protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
21
+ {
22
+ $this->setElement($element);
23
+ $name = $element->getName();
24
+ $element->setScopeLabel('');
25
+ $code = Mage::app()->getRequest()->getParam('store');
26
+ $field = $this->_getField($name);
27
+ $html = '';
28
+ if ($field && $code) {
29
+ $process = Mage::getModel('doofinder_feed/cron')->load($code, 'store_code');
30
+
31
+ if (!$process->getId()) {
32
+ switch ($field) {
33
+ case 'status':
34
+ $msg = Mage::helper('doofinder_feed')->__('Not created');
35
+ break;
36
+
37
+ case 'message':
38
+ $msg = Mage::helper('doofinder_feed')->__('Process not created yet, it will be created automatically by cron job');
39
+ break;
40
+
41
+ default:
42
+ $msg = '';
43
+ }
44
+
45
+ } else {
46
+ $msg = $process->getData($field);
47
+ }
48
+
49
+ $class = 'feed-message ';
50
+
51
+ // Mark message as an error
52
+ if (strpos($msg, self::ERROR_PREFIX) !== false) {
53
+ $msg = str_replace(self::ERROR_PREFIX, '', $msg);
54
+ $class .= 'error';
55
+ }
56
+
57
+ $html = "<p class='{$class}'>{$msg}</p>";
58
+ }
59
+ return $html;
60
+ }
61
+
62
+ private function _getField($name = null) {
63
+
64
+ $pattern = '/groups\[panel\]\[fields\]\[([a-z_-]*)\]\[value\]/';
65
+ $preg = preg_match($pattern, $name, $match);
66
+ if ($preg && isset($match[1])) {
67
+ return $match[1];
68
+ } else {
69
+ return false;
70
+ }
71
+ }
72
+ }
app/code/community/Doofinder/Feed/Helper/Data.php ADDED
@@ -0,0 +1,552 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Helpers
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Data helper for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Helper_Data extends Mage_Core_Helper_Abstract
19
+ {
20
+ private $store = null;
21
+
22
+ private $currencyConvert = false;
23
+
24
+ private $useMinimalPrice = false;
25
+
26
+ private $groupConfigurables = true;
27
+
28
+ private $minTierPrice = null;
29
+
30
+ const CRON_DAILY = Mage_Adminhtml_Model_System_Config_Source_Cron_Frequency::CRON_DAILY;
31
+ const CRON_WEEKLY = Mage_Adminhtml_Model_System_Config_Source_Cron_Frequency::CRON_WEEKLY;
32
+ const CRON_MONTHLY = Mage_Adminhtml_Model_System_Config_Source_Cron_Frequency::CRON_MONTHLY;
33
+
34
+ /**
35
+ * Panel info messages.
36
+ */
37
+ const STATUS_DISABLED = 'Disabled';
38
+ const STATUS_PENDING = Mage_Cron_Model_Schedule::STATUS_PENDING;
39
+ const STATUS_RUNNING = Mage_Cron_Model_Schedule::STATUS_RUNNING;
40
+ const STATUS_SUCCESS = Mage_Cron_Model_Schedule::STATUS_SUCCESS;
41
+ const STATUS_MISSED = Mage_Cron_Model_Schedule::STATUS_MISSED;
42
+ const STATUS_WAITING = 'Waiting...';
43
+ const STATUS_ERROR = Mage_Cron_Model_Schedule::STATUS_ERROR;
44
+ const JOB_CODE = 'doofinder_feed_generate';
45
+
46
+ const MSG_EMPTY = "Currently there is no message.";
47
+ const MSG_PENDING = "The new process of generating the feed has been registered and it's waiting to be activated.";
48
+ const MSG_DISABLED = "The feed generator for this view is currently disabled.";
49
+ const MSG_WAITING = "Waiting for registering the new process of generating the feed.";
50
+
51
+
52
+ /**
53
+ * $product => Product instance
54
+ * $oStore => Store instance
55
+ * $currencyConvert => Boolean, Convert prices to $oStore currency.
56
+ * $useMinimalPrice => Boolean, See below.
57
+ * $groupConfigurables => Boolean
58
+ *
59
+ * If $useMinimalPrice == true then, the price is checked against tier
60
+ * prices. If there is a smaller price in the tier then that price is used
61
+ * instead the regular one.
62
+ *
63
+ * So, if there is a special price defined and it is greater than the
64
+ * minimal price found in tier, then it is not returned as the "sale_price".
65
+ *
66
+ * ----
67
+ *
68
+ * If a Fixed Product Tax exists for the product, then it is applied if
69
+ * the $oStore settings are configured to do so.
70
+ *
71
+ * NOTICE: FPT are ALWAYS applied to prices including taxes. Configuration
72
+ * is only applied to prices excluding taxes.
73
+ */
74
+ public function collectProductPrices(Mage_Catalog_Model_Product $product, $oStore, $currencyConvert=false, $useMinimalPrice=false, $groupConfigurables=true)
75
+ {
76
+ $this->store = $oStore;
77
+ $this->currencyConvert = $currencyConvert;
78
+ $this->useMinimalPrice = $useMinimalPrice;
79
+ $this->groupConfigurables = $groupConfigurables;
80
+
81
+ $weeeHelper = Mage::helper('weee');
82
+ $taxHelper = Mage::helper('tax');
83
+ $coreHelper = Mage::helper('core');
84
+
85
+ // Tier Prices
86
+
87
+ $tierPrices = $this->getProductTierPrices($product, $oStore);
88
+
89
+ foreach ($tierPrices as $tier)
90
+ {
91
+ if ( is_null($this->minTierPrice) || $tier['base_price_excl_tax'] < $this->minTierPrice['base_price_excl_tax'] )
92
+ {
93
+ $this->minTierPrice = $tier;
94
+ continue;
95
+ }
96
+ }
97
+
98
+ if ( $product->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_GROUPED )
99
+ {
100
+ $prices = $this->_getGroupedProductPrice($product);
101
+ }
102
+ elseif ( $product->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_BUNDLE )
103
+ {
104
+ $prices = $this->_getBundleProductPrice($product);
105
+ }
106
+ else /* ! $product->isGrouped */
107
+ {
108
+ $prices = $this->_getProductPrice($product);
109
+ }
110
+
111
+ $prices = $this->_cleanPrices($prices);
112
+
113
+ foreach ( array('price', 'sale_price') as $priceType )
114
+ {
115
+ if ( !isset($prices[$priceType]) )
116
+ continue;
117
+ foreach ( $prices[$priceType] as $priceMode => $priceValue )
118
+ {
119
+ if ( $currencyConvert ) {
120
+ $priceValue = $oStore->convertPrice($priceValue, false, false);
121
+ }
122
+ $prices[$priceType][$priceMode] = $priceValue;
123
+ }
124
+ }
125
+
126
+ return $prices;
127
+ }
128
+
129
+ protected function _cleanPrices($prices)
130
+ {
131
+ if (!isset($prices['price'])) return $prices;
132
+ if ( isset($prices['sale_price']['excluding_tax']) &&
133
+ $prices['price']['excluding_tax'] <= $prices['sale_price']['excluding_tax'] )
134
+ {
135
+ unset($prices['sale_price']['excluding_tax']);
136
+ unset($prices['sale_price']['including_tax']);
137
+ }
138
+
139
+ if ( $prices['price']['excluding_tax'] <= 0 )
140
+ {
141
+ unset($prices['price']['excluding_tax']);
142
+ unset($prices['price']['including_tax']);
143
+ }
144
+
145
+ return $prices;
146
+ }
147
+
148
+ protected function _getProductPrice($product)
149
+ {
150
+ $prices = array();
151
+
152
+ $weeeHelper = Mage::helper('weee');
153
+ $taxHelper = Mage::helper('tax');
154
+ $coreHelper = Mage::helper('core');
155
+
156
+ $prices['price_type'] = 'normal';
157
+
158
+ $weeeTaxAmount = $weeeHelper->getAmountForDisplay($product);
159
+
160
+ $weeeTaxAttributes = null;
161
+
162
+ if ( $weeeHelper->typeOfDisplay($product, array(1, 2, 4), null, $this->store) )
163
+ {
164
+ $weeeTaxAmount = $weeeHelper->getAmount($product, null, null, $this->store->getWebsiteId(), false);
165
+ $weeeTaxAttributes = $weeeHelper->getProductWeeeAttributesForDisplay($product);
166
+ }
167
+
168
+ // Precios originales y finales (segun Magento) sin Weee
169
+
170
+ $base_price_excl_tax = $taxHelper->getPrice($product, $product->getPrice(), false, null, null, null, $this->store, null);
171
+ $base_price_incl_tax = $taxHelper->getPrice($product, $product->getPrice(), true, null, null, null, $this->store, null);
172
+
173
+ $final_price_excl_tax = $taxHelper->getPrice($product, $product->getFinalPrice(), false, null, null, null, $this->store, null);
174
+ $final_price_incl_tax = $taxHelper->getPrice($product, $product->getFinalPrice(), true, null, null, null, $this->store, null);
175
+
176
+ if ( $this->minTierPrice && $this->useMinimalPrice
177
+ && $this->minTierPrice['base_price_excl_tax'] < $final_price_excl_tax)
178
+ {
179
+ $prices['price_type'] = 'minimal';
180
+
181
+ $base_price_excl_tax = $this->minTierPrice['base_price_excl_tax'];
182
+ $base_price_incl_tax = $this->minTierPrice['base_price_incl_tax'];
183
+ }
184
+
185
+ // Algunas preguntas
186
+
187
+ $inclFptOnly = $weeeHelper->typeOfDisplay($product, 0, null, $this->store); // Including FPT only
188
+ $inclFptAndDescription = $weeeHelper->typeOfDisplay($product, 1, null, $this->store); // Including FPT and FPT description
189
+ $exclFptAndDescriptionFinalPrice = $weeeHelper->typeOfDisplay($product, 2, null, $this->store); // Excluding FPT, FPT description, final price
190
+ $exclFpt = $weeeHelper->typeOfDisplay($product, 3, null, $this->store); // Excluding FPT
191
+ $inclFptAndDescriptionWithTaxes = $weeeHelper->typeOfDisplay($product, 4, null, $this->store); // Including FPT and FPT description [incl. FPT VAT]
192
+
193
+ // Elegimos y calculamos los precios finales
194
+
195
+ if ( $final_price_excl_tax >= $base_price_excl_tax )
196
+ {
197
+ $prices['price']['excluding_tax'] = $base_price_excl_tax;
198
+ $prices['price']['including_tax'] = $base_price_incl_tax;
199
+
200
+ if ( $weeeTaxAmount )
201
+ {
202
+ $prices['price']['including_tax'] += $weeeTaxAmount;
203
+
204
+ if ( $inclFptOnly || $inclFptAndDescription || $inclFptAndDescriptionWithTaxes )
205
+ $prices['price']['excluding_tax'] += $weeeTaxAmount;
206
+ }
207
+ }
208
+ else
209
+ {
210
+ $prices['price']['excluding_tax'] = $base_price_excl_tax;
211
+ $prices['price']['including_tax'] = $base_price_incl_tax;
212
+
213
+ $prices['sale_price']['excluding_tax'] = $final_price_excl_tax;
214
+ $prices['sale_price']['including_tax'] = $final_price_incl_tax;
215
+
216
+ $originalWeeeTaxAmount = $weeeHelper->getOriginalAmount($product);
217
+
218
+ if ( $weeeTaxAmount )
219
+ {
220
+ $prices['price']['including_tax'] += $originalWeeeTaxAmount;
221
+ $prices['sale_price']['including_tax'] += $weeeTaxAmount;
222
+
223
+ if ( $inclFptOnly || $inclFptAndDescription || $inclFptAndDescriptionWithTaxes )
224
+ {
225
+ $prices['price']['excluding_tax'] += $originalWeeeTaxAmount;
226
+ $prices['sale_price']['excluding_tax'] += $weeeTaxAmount;
227
+ }
228
+ }
229
+ }
230
+
231
+ if ( $product->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE && $this->groupConfigurables && $this->useMinimalPrice )
232
+ {
233
+ $prices = $this->_getConfigurableProductPrice($product);
234
+ }
235
+ return $prices;
236
+ }
237
+
238
+ protected function _getConfigurableProductPrice($product, $prices)
239
+ {
240
+ $childProducts = $product->getTypeInstance()->getUsedProducts();
241
+
242
+ foreach ( $childProducts as $child )
243
+ {
244
+ $childPrices = $this->collectProductPrices($child, $this->store, false, $this->useMinimalPrice, $this->groupConfigurables);
245
+
246
+ // Compare regular price
247
+ if ( $childPrices['price']['excluding_tax'] < $prices['price']['excluding_tax'] )
248
+ {
249
+ $prices['price']['excluding_tax'] = $childPrices['price']['excluding_tax'];
250
+ $prices['price']['including_tax'] = $childPrices['price']['including_tax'];
251
+ $prices['price']['overriden'] = true;
252
+ }
253
+
254
+ // Compare sale price
255
+ if ( array_key_exists('sale_price', $childPrices) )
256
+ {
257
+ if ( ! array_key_exists('sale_price', $prices)
258
+ || $childPrices['sale_price']['excluding_tax'] < $prices['sale_price']['excluding_tax'] )
259
+ {
260
+ $prices['sale_price']['excluding_tax'] = $childPrices['sale_price']['excluding_tax'];
261
+ $prices['sale_price']['including_tax'] = $childPrices['sale_price']['including_tax'];
262
+ $prices['sale_price']['overriden'] = true;
263
+ }
264
+ }
265
+ }
266
+ return $prices;
267
+ }
268
+
269
+ protected function _getGroupedProductPrice($product)
270
+ {
271
+ $weeeHelper = Mage::helper('weee');
272
+ $taxHelper = Mage::helper('tax');
273
+ $coreHelper = Mage::helper('core');
274
+
275
+ $minimal_prices = array(
276
+ 'price' => array(
277
+ 'including_tax' => 0,
278
+ 'excluding_tax' => 0
279
+ ),
280
+ 'sale_price' => array(
281
+ 'including_tax' => 0,
282
+ 'excluding_tax' => 0
283
+ )
284
+ );
285
+
286
+ $childrenIds = $product->getTypeInstance()->getChildrenIds($product->getId());
287
+ $childrenIds = $childrenIds[Mage_Catalog_Model_Product_Link::LINK_TYPE_GROUPED];
288
+
289
+ if (empty($childrenIds) || !is_array($childrenIds)) {
290
+ return $minimal_prices;
291
+ }
292
+
293
+ $collection = Mage::getModel('catalog/product')->getCollection();
294
+ $collection
295
+ ->addIdFilter($childrenIds)
296
+ ->addAttributeToSelect('*')
297
+ ->load();
298
+
299
+ foreach($collection as $product)
300
+ {
301
+ $sub_prices = $this->collectProductPrices($product, $this->store, $this->currencyConvert, $this->useMinimalPrice, $this->groupConfigurables);
302
+
303
+ if (! empty($sub_prices['price']['excluding_tax'])) {
304
+ if ($minimal_prices['price']['excluding_tax'] === 0 ||
305
+ $minimal_prices['price']['excluding_tax'] > $sub_prices['price']['excluding_tax'])
306
+ $minimal_prices = $sub_prices;
307
+ }
308
+ }
309
+
310
+ return $minimal_prices;
311
+ }
312
+
313
+ protected function _getBundleProductPrice($product)
314
+ {
315
+ $prices = array();
316
+
317
+ $weeeHelper = Mage::helper('weee');
318
+ $taxHelper = Mage::helper('tax');
319
+ $coreHelper = Mage::helper('core');
320
+
321
+ if ( method_exists($product->getPriceModel(), 'getTotalPrices') )
322
+ {
323
+ $bundle_price_excl_tax = $product->getPriceModel()->getTotalPrices($product, 'min', false, true);
324
+ $bundle_price_incl_tax = $product->getPriceModel()->getTotalPrices($product, 'min', true, true);
325
+ }
326
+ else // Magento 1.5.0.1 + 1.5.1.0
327
+ {
328
+ $bundle_price_excl_tax = $product->getPriceModel()->getPricesDependingOnTax($product, 'min', false);
329
+ $bundle_price_incl_tax = $product->getPriceModel()->getPricesDependingOnTax($product, 'min', true);
330
+ }
331
+
332
+ if ( $bundle_price_excl_tax )
333
+ {
334
+ $prices['price_type'] = 'minimal';
335
+
336
+ $prices['price']['excluding_tax'] = $bundle_price_excl_tax;
337
+ $prices['price']['including_tax'] = $bundle_price_incl_tax;
338
+ }
339
+
340
+ return $prices;
341
+ }
342
+
343
+ public function getProductTierPrices(Mage_Catalog_Model_Product $product, $oStore)
344
+ {
345
+ if (is_null($product))
346
+ return array();
347
+
348
+ $prices = array();
349
+ $taxHelper = Mage::helper('tax');
350
+
351
+ // Get Tier Prices
352
+
353
+ $tierPrices = $product->getTierPrice(null);
354
+
355
+ if (! is_array($tierPrices))
356
+ $tierPrices = (array) $tierPrices;
357
+
358
+ foreach ( $tierPrices as $price )
359
+ {
360
+ $result = array();
361
+
362
+ if ( $price['website_id'] != $oStore->getWebsiteId() && $price['website_id'] != 0 )
363
+ continue;
364
+
365
+ $result['price_qty'] = $price['price_qty'] * 1; // make int
366
+
367
+ if ( $price['price'] < $product->getFinalPrice() )
368
+ $result['save_percent'] = ceil(100 - ((100 / $product->getFinalPrice()) * $price['price']));
369
+
370
+ $result['base_price_excl_tax'] = $taxHelper->getPrice($product, $price['website_price'], false, null, null, null, $oStore, null);
371
+ $result['base_price_incl_tax'] = $taxHelper->getPrice($product, $price['website_price'], true, null, null, null, $oStore, null);
372
+
373
+ $prices[] = $result;
374
+ }
375
+
376
+ return $prices;
377
+ }
378
+
379
+ /**
380
+ * Gets store config for cron settings.
381
+ * @param string $storeCode
382
+ * @return array
383
+ */
384
+ public function getStoreConfig($storeCode = '') {
385
+ $xmlName = Mage::getStoreConfig('doofinder_cron/schedule_settings/name', $storeCode);
386
+ $config = array(
387
+ 'enabled' => Mage::getStoreConfig('doofinder_cron/schedule_settings/enabled', $storeCode),
388
+ 'display_price' => Mage::getStoreConfig('doofinder_cron/feed_settings/display_price', $storeCode),
389
+ 'grouped' => Mage::getStoreConfig('doofinder_cron/feed_settings/grouped', $storeCode),
390
+ 'image_size' => Mage::getStoreConfig('doofinder_cron/feed_settings/image_size', $storeCode),
391
+ 'stepSize' => Mage::getStoreConfig('doofinder_cron/schedule_settings/step', $storeCode),
392
+ 'stepDelay' => Mage::getStoreConfig('doofinder_cron/schedule_settings/delay', $storeCode),
393
+ 'frequency' => Mage::getStoreConfig('doofinder_cron/schedule_settings/frequency', $storeCode),
394
+ 'time' => explode(',', Mage::getStoreConfig('doofinder_cron/schedule_settings/time', $storeCode)),
395
+ 'storeCode' => $storeCode,
396
+ 'xmlName' => $this->_processXmlName($xmlName, $storeCode),
397
+ 'reset' => Mage::getStoreConfig('doofinder_cron/schedule_settings/reset', $storeCode),
398
+ );
399
+ return $config;
400
+ }
401
+
402
+ /**
403
+ * Process xml filename
404
+ * @param string $name
405
+ * @return bool
406
+ */
407
+ private function _processXmlName($name = 'doofinder-{store_code}.xml', $code = 'default') {
408
+ $pattern = '/\{\s*store_code\s*\}/';
409
+
410
+ $newName = preg_replace($pattern, $code, $name);
411
+ return $newName;
412
+ }
413
+
414
+ /**
415
+ * Create cron expr string
416
+ * @param string $time
417
+ * @return mixed
418
+ */
419
+ private function _getCronExpr($time = null, $frequency = null) {
420
+
421
+ if (!$time) return false;
422
+ $time = explode(',', $time);
423
+
424
+ $cronExprArray = array(
425
+ intval($time[1]),
426
+ intval($time[0]),
427
+ ($frequency == self::CRON_MONTHLY) ? '1' : '*',
428
+ '*',
429
+ ($frequency == self::CRON_WEEKLY) ? '1' : '*',
430
+ );
431
+ $cronExprString = join(' ', $cronExprArray);
432
+
433
+ return $cronExprString;
434
+ }
435
+
436
+ /**
437
+ * Creates new schedule entry.
438
+ * @param Doofinder_Feed_Model_Cron $process
439
+ */
440
+
441
+ public function createNewSchedule(Doofinder_Feed_Model_Cron $process) {
442
+ $helper = Mage::helper('doofinder_feed');
443
+
444
+ $config = $helper->getStoreConfig($process->getStoreCode());
445
+
446
+ // Set new schedule time
447
+ $delayInMin = intval($config['stepDelay']);
448
+ $timescheduled = strftime("%Y-%m-%d %H:%M:%S", mktime(date("H"), date("i") + $delayInMin, date("s"), date("m"), date("d"), date("Y")));
449
+
450
+ // Prepare new process data
451
+ $status = $helper::STATUS_RUNNING;
452
+ $nextRun = '-';
453
+
454
+ // Set process data and save
455
+ $process->setStatus($status)
456
+ ->setNextRun('-')
457
+ ->setNextIteration($timescheduled)
458
+ ->save();
459
+
460
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::STATUS, $helper->__('Scheduling the next step for %s', $timescheduled));
461
+ }
462
+
463
+ public function getScheduledAt($time = null, $frequency = null, $timezoneOffset = true) {
464
+ $parts = array($time[0], $time[1], $time[2], date('m'), date('d'));
465
+ $offset = $this->getTimezoneOffset();
466
+
467
+ $now = time();
468
+ $start = mktime($parts[0] - $offset, $parts[1], $parts[2], $parts[3], $parts[4]);
469
+
470
+ if ($start < $now) {
471
+ switch ($frequency) {
472
+ case self::CRON_MONTHLY:
473
+ $parts[3] += 1;
474
+ break;
475
+
476
+ case self::CRON_WEEKLY:
477
+ $parts[4] += 7;
478
+ break;
479
+
480
+ case self::CRON_DAILY:
481
+ $parts[4] += 1;
482
+ break;
483
+ }
484
+ }
485
+
486
+ if ($timezoneOffset) {
487
+ $parts[0] -= $offset;
488
+ }
489
+
490
+ return strftime("%Y-%m-%d %H:%M:%S", mktime($parts[0], $parts[1], $parts[2], $parts[3], $parts[4]));
491
+ }
492
+
493
+ public function getTimezoneOffset() {
494
+ $timezone = Mage::getStoreConfig('general/locale/timezone');
495
+ $backTimezone = date_default_timezone_get();
496
+ // Set relative timezone
497
+ date_default_timezone_set($timezone);
498
+ $offset = (date('Z') / 60 / 60);
499
+ // Revoke server timezone
500
+ date_default_timezone_set($backTimezone);
501
+ return $offset;
502
+ }
503
+
504
+ /**
505
+ * Get path to feed file.
506
+ *
507
+ * @return string
508
+ */
509
+ public function getFeedDirectory()
510
+ {
511
+ return Mage::getBaseDir('media').DS.'doofinder';
512
+ }
513
+
514
+ /**
515
+ * Get path to feed file.
516
+ *
517
+ * @return string
518
+ */
519
+ public function getFeedPath($storeCode)
520
+ {
521
+ $config = $this->getStoreConfig($storeCode);
522
+
523
+ return $this->getFeedDirectory().DS.$config['xmlName'];
524
+ }
525
+
526
+ /**
527
+ * Get path to feed file.
528
+ *
529
+ * @return string
530
+ */
531
+ public function getFeedTemporaryPath($storeCode)
532
+ {
533
+ return $this->getFeedPath($storeCode) . '.tmp';
534
+ }
535
+
536
+ /**
537
+ * Creates feed directory.
538
+ *
539
+ * @param string $dir
540
+ * @return bool
541
+ */
542
+ public function createFeedDirectory()
543
+ {
544
+ $dir = $this->getFeedDirectory();
545
+
546
+ if ((!file_exists($dir) && !mkdir($dir, 0777, true)) || !is_dir($dir)) {
547
+ Mage::throwException('Could not create directory: '.$dir);
548
+ }
549
+
550
+ return true;
551
+ }
552
+ }
app/code/community/Doofinder/Feed/Helper/Log.php ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Helpers
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Log helper for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Helper_Log extends Mage_Core_Helper_Abstract
19
+ {
20
+ const STATUS = 'status';
21
+ const WARNING = 'warning';
22
+ const ERROR = 'error';
23
+
24
+ /**
25
+ * Log the feed event.
26
+ *
27
+ * @param Doofinder_Feed_Model_Cron $process
28
+ * @param string $type
29
+ * @param string $message
30
+ */
31
+ function log(Doofinder_Feed_Model_Cron $process, $type, $message)
32
+ {
33
+ $entry = Mage::getModel('doofinder_feed/log')
34
+ ->setProcessId($process->getId())
35
+ ->setType($type)
36
+ ->setMessage($message)
37
+ ->save();
38
+
39
+ return $this;
40
+ }
41
+
42
+ /**
43
+ * Get available log types
44
+ *
45
+ * @return array
46
+ */
47
+ function listLogTypes()
48
+ {
49
+ return array(
50
+ static::STATUS => $this->__('Status'),
51
+ static::WARNING => $this->__('Warning'),
52
+ static::ERROR => $this->__('Error'),
53
+ );
54
+ }
55
+ }
app/code/community/Doofinder/Feed/Helper/Search.php ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ require_once(Mage::getBaseDir('lib') . DS. 'Doofinder' . DS .'doofinder_api.php');
3
+
4
+ class Doofinder_Feed_Helper_Search extends Mage_Core_Helper_Abstract
5
+ {
6
+ const DOOFINDER_PAGE_LIMIT = 100;
7
+ const DOOFINDER_RESULTS_LIMIT = 1000;
8
+
9
+ protected $_lastSearch = null;
10
+ protected $_lastResults = null;
11
+
12
+ /**
13
+ * Perform a doofinder search on given key.
14
+ *
15
+ * @param string $queryText
16
+ * @param int $limit
17
+ * @param int $offset
18
+ *
19
+ * @return array - The array od product ids from first page
20
+ */
21
+ public function performDoofinderSearch($queryText)
22
+ {
23
+ $hashId = Mage::getStoreConfig('doofinder_search/internal_settings/hash_id', Mage::app()->getStore());
24
+ $apiKey = Mage::getStoreConfig('doofinder_search/internal_settings/api_key', Mage::app()->getStore());
25
+ $limit = Mage::getStoreConfig('doofinder_search/internal_settings/request_limit', Mage::app()->getStore());
26
+ $ids = false;
27
+
28
+ $df = new DoofinderApi($hashId, $apiKey);
29
+ $dfResults = $df->query($queryText, null, array('rpp' => $limit, 'transformer' => 'onlyid', 'filter' => array()));
30
+
31
+ // Store objects
32
+ $this->_lastSearch = $df;
33
+ $this->_lastResults = $dfResults;
34
+
35
+ return $this->retrieveIds($dfResults);
36
+ }
37
+
38
+ /**
39
+ * Retrieve ids from Doofinder Results
40
+ *
41
+ * @param DoofinderResults $dfResults
42
+ * @return array
43
+ */
44
+ protected function retrieveIds(DoofinderResults $dfResults)
45
+ {
46
+ $ids = array();
47
+ foreach($dfResults->getResults() as $result) {
48
+ $ids[] = $result['id'];
49
+ }
50
+
51
+ return $ids;
52
+ }
53
+
54
+ /**
55
+ * Fetch all results of last doofinder search
56
+ *
57
+ * @return array - The array of products ids from all pages
58
+ */
59
+ public function getAllResults()
60
+ {
61
+ $limit = Mage::getStoreConfig('doofinder_search/internal_settings/total_limit', Mage::app()->getStore());
62
+ $ids = $this->retrieveIds($this->_lastResults);
63
+
64
+ while (count($ids) < $limit && ($dfResults = $this->_lastSearch->nextPage())) {
65
+ $ids = array_merge($ids, $this->retrieveIds($dfResults));
66
+ }
67
+
68
+ return $ids;
69
+ }
70
+
71
+ /**
72
+ * Returns fetched results count
73
+ *
74
+ * @return int
75
+ */
76
+ public function getResultsCount()
77
+ {
78
+ return $this->_lastResults->getProperty('total');
79
+ }
80
+ }
app/code/community/Doofinder/Feed/Helper/Tax.php ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Helpers
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Tax helper for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Helper_Tax extends Mage_Tax_Helper_Data
19
+ {
20
+ public function needPriceConversion($store = null)
21
+ {
22
+ return true;
23
+ }
24
+ }
app/code/community/Doofinder/Feed/Model/Adminhtml/System/Config/Backend/Cron.php ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Adminhtml_System_Config_Backend_Cron extends Mage_Core_Model_Config_Data {
13
+
14
+ const CRON_STRING_PATH = 'crontab/jobs/doofinder_feed_generate/schedule/cron_expr';
15
+
16
+ protected function _afterSave() {
17
+ $time = $this->getData('groups/settings/fields/time/value');
18
+ $frequency = $this->getData('groups/settings/fields/frequency/value');
19
+ $frequencyDaily = Mage_Adminhtml_Model_System_Config_Source_Cron_Frequency::CRON_DAILY;
20
+ $frequencyWeekly = Mage_Adminhtml_Model_System_Config_Source_Cron_Frequency::CRON_WEEKLY;
21
+ $frequencyMonthly = Mage_Adminhtml_Model_System_Config_Source_Cron_Frequency::CRON_MONTHLY;
22
+
23
+ $cronDayOfWeek = date('N');
24
+
25
+ $cronExprArray = array(
26
+ intval($time[1]), # Minute
27
+ intval($time[0]), # Hour
28
+ ($frequency == $frequencyMonthly) ? '1' : '*', # Day of the Month
29
+ '*', # Month of the Year
30
+ ($frequency == $frequencyWeekly) ? '1' : '*', # Day of the Week
31
+ );
32
+
33
+ $cronExprArray = array(
34
+ '*/1',
35
+ '*',
36
+ '*',
37
+ '*',
38
+ '*',
39
+ );
40
+ $cronExprString = join(' ', $cronExprArray);
41
+
42
+ try {
43
+ Mage::getModel('core/config_data')
44
+ ->load(self::CRON_STRING_PATH, 'path')
45
+ ->setValue($cronExprString)
46
+ ->setPath(self::CRON_STRING_PATH)
47
+ ->save();
48
+ }
49
+ catch (Exception $e) {
50
+ throw new Exception(Mage::helper('cron')->__('Unable to save the cron expression.'));
51
+
52
+ }
53
+ }
54
+ }
app/code/community/Doofinder/Feed/Model/Adminhtml/System/Config/Validation/Hashid.php ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ class Doofinder_Feed_Model_Adminhtml_System_Config_Validation_Hashid extends Mage_Core_Model_Config_Data {
3
+ public function save() {
4
+ // Hash id to save
5
+ $hashId = $this->getValue();
6
+ $stores = Mage::app()->getStores();
7
+ foreach ($stores as $store) {
8
+ if ($this->getStoreCode() === $store->getCode())
9
+ continue;
10
+ $code = $store->getCode();
11
+ $scopeHashId = Mage::getStoreConfig('doofinder_search/internal_settings/hash_id', $code);
12
+ if ($hashId !== '' && $hashId === $scopeHashId) {
13
+ Mage::throwException("HashID ".$hashId." is already used in ".$code." store. It must have a unique value.");
14
+ exit;
15
+ }
16
+ }
17
+ return parent::save();
18
+ }
19
+ }
app/code/community/Doofinder/Feed/Model/CatalogSearch/Resource/Fulltext.php ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class Doofinder_Feed_Model_CatalogSearch_Resource_Fulltext extends Mage_CatalogSearch_Model_Resource_Fulltext
4
+ {
5
+ /**
6
+ * Get stored results select
7
+ *
8
+ * @param int $query_id
9
+ * @param int $attr
10
+ * @return Varien_Db_Select
11
+ */
12
+ protected function getStoredResultsSelect($query_id, $attr = 'product_id')
13
+ {
14
+ $adapter = $this->_getReadAdapter();
15
+
16
+ $select = $adapter->select()
17
+ ->from($this->getTable('catalogsearch/result'), $attr)
18
+ ->where('query_id = ?', $query_id)
19
+ ->order('relevance desc');
20
+
21
+ return $select;
22
+ }
23
+
24
+ /**
25
+ * Get stored results in CatalogSearch cache
26
+ *
27
+ * @param int $query_id
28
+ * @param int $limit
29
+ * @return array
30
+ */
31
+ protected function getStoredResults($query_id, $limit)
32
+ {
33
+ $adapter = $this->_getReadAdapter();
34
+ $select = $this->getStoredResultsSelect($query_id);
35
+ $select->limit($limit);
36
+
37
+ $results = array();
38
+ foreach ($adapter->fetchAll($select) as $result) {
39
+ $results[] = $result['product_id'];
40
+ }
41
+
42
+ return $results;
43
+ }
44
+
45
+ /**
46
+ * Get number of stored results in CatalogSearch cache
47
+ *
48
+ * @param int $query_id
49
+ * @return array
50
+ */
51
+ protected function getStoredResultsCount($query_id)
52
+ {
53
+ $adapter = $this->_getReadAdapter();
54
+ $select = $this->getStoredResultsSelect($query_id, 'COUNT(*)');
55
+
56
+ return (int) $adapter->fetchOne($select);
57
+ }
58
+
59
+ /**
60
+ * Override prepareResult.
61
+ *
62
+ * @param Mage_CatalogSearch_Model_Fulltext $object
63
+ * @param string $queryText
64
+ * @param Mage_CatalogSearch_Model_Query $query
65
+ *
66
+ * @return Doofinder_Feed_Model_CatalogSearch_Resource_Fulltext
67
+ */
68
+ public function prepareResult($object, $queryText, $query)
69
+ {
70
+ if(!Mage::getStoreConfigFlag('doofinder_search/internal_settings/enable', Mage::app()->getStore())) {
71
+ return parent::prepareResult($object, $queryText, $query);
72
+ }
73
+
74
+ $helper = Mage::helper('doofinder_feed/search');
75
+
76
+ // Fetch initial results
77
+ $results = $helper->performDoofinderSearch($queryText);
78
+
79
+ $adapter = $this->_getWriteAdapter();
80
+
81
+ if ($query->getIsProcessed()) {
82
+ $storedResults = $this->getStoredResults($query->getId(), count($results));
83
+ $maxResults = Mage::getStoreConfig('doofinder_search/internal_settings/total_limit', Mage::app()->getStore());
84
+
85
+ // Compare results count and checksum
86
+ if (min($helper->getResultsCount(), $maxResults) == $this->getStoredResultsCount($query->getId()) &&
87
+ $this->calculateChecksum($results) == $this->calculateChecksum($storedResults)) {
88
+ return $this;
89
+ }
90
+
91
+ // Delete results
92
+ $select = $adapter->select()
93
+ ->from($this->getTable('catalogsearch/result'), 'product_id')
94
+ ->where('query_id = ?', $query->getId());
95
+ $adapter->query($adapter->deleteFromSelect($select, $this->getTable('catalogsearch/result')));
96
+ }
97
+
98
+ try {
99
+
100
+ // Fetch all results
101
+ $results = $helper->getAllResults();
102
+
103
+ if (!empty($results)) {
104
+ $data = array();
105
+ $relevance = count($results);
106
+ foreach($results as $product_id) {
107
+ $data[] = array(
108
+ 'query_id' => $query->getId(),
109
+ 'product_id' => $product_id,
110
+ 'relevance' => $relevance--,
111
+ );
112
+ }
113
+
114
+ $adapter->insertOnDuplicate($this->getTable('catalogsearch/result'), $data);
115
+ }
116
+
117
+ $query->setIsProcessed(1);
118
+
119
+ } catch (Exception $e) {
120
+ Mage::logException($e);
121
+ return parent::prepareResult($object, $queryText, $query);
122
+ }
123
+
124
+ return $this;
125
+ }
126
+
127
+ /**
128
+ * Calculate results checksum
129
+ *
130
+ * @param array[int] $results
131
+ * @return string
132
+ */
133
+ protected function calculateChecksum(array $results)
134
+ {
135
+ return hash('sha256', implode(',', $results));
136
+ }
137
+ }
app/code/community/Doofinder/Feed/Model/Config.php ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Config model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Config extends Mage_Core_Model_Config_Data
19
+ {
20
+ const DEFAULT_SECTION = 'feed';
21
+ const BASE_CONFIG_PATH = 'doofinder';
22
+
23
+ protected $_directives = null;
24
+ protected $_product_attribute_codes = null;
25
+ protected $_product_directives = null;
26
+
27
+ const OUT_OF_STOCK = 'out of stock';
28
+ const IN_STOCK = 'in stock';
29
+
30
+ public function getOutOfStockStatus() {
31
+ return self::OUT_OF_STOCK;
32
+ }
33
+
34
+ public function getInStockStatus() {
35
+ return self::IN_STOCK;
36
+ }
37
+
38
+ //
39
+ // Get Config
40
+ //
41
+
42
+ public function getConfigVar($key, $storeId = null,
43
+ $section = self::DEFAULT_SECTION)
44
+ {
45
+ if ($key == 'field_map')
46
+ return $this->getConfigVarFieldMap($key, $storeId, $section);
47
+
48
+ $path = self::BASE_CONFIG_PATH . '/' . $section . '/' . $key;
49
+
50
+ return Mage::getStoreConfig($path, $storeId);
51
+ }
52
+
53
+ public function getConfigVarFieldMap($key, $storeId = null,
54
+ $section = self::DEFAULT_SECTION)
55
+ {
56
+ $path = self::BASE_CONFIG_PATH . '/' . $section . '/' . $key;
57
+ $data = Mage::getStoreConfig($path, $storeId);
58
+
59
+ if (empty($data))
60
+ $data = $this->convertDefaultFieldMap($storeId);
61
+
62
+ return $data;
63
+ }
64
+
65
+ public function getMultipleSelectVar($key, $storeId = null,
66
+ $section = self::DEFAULT_SECTION)
67
+ {
68
+ $str = $this->getConfigVar($key, $storeId, $section);
69
+ $values = array();
70
+
71
+ if (!empty($str))
72
+ {
73
+ $values = explode(',', $str);
74
+ }
75
+
76
+ return array_filter($values);
77
+ }
78
+
79
+
80
+ //
81
+ // Defaults
82
+ //
83
+
84
+ public function convertDefaultFieldMap($storeId = null)
85
+ {
86
+ $result = array();
87
+ $defaultMapping = $this->getConfigVar('default_field_map', $storeId);
88
+
89
+ foreach ($defaultMapping as $field => $config)
90
+ {
91
+ $result[] = array(
92
+ 'label' => $config['label'],
93
+ 'attribute' => $config['attribute'],
94
+ 'field' => $field,
95
+ );
96
+ }
97
+
98
+ return $result;
99
+ }
100
+
101
+
102
+ //
103
+ // Checks
104
+ //
105
+
106
+ public function isDirective($code, $storeId = null)
107
+ {
108
+ if (is_null($this->_directives))
109
+ $this->_directives = $this->getConfigVar('directives', $storeId);
110
+
111
+ return isset($this->_directives[$code]);
112
+ }
113
+
114
+ /**
115
+ * Returns 1 if the current version is greater than the specified in the
116
+ * parameter. 0 if is equal. -1 if is lower.
117
+ */
118
+ public function compareMagentoVersion($infoArray)
119
+ {
120
+ $v = Mage::getVersionInfo();
121
+
122
+ foreach (array('major', 'minor', 'revision', 'patch') as $key)
123
+ {
124
+ if ($v[$key] != $infoArray[$key])
125
+ return $v[$key] > $infoArray[$key] ? 1 : -1;
126
+ }
127
+
128
+ return 0;
129
+ }
130
+
131
+
132
+ //
133
+ // Tools for Dropdowns
134
+ //
135
+
136
+ // protected function _loadProductAttributeCodes($storeId = null)
137
+ // {
138
+ // if (!is_null($this->_product_attribute_codes))
139
+ // return;
140
+
141
+ // $config = Mage::getModel('eav/config');
142
+
143
+ // $this->_product_attribute_codes = array();
144
+
145
+ // $excludedAttrs = $this->getMultipleSelectVar('excluded_attributes');
146
+ // $attributesCodes = $config->getEntityAttributeCodes(
147
+ // 'catalog_product',
148
+ // new Varien_Object(array('store_id' => $storeId))
149
+ // );
150
+
151
+ // foreach ($attributesCodes as $attrCode)
152
+ // {
153
+ // if (array_search($attrCode, $excludedAttrs) !== false)
154
+ // continue;
155
+
156
+ // $attr = $config->getAttribute('catalog_product', $attrCode);
157
+
158
+ // if ($attr !== false && $attr->getAttributeId() > 0)
159
+ // {
160
+ // $code = $attr->getAttributeCode();
161
+ // $this->_product_attribute_codes[$code] = addslashes(
162
+ // $attr->getFrontend()->getLabel().' ('.$code.')'
163
+ // );
164
+ // }
165
+ // }
166
+
167
+ // asort($this->_product_attribute_codes);
168
+ // }
169
+
170
+ protected function _loadProductDirectives($storeId = null)
171
+ {
172
+ if (!is_null($this->_product_directives))
173
+ return;
174
+
175
+ $this->_product_directives = array();
176
+
177
+ foreach ($this->getConfigVar('directives', $storeId) as $code => $cfg)
178
+ {
179
+ $this->_product_directives[$code] = $cfg['label'];
180
+ }
181
+
182
+ asort($this->_product_directives);
183
+ }
184
+
185
+ public function getProductAttributesCodes($storeId = null,
186
+ $includeDirectives = true)
187
+ {
188
+ $this->_loadProductAttributeCodes($storeId);
189
+
190
+ if ($includeDirectives === true)
191
+ {
192
+ $this->_loadProductDirectives($storeId);
193
+
194
+ return array_merge($this->_product_directives,
195
+ $this->_product_attribute_codes);
196
+ }
197
+
198
+ return $this->_product_attribute_codes;
199
+ }
200
+ }
app/code/community/Doofinder/Feed/Model/Cron.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Cron extends Mage_Core_Model_Abstract {
13
+
14
+
15
+ protected function _construct() {
16
+ $this->_init('doofinder_feed/cron');
17
+
18
+ }
19
+
20
+ public function modeDisabled() {
21
+ $helper = Mage::helper('doofinder_feed');
22
+ $this->setStatus($helper::STATUS_DISABLED)
23
+ ->setOffset(0)
24
+ ->setComplete(null)
25
+ ->setNextRun(null)
26
+ ->setNextIteration(null)
27
+ ->setMessage($helper::MSG_DISABLED)
28
+ ->save();
29
+ }
30
+
31
+ public function modeWaiting() {
32
+ $helper = Mage::helper('doofinder_feed');
33
+ $this->setStatus($helper::STATUS_WAITING)
34
+ ->setMessage($helper::MSG_WAITING)
35
+ ->save();
36
+ }
37
+
38
+
39
+ }
40
+
app/code/community/Doofinder/Feed/Model/Generator.php ADDED
@@ -0,0 +1,837 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Generator model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ if (!defined('DS'))
19
+ define('DS', DIRECTORY_SEPARATOR);
20
+
21
+ class Doofinder_Feed_Model_Generator extends Varien_Object
22
+ {
23
+ const DEFAULT_BATCH_SIZE = 100;
24
+ const PRODUCT_ELEMENT = 'item';
25
+ const CATEGORY_SEPARATOR = '%%';
26
+ const CATEGORY_TREE_SEPARATOR = '>';
27
+ const VALUE_SEPARATOR = '/';
28
+
29
+
30
+ protected $_badChars = array('"',"\r\n","\n","\r","\t", "|");
31
+ protected $_repChars = array(""," "," "," "," ", "");
32
+
33
+ protected $_store;
34
+ protected $_oRootCategory;
35
+
36
+ protected $_maxProductId;
37
+
38
+ protected $_attributes = array();
39
+ protected $_categories = array();
40
+ protected $_fieldMap;
41
+
42
+ protected $_iDumped = 0;
43
+ protected $_iSkipped = 0;
44
+
45
+ protected $_oXmlWriter;
46
+
47
+ protected $_response;
48
+
49
+ protected $_errors = array();
50
+
51
+ protected $_lastProcessedProductId;
52
+
53
+ /**
54
+ * Log to doofinder generator logfile
55
+ *
56
+ * @param string $message
57
+ * @param integer $level
58
+ */
59
+ public function logError($message)
60
+ {
61
+ $this->_errors[] = $message;
62
+ }
63
+
64
+ /**
65
+ * Log to doofinder generator logfile only once
66
+ *
67
+ * @param string $message
68
+ */
69
+ public function logErrorOnce($message)
70
+ {
71
+ if (!in_array($message, $this->getErrors())) {
72
+ $this->logError($message);
73
+ }
74
+ }
75
+
76
+ //
77
+ // public::Export
78
+ //
79
+
80
+ public function run()
81
+ {
82
+ // This must NOT depend on cron being enabled because it's used
83
+ // by the front controller!!!
84
+
85
+ Doofinder_Feed_Model_Map_Product_Configurable::setGrouped($this->getData('grouped'));
86
+ // Some config
87
+ $this->_oRootCategory = $this->getRootCategory();
88
+
89
+ // Generate Feed
90
+ $this->_loadAdditionalAttributes();
91
+ $this->_maxProductId = $this->getMaxProductId();
92
+
93
+ // Clear errors
94
+ $this->_errors = array();
95
+
96
+ // Perform run
97
+ $this->_initFeed();
98
+ $this->_batchProcessProducts(
99
+ $this->getData('_offset_'),
100
+ $this->getData('_limit_')
101
+ );
102
+
103
+ // Only close feed if close empty flag is set to true or there was at least one processed product
104
+ if ($this->getData('close_empty') || $this->getLastProcessedProductId() != $this->getData('_offset_')) {
105
+ $this->_closeFeed();
106
+ }
107
+
108
+ return $this->_response;
109
+ }
110
+
111
+ public function getSQL()
112
+ {
113
+ return $this->_getProductCollection(
114
+ $this->getData('_offset_'), $this->getData('_limit_')
115
+ )->getSelect()->assemble();
116
+ }
117
+
118
+ public function getProductCount()
119
+ {
120
+ return $this->_getProductCollection()->getSize();
121
+ }
122
+
123
+ public function getMaxProductId()
124
+ {
125
+ $collection = $this->_getProductCollection();
126
+ $collection->getSelect()->limit(1);
127
+ $collection->getSelect()->order('e.entity_id DESC');
128
+ $item = $collection->fetchItem();
129
+
130
+ return $item ? $item->getEntityId() : 0;
131
+ }
132
+
133
+ /**
134
+ * Is the feed done, are there any products
135
+ * left to process.
136
+ *
137
+ * @return boolean
138
+ */
139
+ public function isFeedDone()
140
+ {
141
+ return $this->_lastProcessedProductId >= $this->_maxProductId;
142
+ }
143
+
144
+ /**
145
+ * Get the ID of the last processed product.
146
+ *
147
+ * @return integer
148
+ */
149
+ public function getLastProcessedProductId()
150
+ {
151
+ return $this->_lastProcessedProductId;
152
+ }
153
+
154
+ /**
155
+ * Get generator progress, it is what part
156
+ * of products has been processed yet.
157
+ *
158
+ * @return double
159
+ */
160
+ public function getProgress()
161
+ {
162
+ $collection = $this->_getProductCollection();
163
+
164
+ $all = $collection->getSize();
165
+
166
+ $collection = $this->_getProductCollection();
167
+ $collection->addAttributeToFilter('entity_id', array('lteq' => $this->_lastProcessedProductId));
168
+ $now = $collection->getSize();
169
+
170
+ return $now / $all;
171
+ }
172
+
173
+ public function addProductToFeed($args)
174
+ {
175
+ try
176
+ {
177
+ $row = $args['row'];
178
+
179
+ $this->_lastProcessedProductId = $row['entity_id'];
180
+
181
+ $parentEntityId = null;
182
+
183
+ $map = $this->_getProductMapModel($row['type_id'], array());
184
+
185
+ if (is_null($map)) {
186
+ Mage::throwException("There is no map definition for product with type {$row['type_id']}");
187
+ }
188
+
189
+
190
+ $product = Mage::getModel('catalog/product');
191
+ $product->setData($row)
192
+ ->setStoreId($this->getStoreId())
193
+ ->setCustomerGroupId($this->getData('customer_group_id'));
194
+
195
+ $product->getResource()->load($product, $row['entity_id']);
196
+ $map->setGenerator($this)
197
+ ->setProduct($product)
198
+ ->setFieldsMap($this->_getFieldsMap())
199
+ ->initialize();
200
+
201
+ if ($map->checkSkipSubmission()->isSkip())
202
+ return;
203
+
204
+ if ($this->_addProductToXml($map))
205
+ $this->_iDumped++;
206
+
207
+ $map->unsetData();
208
+ }
209
+ catch (Exception $e)
210
+ {
211
+ $this->logError('Error processing product (ID: ' . $row['entity_id'] . '): ' . $e->getMessage(), Zend_Log::ERR);
212
+ }
213
+ }
214
+
215
+
216
+ //
217
+ // protected::Export
218
+ //
219
+
220
+ protected function _batchProcessProducts($offset, $limit)
221
+ {
222
+ // Make sure we have this initialized
223
+ // in case of an empty collection
224
+ $this->_lastProcessedProductId = $offset;
225
+
226
+ $collection = $this->_getProductCollection($offset, $limit);
227
+
228
+ Mage::getSingleton('core/resource_iterator')->walk(
229
+ $collection->getSelect(),
230
+ array(array($this, 'addProductToFeed'))
231
+ );
232
+ $this->_flushFeed();
233
+
234
+ }
235
+
236
+ protected function _addProductToXml(
237
+ Doofinder_Feed_Model_Map_Product_Abstract $productMap)
238
+ {
239
+
240
+ $iDumped = 0;
241
+ $displayPrice = $this->getDisplayPrice();
242
+
243
+ try
244
+ {
245
+ if ($productMap->isSkip())
246
+ {
247
+ $this->_iSkipped++;
248
+ return $this;
249
+ }
250
+
251
+ $productData = $productMap->map();
252
+
253
+ if ($productMap->getProduct()->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE
254
+ && $productMap->hasAssocMaps()
255
+ && $productMap->getIsVariants())
256
+ {
257
+ foreach ($productMap->getAssocMaps() as $assocMap)
258
+ if ($assocMap->isSkip())
259
+ $this->_iSkipped++;
260
+ }
261
+
262
+ if (($iProducts = count($productData)) > 1)
263
+ {
264
+ $productData[0] = array_filter($productData[0]);
265
+ // $productData[0]['assoc_id'] = $productData[0]['id'];
266
+
267
+ for ($i = 1; $i < $iProducts; $i++)
268
+ {
269
+ $productData[$i] = array_merge(
270
+ $productData[0],
271
+ array_filter($productData[$i])
272
+ );
273
+ // $productData[$i]['assoc_id'] = $productData[0]['id'];
274
+ }
275
+ }
276
+
277
+ foreach ($productData as $data)
278
+ {
279
+ $this->_oXmlWriter->startElement(self::PRODUCT_ELEMENT);
280
+
281
+ if (!isset($data['description']))
282
+ {
283
+ if (isset($data['long_description'])) {
284
+ $data['description'] = $data['long_description'];
285
+ unset($data['long_description']);
286
+ } else {
287
+ $data['description'] = '';
288
+ }
289
+ }
290
+
291
+ krsort($data);
292
+
293
+ foreach ($data as $field => $value)
294
+ {
295
+
296
+ if (!is_array($value))
297
+ {
298
+ $value = trim($value);
299
+ }
300
+
301
+ if ($field != 'description' && empty($value))
302
+ {
303
+ continue;
304
+ }
305
+
306
+ if (!$displayPrice && ($field === 'price' || $field === 'sale_price'))
307
+ {
308
+ continue;
309
+ }
310
+
311
+ $this->_oXmlWriter->startElement($field);
312
+
313
+ // Make sure $value is a flat array
314
+ if (!is_array($value))
315
+ {
316
+ $value = array($value);
317
+ }
318
+ else if (!$this->_isArrayFlat($value))
319
+ {
320
+ $this->logErrorOnce("Value of $field field is a multidimensional array, encoded value: " . json_encode($value));
321
+ $value = $this->_flattenArray($value);
322
+ }
323
+
324
+ $value = implode(self::VALUE_SEPARATOR, array_filter($value));
325
+
326
+ $written = @$this->_oXmlWriter->writeCData($value);
327
+ if ( ! $written )
328
+ {
329
+ $this->_oXmlWriter->writeComment("Cannot write the value for the $field field.");
330
+
331
+ $this->logErrorOnce("Cannot write the value for the $field field, encoded value: " . json_encode($value));
332
+ }
333
+
334
+ $this->_oXmlWriter->endElement();
335
+ }
336
+
337
+ $this->_oXmlWriter->endElement();
338
+
339
+ $iDumped++;
340
+ }
341
+ }
342
+ catch (Exception $e)
343
+ {
344
+ if ($this->getConfigVar('debug') == 1)
345
+ $this->_debug($e->getMessage());
346
+ }
347
+
348
+ return $iDumped > 0;
349
+ }
350
+
351
+
352
+ //
353
+ // public::Configuration
354
+ //
355
+
356
+ public function getContentType()
357
+ {
358
+ return self::CONTENT_TYPE;
359
+ }
360
+
361
+ public function getConfig()
362
+ {
363
+ return Mage::getSingleton('doofinder_feed/config');
364
+ }
365
+
366
+ public function getConfigVar($key, $storeId = null,
367
+ $section = Doofinder_Feed_Model_Config::DEFAULT_SECTION)
368
+ {
369
+ return $this->getConfig()->getConfigVar($key, $storeId, $section);
370
+ }
371
+
372
+
373
+ //
374
+ // public::Tools
375
+ //
376
+
377
+ public function getStore()
378
+ {
379
+ if (is_null($this->_store))
380
+ $this->_loadStore();
381
+
382
+ return $this->_store;
383
+ }
384
+
385
+ public function getStoreId()
386
+ {
387
+ return $this->getStore()->getStoreId();
388
+ }
389
+
390
+ public function getStoreCode()
391
+ {
392
+ return $this->getStore()->getCode();
393
+ }
394
+
395
+ public function getWebsiteId()
396
+ {
397
+ return $this->getStore()->getWebsiteId();
398
+ }
399
+
400
+ public function getErrors()
401
+ {
402
+ return $this->_errors;
403
+ }
404
+
405
+ public function getRootCategory()
406
+ {
407
+ if (is_null($this->_oRootCategory))
408
+ {
409
+ $this->_oRootCategory = Mage::getModel('catalog/category')->load(
410
+ $this->getStore()->getRootCategoryId()
411
+ );
412
+ }
413
+
414
+ return $this->_oRootCategory;
415
+ }
416
+
417
+ public function getCategories($product)
418
+ {
419
+ $categories = array();
420
+
421
+ $prodCategories = Mage::getResourceModel('catalog/category_collection')
422
+ ->addIdFilter($product->getCategoryIds())
423
+ ->addFieldToFilter('path', array('like' => $this->_oRootCategory->getPath() . '/%'))
424
+ ->addFieldToFilter('is_active', array('eq'=>'1'));
425
+
426
+ $include_in_menu = Mage::getStoreConfig(
427
+ 'doofinder_cron/feed_settings/categories_in_navigation',
428
+ $this->getStoreId()
429
+ );
430
+
431
+ if($include_in_menu == 1) {
432
+ $prodCategories->addFieldToFilter('include_in_menu', array('eq'=> '1'));
433
+ }
434
+
435
+ $prodCategories = $prodCategories->getItems();
436
+
437
+ $prodCategories = array_keys($prodCategories);
438
+
439
+ foreach ($prodCategories as $id)
440
+ {
441
+ if (isset($this->_categories[$id]))
442
+ $tree = $this->_categories[$id];
443
+ else
444
+ $tree = $this->_getCategoryTree($id);
445
+
446
+ if (strlen($tree))
447
+ $categories[] = $tree;
448
+ }
449
+
450
+ sort($categories);
451
+
452
+ $nbcategories = count($categories);
453
+ $result = array();
454
+
455
+ for ($i = 1; $i < $nbcategories; $i++)
456
+ {
457
+ if (strpos($categories[$i], $categories[$i - 1]) === 0)
458
+ continue;
459
+ $result[] = $this->_cleanFieldValue($categories[$i - 1]);
460
+ }
461
+
462
+ if (!empty($categories[$i - 1]))
463
+ $result[] = $this->_cleanFieldValue($categories[$i - 1]);
464
+
465
+ return $result;
466
+ }
467
+
468
+ /**
469
+ * Get all parent category names (including itself) for selected category ID
470
+ * @param int $catId Category ID
471
+ * @return string Category names concat'd by CATEGORY_TREE_SEPARATOR
472
+ */
473
+ protected function _getCategoryTree($catId)
474
+ {
475
+ $category = Mage::getModel('catalog/category')->load($catId);
476
+ $tree = array();
477
+
478
+ $path = $category->getPath();
479
+ $ids = explode('/', $path);
480
+
481
+ unset($ids[0]);
482
+
483
+ $categories = Mage::getModel('catalog/category')
484
+ ->getCollection()
485
+ ->setStoreId($this->getStoreId())
486
+ ->addIdFilter($ids)
487
+ ->addAttributeToSort('path', 'asc')
488
+ ->addAttributeToSelect('*');
489
+
490
+ foreach ($categories as $category)
491
+ {
492
+ if ($category->getId() != $this->_oRootCategory->getId())
493
+ {
494
+ if (strlen($category->getName()))
495
+ {
496
+ $tree[] = strip_tags($category->getName());
497
+ }
498
+ }
499
+ }
500
+
501
+ $tree = $this->_sanitizeData($tree);
502
+ $tree = implode(self::CATEGORY_TREE_SEPARATOR, $tree);
503
+ $this->_categories[$catId] = $tree;
504
+
505
+ return $this->_categories[$catId];
506
+ }
507
+
508
+ public function getAttribute($attrCode)
509
+ {
510
+ if (isset($this->_attributes[$attrCode]))
511
+ return $this->_attributes[$attrCode];
512
+
513
+ return false;
514
+ }
515
+
516
+ public function getTools()
517
+ {
518
+ return Mage::getSingleton('doofinder_feed/tools');
519
+ }
520
+
521
+
522
+ //
523
+ // protected::Output
524
+ //
525
+
526
+ protected function _initFeed()
527
+ {
528
+ $this->_oXmlWriter = new XMLWriter();
529
+ $this->_oXmlWriter->openMemory();
530
+ if (!$this->getData('_offset_'))
531
+ {
532
+ $this->_oXmlWriter->startDocument('1.0', 'UTF-8');
533
+
534
+ // Output the parent rss tag
535
+ $this->_oXmlWriter->startElement('rss');
536
+ $this->_oXmlWriter->writeAttribute('version', '2.0');
537
+
538
+ $this->_oXmlWriter->startElement('channel');
539
+ $this->_oXmlWriter->writeElement('title', 'Product feed');
540
+ $this->_oXmlWriter->startElement('link');
541
+ $this->_oXmlWriter->writeCData(Mage::getBaseUrl().'doofinder/feed');
542
+ $this->_oXmlWriter->endElement();
543
+ $this->_oXmlWriter->writeElement('pubDate', strftime('%a, %d %b %Y %H:%M:%S %Z'));
544
+ $this->_oXmlWriter->writeElement('generator', 'Doofinder/'.Mage::getConfig()->getModuleConfig("Doofinder_Feed")->version);
545
+ $this->_oXmlWriter->writeElement('description', 'Magento Product feed for Doofinder');
546
+
547
+ $this->_flushFeed();
548
+ }
549
+ }
550
+
551
+ protected function _flushFeed()
552
+ {
553
+ $this->_response .= $this->_oXmlWriter->flush(true);
554
+ }
555
+
556
+ protected function _closeFeed()
557
+ {
558
+ if ($this->isFeedDone())
559
+ {
560
+ if (!$this->getData('_offset_'))
561
+ {
562
+ $this->_oXmlWriter->endElement(); // Channel
563
+ $this->_oXmlWriter->endElement(); // RSS
564
+ $this->_oXmlWriter->endDocument();
565
+
566
+ $this->_flushFeed();
567
+ } else
568
+ {
569
+ $this->_response .= '</channel></rss>';
570
+ }
571
+ }
572
+ }
573
+
574
+ protected function _debug($m)
575
+ {
576
+ // $this->_response .= '<pre>';
577
+ // var_dump($m);
578
+ // $this->_response .= '</pre>';
579
+ }
580
+
581
+ protected function _sanitizeData($data)
582
+ {
583
+ $sanitized = array();
584
+
585
+ foreach ($data as $key => $value)
586
+ $sanitized[$key] = str_replace($this->_badChars,
587
+ $this->_repChars,
588
+ $value);
589
+
590
+ return $sanitized;
591
+ }
592
+
593
+ protected function _addProductTypeToFilter($collection)
594
+ {
595
+ $disabled = array_diff(
596
+ array(
597
+ Mage_Catalog_Model_Product_Type::TYPE_BUNDLE,
598
+ Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE,
599
+ Mage_Downloadable_Model_Product_Type::TYPE_DOWNLOADABLE,
600
+ Mage_Catalog_Model_Product_Type::TYPE_GROUPED,
601
+ Mage_Catalog_Model_Product_Type::TYPE_SIMPLE,
602
+ Mage_Catalog_Model_Product_Type::TYPE_VIRTUAL,
603
+ ),
604
+ explode(',', $this->getConfigVar('product_types'))
605
+ );
606
+
607
+ // Check if we should disable specific types
608
+ if (count($disabled) > 0)
609
+ $collection->addAttributeToFilter('type_id',
610
+ array('nin' => $disabled));
611
+
612
+ return $collection;
613
+ }
614
+
615
+
616
+ //
617
+ // protected::Tools
618
+ //
619
+
620
+ protected function _loadStore()
621
+ {
622
+ if (!$this->hasData('store_code'))
623
+ $this->setData('store_code', Mage_Core_Model_Store::DEFAULT_CODE);
624
+
625
+ try
626
+ {
627
+ $this->_store = Mage::app()->getStore($this->getData('store_code'));
628
+ }
629
+ catch (Exception $e)
630
+ {
631
+ $e->setMessage('Invalid Store Code.');
632
+ $this->_stopOnException($e);
633
+ }
634
+ }
635
+
636
+ protected function _loadAdditionalAttributes()
637
+ {
638
+ $storeId = $this->getStoreId();
639
+
640
+ $attributeCodes = $this->getConfig()
641
+ ->getMultipleSelectVar('additional_attributes', $storeId);
642
+ $model = Mage::getModel('catalog/product')->setStoreId($storeId);
643
+
644
+ foreach ($attributeCodes as $attrCode)
645
+ {
646
+ $attribute = $model->getResource()->getAttribute($attrCode);
647
+ $this->_attributes[$attribute->getAttributeCode()] = $attribute;
648
+ }
649
+ }
650
+
651
+ protected function _getProductCollection($offset = 0, $limit = 0)
652
+ {
653
+ $collection = $this->getProductCollection($offset, $limit);
654
+
655
+ if (count($this->getProducts()))
656
+ $collection->addAttributeToFilter('entity_id', array('in' => $this->getProducts()));
657
+
658
+ if ($limit && $limit > 0)
659
+ $collection->getSelect()->limit($limit, 0);
660
+
661
+ if ($offset)
662
+ $collection->addAttributeToFilter('entity_id', array('gt' => $offset));
663
+
664
+
665
+ return $collection;
666
+ }
667
+
668
+ public function getProductCollection()
669
+ {
670
+ $collection = Mage::getModel('catalog/product')
671
+ ->getCollection()
672
+ ->addStoreFilter($this->getStoreId());
673
+
674
+ $this->_addProductTypeToFilter($collection);
675
+
676
+ $collection->addAttributeToFilter('status', 1);
677
+ $collection->addAttributeToFilter('visibility', array(
678
+ Mage_Catalog_Model_Product_Visibility::VISIBILITY_BOTH,
679
+ Mage_Catalog_Model_Product_Visibility::VISIBILITY_IN_SEARCH
680
+ ));
681
+ $collection->addAttributeToSelect('*');
682
+
683
+ return $collection;
684
+ }
685
+
686
+ protected function _getProductMapModel($typeId, $args = array())
687
+ {
688
+ $isAssoc = isset($args['is_assoc']) && $args['is_assoc'] ? true : false;
689
+
690
+ switch ($typeId)
691
+ {
692
+ case 'simple':
693
+ if ($isAssoc)
694
+ $model = 'doofinder_feed/map_product_associated';
695
+ else
696
+ $model = 'doofinder_feed/map_product_simple';
697
+ break;
698
+
699
+ case 'abstract':
700
+ case 'bundle':
701
+ case 'configurable':
702
+ case 'downloadable':
703
+ case 'grouped':
704
+ case 'virtual':
705
+ $model = 'doofinder_feed/map_product_'.$typeId;
706
+ break;
707
+
708
+ default:
709
+ return null;
710
+ }
711
+
712
+ return Mage::getModel($model, array(
713
+ 'store_code' => $this->getStoreCode(),
714
+ 'store_id' => $this->getStoreId(),
715
+ 'website_id' => $this->getWebsiteId(),
716
+ ));
717
+ }
718
+
719
+ protected function _getFieldsMap()
720
+ {
721
+ if (!is_null($this->_fieldMap))
722
+ return $this->_fieldMap;
723
+
724
+ $product = Mage::getModel('catalog/product')
725
+ ->setStoreId($this->getStoreId());
726
+
727
+
728
+ $this->_fieldMap = array();
729
+
730
+ $fields = $this->getConfigVar('fields');
731
+
732
+ $map = Mage::getStoreConfig('doofinder_cron/attributes_mapping', $this->getStore());
733
+ $additional = array();
734
+ if (isset($map['additional'])) {
735
+ $additional = unserialize($map['additional']);
736
+ }
737
+
738
+ unset($map['additional']);
739
+
740
+ if (!empty($additional['additional_mapping']))
741
+ {
742
+ foreach ($additional['additional_mapping'] as $data)
743
+ {
744
+ if (isset($map[$data['field']])) continue;
745
+
746
+ $fields[$data['field']] = array('label' => $data['label']);
747
+ $map[$data['field']] = $data['attribute'];
748
+ }
749
+ }
750
+
751
+ foreach ($map as $key => $attName)
752
+ {
753
+ if (!isset($fields[$key])) continue;
754
+
755
+ if (!$this->getConfig()->isDirective($attName,
756
+ $this->getStoreId()))
757
+ {
758
+ $att = $product->getResource()->getAttribute($attName);
759
+
760
+ if ($att === false)
761
+ {
762
+ continue;
763
+ }
764
+
765
+ $att->setStoreId($this->getStoreId());
766
+ $this->_attributes[$att->getAttributeCode()] = $att;
767
+ }
768
+
769
+ $this->_fieldMap[$key] = array(
770
+ 'label' => $fields[$key]['label'],
771
+ 'attribute' => $attName,
772
+ 'field' => $key,
773
+ );
774
+ }
775
+ return $this->_fieldMap;
776
+ }
777
+
778
+ protected function _stopOnException(Exception $e)
779
+ {
780
+ Mage::logError($e->getMessage());
781
+ }
782
+
783
+ protected function _cleanFieldValue($field)
784
+ {
785
+ // http://stackoverflow.com/questions/4224141/php-removing-invalid-utf-8-characters-in-xml-using-filter
786
+ $valid_utf8 = '/([\x09\x0A\x0D\x20-\x7E]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})|./x';
787
+
788
+ $field = preg_replace('#<br(\s?/)?>#i', ' ', $field);
789
+ $field = strip_tags($field);
790
+ $field = preg_replace('/[ ]{2,}/', ' ', $field);
791
+ $field = trim($field);
792
+ $exField = explode(self::CATEGORY_TREE_SEPARATOR, $field);
793
+ $newField = array();
794
+ foreach ($exField as $el) {
795
+ $newField[] = html_entity_decode($el, null, 'UTF-8');
796
+ }
797
+ $field = implode(self::CATEGORY_TREE_SEPARATOR, $newField );
798
+
799
+ return preg_replace($valid_utf8, '$1', $field);
800
+ }
801
+
802
+ /**
803
+ * Check if array is flat (not multidimensional)
804
+ *
805
+ * @param array $arr
806
+ * @return boolean
807
+ */
808
+ protected function _isArrayFlat(array $arr)
809
+ {
810
+ $isFlat = true;
811
+
812
+ foreach ($arr as $item)
813
+ {
814
+ if (is_array($item))
815
+ {
816
+ $isFlat = false;
817
+ break;
818
+ }
819
+ }
820
+
821
+ return $isFlat;
822
+ }
823
+
824
+ /**
825
+ * Flatten array recursively
826
+ *
827
+ * @notice This requires PHP5.3+
828
+ *
829
+ * @param array @arr
830
+ * @return array
831
+ */
832
+ protected function _flattenArray(array $arr) {
833
+ $flattenedArray = array();
834
+ array_walk_recursive($arr, function($item) use (&$flattenedArray) { $flattenedArray[] = $item; });
835
+ return $flattenedArray;
836
+ }
837
+ }
app/code/community/Doofinder/Feed/Model/Log.php ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Log extends Mage_Core_Model_Abstract {
13
+
14
+ protected function _construct() {
15
+ $this->_init('doofinder_feed/log');
16
+ }
17
+
18
+ }
19
+
app/code/community/Doofinder/Feed/Model/Map/Product/Abstract.php ADDED
@@ -0,0 +1,645 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Abstract Product Map Model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Map_Product_Abstract extends Varien_Object
19
+ {
20
+ protected $_field_map = null;
21
+ protected $skip = false;
22
+ protected $_attributeSetModel;
23
+
24
+
25
+ public function initialize()
26
+ {
27
+ $currency_code = Mage::app()
28
+ ->getStore($this->getData('store_code'))
29
+ ->getCurrentCurrencyCode();
30
+
31
+ $images_url_prefix = Mage::app()
32
+ ->getStore($this->getData('store_id'))
33
+ ->getBaseUrl(Mage_Core_Model_Store::URL_TYPE_MEDIA, false);
34
+ $images_url_prefix .= 'catalog/product';
35
+
36
+ $images_path_prefix = Mage::getSingleton('catalog/product_media_config')
37
+ ->getBaseMediaPath();
38
+
39
+ $this->setData('store_currency_code', $currency_code);
40
+ $this->setData('images_url_prefix', $images_url_prefix);
41
+ $this->setData('images_path_prefix', $images_path_prefix);
42
+
43
+ $this->_attributeSetModel = Mage::getModel('eav/entity_attribute_set');
44
+
45
+ return $this;
46
+ }
47
+
48
+ public function map()
49
+ {
50
+ $this->_beforeMap();
51
+ $rows = $this->_map();
52
+ $this->_afterMap($rows);
53
+
54
+ return $rows;
55
+ }
56
+
57
+ public function _beforeMap()
58
+ {
59
+ return $this;
60
+ }
61
+
62
+ public function _afterMap($rows)
63
+ {
64
+ return $this;
65
+ }
66
+
67
+
68
+ //
69
+ // protected::Mapping
70
+ //
71
+
72
+ /**
73
+ * @return array('column' => 'value')
74
+ */
75
+ protected function _map()
76
+ {
77
+ $fields = array();
78
+
79
+ foreach ($this->_field_map as $column => $arr)
80
+ $fields[$column] = $this->mapField($column);
81
+
82
+ // $fields['magento_store'] = $this->getData('store_code');
83
+
84
+ $this->_attributeSetModel->load(
85
+ $this->getProduct()->getAttributeSetId());
86
+ // $fields['attribute_set'] = $this->_attributeSetModel
87
+ // ->getAttributeSetName();
88
+
89
+ $i = 0;
90
+ $categories = $this->getGenerator()->getCategories($this->getProduct());
91
+ $fields['categories'] = implode(
92
+ Doofinder_Feed_Model_Generator::CATEGORY_SEPARATOR,
93
+ $categories
94
+ );
95
+
96
+ return array($fields);
97
+ }
98
+
99
+ protected function mapField($column)
100
+ {
101
+ $value = "";
102
+
103
+ if (!isset($this->_field_map[$column]))
104
+ return $value;
105
+
106
+ $args = array('map' => $this->_field_map[$column]);
107
+ $method = 'mapField' . $this->_camelize($column);
108
+
109
+ if (method_exists($this, $method))
110
+ $value = $this->$method($args);
111
+ else
112
+ $value = $this->getFieldValue($args);
113
+
114
+ return $value;
115
+ }
116
+
117
+ protected function mapAttribute($params = array())
118
+ {
119
+ $map = $params['map'];
120
+ $product = $this->getProduct();
121
+ $fieldData = '';
122
+
123
+ $attribute = $this->getGenerator()->getAttribute($map['attribute']);
124
+ if ($attribute === false)
125
+ $this->_attributeDoesNotExist($map['attribute']);
126
+
127
+ $fieldData = $this->getAttributeValue($product, $attribute);
128
+
129
+ return $this->cleanField($fieldData);
130
+ }
131
+
132
+ protected function mapDoofinderAttribute($attribute, $product = null)
133
+ {
134
+ if (is_null($product))
135
+ $product = $this->getProduct();
136
+
137
+ if ($attribute === false)
138
+ $this->_attributeDoesNotExist($map['attribute']);
139
+
140
+ $fieldData = $this->getAttributeValue($product, $attribute);
141
+
142
+ return $this->cleanField($fieldData);
143
+ }
144
+
145
+
146
+ //
147
+ // protected::Mapping::Attributes
148
+ //
149
+
150
+ protected function mapAttributeDescription($params = array())
151
+ {
152
+ $map = $params['map'];
153
+ $product = $this->getProduct();
154
+ $fieldData = "";
155
+
156
+ $attribute = $this->getGenerator()
157
+ ->getAttribute($map['attribute']);
158
+
159
+ if ($attribute === false)
160
+ $this->_attributeDoesNotExist($map['attribute']);
161
+
162
+ $description = $this->getAttributeValue($product, $attribute);
163
+
164
+ return $this->cleanField($description);
165
+ }
166
+
167
+
168
+ //
169
+ // protected::Mapping::Directives
170
+ //
171
+
172
+ protected function mapDirectiveId()
173
+ {
174
+ // $storeCode = $this->getStoreCode();
175
+ $fieldData = $this->getProduct()->getId();
176
+ // $fieldData .= '_'.preg_replace('/[^a-zA-Z0-9]/', '', $storeCode);
177
+
178
+ return $this->cleanField($fieldData);
179
+ }
180
+
181
+ protected function mapDirectiveUrl()
182
+ {
183
+ $product = $this->getProduct();
184
+ return $product->getUrlModel()->getUrl($product, array('_nosid' => true));
185
+ }
186
+
187
+ protected function mapDirectiveImageLink($args, $attributeName = 'image')
188
+ {
189
+ $product = $this->getProduct();
190
+ $image = $product->getData('image');
191
+
192
+ if ($image != 'no_selection' && $image != "") {
193
+ $image = Mage::helper('catalog/image')
194
+ ->init($product, $attributeName);
195
+
196
+ if ($size = $this->getGenerator()->getData('image_size')) {
197
+ $image->resize($size);
198
+ }
199
+
200
+ return (string) $image;
201
+ }
202
+
203
+ return "";
204
+ }
205
+
206
+ protected function mapDirectiveImageLinkThumbnail($args)
207
+ {
208
+ return $this->mapDirectiveImageLink($args, 'thumbnail');
209
+ }
210
+
211
+ protected function mapDirectiveImageLinkSmall($args)
212
+ {
213
+ return $this->mapDirectiveImageLink($args, 'small_image');
214
+ }
215
+
216
+ public function collectProductPrices()
217
+ {
218
+ if ( ! $this->getData('collected_product_prices') )
219
+ {
220
+ $dataHelper = Mage::helper('doofinder_feed');
221
+ $taxHelper = Mage::helper('tax');
222
+
223
+ $datum = $dataHelper->collectProductPrices(
224
+ $this->getProduct(),
225
+ $this->getGenerator()->getStore(),
226
+ true,
227
+ $this->getGenerator()->getData('minimal_price'),
228
+ $this->getGenerator()->getData('grouped')
229
+ );
230
+
231
+ $priceDisplayType = $taxHelper->getPriceDisplayType($this->getGenerator()->getStore());
232
+
233
+ if ( $priceDisplayType == Mage_Tax_Model_Config::DISPLAY_TYPE_INCLUDING_TAX
234
+ || $priceDisplayType == Mage_Tax_Model_Config::DISPLAY_TYPE_BOTH )
235
+ {
236
+ $priceKey = 'including_tax';
237
+ }
238
+ else
239
+ {
240
+ $priceKey = 'excluding_tax';
241
+ }
242
+
243
+ $priceType = isset($datum['price_type']) ? $datum['price_type'] : false;
244
+
245
+ $prices = array(
246
+ 'price_type' => $priceType,
247
+ );
248
+
249
+ foreach ( $datum as $priceType => $data ) {
250
+ if ( !is_array($data) ) continue;
251
+
252
+ foreach ( $data as $key => $price ) {
253
+ if ( $key == $priceKey ) {
254
+ $prices[$priceType] = $data[$key];
255
+ }
256
+ }
257
+ }
258
+
259
+ $this->setData('collected_product_prices', $prices);
260
+ }
261
+
262
+ return $this->getData('collected_product_prices');
263
+ }
264
+
265
+ protected function mapDirectivePrice()
266
+ {
267
+ $prices = $this->collectProductPrices();
268
+
269
+ if ( ! array_key_exists('price', $prices) )
270
+ return null;
271
+
272
+ $fieldData = $this->cleanField($prices['price']);
273
+
274
+ if ( $fieldData < 0 )
275
+ $this->skip = true;
276
+
277
+ return $fieldData;
278
+ }
279
+
280
+ protected function mapDirectiveSalePrice()
281
+ {
282
+ $prices = $this->collectProductPrices();
283
+
284
+ if ( ! array_key_exists('sale_price', $prices) )
285
+ return null;
286
+
287
+ $fieldData = $this->cleanField($prices['sale_price']);
288
+
289
+ if ( $fieldData <= 0 )
290
+ return null;
291
+
292
+ return $fieldData;
293
+ }
294
+
295
+ protected function mapDirectiveCurrency()
296
+ {
297
+ return $this->getData('store_currency_code');
298
+ }
299
+
300
+ protected function mapDirectiveAvailability($params = array())
301
+ {
302
+ $map = $params['map'];
303
+ $product = $this->getProduct();
304
+
305
+ $defaultVal = isset($map['default_value']) ? $map['default_value'] : "";
306
+
307
+ if ($defaultVal != "")
308
+ {
309
+ $stock_status = $defaultVal;
310
+ $stock_status = trim(strtolower($stock_status));
311
+
312
+ if (false === array_search($stock_status,
313
+ $this->getConfig()->getAllowedStockStatuses()))
314
+ $stock_status = $this->getConfig()->getOutOfStockStatus();
315
+
316
+ $fieldData = $stock_status;
317
+ $fieldData = $this->cleanField($fieldData);
318
+
319
+ return $fieldData;
320
+ }
321
+
322
+ $fieldData = $this->getConfig()->getOutOfStockStatus();
323
+
324
+ $stockItem = Mage::getModel('cataloginventory/stock_item');
325
+ $stockItem->setStoreId($this->getStoreId());
326
+ $stockItem->getResource()->loadByProductId($stockItem, $product->getId());
327
+ $stockItem->setOrigData();
328
+
329
+ if ($stockItem->getId() && $stockItem->getIsInStock())
330
+ $fieldData = $this->getConfig()->getInStockStatus();
331
+
332
+ return $fieldData;
333
+ }
334
+
335
+ protected function mapDirectiveCondition($params = array())
336
+ {
337
+ $map = $params['map'];
338
+ $product = $this->getProduct();
339
+
340
+ $defaultVal = isset($map['default_value']) ? $map['default_value'] : "";
341
+ $defaultVal = trim(strtolower($defaultVal));
342
+
343
+ if (false === array_search($defaultVal,
344
+ $this->getConfig()->getAllowedConditions()))
345
+ $defaultVal = $this->getConfig()->getConditionNew();
346
+
347
+ $fieldData = $defaultVal;
348
+ $fieldData = $this->cleanField($fieldData);
349
+
350
+ return $fieldData;
351
+ }
352
+
353
+
354
+ //
355
+ // Mapping::Fields
356
+ //
357
+
358
+ public function mapFieldProductType($params = array())
359
+ {
360
+ $args = array('map' => $params['map']);
361
+ $value = "";
362
+
363
+ $map_by_category = $this->getConfig()->getMapCategorySorted(
364
+ 'product_type_by_category',
365
+ $this->getStoreId()
366
+ );
367
+
368
+ $category_ids = $this->getProduct()->getCategoryIds();
369
+
370
+ if (!empty($category_ids) && count($map_by_category) > 0)
371
+ {
372
+ foreach ($map_by_category as $arr)
373
+ {
374
+ if (array_search($arr['category'], $category_ids) !== false)
375
+ {
376
+ $value = $arr['value'];
377
+ break;
378
+ }
379
+ }
380
+ }
381
+
382
+ if ($value != "")
383
+ return htmlspecialchars_decode($value);
384
+
385
+ $value = $this->getFieldValue($args);
386
+
387
+ return htmlspecialchars_decode($value);
388
+ }
389
+
390
+
391
+ //
392
+ // public::Tools
393
+ //
394
+
395
+ public function getFieldValue($args = array())
396
+ {
397
+ $value = "";
398
+ $attName = $args['map']['attribute'];
399
+
400
+ if ($this->getConfig()->isDirective($attName, $this->getStoreId()))
401
+ {
402
+ $attName = str_replace('df_directive_', '', $attName);
403
+ $method = 'mapDirective' . $this->_camelize($attName);
404
+
405
+ if (method_exists($this, $method))
406
+ $value = $this->$method($args);
407
+ }
408
+ else
409
+ {
410
+ $method = 'mapAttribute' . $this->_camelize($attName);
411
+
412
+ if (method_exists($this, $method))
413
+ $value = $this->$method($args);
414
+ else
415
+ $value = $this->mapAttribute($args);
416
+ }
417
+
418
+ return $value;
419
+ }
420
+
421
+ public function getAttributeValue($product, $attribute)
422
+ {
423
+ $attrCode = $attribute->getAttributeCode();
424
+
425
+ if ($attribute->getFrontendInput() == 'select'
426
+ || $attribute->getFrontendInput() == 'multiselect')
427
+ {
428
+ if (!is_null($product->getResource()->getAttribute($attrCode)))
429
+ $value = $product->getAttributeText($attrCode);
430
+ }
431
+ else
432
+ {
433
+ $value = $product->getData($attrCode);
434
+ }
435
+
436
+ return $value;
437
+ }
438
+
439
+ public function loadAssocIds($product, $storeId)
440
+ {
441
+ $assocIds = array();
442
+
443
+ if ($product->getTypeId() != Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE)
444
+ return false;
445
+
446
+ $as = $this->getTools()->getChildsIds($product->getId());
447
+ if ($as === false)
448
+ return $assocIds;
449
+
450
+ $as = $this->getTools()->getProductInStoresIds($as);
451
+ foreach ($as as $assocId => $s)
452
+ {
453
+ $attr = $this->getGenerator()->getAttribute('status');
454
+ $status = $this->getTools()->getProductAttributeValueBySql(
455
+ $attr,
456
+ $attr->getBackendType(),
457
+ $assocId,
458
+ $storeId
459
+ );
460
+
461
+ if ($status != Mage_Catalog_Model_Product_Status::STATUS_ENABLED)
462
+ continue;
463
+
464
+ if (is_array($s) && array_search($storeId, $s) !== false)
465
+ $assocIds[] = $assocId;
466
+ }
467
+
468
+ return $assocIds;
469
+ }
470
+
471
+ public function getPrice()
472
+ {
473
+ return $this->getProduct()->getPrice();
474
+ }
475
+
476
+ public function calcMinimalPrice($product)
477
+ {
478
+ return $product->getMinimalPrice();
479
+ }
480
+
481
+ public function getSpecialPrice()
482
+ {
483
+ return $this->getProduct()->getSpecialPrice();
484
+ }
485
+
486
+ public function hasSpecialPrice()
487
+ {
488
+ $has = false;
489
+ $product = $this->getProduct();
490
+
491
+ if ($this->getSpecialPrice() <= 0)
492
+ return $has;
493
+ if (is_empty_date($product->getSpecialFromDate()))
494
+ return $has;
495
+
496
+ $cDate = Mage::app()->getLocale()->date(null, null, Mage::app()->getLocale()->getDefaultLocale());
497
+ $timezone = Mage::app()->getStore($this->getStoreId())->getConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE);
498
+
499
+ $fromDate = new Zend_Date(null, null, Mage::app()->getLocale()->getDefaultLocale());
500
+ if ($timezone) $fromDate->setTimezone($timezone);
501
+ $fromDate->setDate(substr($product->getSpecialFromDate(), 0, 10), 'yyyy-MM-dd');
502
+ $fromDate->setTime(substr($product->getSpecialFromDate(), 11, 8), 'HH:mm:ss');
503
+
504
+ $toDate = new Zend_Date(null, null, Mage::app()->getLocale()->getDefaultLocale());
505
+ if (!is_empty_date($product->getSpecialToDate())) {
506
+ if ($timezone) $toDate->setTimezone($timezone);
507
+ $toDate->setDate(substr($product->getSpecialToDate(), 0, 10), 'yyyy-MM-dd');
508
+ $toDate->setTime('23:59:59', 'HH:mm:ss');
509
+ } else {
510
+ if ($timezone) $toDate->setTimezone($timezone);
511
+ $toDate->setDate($cDate->toString('yyyy-MM-dd'), 'yyyy-MM-dd');
512
+ $toDate->setTime('23:59:59', 'HH:mm:ss');
513
+ $toDate->add(7, Zend_Date::DAY);
514
+ }
515
+
516
+ if (($fromDate->compare($cDate) == -1
517
+ || $fromDate->compare($cDate) == 0)
518
+ && ($toDate->compare($cDate) == 1
519
+ || $toDate->compare($cDate) == 0))
520
+ {
521
+ $has = true;
522
+ }
523
+
524
+ return $has;
525
+ }
526
+
527
+
528
+ //
529
+ // protected::Tools
530
+ //
531
+
532
+ // protected function hasImage($product)
533
+ // {
534
+ // $image = $product->getData('image');
535
+ // $validator = new Zend_Validate_File_Exists;
536
+
537
+ // if ($image != 'no_selection' && $image != "")
538
+ // {
539
+ // // if ($validator->isValid($this->getData('images_path_prefix') . $image) != 'fileExistsDoesNotExist')
540
+ // // return false;
541
+ // if (!is_file($this->getData('images_path_prefix') . $image))
542
+ // return false;
543
+ // }
544
+ // else
545
+ // {
546
+ // return false;
547
+ // }
548
+
549
+ // return true;
550
+ // }
551
+
552
+ protected function cleanField($field)
553
+ {
554
+ if (is_array($field))
555
+ {
556
+ foreach ($field as &$value)
557
+ {
558
+ $value = $this->cleanFieldValue($value);
559
+ unset($value);
560
+ }
561
+ }
562
+ else
563
+ {
564
+ $field = $this->cleanFieldValue($field);
565
+ }
566
+
567
+ return $field;
568
+ }
569
+
570
+ /**
571
+ * Cleans invalid utf8 characters, strips tags and trims
572
+ *
573
+ * @param string|array $field
574
+ */
575
+ protected function cleanFieldValue($field)
576
+ {
577
+ // Do nothing if field is empty
578
+ if (!$field) return $field;
579
+
580
+ $cleaned = $this->cleanFieldValueArray((array) $field);
581
+ return is_array($field) ? $cleaned : $cleaned[0];
582
+ }
583
+
584
+ protected function cleanFieldValueArray($fields)
585
+ {
586
+ return array_map(array($this, '_cleanFieldValue'), $fields);
587
+ }
588
+
589
+ protected function _cleanFieldValue($field)
590
+ {
591
+ // http://stackoverflow.com/questions/4224141/php-removing-invalid-utf-8-characters-in-xml-using-filter
592
+ $valid_utf8 = '/([\x09\x0A\x0D\x20-\x7E]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})|./x';
593
+
594
+ $field = preg_replace('#<br(\s?/)?>#i', ' ', $field);
595
+ $field = strip_tags($field);
596
+ $field = preg_replace('/[ ]{2,}/', ' ', $field);
597
+ $field = trim($field);
598
+ $field = html_entity_decode($field, null, 'UTF-8');
599
+
600
+ return preg_replace($valid_utf8, '$1', $field);
601
+ }
602
+
603
+ protected function _attributeDoesNotExist($attName)
604
+ {
605
+ Mage::throwException($attName . ' attribute does not exist!');
606
+ }
607
+
608
+
609
+ //
610
+ // public::Config
611
+ //
612
+
613
+ public function getConfig()
614
+ {
615
+ return $this->getGenerator()->getConfig();
616
+ }
617
+
618
+ public function getConfigVar($key,
619
+ $section = Doofinder_Feed_Model_Config::DEFAULT_SECTION)
620
+ {
621
+ return $this->getGenerator()->getConfigVar($key, null, $section);
622
+ }
623
+
624
+ public function getTools()
625
+ {
626
+ return $this->getGenerator()->getTools();
627
+ }
628
+
629
+ public function isSkip()
630
+ {
631
+ return $this->skip;
632
+ }
633
+
634
+ public function checkSkipSubmission()
635
+ {
636
+ return $this;
637
+ }
638
+
639
+ public function setFieldsMap($arr)
640
+ {
641
+ $this->_field_map = $arr;
642
+
643
+ return $this;
644
+ }
645
+ }
app/code/community/Doofinder/Feed/Model/Map/Product/Associated.php ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Associated Product Map Model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Map_Product_Associated
19
+ extends Doofinder_Feed_Model_Map_Product_Abstract
20
+ {
21
+ protected function mapField($column)
22
+ {
23
+ $value = parent::mapField($column);
24
+
25
+ if ($value == "")
26
+ $value = $this->getParentMap()->mapField($column);
27
+
28
+ return $value;
29
+ }
30
+
31
+ public function mapFieldDescription($params = array())
32
+ {
33
+ $value = $this->getCellValue(array('map' => $params['map']));
34
+
35
+ if ($value == "")
36
+ $value = $this->getParentMap()->mapField('description');
37
+
38
+ return $value;
39
+ }
40
+
41
+ public function mapFieldLink($params = array())
42
+ {
43
+ $product = $this->getProduct();
44
+
45
+ if ($product->isVisibleInSiteVisibility())
46
+ {
47
+ $value = $this->getCellValue(array('map' => $params['map']));
48
+ }
49
+ else
50
+ {
51
+ $value = $this->getParentMap()->mapField('link');
52
+
53
+ if ($this->getConfigVar('associated_products_link_add_unique', 'columns'))
54
+ $value = $this->addUrlUniqueParams(
55
+ $value,
56
+ $product,
57
+ $this->getParentMap()->getConfigurableAttributeCodes()
58
+ );
59
+ }
60
+
61
+ return $value;
62
+ }
63
+
64
+ protected function addUrlUniqueParams($value, $product, $codes)
65
+ {
66
+ $params = array();
67
+
68
+ foreach ($codes as $attrCode)
69
+ {
70
+ $data = $product->getData($attrCode);
71
+
72
+ if (empty($data))
73
+ {
74
+ $this->skip = true;
75
+ return $value;
76
+ }
77
+
78
+ $params[$attrCode] = $data;
79
+ }
80
+
81
+ $uri = Zend_Uri::factory($value);
82
+ $scheme = $uri->getScheme();
83
+ $query = $uri->getQueryAsArray();
84
+ $port = $uri->getPort();
85
+
86
+ if ($uri->valid())
87
+ {
88
+ $params = array_merge($query, $params);
89
+ $uri->setQuery($params);
90
+
91
+ if ($uri->valid())
92
+ return $uri->getUri();
93
+
94
+ $this->skip = true;
95
+ }
96
+
97
+ return $value;
98
+ }
99
+
100
+ public function mapFieldImageLink($params = array())
101
+ {
102
+ $value = $this->getCellValue(array('map' => $params['map']));
103
+
104
+ if ($value == '')
105
+ $value = $this->getParentMap()->mapField('image_link');
106
+
107
+ return $value;
108
+ }
109
+
110
+ public function mapFieldAvailability($params = array())
111
+ {
112
+ $args = array('map' => $params['map']);
113
+ $value = "";
114
+ $value = $this->getParentMap()->mapField('availability');
115
+ // gets out of stock if parent is out of stock
116
+ if (strcasecmp($this->getConfig()->getOutOfStockStatus(), $value) == 0)
117
+ return $value;
118
+
119
+ $value = $this->getCellValue($args);
120
+
121
+ return $value;
122
+ }
123
+
124
+ public function mapFieldBrand($params = array())
125
+ {
126
+ $args = array('map' => $params['map']);
127
+ $value = "";
128
+
129
+ // get value from parent first
130
+ $value = $this->getParentMap()->mapField('brand');
131
+ if ($value != "")
132
+ return $value;
133
+
134
+ $value = $this->getCellValue($args);
135
+
136
+ return $value;
137
+ }
138
+
139
+ public function mapFieldProductType($params = array())
140
+ {
141
+ $args = array('map' => $params['map']);
142
+ $value = "";
143
+
144
+ // get value from parent first
145
+ $value = $this->getParentMap()->mapField('product_type');
146
+ if ($value != "")
147
+ return htmlspecialchars_decode($value);
148
+
149
+ $map_by_category = $this->getConfig()->getMapCategorySorted('product_type_by_category', $this->getStoreId());
150
+ $category_ids = $this->getProduct()->getCategoryIds();
151
+ if (empty($category_ids))
152
+ $category_ids = $this->getParentMap()->getProduct()->getCategoryIds();
153
+ if (!empty($category_ids) && count($map_by_category) > 0)
154
+ {
155
+ foreach ($map_by_category as $arr)
156
+ {
157
+ if (array_search($arr['category'], $category_ids) !== false)
158
+ {
159
+ $value = $arr['value'];
160
+ break;
161
+ }
162
+ }
163
+ }
164
+
165
+ if ($value != "")
166
+ return htmlspecialchars_decode($value);
167
+
168
+ $value = $this->getCellValue($args);
169
+
170
+ return htmlspecialchars_decode($value);
171
+ }
172
+ }
app/code/community/Doofinder/Feed/Model/Map/Product/Bundle.php ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Bundle Product Map Model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Map_Product_Bundle
19
+ extends Doofinder_Feed_Model_Map_Product_Abstract
20
+ {
21
+ public function getPrice()
22
+ {
23
+ $price = 0.0;
24
+
25
+ if (!$this->hasSpecialPrice())
26
+ {
27
+ $price = $this->calcMinimalPrice($this->getProduct());
28
+ }
29
+ else
30
+ {
31
+ $price = $this->calcMinimalPrice($this->getProduct());
32
+ }
33
+
34
+ if ($price <= 0)
35
+ $this->skip = true;
36
+
37
+ return $price;
38
+ }
39
+
40
+ public function calcMinimalPrice($product) {
41
+ $price = 0.0;
42
+
43
+ if ($this->getConfig()->compareMagentoVersion(
44
+ array('major' => 1, 'minor' => 6, 'revision' => 0, 'patch' => 0)))
45
+ $_prices = $product->getPriceModel()->getPrices($product);
46
+ else
47
+ $_prices = $product->getPriceModel()->getTotalPrices($product);
48
+
49
+ if (is_array($_prices))
50
+ $price = min($_prices);
51
+ else
52
+ $price = $_prices;
53
+
54
+ return $price;
55
+ }
56
+
57
+ public function getSpecialPrice()
58
+ {
59
+ $price = $this->calcMinimalPrice($this->getProduct());
60
+
61
+ $special_price_percent = $this->getProduct()->getSpecialPrice();
62
+
63
+ if ($special_price_percent <= 0 || $special_price_percent > 100)
64
+ return 0;
65
+
66
+ $special_price = (($special_price = (100 - $special_price_percent) * $price / 100) > 0 ? $special_price : 0);
67
+
68
+ return $special_price;
69
+ }
70
+
71
+ protected function mapDirectiveSalePrice($params = array())
72
+ {
73
+ return null;
74
+ }
75
+ }
app/code/community/Doofinder/Feed/Model/Map/Product/Configurable.php ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Configurable Product Map Model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Map_Product_Configurable
19
+ extends Doofinder_Feed_Model_Map_Product_Abstract
20
+ {
21
+ protected static $_grouped = false;
22
+
23
+ public static function setGrouped($v)
24
+ {
25
+ self::$_grouped = (bool)$v;
26
+ }
27
+
28
+ protected $_assoc_ids;
29
+ protected $_assocs;
30
+ protected $_cache_configurable_attribute_codes;
31
+
32
+ public function _beforeMap()
33
+ {
34
+ $this->_assocs = array();
35
+ $assocIds = $this->getAssocIds();
36
+
37
+ $assoc = Mage::getModel('catalog/product');
38
+ $assoc->setStoreId($this->getStoreId());
39
+
40
+ $associatedProducts = $assoc
41
+ ->getCollection()
42
+ ->addIdFilter($assocIds)
43
+ ->addAttributeToSelect('*')
44
+ ->load();
45
+
46
+ foreach ($associatedProducts as $associated)
47
+ {
48
+ $this->_assocs[$associated->getId()] = $associated;
49
+ }
50
+
51
+ $assocMapArr = array();
52
+ foreach ($this->_assocs as $assoc)
53
+ {
54
+ $assocMap = $this->getAssocMapModel($assoc);
55
+
56
+ if ($assocMap->checkSkipSubmission()->isSkip())
57
+ continue;
58
+
59
+ $assocMapArr[$assoc->getId()] = $assocMap;
60
+ }
61
+
62
+ $this->setAssocMaps($assocMapArr);
63
+
64
+ return parent::_beforeMap();
65
+ }
66
+
67
+ public function _map()
68
+ {
69
+ $rows = array();
70
+ // $grouped = ($this->getConfigVar('group_configurable_products') == 1);
71
+ $grouped = self::$_grouped;
72
+
73
+ $skipFields = array(
74
+ 'id',
75
+ 'title',
76
+ 'description',
77
+ 'price',
78
+ 'normal_price',
79
+ 'sale_price'
80
+ );
81
+
82
+ // Check if this product should be in the feed
83
+ if (!$this->isSkip())
84
+ {
85
+ $masterData = parent::_map();
86
+ reset($masterData);
87
+ $masterData = current($masterData);
88
+
89
+ // Only add the master data is we don't group products
90
+ if ($grouped)
91
+ $rows[] = $masterData;
92
+ }
93
+
94
+ // Map all child products
95
+ foreach ($this->getAssocMaps() as $assocId => $assocMap)
96
+ {
97
+ if (!$assocMap->isSkip())
98
+ {
99
+ $row = $assocMap->map();
100
+ reset($row);
101
+ $row = current($row);
102
+
103
+ // We can group multiple configurable products into the master product
104
+ if (!$grouped)
105
+ {
106
+ foreach ($row as $name => $value)
107
+ {
108
+ if (in_array($name, $skipFields))
109
+ {
110
+ continue;
111
+ }
112
+
113
+ $masterData = $this->_mapGrouped($name, $value, $masterData);
114
+ }
115
+ }
116
+ else
117
+ {
118
+ $rows[] = $row; // Add each product as separate product
119
+ }
120
+ }
121
+ }
122
+
123
+ if (!$grouped) {
124
+ // Make sure boost field has single value
125
+ if (isset($masterData['boost']) && is_array($masterData['boost'])) {
126
+ $masterData['boost'] = max($masterData['boost']);
127
+ }
128
+ $rows[] = $masterData; // Add the complete master data object
129
+ }
130
+
131
+ return $rows;
132
+ }
133
+
134
+ protected function _mapGrouped($name, $childValue, $masterData)
135
+ {
136
+ $value = $masterData[$name];
137
+
138
+ if (!is_array($value)) {
139
+ $value = array($value);
140
+ }
141
+ if (!is_array($childValue)) {
142
+ $childValue = array($childValue);
143
+ }
144
+
145
+ $value = array_merge($value, $childValue);
146
+
147
+ // Remove duplicates
148
+ $value = array_values(array_unique($value));
149
+
150
+ // Remove array if value is single
151
+ if (count($value) == 1) {
152
+ $value = $value[0];
153
+ }
154
+
155
+ $masterData[$name] = $value;
156
+ return $masterData;
157
+ }
158
+
159
+ public function getAssocIds()
160
+ {
161
+ if (is_null($this->_assoc_ids))
162
+ $this->_assoc_ids = $this->loadAssocIds(
163
+ $this->getProduct(),
164
+ $this->getStoreId()
165
+ );
166
+
167
+ asort($this->_assoc_ids);
168
+
169
+ return $this->_assoc_ids;
170
+ }
171
+
172
+ protected function getAssocMapModel($oProduct)
173
+ {
174
+ $params = array(
175
+ 'store_code' => $this->getData('store_code'),
176
+ 'store_id' => $this->getData('store_id'),
177
+ 'website_id' => $this->getData('website_id'),
178
+ );
179
+
180
+ $productMap = Mage::getModel('doofinder_feed/map_product_associated',
181
+ $params);
182
+
183
+ $productMap->setGenerator($this->getGenerator())
184
+ ->setProduct($oProduct)
185
+ ->setFieldsMap($this->_field_map)
186
+ ->setParentMap($this)
187
+ ->initialize();
188
+
189
+ return $productMap;
190
+ }
191
+
192
+ public function getConfigurableAttributeCodes()
193
+ {
194
+ if (is_null($this->_cache_configurable_attribute_codes))
195
+ $this->_cache_configurable_attribute_codes = $this->getTools()
196
+ ->getConfigurableAttributeCodes($this->getProduct()->getId());
197
+
198
+ return $this->_cache_configurable_attribute_codes;
199
+ }
200
+ }
app/code/community/Doofinder/Feed/Model/Map/Product/Downloadable.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Downloadable Product Map Model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Map_Product_Downloadable
19
+ extends Doofinder_Feed_Model_Map_Product_Abstract
20
+ {}
app/code/community/Doofinder/Feed/Model/Map/Product/Grouped.php ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Grouped Product Map Model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Map_Product_Grouped
19
+ extends Doofinder_Feed_Model_Map_Product_Abstract
20
+ {
21
+ /**
22
+ * Grouped products doesn't have special price.
23
+ *
24
+ * @return float
25
+ */
26
+ public function getPrice()
27
+ {
28
+ // $price = $this->calcGroupPrice($this->getProduct());
29
+ $price = $this->getMinPrice($this->getProduct());
30
+
31
+ if ($price <= 0)
32
+ $this->skip = true;
33
+
34
+ return $price;
35
+ }
36
+
37
+ public function calcGroupPrice($product)
38
+ {
39
+ $price = 0.0;
40
+ $ap = $product->getTypeInstance()->getAssociatedProducts();
41
+
42
+ foreach ($ap as $associatedProduct)
43
+ $price += $associatedProduct->getPrice();
44
+
45
+ return $price; // Total price
46
+ }
47
+
48
+ public function getMinPrice($product)
49
+ {
50
+ $price = null;
51
+
52
+ foreach ($product->getTypeInstance()->getAssociatedProducts() as $ap)
53
+ {
54
+ if (is_null($price))
55
+ $price = $ap->getPrice();
56
+ else
57
+ $price = min($price, $ap->getPrice());
58
+ }
59
+
60
+ return is_null($price) ? 0.0 : $price;
61
+ }
62
+ }
app/code/community/Doofinder/Feed/Model/Map/Product/Simple.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Simple Product Map Model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Map_Product_Simple
19
+ extends Doofinder_Feed_Model_Map_Product_Abstract
20
+ {}
app/code/community/Doofinder/Feed/Model/Map/Product/Virtual.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Virtual Product Map Model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Map_Product_Virtual
19
+ extends Doofinder_Feed_Model_Map_Product_Abstract
20
+ {}
app/code/community/Doofinder/Feed/Model/Mysql4/Cron.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Mysql4_Cron extends Mage_Core_Model_Mysql4_Abstract {
13
+
14
+ protected function _construct() {
15
+ $this->_init('doofinder_feed/cron', 'id');
16
+ }
17
+ }
app/code/community/Doofinder/Feed/Model/Mysql4/Cron/Collection.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Mysql4_Cron_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract {
13
+ protected function _construct()
14
+ {
15
+ $this->_init('doofinder_feed/cron');
16
+ }
17
+ }
app/code/community/Doofinder/Feed/Model/Mysql4/Log.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Mysql4_Log extends Mage_Core_Model_Mysql4_Abstract {
13
+
14
+ protected function _construct() {
15
+ $this->_init('doofinder_feed/log', 'id');
16
+ }
17
+ }
app/code/community/Doofinder/Feed/Model/Mysql4/Log/Collection.php ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Mysql4_Log_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
13
+ {
14
+ protected function _construct()
15
+ {
16
+ $this->_init('doofinder_feed/log');
17
+ }
18
+ }
app/code/community/Doofinder/Feed/Model/Observers/Feed.php ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ require_once(Mage::getBaseDir('lib') . DS. 'Doofinder' . DS .'doofinder_management_api.php');
13
+
14
+ class Doofinder_Feed_Model_Observers_Feed
15
+ {
16
+
17
+ private $config;
18
+
19
+ private $storeCode;
20
+
21
+ private $productCount;
22
+
23
+
24
+ public function updateSearchEngineIndexes($observer) {
25
+
26
+ $helper = Mage::helper('doofinder_feed');
27
+
28
+ $product = $observer->getProduct();
29
+ $products[] = $product->getId();
30
+
31
+ $storeCodes = array();
32
+ $store = Mage::getModel('core/store')->load($product->getStoreId());
33
+
34
+ // If current store is admin then get an array of all possible stores for a website
35
+ if ($store->getCode() !== 'admin') {
36
+ $storeCodes[] = $store->getCode();
37
+ } else {
38
+ foreach(Mage::app()->getStores() as $store) {
39
+ $storeCodes[] = $store->getCode();
40
+ }
41
+ }
42
+
43
+ // Filter out disabled stores
44
+ foreach (array_keys($storeCodes) as $key) {
45
+ $storeCode = $storeCodes[$key];
46
+
47
+ $engineEnabled = Mage::getStoreConfig('doofinder_search/internal_settings/enable', $storeCode);
48
+ $atomicUpdatesEnabled = Mage::getStoreConfig('doofinder_cron/feed_settings/atomic_updates_enabled', $storeCode);
49
+
50
+ if (!$engineEnabled || !$atomicUpdatesEnabled) {
51
+ unset($storeCodes[$key]);
52
+ }
53
+ }
54
+
55
+ // Terminate updates where there is no store enabled
56
+ if (empty($storeCodes)) return;
57
+
58
+ // Get search engines
59
+ $apiKey = Mage::getStoreConfig('doofinder_search/internal_settings/api_key', Mage::app()->getStore());
60
+ $dma = new DoofinderManagementApi($apiKey);
61
+ $searchEngines = $dma->getSearchEngines();
62
+
63
+ // Set engines array key as hashid
64
+ foreach ($searchEngines as $key => $searchEngine) {
65
+ $searchEngines[$searchEngine->hashid] = $searchEngine;
66
+ unset($searchEngines[$key]);
67
+ }
68
+
69
+
70
+ // Loop over all stores and update relevant search engines
71
+ foreach ($storeCodes as $storeCode) {
72
+ // Set store code
73
+ $this->storeCode = $storeCode;
74
+
75
+ // Get store config
76
+ $this->config = $helper->getStoreConfig($this->storeCode);
77
+
78
+
79
+
80
+ // Set options
81
+ $options = array(
82
+ 'close_empty' => true, // close xml even if there are no items
83
+ 'products' => $products, // list of products in feed
84
+ 'store_code' => $this->config['storeCode'],
85
+ 'grouped' => $this->_getBoolean($this->config['grouped']),
86
+ 'display_price' => $this->_getBoolean($this->config['display_price']),
87
+ 'minimal_price' => $this->_getBoolean('minimal_price', false),
88
+ 'image_size' => $this->config['image_size'],
89
+ 'customer_group_id' => 0,
90
+ );
91
+
92
+ $generator = Mage::getModel('doofinder_feed/generator', $options);
93
+
94
+ $xmlData = $generator->run();
95
+
96
+ if ($xmlData) {
97
+ $rss = simplexml_load_string($xmlData);
98
+
99
+ $hashId = Mage::getStoreConfig('doofinder_search/internal_settings/hash_id', $this->storeCode);
100
+ if ($hashId === '') {
101
+
102
+ $warning = sprintf('HashID is not set for the \'%s\' store view, therefore, search indexes haven\'t been
103
+ updated for
104
+ this store view. To fix this problem set HashID for a given stor view or disable Internal Search in Doofinder
105
+ Search Configuration.', $this->storeCode);
106
+ Mage::getSingleton('adminhtml/session')->addWarning($warning);
107
+ continue;
108
+ }
109
+
110
+ $searchEngine = $searchEngines[$hashId];
111
+
112
+ // Check if search engine exists and skip foreach iteration if not.
113
+ if (!$searchEngine) {
114
+ $error = sprintf('Search engine with HashID %s doesn\'t exists. Please, check your configuration.', $hashId);
115
+ Mage::getSingleton('adminhtml/session')->addError($error);
116
+ continue;
117
+ }
118
+
119
+ // Declare array of products to update
120
+ $products = array();
121
+ foreach ($rss->channel->item as $item) {
122
+ $product = array();
123
+ foreach ($item as $key => $value) {
124
+ $product[$key] = (string)$value;
125
+ }
126
+ $products[] = $product;
127
+ }
128
+ if (count($products))
129
+ $searchEngine->updateItems('product', $products);
130
+
131
+ }
132
+ }
133
+
134
+
135
+ }
136
+
137
+ public function generateFeed($observer)
138
+ {
139
+ $stores = Mage::app()->getStores();
140
+ $helper = Mage::helper('doofinder_feed');
141
+
142
+ // Get doofinder process model
143
+ $collection = Mage::getModel('doofinder_feed/cron')->getCollection();
144
+ $collection
145
+ ->addFieldToFilter('status', array('in' => array($helper::STATUS_PENDING, $helper::STATUS_RUNNING)))
146
+ ->addFieldToFilter('next_iteration', array('lteq' => Mage::getModel('core/date')->date('Y-m-d H:i:s')))
147
+ ->setOrder('next_iteration', 'asc');
148
+ $collection->getSelect()->limit(1);
149
+
150
+ $process = $collection->fetchItem();
151
+
152
+ if (!$process || !$process->getId()) {
153
+ return;
154
+ }
155
+
156
+ $scheduleId = $process->getScheduleId();
157
+
158
+ // Get store code
159
+ $this->storeCode = $process->getStoreCode();
160
+
161
+ // Set store context
162
+ Mage::app()->setCurrentStore($this->storeCode);
163
+
164
+ // Get store config
165
+ $this->config = $helper->getStoreConfig($this->storeCode);
166
+
167
+ try {
168
+ // Clear out the message
169
+ $process->setMessage($helper::MSG_EMPTY);
170
+
171
+ // Get data model for store cron
172
+ $dataModel = Mage::getModel('cron/schedule');
173
+
174
+
175
+ // Get store cron data
176
+ $data = $dataModel->load($scheduleId);
177
+
178
+ // Get current offset
179
+ $offset = intval($process->getOffset());
180
+
181
+ // Get step size
182
+ $stepSize = intval($this->config['stepSize']);
183
+
184
+ // Set paths
185
+ $path = $helper->getFeedPath($this->storeCode);
186
+ $tmpPath = $helper->getFeedTemporaryPath($this->storeCode);
187
+
188
+ // Get job code
189
+ $jobCode = $helper::JOB_CODE;
190
+
191
+ // Set options for cron generator
192
+ $options = array(
193
+ '_limit_' => $stepSize,
194
+ '_offset_' => $offset,
195
+ 'store_code' => $this->config['storeCode'],
196
+ 'grouped' => $this->_getBoolean($this->config['grouped']),
197
+ 'display_price' => $this->_getBoolean($this->config['display_price']),
198
+ 'minimal_price' => $this->_getBoolean('minimal_price', false),
199
+ 'image_size' => $this->config['image_size'],
200
+ 'customer_group_id' => 0,
201
+ );
202
+
203
+ $generator = Mage::getModel('doofinder_feed/generator', $options);
204
+
205
+ $xmlData = $generator->run();
206
+
207
+ // If there were errors log them
208
+ if ($errors = $generator->getErrors()) {
209
+ $process->setErrorStack($process->getErrorStack() + count($errors));
210
+
211
+ foreach ($errors as $error) {
212
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::ERROR, $error);
213
+ }
214
+ }
215
+
216
+ $message = $helper->__('Processed products with ids in range %d - %d', $offset + 1, $generator->getLastProcessedProductId());
217
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::STATUS, $message);
218
+
219
+ // If there is new data append to xml.tmp else convert into xml
220
+ if ($xmlData) {
221
+ $dir = Mage::getBaseDir('media').DS.'doofinder';
222
+
223
+ // If directory doesn't exist create one
224
+ if (!file_exists($dir)) {
225
+ $helper->createFeedDirectory($dir);
226
+ }
227
+
228
+ // If file can not be save throw an error
229
+ if (!$success = file_put_contents($tmpPath, $xmlData, FILE_APPEND | LOCK_EX)) {
230
+ Mage::throwException($helper->__("File can not be saved: {$tmpPath}"));
231
+ }
232
+
233
+ $this->productCount = $generator->getProductCount();
234
+ } else {
235
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::WARNING, $helper->__('No data added to feed'));
236
+ }
237
+
238
+ // Set process offset and progress
239
+ $process->setOffset($generator->getLastProcessedProductId());
240
+ $process->setComplete(sprintf('%0.1f%%', $generator->getProgress() * 100));
241
+
242
+ if (!$generator->isFeedDone()) {
243
+ $helper->createNewSchedule($process);
244
+ } else {
245
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::STATUS, $helper->__('Feed generation completed'));
246
+
247
+ if (!rename($tmpPath, $path)) {
248
+ Mage::throwException($helper->__("Cannot rename {$tmpPath} to {$path}"));
249
+ }
250
+
251
+ $process->setMessage($helper->__('Last process successfully completed. Now waiting for new schedule.'));
252
+ $this->_endProcess($process);
253
+ }
254
+
255
+ } catch (Exception $e) {
256
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::ERROR, $e->getMessage());
257
+ $process->setErrorStack($process->getErrorStack() + 1);
258
+ $process->setMessage('#error#' . $e->getMessage());
259
+ $helper->createNewSchedule($process);
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Cast any value to bool
265
+ * @param mixed $value
266
+ * @param bool $defaultValue
267
+ * @return bool
268
+ */
269
+ protected function _getBoolean($value, $defaultValue = false)
270
+ {
271
+ if (is_numeric($value)) {
272
+ if ($value)
273
+ return true;
274
+ else
275
+ return false;
276
+ }
277
+
278
+ $yes = array('true', 'on', 'yes');
279
+ $no = array('false', 'off', 'no');
280
+
281
+ if ( in_array($value, $yes) )
282
+ return true;
283
+
284
+ if ( in_array($value, $no) )
285
+ return false;
286
+
287
+ return $defaultValue;
288
+ }
289
+
290
+
291
+ /**
292
+ * Converts time string into array.
293
+ * @param string $time
294
+ * @return array
295
+ */
296
+ protected function timeToArray($time = null) {
297
+ // Declare new time
298
+ $newTime;
299
+ // Validate $time variable
300
+ if(!$time || !is_string($time) || substr_count($time, ',') < 2) {
301
+ Mage::throwException('Incorrect time string.');
302
+ return false;
303
+ }
304
+
305
+ list($min, $day, $month,) = explode(',', $time);
306
+
307
+ $newTime = array(
308
+ 'min' => $min,
309
+ 'day' => $day,
310
+ 'month' => $month,
311
+ );
312
+ return $newTime;
313
+ }
314
+
315
+ /**
316
+ * Concludes process.
317
+ * @param Doofinder_Feed_Model_Cron $process
318
+ */
319
+ private function _endProcess(Doofinder_Feed_Model_Cron $process) {
320
+ $helper = Mage::helper('doofinder_feed');
321
+ // Prepare data
322
+ $data = array(
323
+ 'status' => $helper::STATUS_WAITING,
324
+ 'next_run' => '-',
325
+ 'next_iteration' => '-',
326
+ 'last_feed_name' => $this->config['xmlName'],
327
+ 'schedule_id' => null,
328
+ );
329
+
330
+ $process->addData($data)->save();
331
+ }
332
+
333
+
334
+ public function addButtons($observer) {
335
+ $block = $observer->getBlock();
336
+
337
+ if ($block instanceof Mage_Adminhtml_Block_System_Config_Edit && $block->getRequest()->getParam('section') == 'doofinder_cron') {
338
+ $html = $block->getChild('save_button')->toHtml();
339
+
340
+ $html .= $block->getLayout()->createBlock('doofinder_feed/adminhtml_widget_button_reschedule')->toHtml();
341
+
342
+ $block->setChild('save_button',
343
+ $block->getLayout()->createBlock('core/text')->setText($html)
344
+ );
345
+ }
346
+ }
347
+
348
+
349
+
350
+ }
app/code/community/Doofinder/Feed/Model/Observers/Logs.php ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Observers_Logs
13
+ {
14
+ const MAX_SIZE = 1000;
15
+ const BATCH_LIMIT = 100;
16
+
17
+ /**
18
+ * Clear logs that are beyond the limit
19
+ *
20
+ * @param Varien_Event_Observer $observer
21
+ */
22
+ public function clearLogs($observer)
23
+ {
24
+ $collection = Mage::getModel('doofinder_feed/log')->getCollection();
25
+
26
+ $size = $collection->getSize();
27
+
28
+ if ($size > static::MAX_SIZE) {
29
+ $collection->setOrder('id', $collection::SORT_ORDER_DESC);
30
+
31
+ $offset = max(static::MAX_SIZE, $size - static::BATCH_LIMIT);
32
+
33
+ $collection->getSelect()
34
+ ->limit(static::BATCH_LIMIT, $offset);
35
+
36
+ $ids = array();
37
+ foreach ($collection->getItems() as $item) {
38
+ $item->delete();
39
+ $ids[] = $item->id;
40
+ }
41
+
42
+ Mage::log($ids, null, 'debug.log');
43
+ }
44
+ }
45
+ }
app/code/community/Doofinder/Feed/Model/Observers/Schedule.php ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Observers_Schedule
13
+ {
14
+
15
+ /**
16
+ * Register missing / reset schedules after configuration saves.
17
+ *
18
+ * @param Varien_Event_Observer $observer
19
+ */
20
+ public function saveNewSchedule($observer)
21
+ {
22
+ // Get store code
23
+ $currentStoreCode = $observer->getStore();
24
+
25
+ // Stores array holding all store codes
26
+ $codes = array();
27
+
28
+ // Create stores codes array
29
+ if ($currentStoreCode) {
30
+ $codes[] = $currentStoreCode;
31
+ } else {
32
+ $stores = Mage::app()->getStores();
33
+ foreach ($stores as $store) {
34
+ if ($store->getIsActive()) {
35
+ $codes[] = $store->getCode();
36
+ }
37
+ }
38
+ }
39
+
40
+ // Check if user wants to reset the schedule
41
+ $reset = (bool) Mage::app()->getRequest()->getParam('reset');
42
+
43
+ foreach ($codes as $storeCode) {
44
+ $this->updateProcess($storeCode, $reset, $reset);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Regenerate finished shcedules.
50
+ *
51
+ * @param Varien_Event_Observer $observer
52
+ */
53
+ public function regenerateSchedule()
54
+ {
55
+ // Get store
56
+ $stores = Mage::app()->getStores();
57
+
58
+ foreach ($stores as $store) {
59
+ if ($store->getIsActive()) {
60
+ $this->updateProcess($store->getCode());
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Gets process for given store code
67
+ *
68
+ * @param string $storeCode
69
+ * @return Doofinder_Feed_Model_Cron
70
+ */
71
+ private function _getProcessByStoreCode($storeCode = 'default')
72
+ {
73
+ $process = Mage::getModel('doofinder_feed/cron')->load($storeCode, 'store_code');
74
+ return $process->getId() ? $process : null;
75
+ }
76
+
77
+ /**
78
+ * Checks if process is registered in doofinder cron table
79
+ *
80
+ * @param string $storeCode
81
+ * @return bool
82
+ */
83
+ private function _isProcessRegistered($storeCode = 'default')
84
+ {
85
+ $process = $this->_getProcessByStoreCode($storeCode);
86
+ return $process ? true : false;
87
+ }
88
+
89
+ /**
90
+ * Update process for given store code.
91
+ * If process does not exits - create it.
92
+ * Reschedule the process if it needs it.
93
+ *
94
+ * @param Doofinder_Feed_Model_Cron $process
95
+ * @param boolean $reset
96
+ * @param boolean $now
97
+ * @param boolean $force
98
+ */
99
+ public function updateProcess($storeCode = 'default', $reset = false, $now = false, $force = false)
100
+ {
101
+ // Get store
102
+ $helper = Mage::helper('doofinder_feed');
103
+ $config = $helper->getStoreConfig($storeCode);
104
+ $store = Mage::getModel('core/store')->load($storeCode);
105
+
106
+ // Override time if $now is enabled
107
+ if ($now) {
108
+ $config['time'] = array(date('H') + $helper->getTimezoneOffset(), date('i'), date('s'));
109
+ }
110
+
111
+ $isEnabled = (bool) $config['enabled'];
112
+
113
+ // Try loading store process
114
+ $process = $this->_getProcessByStoreCode($storeCode);
115
+
116
+ // Create new process if it not exists
117
+ if (!$process) {
118
+ $process = $this->_registerProcess($storeCode);
119
+ }
120
+
121
+ // Enable/disable process if it needs to
122
+ if ($isEnabled || $force) {
123
+ if ($process->getStatus() == $helper::STATUS_DISABLED) {
124
+ $this->_enableProcess($process);
125
+ }
126
+ } else {
127
+ if ($process->getStatus() != $helper::STATUS_DISABLED) {
128
+
129
+ Mage::getSingleton('adminhtml/session')->addSuccess($helper->__('Process for store "%s" has been disabled', $store->getName()));
130
+ $this->_removeTmpXml($storeCode);
131
+ $this->_disableProcess($process);
132
+ }
133
+ return $this;
134
+ }
135
+
136
+ // Do not process the schedule if it has insufficient file permissions
137
+ if (!$this->_checkFeedFilePermission($storeCode)) {
138
+ Mage::getSingleton('adminhtml/session')->addError($helper->__('Insufficient file permissions for store: %s. Check if the feed file is writeable', $store->getName()));
139
+ return $this;
140
+ }
141
+
142
+ // Reschedule the process if it needs to
143
+ if ($reset || $process->getStatus() == $helper::STATUS_WAITING) {
144
+ Mage::getSingleton('adminhtml/session')->addSuccess($helper->__('Process for store "%s" has been rescheduled', $store->getName()));
145
+ $this->_removeTmpXml($storeCode);
146
+ $this->_rescheduleProcess($config, $process);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Register a new process
152
+ *
153
+ * @return Doofinder_Feed_Model_Cron
154
+ */
155
+ private function _registerProcess($storeCode = 'default')
156
+ {
157
+ $helper = Mage::helper('doofinder_feed');
158
+ $config = $helper->getStoreConfig($storeCode);
159
+ if (empty($status)) {
160
+ $status = $config['enabled'] ? $helper::STATUS_WAITING : $helper::STATUS_DISABLED;
161
+ }
162
+
163
+ $data = array(
164
+ 'store_code' => $storeCode,
165
+ 'status' => $status,
166
+ 'message' => $helper::MSG_EMPTY,
167
+ 'complete' => '-',
168
+ 'next_run' => '-',
169
+ 'next_iteration'=> '-',
170
+ 'last_feed_name'=> 'None',
171
+ );
172
+ $process = Mage::getModel('doofinder_feed/cron')->setData($data)->save();
173
+
174
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::STATUS, $helper->__('Process has been registered'));
175
+
176
+ return $process;
177
+ }
178
+
179
+ /**
180
+ * Enable the process
181
+ *
182
+ * @param Doofinder_Feed_Model_Cron $process
183
+ */
184
+ private function _enableProcess(Doofinder_Feed_Model_Cron $process)
185
+ {
186
+ $helper = Mage::helper('doofinder_feed');
187
+ $process->setStatus($helper::STATUS_WAITING)->save();
188
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::STATUS, $helper->__('Process has been enabled'));
189
+ }
190
+
191
+ /**
192
+ * Disable the process
193
+ *
194
+ * @param Doofinder_Feed_Model_Cron $process
195
+ */
196
+ private function _disableProcess(Doofinder_Feed_Model_Cron $process)
197
+ {
198
+ $helper = Mage::helper('doofinder_feed');
199
+ $process->setStatus($helper::STATUS_DISABLED)->save();
200
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::STATUS, $helper->__('Process has been disabled'));
201
+ }
202
+
203
+ /**
204
+ * Remove tmp xml file.
205
+ *
206
+ * @param string $store_code
207
+ * @return bool
208
+ */
209
+ private function _removeTmpXml($store_code = null)
210
+ {
211
+ if (empty($store_code)) {
212
+ return false;
213
+ }
214
+ $helper = Mage::helper('doofinder_feed');
215
+ $config = $helper->getStoreConfig($store_code);
216
+ $filePath = Mage::getBaseDir('media').DS.'doofinder'.DS.$config['xmlName'].'.tmp';
217
+ if (file_exists($filePath)) {
218
+ $success = unlink($filePath);
219
+ if ($success) {
220
+ Mage::getSingleton('core/session')->addSuccess("Temporary xml file: {$filePath} has beed removed.");
221
+ return true;
222
+ } else {
223
+ Mage::getSingleton('core/session')->addError("Could not remove {$filePath}; This can lead to some errors. Remove this file manually.");
224
+ return false;
225
+ }
226
+ }
227
+
228
+ return false;
229
+ }
230
+
231
+ /**
232
+ * Validate file permissions for feed generation.
233
+ *
234
+ * @return boolean
235
+ */
236
+ protected function _checkFeedFilePermission($storeCode)
237
+ {
238
+ $helper = Mage::helper('doofinder_feed');
239
+
240
+ try {
241
+ $helper->createFeedDirectory();
242
+ } catch (Exception $e) {
243
+ return false;
244
+ }
245
+
246
+ $dir = $helper->getFeedDirectory();
247
+ $path = $helper->getFeedPath($storeCode);
248
+ $tmpPath = $helper->getFeedTemporaryPath($storeCode);
249
+
250
+ return is_writeable($dir) && (!file_exists($path) || is_writeable($path)) && (!file_exists($tmpPath) || is_writeable($tmpPath));
251
+ }
252
+
253
+ /**
254
+ * Reschedule the process accordingly to process configuration.
255
+ *
256
+ * @param array $storeConfig
257
+ * @param Doofinder_Feed_Model_Cron $process
258
+ */
259
+ protected function _rescheduleProcess($config, Doofinder_Feed_Model_Cron $process)
260
+ {
261
+ $helper = Mage::helper('doofinder_feed');
262
+
263
+ $timecreated = strftime("%Y-%m-%d %H:%M:%S", mktime(date("H"), date("i"), date("s"), date("m"), date("d"), date("Y")));
264
+ $timescheduled = $helper->getScheduledAt($config['time'], $config['frequency']);
265
+ $jobCode = $helper::JOB_CODE;
266
+
267
+ $process->setStatus($helper::STATUS_PENDING)
268
+ ->setComplete('0%')
269
+ ->setNextRun($timescheduled)
270
+ ->setNextIteration($timescheduled)
271
+ ->setOffset(0)
272
+ ->setMessage($helper::MSG_PENDING)
273
+ ->setErrorStack(0)
274
+ ->save();
275
+
276
+ Mage::helper('doofinder_feed/log')->log($process, Doofinder_Feed_Helper_Log::STATUS, $helper->__('Process has been scheduled'));
277
+ }
278
+ }
app/code/community/Doofinder/Feed/Model/Resource/Mysql4/Setup.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_Resource_Mysql4_Setup extends Mage_Core_Model_Resource_Setup {
13
+
14
+ }
app/code/community/Doofinder/Feed/Model/System/Config/Backend/Map/Additional.php ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_System_Config_Backend_Map_Additional extends Mage_Adminhtml_Model_System_Config_Backend_Serialized
13
+ {
14
+ protected function _beforeSave()
15
+ {
16
+ $_value = $this->getValue();
17
+
18
+ unset($_value['additional_mapping'][-1]);
19
+
20
+ $this->setValue($_value);
21
+
22
+ parent::_beforeSave();
23
+ }
24
+ }
app/code/community/Doofinder/Feed/Model/System/Config/Backend/Total/Limit.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class Doofinder_Feed_Model_System_Config_Backend_Total_Limit extends Mage_Core_Model_Config_Data
4
+ {
5
+ protected function _beforeSave()
6
+ {
7
+ if (!$this->getValue()) {
8
+ throw new Exception(Mage::helper('doofinder_feed')->__('Total limit is required.'));
9
+ } else if (!is_numeric($this->getValue())) {
10
+ throw new Exception(Mage::helper('doofinder_feed')->__('Total limit is not a number.'));
11
+ }
12
+
13
+ $this->setValue(intval($this->getValue()));
14
+
15
+ return parent::_beforeSave();
16
+ }
17
+ }
app/code/community/Doofinder/Feed/Model/System/Config/Reset.php ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_System_Config_Reset extends Mage_Core_Model_Config_Data
13
+ {
14
+ protected function _afterLoad()
15
+ {
16
+ $this->setValue(0);
17
+ }
18
+ }
app/code/community/Doofinder/Feed/Model/System/Config/Source/Product/Attributes.php ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_Model_System_Config_Source_Product_Attributes
13
+ {
14
+ protected $_options;
15
+
16
+ public function toOptionArray()
17
+ {
18
+ if (!$this->_options) {
19
+ $attributes = array();
20
+
21
+ $result = Mage::getResourceModel('catalog/product_attribute_collection')->load();
22
+
23
+ foreach ($result as $attribute) {
24
+ $code = $attribute->getAttributeCode();
25
+ $label = $attribute->getFrontendLabel();
26
+ $attributes[$code] = 'Attribute: ' . $code . ($label ? ' (' . $label . ')' : '');
27
+ }
28
+
29
+ $this->_options = array_merge(
30
+ $this->_getDoofinderDirectivesOptionArray(),
31
+ $attributes
32
+ );
33
+ }
34
+
35
+ return $this->_options;
36
+ }
37
+
38
+ protected function _getDoofinderDirectivesOptionArray()
39
+ {
40
+ $options = array();
41
+
42
+ foreach (Mage::getSingleton('doofinder_feed/config')->getConfigVar('directives') as $directive => $info) {
43
+ $options[$directive] = 'Doofinder: ' . $info['label'];
44
+ }
45
+
46
+ return $options;
47
+ }
48
+ }
app/code/community/Doofinder/Feed/Model/Tools.php ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category Models
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Tools model for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_Model_Tools extends Varien_Object
19
+ {
20
+ public function _construct()
21
+ {
22
+ parent::_construct();
23
+ $this->loadEntityType('catalog_product');
24
+ }
25
+
26
+ public function loadEntityType($type)
27
+ {
28
+ if (is_array($type))
29
+ {
30
+ foreach ($type as $t)
31
+ if (is_string($t))
32
+ $this->loadEntityType($t);
33
+ }
34
+ else
35
+ {
36
+ $entityType = Mage::getModel('eav/config')->getEntityType('catalog_product');
37
+
38
+ Mage::unregister('doofinder_feed/entity_type/'.$type);
39
+ Mage::register('doofinder_feed/entity_type/'.$type, $entityType);
40
+ }
41
+ return $this;
42
+ }
43
+
44
+ public function getEntityType($type)
45
+ {
46
+ return Mage::registry('doofinder_feed/entity_type/'.$type);
47
+ }
48
+
49
+ public function getProductAttributeValueBySql($attribute, $type = "text", $productId, $storeId = null, $strict = false, $debug = false)
50
+ {
51
+ if (array_search($type, array('text', 'int', 'decimal', 'varchar', 'datetime')) === false)
52
+ {
53
+ Mage::throwException(sprintf("Unknown attribute backend type %s for attribute code %s.", $type, $attribute->getAttributeCode()));
54
+ }
55
+
56
+ if (is_null($storeId))
57
+ {
58
+ return $this->getProductAttributeValueBySql($attribute, $type, $productId, Mage_Core_Model_App::ADMIN_STORE_ID, true, $debug);
59
+ }
60
+
61
+ $attributeId = $attribute->getAttributeId();
62
+
63
+ $conn = Mage::getSingleton('core/resource')->getConnection('core_read');
64
+ $query = $conn->select()
65
+ ->from(array('val' => $this->getRes()->getTableName('catalog/product')."_".$type),
66
+ array('value'))
67
+ ->joinInner(array('eav' => $this->getRes()->getTableName('eav/attribute')),
68
+ 'val.attribute_id=eav.attribute_id',
69
+ array())
70
+ ->where('val.entity_id = ?', $productId)
71
+ ->where('val.entity_type_id = ?', $this->getEntityType('catalog_product')->getEntityTypeId())
72
+ ->where('val.store_id = ?', $storeId)
73
+ ->where('val.attribute_id = ?', $attributeId);
74
+
75
+ $value = $this->getConnRead()->fetchCol($query);
76
+ if (is_array($value) && @$value[0] === null)
77
+ $value = null;
78
+ elseif (is_array($value) && isset($value[0]))
79
+ $value = $value[0];
80
+ else if (is_array($value) && count($value) == 0)
81
+ $value = null;
82
+
83
+ if (is_null($value) && $storeId != Mage_Core_Model_App::ADMIN_STORE_ID && $strict === false)
84
+ {
85
+ return $this->getProductAttributeValueBySql($attribute, $type, $productId, Mage_Core_Model_App::ADMIN_STORE_ID, true, $debug);
86
+ }
87
+
88
+ return $value;
89
+ }
90
+
91
+ /**
92
+ * Check if there is a parent of type (configurable, ..)
93
+ *
94
+ * @param string $type_id
95
+ * @param string $sku
96
+ * @param string $parent_type_id
97
+ * @return array|false
98
+ */
99
+ public function isChildOfProductType($type_id, $sku, $parent_type_id)
100
+ {
101
+ $data = false;
102
+
103
+ if ($type_id != Mage_Catalog_Model_Product_Type::TYPE_SIMPLE)
104
+ return $data;
105
+
106
+ $conn = Mage::getSingleton('core/resource')->getConnection('core_read');
107
+ $query = $conn->select()
108
+ ->from(array('cpe' => $this->getRes()->getTableName('catalog/product')),
109
+ array('entity_id' => 'cpe.entity_id',
110
+ 'sku' => 'cpe.sku',
111
+ 'parent_entity_id' => 'cpe_parent.entity_id',
112
+ 'parent_sku' => 'cpe_parent.sku'))
113
+ ->joinInner(array('cpsl' => $this->getRes()->getTableName('catalog/product_super_link')),
114
+ 'cpe.entity_id = cpsl.product_id',
115
+ array())
116
+ ->joinInner(array('cpe_parent' => $this->getRes()->getTableName('catalog/product')),
117
+ 'cpsl.parent_id = cpe_parent.entity_id',
118
+ array())
119
+ ->where('cpe.sku', $sku)
120
+ ->where('cpe_parent.type_id', $parent_type_id);
121
+
122
+ $result = $this->getConnRead()->fetchRow($query);
123
+
124
+ if ($result !== false)
125
+ {
126
+ $data = $result;
127
+ }
128
+
129
+ return $data;
130
+ }
131
+
132
+ public function getProductAttributeSelectValue($attribute, $valueId, $storeId = null, $strict = false, $debug = false)
133
+ {
134
+ if (is_null($storeId))
135
+ {
136
+ return $this->getProductAttributeSelectValue($attribute, $valueId, Mage_Core_Model_App::ADMIN_STORE_ID, true, $debug);
137
+ }
138
+
139
+ $attributeId = $attribute->getAttributeId();
140
+
141
+ $conn = Mage::getSingleton('core/resource')->getConnection('core_read');
142
+ $query = $conn->select()
143
+ ->from($this->getRes()->getTableName('eav/attribute_option'),
144
+ array('opt'))
145
+ ->where('opt.option_id = ?', $valueId)
146
+ ->where('opt.attribute_id = ?', $attributeId)
147
+ ->where('opt.store_id = ?', $storeId);
148
+
149
+ $value = $this->getConnRead()->fetchCol($query);
150
+ if (is_array($value) && @$value[0] === null)
151
+ $value = null;
152
+ elseif (is_array($value) && isset($value[0]))
153
+ $value = $value[0];
154
+ else if (is_array($value) && count($value) == 0)
155
+ $value = null;
156
+
157
+ if (is_null($value) && $storeId != Mage_Core_Model_App::ADMIN_STORE_ID && $strict === false)
158
+ {
159
+ return $this->getProductAttributeSelectValue($attribute, $valueId, Mage_Core_Model_App::ADMIN_STORE_ID, true, $debug);
160
+ }
161
+
162
+ return $value;
163
+ }
164
+
165
+ /**
166
+ * Get categories ids by product id.
167
+ *
168
+ * @param string $type_id
169
+ * @param string $sku
170
+ * @param string $parent_type_id
171
+ * @return array|false
172
+ */
173
+ public function getCategoriesById($productId)
174
+ {
175
+ $data = false;
176
+
177
+ $conn = Mage::getSingleton('core/resource')->getConnection('core_read');
178
+ $query = $conn->select()
179
+ ->from($this->getRes()->getTableName('catalog/category_product'),
180
+ array('category_id'))
181
+ ->where('product_id = ?', $productId);
182
+
183
+ $result = $this->getConnRead()->fetchAll($query);
184
+
185
+ if ($result !== false)
186
+ {
187
+ $data = array();
188
+ foreach ($result as $k => $row)
189
+ $data[] = $row['category_id'];
190
+ }
191
+ return $data;
192
+ }
193
+
194
+ /**
195
+ * Gets stores ids of product(s).
196
+ * @param int|array $productId
197
+ * @return array()
198
+ */
199
+ public function getProductInStoresIds($productId)
200
+ {
201
+
202
+ $conn = Mage::getSingleton('core/resource')->getConnection('core_read');
203
+
204
+ if (is_array($productId))
205
+ {
206
+ $value = array();
207
+ foreach ($productId as $pid)
208
+ $value[$pid] = array();
209
+
210
+ $query = $conn->select()
211
+ ->from(array('pw' => $this->getRes()->getTableName('catalog/product_website')),
212
+ array('product_id' => 'pw.product_id',
213
+ 'store_id' => 's.store_id'))
214
+ ->joinInner(array('s' => $this->getRes()->getTableName('core/store')),
215
+ 's.website_id = pw.website_id',
216
+ array())
217
+ ->where('pw.product_id IN (?)', $productId);
218
+
219
+ $rows = $this->getConnRead()->fetchAll($query);
220
+ foreach ($rows as $row)
221
+ {
222
+ if (!isset($value[$row['product_id']]))
223
+ $value[$row['product_id']] = array();
224
+ $value[$row['product_id']][] = $row['store_id'];
225
+ }
226
+ return $value;
227
+ }
228
+
229
+ $query = $conn->select()
230
+ ->from(array('pw' => $this->getRes()->getTableName('catalog/product_website')),
231
+ 's.store_id')
232
+ ->joinInner(array('s' => $this->getRes()->getTableName('core/store')),
233
+ 's.website_id = pw.website_id',
234
+ array())
235
+ ->where('pw.product_id = ?', $productId);
236
+
237
+ $value = $this->getConnRead()->fetchCol($query);
238
+
239
+ return $value;
240
+ }
241
+
242
+ /**
243
+ * @param int $productId - parent product id
244
+ * @return array
245
+ */
246
+ public function getChildsIds($productId)
247
+ {
248
+ $data = false;
249
+
250
+ $conn = Mage::getSingleton('core/resource')->getConnection('core_read');
251
+ $query = $conn->select()
252
+ ->from(array('cpe' => $this->getRes()->getTableName('catalog/product')),
253
+ array('cpe.entity_id')
254
+ )
255
+ ->joinInner(array('cpsl' => $this->getRes()->getTableName('catalog/product_super_link')),
256
+ 'cpe.entity_id = cpsl.product_id',
257
+ array())
258
+ ->joinInner(array('cpe_parent' => $this->getRes()->getTableName('catalog/product')),
259
+ 'cpsl.parent_id = cpe_parent.entity_id',
260
+ array())
261
+ ->where('cpe_parent.entity_id = ?', $productId);
262
+
263
+ $result = $this->getConnRead()->fetchAll($query);
264
+
265
+ if ($result !== false)
266
+ {
267
+ foreach ($result as $row)
268
+ {
269
+ $data[] = $row['entity_id'];
270
+ }
271
+ }
272
+
273
+ return $data;
274
+ }
275
+
276
+ /**
277
+ * @param int $productId - parent product id
278
+ * @return array
279
+ */
280
+ public function getConfigurableAttributeCodes($productId)
281
+ {
282
+ $data = false;
283
+
284
+ $conn = Mage::getSingleton('core/resource')->getConnection('core_read');
285
+ $query = $conn->select()
286
+ ->from(array('csa' => $this->getRes()->getTableName('catalog/product_super_attribute')),
287
+ array('eav.attribute_code'))
288
+ ->joinInner(array('eav' => $this->getRes()->getTableName('eav/attribute')),
289
+ 'eav.attribute_id = csa.attribute_code',
290
+ array())
291
+ ->where('csa.product_id = ?', $productId);
292
+
293
+ $result = $this->getConnRead()->fetchAll($query);
294
+
295
+ if ($result !== false)
296
+ {
297
+ foreach ($result as $row)
298
+ {
299
+ $data[] = $row['attribute_code'];
300
+ }
301
+ }
302
+
303
+ return $data;
304
+ }
305
+
306
+ public function explodeMultiselectValue($value)
307
+ {
308
+ $arr = array();
309
+ if (!empty($value))
310
+ {
311
+ $arr = explode(',', $value);
312
+ foreach ($arr as $k => $v) $arr[$k] = trim($v);
313
+ }
314
+ return $arr;
315
+ }
316
+
317
+ /**
318
+ * @return Mage_Core_Model_Resource
319
+ */
320
+ public function getRes()
321
+ {
322
+ if (is_null($this->_res))
323
+ {
324
+ $this->_res = Mage::getSingleton('core/resource');
325
+ }
326
+ return $this->_res;
327
+ }
328
+
329
+ /**
330
+ * @return Varien_Db_Adapter_Pdo_Mysql
331
+ */
332
+ public function getConnRead()
333
+ {
334
+ if (is_null($this->_conn_read))
335
+ {
336
+ $this->_conn_read = $this->getRes()->getConnection('core_read');
337
+ }
338
+ return $this->_conn_read;
339
+ }
340
+
341
+ /**
342
+ * @return Varien_Db_Adapter_Pdo_Mysql
343
+ */
344
+ public function getConnWrite()
345
+ {
346
+ if (is_null($this->_conn_write))
347
+ {
348
+ $this->_conn_write = $this->getRes()->getConnection('core_write');
349
+ }
350
+ return $this->_conn_write;
351
+ }
352
+
353
+ public function getMagentoEdition()
354
+ {
355
+ if (is_callable('Mage::getEdition'))
356
+ {
357
+ return Mage::getEdition();
358
+ }
359
+ else
360
+ {
361
+ $features = array('Enterprise_Enterprise', 'Enterprise_AdminGws',
362
+ 'Enterprise_Checkout', 'Enterprise_Customer');
363
+ $editions = array(
364
+ 'Enterprise' => array(true, true, true, true), // ALL features
365
+ 'Professional' => array(true, false), // ONLY the first
366
+ 'Community' => array(false), // NO features
367
+ );
368
+
369
+ foreach ($editions as $editionName => $featuresMap)
370
+ {
371
+ $match = true;
372
+
373
+ foreach($featuresMap as $i => $featureValue)
374
+ $match = $match && ($featureValue === (bool) Mage::getConfig()->getModuleConfig($features[$i]));
375
+
376
+ if ($match)
377
+ return $editionName;
378
+ }
379
+
380
+ return "Unknown";
381
+ }
382
+ }
383
+ }
app/code/community/Doofinder/Feed/Test/Controller/Index.php ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ class Doofinder_Feed_Test_Controller_Index extends EcomDev_PHPUnit_Test_Case_Controller
3
+ {
4
+ /**
5
+ * Index controller test
6
+ *
7
+ * @test
8
+ * @doNotIndexAll
9
+ */
10
+ public function testIndex()
11
+ {
12
+ $this->dispatch('doofinder');
13
+ $this->assertRequestRoute('doofinder_feed/index/index');
14
+ }
15
+
16
+ /**
17
+ * Feed controller test
18
+ *
19
+ * @test
20
+ * @loadFixture
21
+ * @doNotIndexAll
22
+ * @dataProvider dataProvider
23
+ */
24
+ public function testFeed()
25
+ {
26
+ $this->dispatch('doofinder/feed');
27
+ $this->assertRequestRoute('doofinder_feed/feed/index');
28
+ // var_dump($this->getResponse());
29
+ // $this->reset;
30
+ // $this->assertRequestRoute('cms');
31
+ }
32
+
33
+ /**
34
+ * Feed controller test
35
+ *
36
+ * @test
37
+ * @loadFixture
38
+ * @doNotIndexAll
39
+ * @dataProvider dataProvider
40
+ */
41
+ public function testConfig()
42
+ {
43
+ $this->dispatch('doofinder/feed/config');
44
+ $this->assertRequestRoute('doofinder_feed/feed/config');
45
+ // $this->assertRequestRoute('cms');
46
+ }
47
+ }
app/code/community/Doofinder/Feed/Test/Controller/Index/fixtures/testConfig.yaml ADDED
@@ -0,0 +1 @@
 
1
+ -
app/code/community/Doofinder/Feed/Test/Controller/Index/fixtures/testFeed.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <<<<<<< HEAD
2
+ -
3
+ =======
4
+ config:
5
+ default/catalog/price/scope: 1 # Set price scope to website
6
+ >>>>>>> bug/price-calculation
app/code/community/Doofinder/Feed/Test/Controller/Index/fixtures/testIndex.yaml ADDED
@@ -0,0 +1 @@
 
1
+ -
app/code/community/Doofinder/Feed/Test/Controller/Index/providers/testConfig.yaml ADDED
@@ -0,0 +1 @@
 
1
+ -
app/code/community/Doofinder/Feed/Test/Controller/Index/providers/testFeed.yaml ADDED
@@ -0,0 +1,2 @@
 
 
1
+ -
2
+ -
app/code/community/Doofinder/Feed/Test/Controller/Index/providers/testIndex.yaml ADDED
@@ -0,0 +1 @@
 
1
+ -
app/code/community/Doofinder/Feed/Test/Model/Product.php ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ class Doofinder_Feed_Test_Model_Product extends EcomDev_PHPUnit_Test_Case
3
+ {
4
+ /**
5
+ * Product price calculation test
6
+ *
7
+ * @test
8
+ * @loadFixture
9
+ * @doNotIndexAll
10
+ * @dataProvider dataProvider
11
+ */
12
+ public function testGenerator($productId, $storeId)
13
+ {
14
+ $storeId = Mage::app()->getStore($storeId)->getId();
15
+
16
+ // var_dump(Mage::getConfig()->getNode('default/tax/calculation'));
17
+
18
+ $generator = Mage::helper('doofinder_feed');
19
+
20
+ /* @var $product Mage_Catalog_Model_Product */
21
+ $product = Mage::getModel('catalog/product')
22
+ ->setStoreId($storeId)
23
+ ->load($productId);
24
+
25
+ $prices = $generator->collectProductPrices(
26
+ $product,
27
+ $storeId,
28
+ false,
29
+ false,
30
+ true
31
+ );
32
+
33
+ $finalPriceInclTax = Mage::helper('tax')->getPrice($product, $product->getFinalPrice(), true);
34
+ $finalPriceExclTax = Mage::helper('tax')->getPrice($product, $product->getFinalPrice(), false);
35
+
36
+ // Check that prices w/ and without tax are different
37
+ // $this->assertNotEquals(
38
+ // $finalPriceInclTax,
39
+ // $finalPriceExclTax
40
+ // );
41
+
42
+ $expected = $this->expected('%s-%s', $productId, $storeId);
43
+
44
+ // Check that final price matches expected
45
+ if (isset($prices['sale_price']))
46
+ {
47
+ // With tax
48
+ $this->assertEquals(
49
+ Mage::helper('core')->currency($finalPriceInclTax, true, false),
50
+ Mage::helper('core')->currency($prices['sale_price']['including_tax'], true, false)
51
+ );
52
+ // Without tax
53
+ $this->assertEquals(
54
+ Mage::helper('core')->currency($finalPriceExclTax, true, false),
55
+ Mage::helper('core')->currency($prices['sale_price']['excluding_tax'], true, false)
56
+ );
57
+ }
58
+ }
59
+ }
app/code/community/Doofinder/Feed/Test/Model/Product/expectations/testGenerator.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 1-1:
2
+ final_price: 12.99
3
+ price: 12.99
4
+ 1-2: # Product=Book Store=USA
5
+ final_price: 9.99
6
+ price: 12.99
7
+ 1-3: # Product=Book Store=Canada
8
+ final_price: 12.99
9
+ price: 12.99
10
+ 1-4: # Product=Book Store=Germany
11
+ final_price: 5.99
12
+ price: 9.99
app/code/community/Doofinder/Feed/Test/Model/Product/fixtures/testGenerator.yaml ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ scope:
2
+ website: # Initialize websites
3
+ - website_id: 2
4
+ code: usa_website
5
+ name: USA Website
6
+ default_group_id: 2
7
+ - website_id: 3
8
+ code: canada_website
9
+ name: Canada Website
10
+ default_group_id: 3
11
+ - website_id: 4
12
+ code: german_website
13
+ name: German Website
14
+ default_group_id: 4
15
+ group: # Initializes store groups
16
+ - group_id: 2
17
+ website_id: 2
18
+ name: USA Store Group
19
+ default_store_id: 2
20
+ root_category_id: 2 # Default Category
21
+ - group_id: 3
22
+ website_id: 3
23
+ name: Canada Store Group
24
+ default_store_id: 3
25
+ root_category_id: 2 # Default Category
26
+ - group_id: 4
27
+ website_id: 4
28
+ name: German Store Group
29
+ default_store_id: 4
30
+ root_category_id: 2 # Default Category
31
+ store: # Initializes store views
32
+ - store_id: 2
33
+ website_id: 2
34
+ group_id: 2
35
+ code: usa
36
+ name: USA Store
37
+ is_active: 1
38
+ - store_id: 3
39
+ website_id: 3
40
+ group_id: 3
41
+ code: canada
42
+ name: Canada Store
43
+ is_active: 1
44
+ - store_id: 4
45
+ website_id: 4
46
+ group_id: 4
47
+ code: germany
48
+ name: Germany Store
49
+ is_active: 1
50
+ config:
51
+ default/catalog/price/scope: 1 # Set price scope to website
52
+ default/tax/calculation/algorithm: UNIT_BASE_CACLULATION
53
+ default/tax/calculation/based_on: origin
54
+ default/tax/calculation/apply_tax_on: 0
55
+ default/tax/calculation/price_includes_tax: 0
56
+ default/tax/calculation/apply_after_discount: 1
57
+ default/tax/defaults/country: DE
58
+ default/tax/defaults/region: 0
59
+ default/tax/defaults/postcode: *
60
+ default/tax/display/type: 2
61
+ default/tax/sales_display/price: 1
62
+ tables:
63
+ tax/tax_class:
64
+ - class_id: 3
65
+ class_name: Retail Customer
66
+ class_type: CUSTOMER
67
+ tax/tax_calculation_rate:
68
+ - tax_calculation_rate_id: 3
69
+ tax_country_id: DE
70
+ tax_region_id: 12
71
+ tax_postcode: *
72
+ code: VAT
73
+ rate: 19
74
+ tax/tax_calculation_rule:
75
+ - tax_calculation_rule_id: 1
76
+ code: Retail Customer-VAT
77
+ priority: 1
78
+ position: 1
79
+ calculate_subtotal: 0
80
+ tax/tax_calculation:
81
+ - tax_calculation_id: 3
82
+ tax_calculation_rule_id: 1
83
+ tax_calculation_rate_id: 3
84
+ customer_tax_class_id: 3
85
+ product_tax_class_id: 3
86
+ customer/customer_group:
87
+ - customer_group_id: 1
88
+ customer_group_code: General
89
+ tax_class_id: 3
90
+ eav:
91
+ catalog_product:
92
+ - entity_id: 1
93
+ attribute_set_id: 4
94
+ type_id: simple
95
+ sku: book
96
+ name: Book
97
+ short_description: Book
98
+ description: Book
99
+ url_key: book
100
+ stock:
101
+ qty: 100.00
102
+ is_in_stock: 1
103
+ website_ids:
104
+ - usa_website
105
+ - canada_website
106
+ - german_website
107
+ category_ids:
108
+ - 2 # Default Category
109
+ price: 12.99
110
+ tax_class_id: 3 # Retail Customer-VAT
111
+ status: 1 # Enabled
112
+ visibility: 4 # Visible in Catalog & Search
113
+ /websites: # Set different prices per website
114
+ usa_website:
115
+ special_price: 9.99
116
+ german_website:
117
+ price: 9.99
118
+ special_price: 5.99
app/code/community/Doofinder/Feed/Test/Model/Product/providers/testGenerator.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -
2
+ - 1
3
+ - default
4
+ -
5
+ - 1
6
+ - usa
7
+ -
8
+ - 1
9
+ - canada
10
+ -
11
+ - 1
12
+ - germany
app/code/community/Doofinder/Feed/controllers/DoofinderFeedFeedController.php ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category controllers
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_DoofinderFeedFeedController extends Mage_Adminhtml_Controller_Action
13
+ {
14
+ /**
15
+ * Generate feed action
16
+ */
17
+ public function generateAction()
18
+ {
19
+ $storeCode = $this->getRequest()->getParam('store', false);
20
+
21
+ $codes = array();
22
+
23
+ // Create stores codes array
24
+ if ($storeCode) {
25
+ $codes[] = $storeCode;
26
+ } else {
27
+ $stores = Mage::app()->getStores();
28
+ foreach ($stores as $store) {
29
+ if ($store->getIsActive()) {
30
+ $codes[] = $store->getCode();
31
+ }
32
+ }
33
+ }
34
+
35
+ $scheduleObserver = Mage::getSingleton('doofinder_feed/observers_schedule');
36
+
37
+ foreach ($codes as $storeCode) {
38
+ $scheduleObserver->updateProcess($storeCode, true, true, true);
39
+ }
40
+
41
+ $this->getResponse()->setBody('Feed generation has been scheduled.');
42
+ }
43
+ }
app/code/community/Doofinder/Feed/controllers/DoofinderFeedLogController.php ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category controllers
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ class Doofinder_Feed_DoofinderFeedLogController extends Mage_Adminhtml_Controller_Action
13
+ {
14
+ /**
15
+ * View log for specified process.
16
+ */
17
+ public function viewAction()
18
+ {
19
+ $this->loadLayout();
20
+ $this->renderLayout();
21
+ }
22
+ }
app/code/community/Doofinder/Feed/controllers/FeedController.php ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category controllers
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Feed controller for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_FeedController extends Mage_Core_Controller_Front_Action
19
+ {
20
+
21
+ /**
22
+ * Send JSON headers
23
+ */
24
+ protected function _setJSONHeaders()
25
+ {
26
+ $this->getResponse()
27
+ ->clearHeaders()
28
+ ->setHeader('Content-type', 'application/json; charset="utf-8"', true);
29
+ }
30
+
31
+ /**
32
+ * Send XML headers
33
+ */
34
+ protected function _setXMLHeaders()
35
+ {
36
+ $this->getResponse()
37
+ ->clearHeaders()
38
+ ->setHeader('Content-type', 'application/xml; charset="utf-8"', true);
39
+ }
40
+
41
+ public function indexAction()
42
+ {
43
+ $storeCode = $this->_getStoreCode();
44
+ $config = Mage::helper('doofinder_feed')->getStoreConfig($storeCode);
45
+
46
+ // Set options for cron generator
47
+ $options = array(
48
+ '_limit_' => $this->_getInteger('limit', null),
49
+ '_offset_' => 0,
50
+ 'store_code' => $config['storeCode'],
51
+ 'grouped' => (bool) $config['grouped'],
52
+ 'display_price' => (bool) $config['display_price'],
53
+ 'minimal_price' => $this->_getBoolean('minimal_price', false),
54
+ 'customer_group_id' => 0,
55
+ 'image_size' => $config['image_size'],
56
+ );
57
+
58
+ $generator = Mage::getSingleton('doofinder_feed/generator', $options);
59
+
60
+ // Convert offset to product id
61
+ $offset = $this->_getInteger('offset', 0);
62
+
63
+ if ($offset > 0) {
64
+ $collection = $generator->getProductCollection();
65
+ $collection->getSelect()->limit(1, $offset);
66
+
67
+ $item = $collection->fetchItem();
68
+
69
+ $offset = $item ? $item->getEntityId() - 1 : -1;
70
+ }
71
+
72
+ $response = '';
73
+ if ($offset >= 0) {
74
+ $generator->setData('_offset_', $offset);
75
+ $this->_setXMLHeaders();
76
+
77
+ $response = $generator->run();
78
+
79
+ ob_end_clean();
80
+ }
81
+
82
+ $this->getResponse()->setBody($response);
83
+ }
84
+
85
+ public function configAction()
86
+ {
87
+ $this->_setJSONHeaders();
88
+
89
+ $helper = Mage::helper('doofinder_feed');
90
+
91
+ $tools = Mage::getModel('doofinder_feed/tools');
92
+
93
+ $storeCodes = array_keys(Mage::app()->getStores(false, true));
94
+ $storesConfiguration = array();
95
+
96
+ // Get file spath
97
+ $filesUrl = Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_MEDIA).'doofinder'.DS;
98
+ $filesPath = Mage::getBaseDir('media').DS.'doofinder'.DS;
99
+
100
+ foreach ($storeCodes as $code)
101
+ {
102
+ $settings = $helper->getStoreConfig($code);
103
+
104
+ if ($settings['enabled'])
105
+ {
106
+ $filepath = $filesPath.$settings['xmlName'];
107
+ $fileurl = $filesUrl.$settings['xmlName'];
108
+ $feedUrl = $filesUrl.$settings['xmlName'];
109
+ $feedExists = (bool) $this->_feedExists($filepath);
110
+ }
111
+ else
112
+ {
113
+ $feedUrl = Mage::getUrl('doofinder/feed', array('_store' => $code));
114
+ $feedExists = true;
115
+ }
116
+
117
+ $oStore = Mage::app()->getStore($code);
118
+ $L = Mage::getStoreConfig('general/locale/code', $oStore->getId());
119
+ $storesConfiguration[$code] = array(
120
+ 'language' => strtoupper(substr($L, 0, 2)),
121
+ 'currency' => $oStore->getCurrentCurrencyCode(),
122
+ 'feed' => $feedUrl,
123
+ 'feed_exists' => $feedExists,
124
+ );
125
+ }
126
+
127
+ $config = array(
128
+ 'platform' => array(
129
+ 'name' => 'Magento',
130
+ 'edition' => $tools->getMagentoEdition(),
131
+ 'version' => Mage::getVersion()
132
+ ),
133
+ 'module' => array(
134
+ 'version' => $this->_getVersion(),
135
+ 'feed' => Mage::getUrl('doofinder/feed'),
136
+ 'options' => array(
137
+ 'language' => $storeCodes,
138
+ ),
139
+ 'configuration' => $storesConfiguration,
140
+ ),
141
+ );
142
+
143
+ $response = Mage::helper('core')->jsonEncode($config);
144
+ $this->getResponse()->setBody($response);
145
+ }
146
+ /**
147
+ * Check if feed on filepath exists.
148
+ * @param string $filepath
149
+ * @return bool
150
+ */
151
+ protected function _feedExists($filepath = null) {
152
+ if (file_exists($filepath)) {
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+
158
+ protected function _dumpMessage($s_level, $s_message, $a_extra=array())
159
+ {
160
+ $error = array('status' => $s_level, 'message' => $s_message);
161
+
162
+ if (is_array($a_extra) && count($a_extra))
163
+ $error = array_merge($error, $a_extra);
164
+
165
+ $this->_setJSONHeaders();
166
+
167
+ $response = Mage::helper('core')->jsonEncode($error);
168
+ $this->getResponse()->setBody($response);
169
+ }
170
+
171
+ protected function _getVersion()
172
+ {
173
+ return Mage::getConfig()
174
+ ->getNode()
175
+ ->modules
176
+ ->Doofinder_Feed
177
+ ->version
178
+ ->asArray();
179
+ }
180
+
181
+ protected function _getStoreCode()
182
+ {
183
+ $storeCode = $this->getRequest()->getParam('language');
184
+
185
+ if (is_null($storeCode))
186
+ $storeCode = $this->getRequest()->getParam('store'); // Backwards...
187
+
188
+ if (is_null($storeCode))
189
+ $storeCode = Mage::app()->getStore()->getCode();
190
+
191
+ try
192
+ {
193
+ return Mage::app()->getStore($storeCode)->getCode();
194
+ }
195
+ catch(Mage_Core_Model_Store_Exception $e)
196
+ {
197
+ $this->_dumpMessage('error', 'Invalid <language> parameter.',
198
+ array('code' => 'INVALID_OPTIONS'));
199
+ }
200
+ }
201
+
202
+ protected function _getBoolean($param, $defaultValue = false)
203
+ {
204
+ $value = strtolower($this->getRequest()->getParam($param));
205
+
206
+ if ( is_numeric($value) )
207
+ return ((int)($value *= 1) > 0);
208
+
209
+ $yes = array('true', 'on', 'yes');
210
+ $no = array('false', 'off', 'no');
211
+
212
+ if ( in_array($value, $yes) )
213
+ return true;
214
+
215
+ if ( in_array($value, $no) )
216
+ return false;
217
+
218
+ return $defaultValue;
219
+ }
220
+
221
+ protected function _getInteger($param, $defaultValue)
222
+ {
223
+ $value = $this->getRequest()->getParam($param);
224
+ if ( is_numeric($value) )
225
+ return (int)($value *= 1);
226
+ return $defaultValue;
227
+ }
228
+
229
+ /**
230
+ * Creates directory.
231
+ * @param string $dir
232
+ * @return bool
233
+ */
234
+ protected function _createDirectory($dir = null) {
235
+ if (!$dir) return false;
236
+
237
+ if(!mkdir($dir, 0777, true)) {
238
+ Mage::throwException('Could not create directory: '.$dir);
239
+ }
240
+
241
+ return true;
242
+ }
243
+
244
+ /*
245
+ TEST TOOLS
246
+ */
247
+
248
+ // public function testsAction()
249
+ // {
250
+ // if ( !in_array(Mage::helper('core/http')->getRemoteAddr(), array('127.0.0.1', '::1')) )
251
+ // {
252
+ // $this->norouteAction();
253
+ // return false;
254
+ // }
255
+
256
+ // $oStore = Mage::app()->getStore($this->_getStoreCode());
257
+ // $bGrouped = $this->_getBoolean('grouped', true);
258
+ // $bMinimalPrice = $this->_getBoolean('minimal_price', false);
259
+ // $bCurrencyConvert = $this->_getBoolean('convert_currency', true);
260
+ // $iCustomerGroupId = $this->_getInteger('customer_group', 0);
261
+
262
+ // $ids = array(
263
+ // 'simple' => array(166, 27),
264
+ // 'grouped' => 54,
265
+ // 'configurable' => 83,
266
+ // 'virtual' => 142,
267
+ // 'bundle' => 158,
268
+ // 'downloadable' => 167
269
+ // );
270
+
271
+ // $data = array(
272
+ // 'store' => array(
273
+ // 'store_id' => $oStore->getStoreId(),
274
+ // 'website_id' => $oStore->getWebsiteId(),
275
+ // 'base_currency' => $oStore->getBaseCurrencyCode(),
276
+ // 'current_currency' => $oStore->getCurrentCurrencyCode(),
277
+ // 'default_currency' => $oStore->getDefaultCurrencyCode(),
278
+ // ),
279
+ // 'products' => array(),
280
+ // );
281
+
282
+ // $rule = Mage::getModel('catalogrule/rule');
283
+ // $dataHelper = Mage::helper('doofinder_feed');
284
+
285
+ // foreach ($ids as $product_type => $ids)
286
+ // {
287
+ // foreach ((array) $ids as $id)
288
+ // {
289
+ // $product = Mage::getModel('catalog/product')
290
+ // ->setStoreId($oStore->getStoreId())
291
+ // ->setCustomerGroupId($iCustomerGroupId)
292
+ // ->load($id);
293
+
294
+ // $data['products'][$id] = array(
295
+ // 'product_type' => $product_type,
296
+ // 'name' => $product->getName(),
297
+ // );
298
+
299
+ // $data['products'][$id] = array_merge(
300
+ // $data['products'][$id],
301
+ // $dataHelper->collectProductPrices($product, $oStore, $bCurrencyConvert, $bMinimalPrice, $bGrouped)
302
+ // );
303
+ // }
304
+ // }
305
+
306
+ // $this->_setJSONHeaders();
307
+
308
+ // $response = Mage::helper('core')->jsonEncode($data);
309
+ // $this->getResponse()->setBody($response);
310
+ // }
311
+ }
app/code/community/Doofinder/Feed/controllers/IndexController.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This file is part of Doofinder_Feed.
4
+ */
5
+
6
+ /**
7
+ * @category controllers
8
+ * @package Doofinder_Feed
9
+ * @version 1.6.5
10
+ */
11
+
12
+ /**
13
+ * Index controller for Doofinder Feed
14
+ *
15
+ * @version 1.6.5
16
+ * @package Doofinder_Feed
17
+ */
18
+ class Doofinder_Feed_IndexController extends Mage_Core_Controller_Front_Action
19
+ {
20
+ public function indexAction()
21
+ {
22
+ $this->_redirect('/');
23
+ }
24
+
25
+ public function testAction() {
26
+ $allStores = Mage::app()->getStores();
27
+ foreach ($allStores as $store) {
28
+ var_dump($store->getCode());
29
+ }
30
+
31
+ }
32
+ }
app/code/community/Doofinder/Feed/etc/config.xml ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <config>
3
+
4
+ <modules>
5
+ <Doofinder_Feed>
6
+ <version>1.6.5</version>
7
+ </Doofinder_Feed>
8
+ </modules>
9
+ <global>
10
+ <helpers>
11
+ <doofinder_feed>
12
+ <class>Doofinder_Feed_Helper</class>
13
+ </doofinder_feed>
14
+ </helpers>
15
+ <models>
16
+ <doofinder_feed>
17
+ <class>Doofinder_Feed_Model</class>
18
+ <resourceModel>doofinder_feed_mysql4</resourceModel>
19
+ </doofinder_feed>
20
+ <doofinder_feed_mysql4>
21
+ <class>Doofinder_Feed_Model_Mysql4</class>
22
+ <entities>
23
+ <cron>
24
+ <table>doofinder_feed</table>
25
+ </cron>
26
+ <log>
27
+ <table>doofinder_log</table>
28
+ </log>
29
+ </entities>
30
+ </doofinder_feed_mysql4>
31
+ <catalogsearch_resource>
32
+ <rewrite>
33
+ <fulltext>Doofinder_Feed_Model_CatalogSearch_Resource_Fulltext</fulltext>
34
+ </rewrite>
35
+ </catalogsearch_resource>
36
+ </models>
37
+ <resources>
38
+ <doofinder_feed_write>
39
+ <connection>
40
+ <use>core_write</use>
41
+ </connection>
42
+ </doofinder_feed_write>
43
+ <doofinder_feed_read>
44
+ <connection>
45
+ <use>core_read</use>
46
+ </connection>
47
+ </doofinder_feed_read>
48
+ <doofinder_feed_setup>
49
+ <setup>
50
+ <module>Doofinder_Feed</module>
51
+ <class>Doofinder_Feed_Model_Resource_Mysql4_Setup</class>
52
+ </setup>
53
+ <connection>
54
+ <use>core_setup</use>
55
+ </connection>
56
+ </doofinder_feed_setup>
57
+ </resources>
58
+ <blocks>
59
+ <doofinder_feed>
60
+ <class>Doofinder_Feed_Block</class>
61
+ </doofinder_feed>
62
+ </blocks>
63
+ <events>
64
+ <admin_system_config_changed_section_doofinder_cron>
65
+ <observers>
66
+ <doofinder_feed>
67
+ <type>singleton</type>
68
+ <class>doofinder_feed/observers_schedule</class>
69
+ <method>saveNewSchedule</method>
70
+ </doofinder_feed>
71
+ </observers>
72
+ </admin_system_config_changed_section_doofinder_cron>
73
+ <catalog_product_save_after>
74
+ <observers>
75
+ <doofinder_feed>
76
+ <class>doofinder_feed/observers_feed</class>
77
+ <method>updateSearchEngineIndexes</method>
78
+ </doofinder_feed>
79
+ </observers>
80
+ </catalog_product_save_after>
81
+ </events>
82
+ </global>
83
+
84
+ <adminhtml>
85
+ <layout>
86
+ <updates>
87
+ <doofinder_feed>
88
+ <file>doofinder.xml</file>
89
+ </doofinder_feed>
90
+ </updates>
91
+ </layout>
92
+ <acl>
93
+ <resources>
94
+ <admin>
95
+ <children>
96
+ <system>
97
+ <children>
98
+ <config>
99
+ <children>
100
+ <doofinder_cron>
101
+ <title>Cron Options</title>
102
+ </doofinder_cron>
103
+ <doofinder_search>
104
+ <title>Search Options</title>
105
+ </doofinder_search>
106
+ </children>
107
+ </config>
108
+ </children>
109
+ </system>
110
+ </children>
111
+ </admin>
112
+ </resources>
113
+ </acl>
114
+ </adminhtml>
115
+
116
+ <install></install>
117
+
118
+ <frontend>
119
+ <layout>
120
+ <updates>
121
+ <doofinder_feed>
122
+ <file>doofinder.xml</file>
123
+ </doofinder_feed>
124
+ </updates>
125
+ </layout>
126
+ <routers>
127
+ <doofinder_feed>
128
+ <use>standard</use>
129
+ <args>
130
+ <module>Doofinder_Feed</module>
131
+ <frontName>doofinder</frontName>
132
+ </args>
133
+ </doofinder_feed>
134
+ </routers>
135
+ </frontend>
136
+
137
+ <default>
138
+ <doofinder>
139
+ <feed>
140
+
141
+ <enabled>0</enabled>
142
+ <debug>1</debug>
143
+
144
+ <group_configurable_products>1</group_configurable_products>
145
+
146
+ <product_types><![CDATA[simple,virtual,bundle,configurable,downloadable,grouped]]></product_types>
147
+ <!--product_types><![CDATA[bundle]]></product_types-->
148
+ <excluded_attributes><![CDATA[gallery,image,small_image,special_price,special_from_date,special_to_date,price_view,url_key]]></excluded_attributes>
149
+ <additional_attributes><![CDATA[status]]></additional_attributes>
150
+
151
+ <directives>
152
+ <df_directive_id>
153
+ <label><![CDATA[Product Id]]></label>
154
+ </df_directive_id>
155
+ <df_directive_url>
156
+ <label><![CDATA[Product URL]]></label>
157
+ </df_directive_url>
158
+ <df_directive_image_link>
159
+ <label><![CDATA[Product Image URL (Base)]]></label>
160
+ </df_directive_image_link>
161
+ <df_directive_image_link_thumbnail>
162
+ <label><![CDATA[Product Image URL (Thumbnail)]]></label>
163
+ </df_directive_image_link_thumbnail>
164
+ <df_directive_image_link_small>
165
+ <label><![CDATA[Product Image URL (Small Image)]]></label>
166
+ </df_directive_image_link_small>
167
+ <df_directive_sale_price>
168
+ <label><![CDATA[Sale Price]]></label>
169
+ </df_directive_sale_price>
170
+ <df_directive_price>
171
+ <label><![CDATA[Price]]></label>
172
+ </df_directive_price>
173
+ <df_directive_currency>
174
+ <label><![CDATA[Store Currency]]></label>
175
+ </df_directive_currency>
176
+ <df_directive_availability>
177
+ <label><![CDATA[Availability]]></label>
178
+ </df_directive_availability>
179
+ <df_directive_condition>
180
+ <label><![CDATA[Product Condition]]></label>
181
+ </df_directive_condition>
182
+ </directives>
183
+
184
+ <fields>
185
+ <id>
186
+ <label>Unique Identifier</label>
187
+ </id>
188
+ <mpn>
189
+ <label>Product SKU</label>
190
+ </mpn>
191
+ <title>
192
+ <label>Title</label>
193
+ </title>
194
+ <description>
195
+ <label>Description</label>
196
+ </description>
197
+ <long_description>
198
+ <label>Long Description</label>
199
+ </long_description>
200
+ <meta_title>
201
+ <label>Meta Title</label>
202
+ </meta_title>
203
+ <meta_keyword>
204
+ <label>Meta Keywords</label>
205
+ </meta_keyword>
206
+ <meta_description>
207
+ <label>Meta Description</label>
208
+ </meta_description>
209
+ <brand>
210
+ <label>Brand</label>
211
+ </brand>
212
+ <link>
213
+ <label>Product Link</label>
214
+ </link>
215
+ <image_link>
216
+ <label>Image</label>
217
+ </image_link>
218
+ <price>
219
+ <label>Normal Price</label>
220
+ </price>
221
+ <sale_price>
222
+ <label>Sale Price</label>
223
+ </sale_price>
224
+ <availability>
225
+ <label>Availability</label>
226
+ </availability>
227
+ <currency>
228
+ <label>Currency</label>
229
+ </currency>
230
+ </fields>
231
+
232
+ </feed>
233
+ </doofinder>
234
+ <doofinder_cron>
235
+ <schedule_settings>
236
+ <enabled>1</enabled>
237
+ <name>doofinder-{store_code}.xml</name>
238
+ <time>0,0,0</time>
239
+ <frequency>D</frequency>
240
+ <delay>5</delay>
241
+ <step>1000</step>
242
+ <reset>0</reset>
243
+ </schedule_settings>
244
+ <feed_settings>
245
+ <display_price>1</display_price>
246
+ <grouped>0</grouped>
247
+ <image_size></image_size>
248
+ <atomic_updates_enabled>0</atomic_updates_enabled>
249
+ <categories_in_navigation>0</categories_in_navigation>
250
+ </feed_settings>
251
+ <attributes_mapping>
252
+ <id>df_directive_id</id>
253
+ <mpn>sku</mpn>
254
+ <title>name</title>
255
+ <description>short_description</description>
256
+ <brand>manufacturer</brand>
257
+ <link>df_directive_url</link>
258
+ <image_link>df_directive_image_link</image_link>
259
+ <price>df_directive_price</price>
260
+ <sale_price>df_directive_sale_price</sale_price>
261
+ <availability>df_directive_availability</availability>
262
+ <currency>df_directive_currency</currency>
263
+ <!--long_description>description</long_description>
264
+ <meta_title>meta_title</meta_title>
265
+ <meta_keyword>meta_keyword</meta_keyword>
266
+ <meta_description>meta_description</meta_description-->
267
+ </attributes_mapping>
268
+ </doofinder_cron>
269
+ <doofinder_search>
270
+ <layer_settings>
271
+ <enabled>0</enabled>
272
+ </layer_settings>
273
+
274
+ <internal_settings>
275
+ <request_limit>100</request_limit>
276
+ <total_limit>500</total_limit>
277
+ </internal_settings>
278
+ </doofinder_search>
279
+ </default>
280
+
281
+ <stores></stores>
282
+
283
+ <websites></websites>
284
+
285
+ <crontab>
286
+ <jobs>
287
+ <doofinder_feed_generate>
288
+ <schedule>
289
+ <cron_expr>* * * * *</cron_expr>
290
+ </schedule>
291
+ <run>
292
+ <model>doofinder_feed/observers_feed::generateFeed</model>
293
+ </run>
294
+ </doofinder_feed_generate>
295
+ <doofinder_feed_schedule>
296
+ <schedule>
297
+ <cron_expr>1 */12 * * *</cron_expr>
298
+ </schedule>
299
+ <run>
300
+ <model>doofinder_feed/observers_schedule::regenerateSchedule</model>
301
+ </run>
302
+ </doofinder_feed_schedule>
303
+ <doofinder_feed_clear_logs>
304
+ <schedule>
305
+ <cron_expr>30 2 * * *</cron_expr>
306
+ </schedule>
307
+ <run>
308
+ <model>doofinder_feed/observers_logs::clearLogs</model>
309
+ </run>
310
+ </doofinder_feed_clear_logs>
311
+ </jobs>
312
+ </crontab>
313
+
314
+ <phpunit>
315
+ <suite>
316
+ <modules>
317
+ <Doofinder_Feed />
318
+ </modules>
319
+ <groups>
320
+ <controller>Controller</controller>
321
+ </groups>
322
+ </suite>
323
+ </phpunit>
324
+
325
+ <admin>
326
+ <routers>
327
+ <adminhtml>
328
+ <args>
329
+ <modules>
330
+ <Doofinder_Feed after="Mage_Adminhtml">Doofinder_Feed</Doofinder_Feed>
331
+ </modules>
332
+ </args>
333
+ </adminhtml>
334
+ </routers>
335
+ </admin>
336
+
337
+ </config>
app/code/community/Doofinder/Feed/etc/system.xml ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <config>
2
+ <tabs>
3
+ <doofinder_config translate="label" module="doofinder_feed">
4
+ <label>Doofinder Search</label>
5
+ <sort_order>99999</sort_order>
6
+ </doofinder_config>
7
+ </tabs>
8
+ <sections>
9
+ <doofinder_cron translate="label" module="doofinder_feed">
10
+ <label>Product Data Feed</label>
11
+ <tab>doofinder_config</tab>
12
+ <frontend_type>text</frontend_type>
13
+ <sort_order>1000</sort_order>
14
+ <show_in_default>1</show_in_default>
15
+ <show_in_website>0</show_in_website>
16
+ <show_in_store>1</show_in_store>
17
+ <groups>
18
+ <attributes_mapping translate="label">
19
+ <label>Feed Attributes</label>
20
+ <expanded>false</expanded>
21
+ <frontend_type>text</frontend_type>
22
+ <sort_order>10</sort_order>
23
+ <show_in_default>1</show_in_default>
24
+ <show_in_website>0</show_in_website>
25
+ <show_in_store>1</show_in_store>
26
+ <fields>
27
+ <id translate="label">
28
+ <label>Id</label>
29
+ <frontend_type>select</frontend_type>
30
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
31
+ <sort_order>1</sort_order>
32
+ <show_in_default>1</show_in_default>
33
+ <show_in_website>0</show_in_website>
34
+ <show_in_store>1</show_in_store>
35
+ </id>
36
+ <title translate="label">
37
+ <label>Title</label>
38
+ <frontend_type>select</frontend_type>
39
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
40
+ <sort_order>2</sort_order>
41
+ <show_in_default>1</show_in_default>
42
+ <show_in_website>0</show_in_website>
43
+ <show_in_store>1</show_in_store>
44
+ </title>
45
+ <description translate="label">
46
+ <label>Description</label>
47
+ <frontend_type>select</frontend_type>
48
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
49
+ <sort_order>3</sort_order>
50
+ <show_in_default>1</show_in_default>
51
+ <show_in_website>0</show_in_website>
52
+ <show_in_store>1</show_in_store>
53
+ </description>
54
+ <brand translate="label">
55
+ <label>Brand</label>
56
+ <frontend_type>select</frontend_type>
57
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
58
+ <sort_order>4</sort_order>
59
+ <show_in_default>1</show_in_default>
60
+ <show_in_website>0</show_in_website>
61
+ <show_in_store>1</show_in_store>
62
+ </brand>
63
+ <link translate="label">
64
+ <label>Link</label>
65
+ <frontend_type>select</frontend_type>
66
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
67
+ <sort_order>5</sort_order>
68
+ <show_in_default>1</show_in_default>
69
+ <show_in_website>0</show_in_website>
70
+ <show_in_store>1</show_in_store>
71
+ </link>
72
+ <image_link translate="label">
73
+ <label>Image Link</label>
74
+ <frontend_type>select</frontend_type>
75
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
76
+ <sort_order>6</sort_order>
77
+ <show_in_default>1</show_in_default>
78
+ <show_in_website>0</show_in_website>
79
+ <show_in_store>1</show_in_store>
80
+ </image_link>
81
+ <price translate="label">
82
+ <label>Price</label>
83
+ <frontend_type>select</frontend_type>
84
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
85
+ <sort_order>7</sort_order>
86
+ <show_in_default>1</show_in_default>
87
+ <show_in_website>0</show_in_website>
88
+ <show_in_store>1</show_in_store>
89
+ </price>
90
+ <sale_price translate="label">
91
+ <label>Sale price</label>
92
+ <frontend_type>select</frontend_type>
93
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
94
+ <sort_order>8</sort_order>
95
+ <show_in_default>1</show_in_default>
96
+ <show_in_website>0</show_in_website>
97
+ <show_in_store>1</show_in_store>
98
+ </sale_price>
99
+ <mpn translate="label">
100
+ <label>MPN</label>
101
+ <frontend_type>select</frontend_type>
102
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
103
+ <sort_order>9</sort_order>
104
+ <show_in_default>1</show_in_default>
105
+ <show_in_website>0</show_in_website>
106
+ <show_in_store>1</show_in_store>
107
+ </mpn>
108
+ <availability translate="label">
109
+ <label>Availability</label>
110
+ <frontend_type>select</frontend_type>
111
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
112
+ <sort_order>10</sort_order>
113
+ <show_in_default>1</show_in_default>
114
+ <show_in_website>0</show_in_website>
115
+ <show_in_store>1</show_in_store>
116
+ </availability>
117
+ <currency translate="label">
118
+ <label>Currency</label>
119
+ <frontend_type>select</frontend_type>
120
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
121
+ <sort_order>11</sort_order>
122
+ <show_in_default>1</show_in_default>
123
+ <show_in_website>0</show_in_website>
124
+ <show_in_store>1</show_in_store>
125
+ </currency>
126
+ <additional translate="label">
127
+ <label>Additional Attributes</label>
128
+ <frontend_model>doofinder_feed/adminhtml_map_additional</frontend_model>
129
+ <backend_model>doofinder_feed/system_config_backend_map_additional</backend_model>
130
+ <sort_order>12</sort_order>
131
+ <show_in_default>1</show_in_default>
132
+ <show_in_website>0</show_in_website>
133
+ <show_in_store>1</show_in_store>
134
+ </additional>
135
+ <!--long_description translate="label">
136
+ <label>Long Description</label>
137
+ <frontend_type>select</frontend_type>
138
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
139
+ <sort_order>5</sort_order>
140
+ <show_in_default>1</show_in_default>
141
+ <show_in_website>0</show_in_website>
142
+ <show_in_store>1</show_in_store>
143
+ </long_description>
144
+ <meta_title translate="label">
145
+ <label>Meta Title</label>
146
+ <frontend_type>select</frontend_type>
147
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
148
+ <sort_order>6</sort_order>
149
+ <show_in_default>1</show_in_default>
150
+ <show_in_website>0</show_in_website>
151
+ <show_in_store>1</show_in_store>
152
+ </meta_title>
153
+ <meta_keyword translate="label">
154
+ <label>Meta Keyword</label>
155
+ <frontend_type>select</frontend_type>
156
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
157
+ <sort_order>7</sort_order>
158
+ <show_in_default>1</show_in_default>
159
+ <show_in_website>0</show_in_website>
160
+ <show_in_store>1</show_in_store>
161
+ </meta_keyword>
162
+ <meta_description translate="label">
163
+ <label>Meta Description</label>
164
+ <frontend_type>select</frontend_type>
165
+ <source_model>doofinder_feed/system_config_source_product_attributes</source_model>
166
+ <sort_order>8</sort_order>
167
+ <show_in_default>1</show_in_default>
168
+ <show_in_website>0</show_in_website>
169
+ <show_in_store>1</show_in_store>
170
+ </meta_description-->
171
+ </fields>
172
+ </attributes_mapping>
173
+
174
+ <feed_settings translate="label">
175
+ <label>Feed Settings</label>
176
+ <expanded>true</expanded>
177
+ <frontend_type>text</frontend_type>
178
+ <sort_order>15</sort_order>
179
+ <show_in_default>1</show_in_default>
180
+ <show_in_website>0</show_in_website>
181
+ <show_in_store>1</show_in_store>
182
+ <fields>
183
+ <display_price translate="label">
184
+ <label>Export Product Prices</label>
185
+ <frontend_type>select</frontend_type>
186
+ <source_model>adminhtml/system_config_source_yesno</source_model>
187
+ <sort_order>7</sort_order>
188
+ <show_in_default>1</show_in_default>
189
+ <show_in_website>0</show_in_website>
190
+ <show_in_store>1</show_in_store>
191
+ </display_price>
192
+ <grouped translate="label">
193
+ <label>Split Configurable Products</label>
194
+ <comment>Export each component of a configurable product separately, instead of exporting them as a single product.</comment>
195
+ <frontend_type>select</frontend_type>
196
+ <source_model>adminhtml/system_config_source_yesno</source_model>
197
+ <sort_order>8</sort_order>
198
+ <show_in_default>1</show_in_default>
199
+ <show_in_website>0</show_in_website>
200
+ <show_in_store>1</show_in_store>
201
+ </grouped>
202
+ <image_size translate="label">
203
+ <label>Image size</label>
204
+ <comment>Export product image with given width. Leave empty to use original size.</comment>
205
+ <frontend_type>text</frontend_type>
206
+ <sort_order>9</sort_order>
207
+ <show_in_default>1</show_in_default>
208
+ <show_in_website>0</show_in_website>
209
+ <show_in_store>1</show_in_store>
210
+ </image_size>
211
+ <atomic_updates_enabled translate="label">
212
+ <label>Use Doofinder API to update products on save</label>
213
+ <frontend_type>select</frontend_type>
214
+ <comment>Requires a Management API key configured in Search Configuration</comment>
215
+ <source_model>adminhtml/system_config_source_yesno</source_model>
216
+ <sort_order>1</sort_order>
217
+ <show_in_default>1</show_in_default>
218
+ <show_in_website>0</show_in_website>
219
+ <show_in_store>1</show_in_store>
220
+ </atomic_updates_enabled>
221
+ <categories_in_navigation translate="label">
222
+ <label>Export only categories present in navigation menus</label>
223
+ <frontend_type>select</frontend_type>
224
+ <comment>Whether this option is enabled or not, only active categories will be exported.</comment>
225
+ <source_model>adminhtml/system_config_source_yesno</source_model>
226
+ <sort_order>10</sort_order>
227
+ <show_in_default>1</show_in_default>
228
+ <show_in_website>0</show_in_website>
229
+ <show_in_store>1</show_in_store>
230
+ </categories_in_navigation>
231
+ </fields>
232
+ </feed_settings>
233
+
234
+ <schedule_settings translate="label">
235
+ <label>Schedule Settings (Beta!)</label>
236
+ <expanded>false</expanded>
237
+ <frontend_type>text</frontend_type>
238
+ <sort_order>20</sort_order>
239
+ <show_in_default>1</show_in_default>
240
+ <show_in_website>0</show_in_website>
241
+ <show_in_store>1</show_in_store>
242
+ <fields>
243
+ <instruction>
244
+ <frontend_model>doofinder_feed/settings_panel_crondescription</frontend_model>
245
+ <sort_order>1</sort_order>
246
+ <show_in_default>1</show_in_default>
247
+ <show_in_website>0</show_in_website>
248
+ <show_in_store>0</show_in_store>
249
+ </instruction>
250
+ <enabled>
251
+ <label>Enabled</label>
252
+ <frontend_type>select</frontend_type>
253
+ <comment>Activate/deactivate the feed for this store</comment>
254
+ <source_model>adminhtml/system_config_source_yesno</source_model>
255
+ <sort_order>1</sort_order>
256
+ <show_in_default>1</show_in_default>
257
+ <show_in_website>0</show_in_website>
258
+ <show_in_store>1</show_in_store>
259
+ </enabled>
260
+ <time translate="label">
261
+ <label>Feed generation start time</label>
262
+ <frontend_type>time</frontend_type>
263
+ <comment>The time at which the feed generation starts.</comment>
264
+ <sort_order>2</sort_order>
265
+ <show_in_default>1</show_in_default>
266
+ <show_in_website>0</show_in_website>
267
+ <show_in_store>1</show_in_store>
268
+ </time>
269
+ <frequency translate="label">
270
+ <label>Frequency</label>
271
+ <frontend_type>select</frontend_type>
272
+ <comment>Feed generating frequency (e.g. "monthly" means that the feed will be generated on the first day of each month at a given hour).</comment>
273
+ <source_model>adminhtml/system_config_source_cron_frequency</source_model>
274
+ <sort_order>3</sort_order>
275
+ <show_in_default>1</show_in_default>
276
+ <show_in_website>0</show_in_website>
277
+ <show_in_store>1</show_in_store>
278
+ </frequency>
279
+ <step translate="label">
280
+ <label>Step Size</label>
281
+ <frontend_type>text</frontend_type>
282
+ <comment>The maximum number of products added to the feed on each iteration of the generation process (maximum recommended value: 1000)</comment>
283
+ <sort_order>4</sort_order>
284
+ <show_in_default>1</show_in_default>
285
+ <show_in_website>0</show_in_website>
286
+ <show_in_store>1</show_in_store>
287
+ </step>
288
+ <delay translate="label">
289
+ <label>Step Delay</label>
290
+ <frontend_type>text</frontend_type>
291
+ <comment><![CDATA[The interval (in minutes) between subsequent stages of the feed generation process. It should not be smaller than the frequency of execution of your cron.sh file. (<a href="http://devdocs.magento.com/guides/v2.0/config-guide/cli/config-cli-subcommands-cron.html" target="step-delay">What's this?</a>)]]></comment>
292
+ <sort_order>5</sort_order>
293
+ <show_in_default>1</show_in_default>
294
+ <show_in_website>0</show_in_website>
295
+ <show_in_store>1</show_in_store>
296
+ </delay>
297
+ <generate_global translate="label">
298
+ <frontend_type>button</frontend_type>
299
+ <frontend_model>doofinder_feed/settings_buttons_generate</frontend_model>
300
+ <sort_order>20</sort_order>
301
+ <show_in_default>1</show_in_default>
302
+ <show_in_website>0</show_in_website>
303
+ <show_in_store>0</show_in_store>
304
+ </generate_global>
305
+ <generate translate="label">
306
+ <frontend_type>button</frontend_type>
307
+ <frontend_model>doofinder_feed/settings_buttons_generate</frontend_model>
308
+ <sort_order>20</sort_order>
309
+ <show_in_default>0</show_in_default>
310
+ <show_in_website>0</show_in_website>
311
+ <show_in_store>1</show_in_store>
312
+ </generate>
313
+ </fields>
314
+ </schedule_settings>
315
+ <panel translate="label">
316
+ <label>Feed Status</label>
317
+ <expanded>false</expanded>
318
+ <frontend_type>text</frontend_type>
319
+ <sort_order>30</sort_order>
320
+ <show_in_default>1</show_in_default>
321
+ <show_in_website>0</show_in_website>
322
+ <show_in_store>1</show_in_store>
323
+ <fields>
324
+ <cron_issue>
325
+ <label>Cron Issues</label>
326
+ <frontend_model>doofinder_feed/settings_panel_cron</frontend_model>
327
+ <sort_order>5</sort_order>
328
+ <show_in_default>1</show_in_default>
329
+ <show_in_website>1</show_in_website>
330
+ <show_in_store>1</show_in_store>
331
+ </cron_issue>
332
+ <status>
333
+ <label>Status</label>
334
+ <frontend_model>doofinder_feed/settings_panel_message</frontend_model>
335
+ <sort_order>10</sort_order>
336
+ <show_in_default>0</show_in_default>
337
+ <show_in_website>0</show_in_website>
338
+ <show_in_store>1</show_in_store>
339
+ </status>
340
+ <message>
341
+ <label>Message</label>
342
+ <frontend_model>doofinder_feed/settings_panel_message</frontend_model>
343
+ <sort_order>20</sort_order>
344
+ <show_in_default>0</show_in_default>
345
+ <show_in_website>0</show_in_website>
346
+ <show_in_store>1</show_in_store>
347
+ </message>
348
+ <complete>
349
+ <label>Complete</label>
350
+ <frontend_model>doofinder_feed/settings_panel_message</frontend_model>
351
+ <sort_order>30</sort_order>
352
+ <show_in_default>0</show_in_default>
353
+ <show_in_website>0</show_in_website>
354
+ <show_in_store>1</show_in_store>
355
+ </complete>
356
+ <next_run>
357
+ <label>Next Run</label>
358
+ <frontend_model>doofinder_feed/settings_panel_datetime</frontend_model>
359
+ <sort_order>40</sort_order>
360
+ <show_in_default>0</show_in_default>
361
+ <show_in_website>0</show_in_website>
362
+ <show_in_store>1</show_in_store>
363
+ </next_run>
364
+ <next_iteration>
365
+ <label>Next Iteration</label>
366
+ <frontend_model>doofinder_feed/settings_panel_datetime</frontend_model>
367
+ <sort_order>50</sort_order>
368
+ <show_in_default>0</show_in_default>
369
+ <show_in_website>0</show_in_website>
370
+ <show_in_store>1</show_in_store>
371
+ </next_iteration>
372
+ <last_file>
373
+ <label>Last generated feed</label>
374
+ <frontend_model>doofinder_feed/settings_panel_file</frontend_model>
375
+ <sort_order>60</sort_order>
376
+ <show_in_default>0</show_in_default>
377
+ <show_in_website>0</show_in_website>
378
+ <show_in_store>1</show_in_store>
379
+ </last_file>
380
+ <last_files>
381
+ <label>Last generated feeds</label>
382
+ <frontend_model>doofinder_feed/settings_panel_file</frontend_model>
383
+ <sort_order>60</sort_order>
384
+ <show_in_default>1</show_in_default>
385
+ <show_in_website>0</show_in_website>
386
+ <show_in_store>0</show_in_store>
387
+ </last_files>
388
+ <error_stack>
389
+ <label>Errors</label>
390
+ <frontend_model>doofinder_feed/settings_panel_message</frontend_model>
391
+ <sort_order>70</sort_order>
392
+ <show_in_default>0</show_in_default>
393
+ <show_in_website>0</show_in_website>
394
+ <show_in_store>1</show_in_store>
395
+ </error_stack>
396
+ <view_log>
397
+ <frontend_type>button</frontend_type>
398
+ <frontend_model>doofinder_feed/settings_buttons_viewLog</frontend_model>
399
+ <sort_order>80</sort_order>
400
+ <show_in_default>0</show_in_default>
401
+ <show_in_website>0</show_in_website>
402
+ <show_in_store>1</show_in_store>
403
+ </view_log>
404
+ </fields>
405
+ </panel>
406
+ </groups>
407
+ </doofinder_cron>
408
+ <doofinder_search translate="label" module="doofinder_feed">
409
+ <label>Search Configuration</label>
410
+ <tab>doofinder_config</tab>
411
+ <frontend_type>text</frontend_type>
412
+ <sort_order>1010</sort_order>
413
+ <show_in_default>1</show_in_default>
414
+ <show_in_website>0</show_in_website>
415
+ <show_in_store>1</show_in_store>
416
+ <groups>
417
+ <layer_settings translate="label">
418
+ <label>Doofinder Layer</label>
419
+ <expanded>true</expanded>
420
+ <frontend_type>text</frontend_type>
421
+ <sort_order>10</sort_order>
422
+ <show_in_default>1</show_in_default>
423
+ <show_in_website>0</show_in_website>
424
+ <show_in_store>1</show_in_store>
425
+ <fields>
426
+ <enabled>
427
+ <label>Enabled</label>
428
+ <frontend_type>select</frontend_type>
429
+ <comment>Activate/deactivate the search layer</comment>
430
+ <source_model>adminhtml/system_config_source_yesno</source_model>
431
+ <sort_order>0</sort_order>
432
+ <show_in_default>1</show_in_default>
433
+ <show_in_website>0</show_in_website>
434
+ <show_in_store>1</show_in_store>
435
+ </enabled>
436
+ <instruction>
437
+ <frontend_model>doofinder_feed/settings_panel_layerdescription</frontend_model>
438
+ <sort_order>1</sort_order>
439
+ <show_in_default>1</show_in_default>
440
+ <show_in_website>0</show_in_website>
441
+ <show_in_store>0</show_in_store>
442
+ </instruction>
443
+ <script translate="label">
444
+ <label>Script</label>
445
+ <frontend_type>textarea</frontend_type>
446
+ <comment>Paste your integration script here.</comment>
447
+ <sort_order>2</sort_order>
448
+ <show_in_default>0</show_in_default>
449
+ <show_in_website>0</show_in_website>
450
+ <show_in_store>1</show_in_store>
451
+ </script>
452
+ </fields>
453
+ </layer_settings>
454
+ <internal_settings translate="label">
455
+ <label>Internal Search</label>
456
+ <expanded>true</expanded>
457
+ <frontend_type>text</frontend_type>
458
+ <sort_order>20</sort_order>
459
+ <show_in_default>1</show_in_default>
460
+ <show_in_website>0</show_in_website>
461
+ <show_in_store>1</show_in_store>
462
+ <fields>
463
+ <enable translate="label">
464
+ <label>Enabled</label>
465
+ <frontend_type>select</frontend_type>
466
+ <source_model>adminhtml/system_config_source_yesno</source_model>
467
+ <comment>Use Doofinder Search instead of Magento default search.</comment>
468
+ <sort_order>1</sort_order>
469
+ <show_in_default>1</show_in_default>
470
+ <show_in_website>0</show_in_website>
471
+ <show_in_store>1</show_in_store>
472
+ </enable>
473
+ <api_key translate="label">
474
+ <label>API Key</label>
475
+ <frontend_type>text</frontend_type>
476
+ <sort_order>2</sort_order>
477
+ <show_in_default>1</show_in_default>
478
+ <show_in_website>0</show_in_website>
479
+ <show_in_store>0</show_in_store>
480
+ </api_key>
481
+
482
+ <hash_id translate="label">
483
+ <label>Hash ID</label>
484
+ <frontend_type>text</frontend_type>
485
+ <backend_model>doofinder_feed/adminhtml_system_config_validation_hashid</backend_model>
486
+ <sort_order>3</sort_order>
487
+ <show_in_default>0</show_in_default>
488
+ <show_in_website>0</show_in_website>
489
+ <show_in_store>1</show_in_store>
490
+ </hash_id>
491
+ <instruction>
492
+ <frontend_model>doofinder_feed/settings_panel_hashdescription</frontend_model>
493
+ <custom>hola</custom>
494
+ <sort_order>4</sort_order>
495
+ <show_in_default>1</show_in_default>
496
+ <show_in_website>0</show_in_website>
497
+ <show_in_store>0</show_in_store>
498
+ </instruction>
499
+
500
+ <total_limit translate="label">
501
+ <label>Total limit</label>
502
+ <frontend_type>text</frontend_type>
503
+ <backend_model>doofinder_feed/system_config_backend_total_limit</backend_model>
504
+ <comment>Number of results returned for search query.</comment>
505
+ <sort_order>6</sort_order>
506
+ <show_in_default>1</show_in_default>
507
+ <show_in_website>0</show_in_website>
508
+ <show_in_store>1</show_in_store>
509
+ </total_limit>
510
+ </fields>
511
+ </internal_settings>
512
+ </groups>
513
+ </doofinder_search>
514
+ </sections>
515
+ </config>
app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-install-1.5.4.php ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+
5
+ $installer->startSetup();
6
+
7
+ /**
8
+ * Cron table
9
+ */
10
+
11
+ if (version_compare(Mage::getVersion(), '1.6', '<'))
12
+ {
13
+ $installer->run("DROP TABLE IF EXISTS {$installer->getTable('doofinder_feed/cron')};");
14
+ }
15
+ else
16
+ {
17
+ $installer->getConnection()->dropTable( $installer->getTable('doofinder_feed/cron') );
18
+ }
19
+
20
+ $table = $installer->getConnection()
21
+ ->newTable($installer->getTable('doofinder_feed/cron'))
22
+ ->addColumn('id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
23
+ 'nullable' => false,
24
+ 'identity' => true,
25
+ 'primary' => true,
26
+ ), 'ID')
27
+ ->addColumn('store_code', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
28
+ 'nullable' => false,
29
+ ), 'Store Code')
30
+ ->addColumn('status', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
31
+ 'length' => 255,
32
+ ), 'Status')
33
+ ->addColumn('message', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
34
+ ), 'Message')
35
+ ->addColumn('error_stack', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
36
+ 'default' => 0,
37
+ ), 'Error Stack')
38
+ ->addColumn('complete', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
39
+ 'length' => 12,
40
+ ), 'Complete')
41
+ ->addColumn('next_run', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
42
+ 'length' => 255,
43
+ ), 'Next Run')
44
+ ->addColumn('next_iteration', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
45
+ 'length' => 255,
46
+ ), 'Next Iteration')
47
+ ->addColumn('last_feed_name', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
48
+ 'length' => 255,
49
+ ), 'Last Feed Name')
50
+ ->addColumn('offset', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
51
+ 'default' => 0,
52
+ ), 'Offset')
53
+ ->addColumn('schedule_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
54
+ 'default' => null,
55
+ ), 'Schedule ID');
56
+
57
+ $installer->getConnection()->createTable($table);
58
+
59
+ $installer->endSetup();
app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-install-1.5.7.php ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+
5
+ $installer->startSetup();
6
+
7
+ // 1.5
8
+ if ( version_compare(Mage::getVersion(), '1.6', '<') )
9
+ {
10
+ $installer->run("DROP TABLE IF EXISTS {$installer->getTable('doofinder_feed/cron')};");
11
+ $installer->run("DROP TABLE IF EXISTS {$installer->getTable('doofinder_feed/log')};");
12
+ }
13
+ // 1.6+
14
+ else
15
+ {
16
+ $installer->getConnection()->dropTable( $installer->getTable('doofinder_feed/cron') );
17
+ $installer->getConnection()->dropTable( $installer->getTable('doofinder_feed/log') );
18
+ }
19
+
20
+ /**
21
+ * Cron table
22
+ */
23
+
24
+ $table = $installer->getConnection()
25
+ ->newTable($installer->getTable('doofinder_feed/cron'))
26
+ ->addColumn('id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
27
+ 'nullable' => false,
28
+ 'identity' => true,
29
+ 'primary' => true,
30
+ ), 'ID')
31
+ ->addColumn('store_code', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
32
+ 'nullable' => false,
33
+ ), 'Store Code')
34
+ ->addColumn('status', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
35
+ 'length' => 255,
36
+ ), 'Status')
37
+ ->addColumn('message', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
38
+ ), 'Message')
39
+ ->addColumn('error_stack', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
40
+ 'default' => 0,
41
+ ), 'Error Stack')
42
+ ->addColumn('complete', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
43
+ 'length' => 12,
44
+ ), 'Complete')
45
+ ->addColumn('next_run', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
46
+ 'length' => 255,
47
+ ), 'Next Run')
48
+ ->addColumn('next_iteration', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
49
+ 'length' => 255,
50
+ ), 'Next Iteration')
51
+ ->addColumn('last_feed_name', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
52
+ 'length' => 255,
53
+ ), 'Last Feed Name')
54
+ ->addColumn('offset', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
55
+ 'default' => 0,
56
+ ), 'Offset');
57
+
58
+ $installer->getConnection()->createTable($table);
59
+
60
+ // 1.5
61
+ if ( version_compare(Mage::getVersion(), '1.6', '<') )
62
+ {
63
+ $installer->run("
64
+
65
+ ALTER TABLE {$installer->getTable('doofinder_feed/cron')}
66
+ MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
67
+
68
+ ");
69
+ }
70
+
71
+ /**
72
+ * Log table
73
+ */
74
+
75
+ // 1.6+
76
+ if ( ! version_compare(Mage::getVersion(), '1.6', '<') )
77
+ {
78
+ // Add log table
79
+ $table = $installer->getConnection()
80
+ ->newTable($installer->getTable('doofinder_feed/log'))
81
+ ->addColumn('id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
82
+ 'nullable' => false,
83
+ 'identity' => true,
84
+ 'primary' => true,
85
+ ), 'ID')
86
+ ->addColumn('process_id', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
87
+ 'nullable' => false,
88
+ ), 'Store Code')
89
+ ->addColumn('type', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
90
+ 'nullable' => false,
91
+ ), 'Type')
92
+ ->addColumn('time', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null, array(
93
+ 'nullable' => false,
94
+ 'default' => Varien_Db_Ddl_Table::TIMESTAMP_INIT,
95
+ ), 'Type')
96
+ ->addColumn('message', Varien_Db_Ddl_Table::TYPE_TEXT, null, array(
97
+ 'nullable' => false,
98
+ ), 'Message');
99
+
100
+ // Add indexes to log table
101
+ $table->addIndex(
102
+ $installer->getIdxName(
103
+ 'doofinder_feed/log',
104
+ array(
105
+ 'process_id',
106
+ 'type',
107
+ ),
108
+ Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX
109
+ ),
110
+ array(
111
+ 'process_id',
112
+ 'type',
113
+ ),
114
+ array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX)
115
+ );
116
+ $table->addIndex(
117
+ $installer->getIdxName(
118
+ 'doofinder_feed/log',
119
+ array(
120
+ 'time',
121
+ ),
122
+ Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX
123
+ ),
124
+ array(
125
+ 'time',
126
+ ),
127
+ array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX)
128
+ );
129
+
130
+ $installer->getConnection()->createTable($table);
131
+ }
132
+ // 1.5
133
+ else
134
+ {
135
+ $installer->run("
136
+
137
+ CREATE TABLE {$installer->getTable('doofinder_feed/log')} (
138
+ `id` int(11) NOT NULL COMMENT 'ID',
139
+ `process_id` varchar(255) NOT NULL COMMENT 'Store Code',
140
+ `type` varchar(255) NOT NULL COMMENT 'Type',
141
+ `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Type',
142
+ `message` text NOT NULL COMMENT 'Message'
143
+ );
144
+
145
+ ALTER TABLE {$installer->getTable('doofinder_feed/log')}
146
+ ADD PRIMARY KEY (`id`), ADD KEY `IDX_DOOFINDER_LOG_PROCESS_ID_TYPE` (`process_id`,`type`), ADD KEY `IDX_DOOFINDER_LOG_TIME` (`time`);
147
+
148
+ ALTER TABLE {$installer->getTable('doofinder_feed/log')}
149
+ MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
150
+
151
+ ");
152
+ }
153
+
154
+ // /**
155
+ // * Trigger feed generation
156
+ // */
157
+ // $scheduleObserver = Mage::getSingleton('doofinder_feed/observers_schedule');
158
+
159
+ // foreach (Mage::getModel('core/store')->getCollection() as $store) {
160
+ // $scheduleObserver->updateProcess($store->getCode(), true, true);
161
+ // }
162
+
163
+ $installer->endSetup();
app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-upgrade-1.5.4-1.5.5.php ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+
5
+ $installer->startSetup();
6
+
7
+ // 1.5
8
+ if ( version_compare(Mage::getVersion(), '1.6', '<') )
9
+ {
10
+ $installer->run("
11
+
12
+ ALTER TABLE {$installer->getTable('doofinder_feed/cron')}
13
+ MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
14
+
15
+ ");
16
+ }
17
+
18
+ $installer->endSetup();
app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-upgrade-1.5.5-1.5.6.php ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+
5
+ $installer->startSetup();
6
+
7
+ // 1.5
8
+ if ( version_compare(Mage::getVersion(), '1.6', '<') )
9
+ {
10
+ $installer->run("DROP TABLE IF EXISTS {$installer->getTable('doofinder_feed/log')};");
11
+ }
12
+ // 1.6+
13
+ else
14
+ {
15
+ $installer->getConnection()->dropTable( $installer->getTable('doofinder_feed/log') );
16
+ }
17
+
18
+ /**
19
+ * Log table
20
+ */
21
+
22
+ // 1.6+
23
+ if ( ! version_compare(Mage::getVersion(), '1.6', '<') )
24
+ {
25
+ // Add log table
26
+ $table = $installer->getConnection()
27
+ ->newTable($installer->getTable('doofinder_feed/log'))
28
+ ->addColumn('id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
29
+ 'nullable' => false,
30
+ 'identity' => true,
31
+ 'primary' => true,
32
+ ), 'ID')
33
+ ->addColumn('process_id', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
34
+ 'nullable' => false,
35
+ ), 'Store Code')
36
+ ->addColumn('type', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array(
37
+ 'nullable' => false,
38
+ ), 'Type')
39
+ ->addColumn('time', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null, array(
40
+ 'nullable' => false,
41
+ 'default' => Varien_Db_Ddl_Table::TIMESTAMP_INIT,
42
+ ), 'Type')
43
+ ->addColumn('message', Varien_Db_Ddl_Table::TYPE_TEXT, null, array(
44
+ 'nullable' => false,
45
+ ), 'Message');
46
+
47
+ // Add indexes to log table
48
+ $table->addIndex(
49
+ $installer->getIdxName(
50
+ 'doofinder_feed/log',
51
+ array(
52
+ 'process_id',
53
+ 'type',
54
+ ),
55
+ Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX
56
+ ),
57
+ array(
58
+ 'process_id',
59
+ 'type',
60
+ ),
61
+ array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX)
62
+ );
63
+ $table->addIndex(
64
+ $installer->getIdxName(
65
+ 'doofinder_feed/log',
66
+ array(
67
+ 'time',
68
+ ),
69
+ Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX
70
+ ),
71
+ array(
72
+ 'time',
73
+ ),
74
+ array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX)
75
+ );
76
+
77
+ $installer->getConnection()->createTable($table);
78
+ }
79
+ // 1.5
80
+ else
81
+ {
82
+ $installer->run("
83
+
84
+ CREATE TABLE {$installer->getTable('doofinder_feed/log')} (
85
+ `id` int(11) NOT NULL COMMENT 'ID',
86
+ `process_id` varchar(255) NOT NULL COMMENT 'Store Code',
87
+ `type` varchar(255) NOT NULL COMMENT 'Type',
88
+ `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Type',
89
+ `message` text NOT NULL COMMENT 'Message'
90
+ );
91
+
92
+ ALTER TABLE {$installer->getTable('doofinder_feed/log')}
93
+ ADD PRIMARY KEY (`id`), ADD KEY `IDX_DOOFINDER_LOG_PROCESS_ID_TYPE` (`process_id`,`type`), ADD KEY `IDX_DOOFINDER_LOG_TIME` (`time`);
94
+
95
+ ALTER TABLE {$installer->getTable('doofinder_feed/log')}
96
+ MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
97
+
98
+ ");
99
+ }
100
+
101
+ $installer->endSetup();
app/code/community/Doofinder/Feed/sql/doofinder_feed_setup/mysql4-upgrade-1.5.6-1.5.7.php ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+
5
+ $installer->startSetup();
6
+
7
+ // Drop schedule id column in doofinder_feed table
8
+ $installer->getConnection()
9
+ ->dropColumn($installer->getTable('doofinder_feed/cron'), 'schedule_id');
10
+
11
+ // Drop all scheduled doofinder jobs
12
+ $collection = Mage::getModel('cron/schedule')->getCollection()
13
+ ->addFieldToFilter('job_code', Doofinder_Feed_Helper_Data::JOB_CODE);
14
+
15
+ foreach ($collection->getItems() as $item) {
16
+ $item->delete();
17
+ }
18
+
19
+ $installer->endSetup();
app/design/adminhtml/default/default/layout/doofinder.xml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <layout>
2
+ <default>
3
+ <reference name="head">
4
+ <action method="addCss"><name>doofinder/styles.css</name></action>
5
+ </reference>
6
+ <reference name="head">
7
+ <action method="addJs"><name>doofinder/admin.js</name></action>
8
+ </reference>
9
+ </default>
10
+
11
+ <adminhtml_doofinderfeedlog_view>
12
+ <reference name="content">
13
+ <block type="doofinder_feed/adminhtml_log_view" name="doofinder_log_view"></block>
14
+ </reference>
15
+ </adminhtml_doofinderfeedlog_view>
16
+ </layout>
app/design/frontend/base/default/layout/doofinder.xml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <layout>
2
+ <default>
3
+ <reference name="head">
4
+ <block type="doofinder_feed/integration" name="doofinder.integration"></block>
5
+ </reference>
6
+ </default>
7
+ </layout>
app/etc/modules/Doofinder_Feed.xml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <config>
3
+ <modules>
4
+ <Doofinder_Feed>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ </Doofinder_Feed>
8
+ </modules>
9
+ </config>
js/doofinder/admin.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ;(function() {
2
+ 'use strict';
3
+
4
+ /**
5
+ * This file is part of Doofinder_Feed.
6
+ */
7
+
8
+ /**
9
+ * @category Javascript
10
+ * @package Doofinder_Feed
11
+ * @version 1.5.9
12
+ */
13
+
14
+ var add_message = function(message_type, text) {
15
+ var html = '' +
16
+ '<ul class="messages">' +
17
+ ' <li class="' + message_type + '-msg">' + text + '</li>' +
18
+ '</ul>';
19
+ $('messages').insert(html);
20
+ };
21
+
22
+ document.observe("dom:loaded", function() {
23
+ try {
24
+ var $td = $('row_doofinder_cron_schedule_settings_time').select('td.value')[0];
25
+ $td.innerHTML = $td.innerHTML.replace(/&nbsp;:&nbsp;/g, '<span class="df-separator"></span>');
26
+ $td.select('.df-separator')[1].hide();
27
+ $td.select('select')[2].hide();
28
+ } catch (e) {}
29
+
30
+ if ($('doofinder_cron_feed_settings')) {
31
+ var changed = false;
32
+ new Form.Observer('config_edit_form', 0.3, function(form, value) {
33
+ if (changed) return;
34
+ add_message('notice', 'Configuration has changed. The feed generation will be rescheduled after saving.');
35
+ form.insert('<input type="hidden" name="reset" value="1"/>');
36
+ changed = true;
37
+ });
38
+ }
39
+ });
40
+
41
+ })();
lib/Doofinder/doofinder_api.php ADDED
@@ -0,0 +1,804 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Author:: JoeZ99 (<jzarate@gmail.com>). all credit to Gilles Devaux (<gilles.devaux@gmail.com>) (https://github.com/flaptor/indextank-php)
4
+ *
5
+ * License:: Apache License, Version 2.0
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
8
+ * not use this file except in compliance with the License. You may obtain
9
+ * a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
+ * License for the specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+
20
+
21
+ class DoofinderApi{
22
+ /*
23
+ * Basic client for an account.
24
+ * It needs an API url to be constructed.
25
+ * Its only method is to query the doofinder search server
26
+ * Returns a DoofinderResults object
27
+ */
28
+
29
+ const URL_SUFFIX = '-search.doofinder.com';
30
+ const DEFAULT_TIMEOUT = 10000;
31
+ const DEFAULT_RPP = 10;
32
+ const DEFAULT_PARAMS_PREFIX = 'dfParam_';
33
+ const DEFAULT_API_VERSION = '4';
34
+ const VERSION = '5.2.5';
35
+
36
+ private $api_key = null; // user API_KEY
37
+ private $hashid = null; // hashid of the doofinder account
38
+
39
+ private $apiVersion = null;
40
+ private $url = null;
41
+ private $results = null;
42
+ private $query = null;
43
+ private $search_options = array(); // assoc. array with doofinder options to be sent as request parameters
44
+
45
+ private $page = 1; // the page of the search results we're at
46
+ private $queryName = null; // the name of the last successfull query made
47
+ private $lastQuery = null; // the last successfull query made
48
+ private $total = null; // total number of results obtained
49
+ private $maxScore = null;
50
+ private $paramsPrefix = self::DEFAULT_PARAMS_PREFIX;
51
+ private $serializationArray = null;
52
+ private $queryParameter = 'query'; // the parameter used for querying
53
+ private $allowedParameters = array('page', 'rpp', 'timeout', 'types', 'filter', 'query_name', 'transformer'); // request parameters that doofinder handle
54
+
55
+ /**
56
+ * Constructor. account's hashid and api version set here
57
+ *
58
+ * @param string $hashid the account's hashid
59
+ * @param boolean $fromParams if set, the object is unserialized from GET or POST params
60
+ * @param array $init_options. associative array with some options:
61
+ * -'prefix' (default: 'dfParam_')=> the prefix to use when serializing.
62
+ * -'queryParameter' (default: 'query') => the parameter used for querying
63
+ * -'apiVersion' (default: '4')=> the api of the search server to query
64
+ * -'restrictedRequest'(default: $_REQUEST): =>restrict request object
65
+ * to look for params when unserializing. either 'get' or 'post'
66
+ * @throws DoofinderException if $hashid is not a md5 hash or api is no 4, 3.0 or 1.0
67
+ */
68
+ function __construct($hashid, $api_key, $fromParams=false, $init_options = array()){
69
+ $zone_key_array = explode('-', $api_key);
70
+
71
+ if(2 === count($zone_key_array)){
72
+ $this->api_key = $zone_key_array[1];
73
+ $this->zone = $zone_key_array[0];
74
+ $this->url = "https://" . $this->zone . self::URL_SUFFIX;
75
+ } else {
76
+ throw new DoofinderException("API Key is no properly set.");
77
+ }
78
+
79
+ if(array_key_exists('prefix', $init_options)){
80
+ $this->paramsPrefix = $init_options['prefix'];
81
+ }
82
+
83
+
84
+ $this->allowedParameters = array_map(array($this, 'addprefix'), $this->allowedParameters);
85
+
86
+
87
+ if(array_key_exists('queryParameter', $init_options)){
88
+ $this->queryParameter = $init_options['queryParameter'];
89
+ } else {
90
+ $this->queryParameter = $this->paramsPrefix.$this->queryParameter;
91
+ }
92
+
93
+
94
+ $this->apiVersion = array_key_exists('apiVersion', $init_options) ?
95
+ $init_options['apiVersion'] : self::DEFAULT_API_VERSION;
96
+ $this->serializationArray = $_REQUEST;
97
+ if(array_key_exists('restrictedRequest', $init_options))
98
+ {
99
+ switch(strtolower($init_options['restrictedRequest'])){
100
+ case 'get':
101
+ $this->serializationArray = $_GET;
102
+ break;
103
+ case 'post':
104
+ $this->serializationArray = $_POST;
105
+ break;
106
+ }
107
+ }
108
+ $patt = '/^[0-9a-f]{32}$/i';
109
+ if(!preg_match($patt, $hashid))
110
+ {
111
+ throw new DoofinderException("Wrong hashid");
112
+ }
113
+ if(!in_array($this->apiVersion, array('5', '4', '3.0','1.0')))
114
+ {
115
+ throw new DoofinderException('Wrong API');
116
+ }
117
+ $this->hashid = $hashid;
118
+ if($fromParams)
119
+ {
120
+ $this->fromQuerystring();
121
+ }
122
+
123
+ }
124
+
125
+ private function addprefix($value){
126
+ return $this->paramsPrefix.$value;
127
+ }
128
+
129
+ /*
130
+ * translateFilter
131
+ *
132
+ * translates a range filter to the new ES format
133
+ * 'from'=>9, 'to'=>20 to 'gte'=>9, 'lte'=>20
134
+ *
135
+ * @param array $filter
136
+ * @return array the translated filter
137
+ */
138
+ private function translateFilter($filter){
139
+ $new_filter = array();
140
+ foreach($filter as $key => $value){
141
+ if ($key === 'from') {
142
+ $new_filter['gte'] = $value;
143
+ } else if ($key === 'to') {
144
+ $new_filter['lte'] = $value;
145
+ } else {
146
+ $new_filter[$key] = $value;
147
+ }
148
+ }
149
+ return $new_filter;
150
+ }
151
+
152
+ private function reqHeaders(){
153
+ $headers = array();
154
+ $headers[] = 'Expect:'; //Fixes the HTTP/1.1 417 Expectation Failed
155
+ $authHeaderName = $this->apiVersion == '4' ? 'API Token: ' : 'authorization: ';
156
+ $headers[] = $authHeaderName . $this->api_key; //API Authorization
157
+ return $headers;
158
+ }
159
+
160
+ private function apiCall($entry_point='search', $params=array()){
161
+ $params['hashid'] = $this->hashid;
162
+ $args = http_build_query($this->sanitize($params)); // remove any null value from the array
163
+
164
+ $url = $this->url.'/'.$this->apiVersion.'/'.$entry_point.'?'.$args;
165
+
166
+ $session = curl_init($url);
167
+ curl_setopt($session, CURLOPT_CUSTOMREQUEST, 'GET');
168
+ curl_setopt($session, CURLOPT_HEADER, false); // Tell curl not to return headers
169
+ curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Tell curl to return the response
170
+ curl_setopt($session, CURLOPT_HTTPHEADER, $this->reqHeaders()); // Adding request headers
171
+ $response = curl_exec($session);
172
+ $httpCode = curl_getinfo($session, CURLINFO_HTTP_CODE);
173
+ curl_close($session);
174
+
175
+ if (floor($httpCode / 100) == 2) {
176
+ return $response;
177
+ }
178
+ throw new DoofinderException($httpCode.' - '.$response, $httpCode);
179
+ }
180
+
181
+ public function getOptions(){
182
+ return $this->apiCall('options/'.$this->hashid);
183
+ }
184
+
185
+ /**
186
+ * query. makes the query to the doofinder search server.
187
+ * also set several search parameters through it's $options argument
188
+ *
189
+ * @param string $query the search query
190
+ * @param int $page the page number or the results to show
191
+ * @param array $options query options:
192
+ * - 'rpp'=> number of results per page. default 10
193
+ * - 'timeout' => timeout after which the search server drops the conn.
194
+ * defaults to 10 seconds
195
+ * - 'types' => types of index to search at. default: all.
196
+ * - 'filter' => filter to apply. ['color'=>['red','blue'], 'price'=>['from'=>33]]
197
+ * - any other param will be sent as a request parameter
198
+ * @return DoofinderResults results
199
+ */
200
+ public function query($query=null, $page=null, $options = array()){
201
+ if($query){
202
+ $this->search_options['query'] = $query;
203
+ }
204
+ if($page){
205
+ $this->search_options['page'] = (int)$page;
206
+ }
207
+ foreach($options as $optionName => $optionValue){
208
+ $this->search_options[$optionName] = $options[$optionName];
209
+ }
210
+
211
+ $params = $this->search_options;
212
+
213
+ // translate filters
214
+ if(!empty($params['filter']))
215
+ {
216
+ foreach($params['filter'] as $filterName => $filterValue){
217
+ $params['filter'][$filterName] = $this->translateFilter($filterValue);
218
+ }
219
+ }
220
+
221
+ // no query? then match all documents
222
+ if(!$this->optionExists('query') || !trim($this->search_options['query'])){
223
+ $params['query_name'] = 'match_all';
224
+ }
225
+
226
+ // if filters without query_name, pre-query first to obtain it.
227
+ if (empty($params['query_name']) && !empty($params['filter']))
228
+ {
229
+ $filter = $params['filter'];
230
+ unset($params['filter']);
231
+ $dfResults = new DoofinderResults($this->apiCall('search', $params));
232
+ $params['query_name'] = $dfResults->getProperty('query_name');
233
+ $params['filter'] = $filter;
234
+ }
235
+ $dfResults = new DoofinderResults($this->apiCall('search', $params));
236
+ $this->page = $dfResults->getProperty('page');
237
+ $this->total = $dfResults->getProperty('total');
238
+ $this->search_options['query'] = $dfResults->getProperty('query');
239
+ $this->maxScore = $dfResults->getProperty('max_score');
240
+ $this->queryName = $dfResults->getProperty('query_name');
241
+ $this->lastQuery = $dfResults->getProperty('query');
242
+
243
+ return $dfResults;
244
+ }
245
+
246
+ /**
247
+ * hasNext
248
+ *
249
+ * @return boolean true if there is another page of results
250
+ */
251
+ public function hasNext(){
252
+ return $this->page*$this->getRpp() < $this->total;
253
+ }
254
+
255
+ /**
256
+ * hasPrev
257
+ *
258
+ * @return true if there is a previous page of results
259
+ */
260
+ public function hasPrev(){
261
+ return ($this->page-1)*$this->getRpp() > 0;
262
+ }
263
+
264
+
265
+ /**
266
+ * getPage
267
+ *
268
+ * obtain the current page number
269
+ * @return int the page number
270
+ */
271
+ public function getPage(){
272
+ return $this->page;
273
+ }
274
+
275
+ /**
276
+ * setFilter
277
+ *
278
+ * set a filter for the query
279
+ * @param string filterName the name of the filter to set
280
+ * @param array filter if simple array, terms filter assumed
281
+ * if 'from', 'to' in keys, range filter assumed
282
+ */
283
+ public function setFilter($filterName, $filter){
284
+ if(!$this->optionExists('filter')){
285
+ $this->search_options['filter'] = array();
286
+ }
287
+ $this->search_options['filter'][$filterName] = $filter;
288
+ }
289
+
290
+ /**
291
+ * getFilter
292
+ *
293
+ * get conditions for certain filter
294
+ * @param string filterName
295
+ * @return array filter conditions: - simple array if terms filter
296
+ * - 'from', 'to' assoc array if range f.
297
+ * @return false if no filter definition found
298
+ */
299
+ public function getFilter($filterName){
300
+ if($this->optionExists('filter') && isset($this->search_options['filter'][$filterName])){
301
+ return $this->filter[$filterName];
302
+ }
303
+ return false;
304
+ }
305
+
306
+ /**
307
+ * getFilters
308
+ *
309
+ * get all filters and their configs
310
+ * @return array assoc array filterName => filterConditions
311
+ */
312
+ public function getFilters(){
313
+ return $this->search_options['filter'];
314
+ }
315
+
316
+
317
+ /**
318
+ * addTerm
319
+ *
320
+ * add a term to a terms filter
321
+ * @param string filterName the filter to add the term to
322
+ * @param string term the term to add
323
+ */
324
+ public function addTerm($filterName, $term){
325
+ if(!$this->optionExists('filter')){
326
+ $this->search_options['filter'] = array($filterName => array());
327
+ }
328
+ if(!isset($this->search_options['filter'][$filterName]))
329
+ {
330
+ $this->filter[$filterName] = array();
331
+ $this->search_options['filter'][$filterName] = array();
332
+ }
333
+ $this->filter[$filterName][] = $term;
334
+ $this->search_options['filter'][$filterName][] = $term;
335
+ }
336
+
337
+ /**
338
+ * removeTerm
339
+ *
340
+ * remove a term from a terms filter
341
+ * @param string filterName the filter to remove the term from
342
+ * @param string term the term to be removed
343
+ */
344
+ public function removeTerm($filterName, $term){
345
+ if($this->optionExists('filter') && isset($this->search_options['filter'][$filterName]) &&
346
+ in_array($term, $this->search_options['filter'][$filterName]))
347
+ {
348
+ function filter_me($value){
349
+ global $term;
350
+ return $value != $term;
351
+ }
352
+ $this->search_options['filter'][$filterName] =
353
+ array_filter($this->search_options['filter'][$filterName], 'filter_me');
354
+ }
355
+ }
356
+
357
+ /**
358
+ * setRange
359
+ *
360
+ * set a range filter
361
+ * @param string filterName the filter to set
362
+ * @param int from the lower bound value. included
363
+ * @param int to the upper bound value. included
364
+ */
365
+ public function setRange($filterName, $from=null, $to=null){
366
+ if(!$this->optionExists('filter')){
367
+ $this->search_options['filter'] = array($filterName=>array());
368
+ }
369
+ if(!isset($this->search_options['filter'][$filterName]))
370
+ {
371
+ $this->search_options['filter'][$filterName] = array();
372
+ }
373
+ if($from)
374
+ {
375
+ $this->search_options['filter'][$filterName]['from'] = $from;
376
+ }
377
+ if($to)
378
+ {
379
+ $this->search_options['filter'][$filterName]['to'] = $from;
380
+ }
381
+ }
382
+
383
+ /**
384
+ * toQuerystring
385
+ *
386
+ * 'serialize' the object's state to querystring params
387
+ * @param int $page the pagenumber. defaults to the current page
388
+ */
389
+ public function toQuerystring($page=null){
390
+
391
+ foreach($this->search_options as $paramName => $paramValue){
392
+ if($paramName == 'query'){
393
+ $toParams[$this->queryParameter] = $paramValue;
394
+ } else {
395
+ $toParams[$this->paramsPrefix.$paramName] = $paramValue;
396
+ }
397
+ }
398
+ if($page){
399
+ $toParams[$this->paramsPrefix.'page'] = $page;
400
+ }
401
+ return http_build_query($toParams);
402
+ }
403
+
404
+ /**
405
+ * fromQuerystring
406
+ *
407
+ * obtain object's state from querystring params
408
+ * @param string $params where to obtain params from:
409
+ * - 'GET' $_GET params (default)
410
+ * - 'POST' $_POST params
411
+ */
412
+ public function fromQuerystring(){
413
+ $doofinderReqParams = array_filter(array_keys($this->serializationArray),
414
+ array($this, 'belongsToDoofinder'));
415
+
416
+ foreach($doofinderReqParams as $dfReqParam){
417
+ if($dfReqParam == $this->queryParameter){
418
+ $keey = 'query';
419
+ } else {
420
+ $keey = substr($dfReqParam, strlen($this->paramsPrefix));
421
+ }
422
+ $this->search_options[$keey] = $this->serializationArray[$dfReqParam];
423
+ }
424
+ }
425
+
426
+ /**
427
+ * sanitize
428
+ *
429
+ * Clean array of keys with empty values
430
+ *
431
+ * @param array $params array to be cleaned
432
+ * @return array array with no empty keys
433
+ */
434
+ private function sanitize($params){
435
+ $result = array();
436
+ foreach($params as $name => $value){
437
+ if (is_array($value)){
438
+ $result[$name] = $this->sanitize($value);
439
+ } else if (trim($value)){
440
+ $result[$name] = $value;
441
+ }
442
+ }
443
+ return $result;
444
+ }
445
+
446
+ /**
447
+ * belongsToDoofinder
448
+ *
449
+ * to know if certain parameter name belongs to doofinder serialization parameters
450
+ *
451
+ * @param string $paramName name of the param
452
+ * @return boolean true or false.
453
+ */
454
+ private function belongsToDoofinder($paramName){
455
+ if($pos = strpos($paramName, '[')){
456
+ $paramName = substr($paramName, 0, $pos);
457
+ }
458
+ return in_array($paramName, $this->allowedParameters) || $paramName == $this->queryParameter;
459
+ }
460
+
461
+ /**
462
+ * optionExists
463
+ *
464
+ * checks whether a search option is defined in $this->search_options
465
+ *
466
+ * @param string $optionName
467
+ * @return boolean
468
+ */
469
+ private function optionExists($optionName){
470
+ return array_key_exists($optionName, $this->search_options);
471
+ }
472
+
473
+ /**
474
+ * nextPage
475
+ *
476
+ * obtain the results for the next page
477
+ * @return DoofinderResults if there are results.
478
+ * @return null otherwise
479
+ */
480
+ public function nextPage(){
481
+ if($this->hasNext())
482
+ {
483
+ return $this->query($this->lastQuery, $this->page+1 );
484
+ }
485
+ return null;
486
+ }
487
+
488
+
489
+ /**
490
+ * prevPage
491
+ *
492
+ * obtain results for the previous page
493
+ * @return DoofinderResults
494
+ * @return null otherwise
495
+ */
496
+ public function prevPage(){
497
+ if($this->hasPrev())
498
+ {
499
+ return $this->query($this->lastQuery, $this->page-1 );
500
+ }
501
+ return null;
502
+ }
503
+
504
+ /**
505
+ * numPages
506
+ *
507
+ * @return integer the number of pages
508
+ */
509
+ public function numPages(){
510
+ return ceil($this->total / $this->getRpp());
511
+ }
512
+
513
+ public function getRpp(){
514
+ $rpp = $this->optionExists('rpp') ? $this->search_options['rpp'] : null;
515
+ $rpp = $rpp ? $rpp: self::DEFAULT_RPP;
516
+ return $rpp;
517
+ }
518
+ /**
519
+ * setApiVersion
520
+ *
521
+ * sets the api version to use.
522
+ * @param string $apiVersion the api version , '1.0' or '3.0' or '4'
523
+ */
524
+ public function setApiVersion($apiVersion){
525
+ $this->apiVersion = $apiVersion;
526
+ }
527
+
528
+ /**
529
+ * setPrefix
530
+ *
531
+ * sets the prefix that will be used for serialization to querystring params
532
+ * @param string $prefix the prefix
533
+ */
534
+ public function setPrefix($prefix){
535
+ $this->paramsPrefix = $prefix;
536
+ }
537
+
538
+ /**
539
+ * setQueryName
540
+ *
541
+ * sets query_name
542
+ * CAUTION: node will complain if this is wrong
543
+ */
544
+ public function setQueryName($queryName){
545
+ $this->queryName = $queryName;
546
+ }
547
+
548
+ /**
549
+ * getFilterType
550
+ * obtain the filter type (i.e. 'terms' or 'numeric range' from its conditions)
551
+ * @param array filter conditions
552
+ * @return string 'terms' or 'numericrange' false otherwise
553
+ */
554
+ private function getFilterType($filter){
555
+ if(!is_array($filter))
556
+ {
557
+ return false;
558
+ }
559
+ if(count(array_intersect(array('from', 'to'), array_keys($filter)))>0)
560
+ {
561
+ return 'numericrange';
562
+ }
563
+ return 'terms';
564
+ }
565
+
566
+
567
+ }
568
+
569
+ /**
570
+ * @author JoeZ99 <jzarate@gmail.com>
571
+ *
572
+ * DoofinderResults
573
+ *
574
+ * Very thin wrapper of the results obtained from the doofinder server
575
+ * it holds to accessor:
576
+ * - getProperty : get single property of the search results (rpp, page, etc....)
577
+ * - getResults: get an array with the results
578
+ */
579
+ class DoofinderResults{
580
+
581
+ // doofinder status
582
+ const SUCCESS = 'success'; // everything ok
583
+ const NOTFOUND = 'notfound'; // no account with the provided hashid found
584
+ const EXHAUSTED = 'exhausted'; // the account has reached its query limit
585
+
586
+ private $properties = null;
587
+ private $results = null;
588
+ private $facets = null;
589
+ private $filter = null;
590
+ public $status = null;
591
+
592
+ /**
593
+ * Constructor
594
+ *
595
+ * @param string $jsonString stringified json returned by doofinder search server
596
+ */
597
+ function __construct($jsonString){
598
+ $rawResults = json_decode($jsonString, true);
599
+ foreach($rawResults as $kkey => $vall){
600
+ if(!is_array($vall)){
601
+ $this->properties[$kkey] = $vall;
602
+ }
603
+ }
604
+ // doofinder status
605
+ $this->status = isset($this->properties['doofinder_status'])?
606
+ $this->properties['doofinder_status'] : self::SUCCESS;
607
+
608
+ // results
609
+ $this->results = array();
610
+
611
+ if(isset($rawResults['results']) && is_array($rawResults['results']))
612
+ {
613
+ $this->results = $rawResults['results'];
614
+ }
615
+
616
+ // build a friendly filter array
617
+ $this->filter = array();
618
+ // reorder filter, before assigning it to $this
619
+ if(isset($rawResults['filter']))
620
+ {
621
+ foreach($rawResults['filter'] as $filterType => $filters)
622
+ {
623
+ foreach($filters as $filterName => $filterProperties)
624
+ {
625
+ $this->filter[$filterName] = $filterProperties;
626
+ }
627
+ }
628
+ }
629
+
630
+ // facets
631
+ $this->facets = array();
632
+ if(isset($rawResults['facets']))
633
+ {
634
+ $this->facets = $rawResults['facets'];
635
+
636
+ // mark "selected" true or false according to filters presence
637
+ foreach($this->facets as $facetName => $facetProperties){
638
+ switch($facetProperties['_type']){
639
+ case 'terms':
640
+ foreach($facetProperties['terms'] as $pos => $term){
641
+ if(isset($this->filter[$facetName]) && in_array($term['term'], $this->filter[$facetName])){
642
+ $this->facets[$facetName]['terms'][$pos]['selected'] = true;
643
+ } else {
644
+ $this->facets[$facetName]['terms'][$pos]['selected'] = false;
645
+ }
646
+ }
647
+ break;
648
+ case 'range':
649
+ foreach($facetProperties['ranges'] as $pos => $range){
650
+ $this->facets[$facetName]['ranges'][$pos]['selected_from'] = false;
651
+ $this->facets[$facetName]['ranges'][$pos]['selected_to'] = false;
652
+ if(isset($this->filter[$facetName]) && isset($this->filter[$facetName]['gte'])){
653
+ $this->facets[$facetName]['ranges'][$pos]['selected_from'] = $this->filter[$facetName]['gte'];
654
+ }
655
+ if(isset($this->filter[$facetName]) && isset($this->filter[$facetName]['lte'])){
656
+ $this->facets[$facetName]['ranges'][$pos]['selected_to'] = $this->filter[$facetName]['lte'];
657
+ }
658
+
659
+ }
660
+ break;
661
+ }
662
+ }
663
+ }
664
+ }
665
+
666
+ /**
667
+ * getProperty
668
+ *
669
+ * get single property from the results
670
+ * @param string @propertyName: 'results_per_page', 'query', 'max_score', 'page', 'total', 'hashid'
671
+ * @return mixed the value of the property
672
+ */
673
+ public function getProperty($propertyName){
674
+ return array_key_exists($propertyName, $this->properties) ?
675
+ $this->properties[$propertyName]: null;
676
+ }
677
+
678
+ /**
679
+ * getResults
680
+ *
681
+ * @return array search results. at the moment, only the 'cooked' version.
682
+ * Each result is of the form:
683
+ * array('header'=>...,
684
+ * 'body' => ..,
685
+ * 'price' => ..,
686
+ * 'href' => ...,
687
+ * 'image' => ...,
688
+ * 'type' => ...,
689
+ * 'id' => ..)
690
+ */
691
+ public function getResults(){
692
+ return $this->results;
693
+ }
694
+
695
+ /**
696
+ *
697
+ * getFacetsNames
698
+ *
699
+ * @return array facets names.
700
+ */
701
+ public function getFacetsNames(){
702
+ return array_keys($this->facets);
703
+ }
704
+
705
+ /**
706
+ * getFacet
707
+ *
708
+ * @param string name the facet name whose results are wanted
709
+ *
710
+ * @return array facet search data
711
+ * - for terms facets
712
+ * array(
713
+ * '_type'=> 'terms', // type of facet 'terms' or 'range'
714
+ * 'missing'=> 3, // # of elements with no value for this facet
715
+ * 'others'=> 2, // # of terms not present in the search response
716
+ * 'total'=> 6, // # number of possible terms for this facet
717
+ * 'terms'=> array(
718
+ * array('count'=>6, 'term'=>'Blue', 'selected'=>false), // in the response, there are 6 'blue' terms
719
+ * array('count'=>3, 'term': 'Red', 'selected'=>true), // if 'selected'=>true, that term has been selected as filter
720
+ * ...
721
+ * )
722
+ * )
723
+ * - for range facets
724
+ * array(
725
+ * '_type'=> 'range',
726
+ * 'ranges'=> array(
727
+ * array(
728
+ * 'count'=>6, // in the response, 6 elements within that range.
729
+ * 'from':0,
730
+ * 'min': 30
731
+ * 'max': 90,
732
+ * 'mean'=>33.2,
733
+ * 'total'=>432,
734
+ * 'total_count'=>6,
735
+ * 'selected_from'=> 34.3 // if present. this value has been used as filter. false otherwise
736
+ * 'selected_to'=> 99.3 // if present. this value has been used as filter. false otherwise
737
+ * ),
738
+ * ...
739
+ * )
740
+ * )
741
+ *
742
+ *
743
+ */
744
+ public function getFacet($facetName){
745
+ return $this->facets[$facetName];
746
+ }
747
+
748
+ /**
749
+ * getFacets
750
+ *
751
+ * get the whole facets associative array:
752
+ * array('color'=>array(...), 'brand'=>array(...))
753
+ * each array is defined as in getFacet() docstring
754
+ *
755
+ * @return array facets assoc. array
756
+ */
757
+ public function getFacets(){
758
+ return $this->facets;
759
+ }
760
+
761
+ /**
762
+ * getAppliedFilters
763
+ *
764
+ * get the filters the query has defined
765
+ * array('categories' => array( // filter name . same as facet name
766
+ * 'Sillas de paseo', // if simple array, it's a terms facet
767
+ * 'Sacos sillas de paseo'
768
+ * ),
769
+ * 'color' => array(
770
+ * 'red',
771
+ * 'blue'
772
+ * ),
773
+ * 'price' => array(
774
+ * 'include_upper'=>true, // if 'from' , 'to' keys, it's a range facet
775
+ * 'from'=>35.19,
776
+ * 'to'=>9999
777
+ * )
778
+ * )
779
+ * MEANING OF THE EXAMPLE FILTER:
780
+ * "FROM the query results, filter only results that have ('Sillas de paseo' OR 'Sacos sillas de paseo') categories
781
+ * AND ('red' OR 'blue') color AND price is BETWEEN 34.3 and 99.3"
782
+
783
+ */
784
+ public function getAppliedFilters(){
785
+ return $this->filter;
786
+ }
787
+
788
+ /**
789
+ * isOk
790
+ *
791
+ * checks if all went well
792
+ * @return boolean true if the status is 'success'.
793
+ * false if the status is not.
794
+ */
795
+ public function isOk(){
796
+ return $this->status == self::SUCCESS;
797
+ }
798
+ }
799
+
800
+
801
+ class DoofinderException extends Exception{
802
+
803
+ }
804
+
lib/Doofinder/doofinder_management_api.php ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Author:: JoeZ99 (<jzarate@gmail.com>).
4
+ *
5
+ * License:: Apache License, Version 2.0
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
8
+ * not use this file except in compliance with the License. You may obtain
9
+ * a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
+ * License for the specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ require_once dirname(__FILE__).'/errors.php';
20
+
21
+
22
+ /**
23
+ * Class to manage the connection with the API servers.
24
+ *
25
+ * Needs APIKEY in initialization.
26
+ * Example: $dma = new DoofinderManagementApi('eu1-d531af87f10969f90792a4296e2784b089b8a875')
27
+ */
28
+ class DoofinderManagementApi
29
+ {
30
+ const REMOTE_API_ENDPOINT = "https://%s-api.doofinder.com/v1";
31
+ const LOCAL_API_ENDPOINT = "http://localhost:8000/api/v1";
32
+
33
+ private $apiKey = null;
34
+ private $clusterRegion = 'eu1';
35
+ private $token = null;
36
+ private $baseManagementUrl = null;
37
+
38
+ function __construct($apiKey, $local = false){
39
+ $this->apiKey = $apiKey;
40
+ $clusterToken = explode('-', $apiKey);
41
+ $this->clusterRegion = $clusterToken[0];
42
+ $this->token = $clusterToken[1];
43
+ if ($local === true){
44
+ $this->baseManagementUrl = self::LOCAL_API_ENDPOINT;
45
+ } else {
46
+ $this->baseManagementUrl = sprintf(self::REMOTE_API_ENDPOINT, $this->clusterRegion);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Makes the actual request to the API server and normalize response
52
+ *
53
+ * @param string $method The HTTP method to use. 'GET|PUT|POST|DELETE'
54
+ * @param string $entryPoint The path to use. '/<hashid>/items/product'
55
+ * @param array $params If any, url request parameters
56
+ * @param array $data If any, body request parameters
57
+ * @return array Array with both status code and response .
58
+ */
59
+ function managementApiCall($method='GET', $entryPoint='', $params=null, $data=null){
60
+ $headers = array('Authorization: Token '.$this->token, // for Auth
61
+ 'Content-Type: application/json',
62
+ 'Expect:'); // Fixes the HTTP/1.1 417 Expectation Failed
63
+
64
+ $url = $this->baseManagementUrl.'/'.$entryPoint;
65
+ if (is_array($params) && sizeof($params) > 0){
66
+ $url .= '?'.http_build_query($params);
67
+ }
68
+
69
+ $session = curl_init($url);
70
+ curl_setopt($session, CURLOPT_CUSTOMREQUEST, $method);
71
+ curl_setopt($session, CURLOPT_HEADER, false);
72
+ curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Tell curl to return the response
73
+ curl_setopt($session, CURLOPT_HTTPHEADER, $headers); // Adding request headers
74
+
75
+ if (in_array($method, array('POST', 'PUT'))){
76
+ curl_setopt($session, CURLOPT_POSTFIELDS, $data);
77
+ }
78
+
79
+ $response = curl_exec($session);
80
+ $httpCode = curl_getinfo($session, CURLINFO_HTTP_CODE);
81
+ curl_close($session);
82
+
83
+ handleErrors($httpCode, $response);
84
+
85
+ $return = array('statusCode' => $httpCode);
86
+ $return['response'] = ($decoded = json_decode($response, true)) ? $decoded : $response;
87
+
88
+ return $return;
89
+ }
90
+
91
+ /**
92
+ * To get info on all possible api entry points
93
+ * @return array An assoc. array with the different entry points
94
+ */
95
+ function getApiRoot(){
96
+ $response = $this->managementApiCall();
97
+ return $response['response'];
98
+ }
99
+
100
+ /**
101
+ * Obtain a list of SearchEngines objects, ready to interact with the API
102
+ * @return array list of searchEngines objects
103
+ */
104
+ function getSearchEngines(){
105
+ $searchEngines = array();
106
+ $apiRoot = $this->getApiRoot();
107
+ unset($apiRoot['searchengines']);
108
+
109
+ foreach($apiRoot as $hashid => $props){
110
+ $searchEngines[] = new SearchEngine($this, $hashid, $props['name']);
111
+ }
112
+
113
+ return $searchEngines;
114
+ }
115
+
116
+ }
117
+
118
+ /**
119
+ * Class with all the capabilities described in the API
120
+ *
121
+ * see http://www.doofinder.com/developer/topics/api/management-api
122
+ */
123
+ class SearchEngine {
124
+ public $name = null;
125
+ public $hashid = null;
126
+
127
+ private $dma = null; // DoofinderManagementApi instance
128
+
129
+ function __construct($dma, $hashid, $name){
130
+ $this->name = $name;
131
+ $this->hashid = $hashid;
132
+ $this->dma = $dma;
133
+ }
134
+
135
+ /**
136
+ * Get a list of searchengine's types
137
+ *
138
+ * @return array list of types
139
+ */
140
+ function getDatatypes(){
141
+ return $this->getTypes();
142
+ }
143
+
144
+ /**
145
+ * Get a list of searchengine's types
146
+ *
147
+ * @return array list of types
148
+ */
149
+ function getTypes(){
150
+ $result = $this->dma->managementApiCall('GET', "{$this->hashid}/types");
151
+ return $result['response'];
152
+ }
153
+
154
+ /**
155
+ * Add a type to the searchengine
156
+ *
157
+ * @param string $dType the type name
158
+ * @return new list of searchengine's types
159
+ */
160
+ function addType($dType){
161
+ $result = $this->dma->managementApiCall('POST', "{$this->hashid}/types", null,
162
+ json_encode(array('name' => $dType)));
163
+ return $result['response'];
164
+ }
165
+
166
+ /**
167
+ * Delete a type and all its items. HANDLE WITH CARE
168
+ *
169
+ * @param string $dType the Type to delete. All items belonging
170
+ * to that type will be removed. mandatory
171
+ * @return boolean true on success
172
+ */
173
+ function deleteType($dType){
174
+ $result = $this->dma->managementApiCall('DELETE', "{$this->hashid}/types/{$dType}");
175
+ return $result['statusCode'] == 204;
176
+ }
177
+
178
+ /**
179
+ * Get paginated indexed items belonging to a searchengine's type
180
+ *
181
+ * It only paginates forward. Can't go backwards
182
+ * @param string $dType Type of the items to list
183
+ * @param string $scrollId identifier of the pagination set
184
+ * @return array Assoc array with scroll_id ,paginated results and total results.
185
+ */
186
+ public function getScrolledItemsPage($dType, $scrollId = null){
187
+ $params = $scrollId ? array("scroll_id" => $scrollId) : null;
188
+ $result = $this->dma->managementApiCall('GET', "{$this->hashid}/items/{$dType}", $params);
189
+ return array(
190
+ 'scroll_id' => $result['response']['scroll_id'],
191
+ 'results' => $result['response']['results'],
192
+ 'total' => $result['response']['count'],
193
+ );
194
+ }
195
+
196
+ function items($dType){
197
+ return new ItemsRS($this, $dType);
198
+ }
199
+
200
+ /**
201
+ * Get details of a specific item
202
+ *
203
+ * @param string $dType Type of the item.
204
+ * @param string $itemId the id of the item
205
+ * @return array Assoc array representing the item.
206
+ */
207
+ function getItem($dType, $itemId){
208
+ $result = $this->dma->managementApiCall('GET', "{$this->hashid}/items/{$dType}/{$itemId}");
209
+ return $result['response'];
210
+ }
211
+
212
+ /**
213
+ * Add an item to the search engine
214
+ *
215
+ * - If the 'id' field is present, use that as item's id or overwrite an existing
216
+ * item with that id.
217
+ * - It the 'id' field is not present, create one.
218
+ *
219
+ * @param string $dType type of the. If not provided, first available type is used
220
+ * @param array $itemDescription Assoc array representation of the item
221
+ * @return string the id of the item just created
222
+ */
223
+ function addItem($dType, $itemDescription){
224
+ $result = $this->dma->managementApiCall('POST', "{$this->hashid}/items/{$dType}", null,
225
+ json_encode($itemDescription));
226
+ return $result['response']['id'];
227
+ }
228
+
229
+ /**
230
+ * Add items in bulk to the search engine
231
+ *
232
+ * For each item:
233
+ * - If the 'id' field is present, use that as item's id or overwrite an existing
234
+ * item with that id.
235
+ * - It the 'id' field is not present, create one.
236
+ *
237
+ * @param string $dType type of the. If not provided, first available type is used
238
+ * @param array $itemsDescription List of Assoc array representation of the item
239
+ * @return array List of ids of the added items
240
+ */
241
+ function addItems($dType, $itemsDescription){
242
+ $result = $this->dma->managementApiCall('POST', "{$this->hashid}/items/{$dType}", null,
243
+ json_encode($itemsDescription));
244
+
245
+ function fetchId($item){
246
+ return $item['id'];
247
+ };
248
+
249
+ return array_map('fetchId', $result['response']);
250
+ }
251
+
252
+ /**
253
+ * Update or create an item of the search engine
254
+ *
255
+ * In case of conflict between itemDescription's id or $itemId,
256
+ * the latter is used.
257
+ *
258
+ * @param string $dType type of the Item.
259
+ * @param string $itemId Id of the item to be updated/added
260
+ * @param array $itemDescription Assoc array representating the item.
261
+ * @return boolean true on success.
262
+ */
263
+ function updateItem($dType, $itemId, $itemDescription){
264
+ $result = $this->dma->managementApiCall('PUT', "{$this->hashid}/items/{$dType}/{$itemId}", null,
265
+ json_encode($itemDescription));
266
+ return $result['statusCode'] == 200;
267
+ }
268
+
269
+ /**
270
+ * Bulk update of several items
271
+ *
272
+ * Each item description must contain the 'id' field
273
+ *
274
+ * @param string $dType type of the items.
275
+ * @param array $itemsDescription List of assoc array representing items
276
+ * @return boolean true on success
277
+ */
278
+ function updateItems($dType, $itemsDescription){
279
+ $result = $this->dma->managementApiCall('PUT', "{$this->hashid}/items/{$dType}", null,
280
+ json_encode($itemsDescription));
281
+ return $result['statusCode'] == 200;
282
+ }
283
+
284
+ /**
285
+ * Delete an item
286
+ *
287
+ * @param string $dType type of the item
288
+ * @param string $itemId id of the item
289
+ * @return boolean true if success, false if failure
290
+ */
291
+ function deleteItem($dType, $itemId){
292
+ $result = $this->dma->managementApiCall('DELETE', "{$this->hashid}/items/{$dType}/{$itemId}");
293
+ return $result['statusCode'] == 204 ;
294
+ }
295
+
296
+ /**
297
+ * Ask the server to process the search engine's feeds
298
+ *
299
+ * @return array Assoc array with:
300
+ * - 'task_created': boolean true if a new task has been created
301
+ * - 'task_id': if task created, the id of the task.
302
+ */
303
+ function process(){
304
+ $result = $this->dma->managementApiCall('POST', "{$this->hashid}/tasks/process");
305
+ $taskCreated = ($result['statusCode'] == 201);
306
+ $taskId = $taskCreated ? obtainId($result['response']['link']) : null;
307
+ return array('task_created'=>$taskCreated, 'task_id' => $taskId);
308
+ }
309
+
310
+ /**
311
+ * Obtain info of the last processing task sent to the server
312
+ *
313
+ * @return array Assoc array with 'state' and 'message' indicating status of the
314
+ * last asked processing task
315
+ */
316
+ function processInfo(){
317
+ $result = $this->dma->managementApiCall('GET', "{$this->hashid}/tasks/process");
318
+ unset($result['response']['task_name']);
319
+ return $result['response'];
320
+ }
321
+
322
+ /**
323
+ * Obtain info about how a task is going or its result
324
+ *
325
+ * @return array Assoc array with 'state' and 'message' indicating the status
326
+ * of the task
327
+ */
328
+ function taskInfo($taskId){
329
+ $result = $this->dma->managementApiCall('GET', "{$this->hashid}/tasks/{$taskId}");
330
+ unset($result['response']['task_name']);
331
+ return $result['response'];
332
+ }
333
+
334
+ /**
335
+ * Obtain logs of the latest feed processing tasks done
336
+ *
337
+ * @return array list of arrays representing the logs
338
+ */
339
+ function logs(){
340
+ $result = $this->dma->managementApiCall("GET", "{$this->hashid}/logs");
341
+ return $result['response'];
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Helper class to iterate through the search engine's items
347
+ *
348
+ * Implemets Iterator interface so foreach() can work with ItemRS
349
+ */
350
+ class ItemsRS implements Iterator {
351
+
352
+ private $searchEngine = null;
353
+ private $resultsPage = null;
354
+ private $scrollId = null;
355
+ private $position = 0;
356
+ private $total = null;
357
+
358
+ function __construct($searchEngine, $dType){
359
+ $this->dType = $dType;
360
+ $this->searchEngine = $searchEngine;
361
+ }
362
+
363
+ private function fetchResults(){
364
+ $apiResults = $this->searchEngine->getScrolledItemsPage($this->dType, $this->scrollId);
365
+ $this->total = $apiResults['total'];
366
+ $this->resultsPage = $apiResults['results'];
367
+ $this->scrollId = $apiResults['scroll_id'];
368
+ $this->currentItem = each($this->resultsPage);
369
+ }
370
+
371
+ function rewind(){
372
+ $this->scrollId = null;
373
+ $this->fetchResults();
374
+ }
375
+
376
+ function valid(){
377
+ return $this->position < $this->total;
378
+ }
379
+
380
+ function current(){
381
+ return $this->currentItem['value'];
382
+ }
383
+
384
+ function key(){
385
+ return $this->position;
386
+ }
387
+
388
+ function next(){
389
+ ++$this->position;
390
+ $this->currentItem = each($this->resultsPage);
391
+ if (!$this->currentItem && $this->position < $this->total){
392
+ $this->fetchResults();
393
+ reset($this->resultsPage);
394
+ $this->currentItem = each($this->resultsPage);
395
+ }
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Extracts identificator from an item or task url.
401
+ *
402
+ * @param string $url item or task resource locator
403
+ * @return the item identificator
404
+ */
405
+ function obtainId($url){
406
+ preg_match('~/\w{32}/(items/\w+|tasks)/([\w-_]+)/?$~', $url, $matches);
407
+ return $matches[2];
408
+ }
lib/Doofinder/errors.php ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class NotAllowed extends Exception {}
4
+
5
+ class BadRequest extends Exception {}
6
+
7
+ class QuotaExhausted extends Exception {}
8
+
9
+ class WrongResponse extends Exception {}
10
+
11
+ class ThrottledResponse extends Exception {}
12
+
13
+ function readError($response) {
14
+ $error = json_decode($response, true);
15
+ $error = $error['detail'];
16
+ if (!isset($error)) {
17
+ $error = $response;
18
+ }
19
+ return $error;
20
+ }
21
+
22
+ function handleErrors($statusCode, $response){
23
+ switch($statusCode)
24
+ {
25
+ case 403:
26
+ throw new NotAllowed("The user does not have permissions to perform this operation: ".readError($response));
27
+ case 401:
28
+ throw new NotAllowed("The user hasn't provided valid authorization: ".readError($response));
29
+ case 404:
30
+ throw new BadRequest("Not Found: ".readError($response));
31
+ case 409: // trying to post with an already used id
32
+ throw new BadRequest("Request conflict: ".readError($response));
33
+ case 429:
34
+ if (stripos($response, 'throttled')) {
35
+ throw new ThrottledResponse(readError($response));
36
+ } else {
37
+ throw new QuotaExhausted("The query quota has been reached. No more queries can be requested right now");
38
+ }
39
+ }
40
+
41
+ if ($statusCode >= 500) {
42
+ throw new WrongResponse("Server error: ".readError($response));
43
+ }
44
+
45
+ if ($statusCode >= 400) {
46
+ throw new BadRequest("The client made a bad request: ".readError($response));
47
+ }
48
+ }
package.xml ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <package>
3
+ <name>Doofinder_Feed</name>
4
+ <version>1.6.5</version>
5
+ <stability>stable</stability>
6
+ <license uri="http://opensource.org/licenses/osl-3.0.php">Open Software License (OSL 3.0)</license>
7
+ <channel>community</channel>
8
+ <extends/>
9
+ <summary>This plugin allows you to integrate Doofinder in your store with almost no effort.</summary>
10
+ <description>Doofinder is the best on-site search engine service for Magento. This free extension allows you to populate the data feed file required to use the Doofinder search service for each store view in a URL (the data feed displays only public information of your products).&#xD;
11
+ &#xD;
12
+ # Why use Doofinder?&#xD;
13
+ &#xD;
14
+ Doofinder provides fast and accurate results based on your website content. Accurate and relevant results appear in your search box at an incredible speed as the user types. With Doofinder you are confident that your visitors are finding what they are looking for regardless of the number of products in your site. These are some advantages of using Doofinder in your site:&#xD;
15
+ &#xD;
16
+ - Instant, relevant results.&#xD;
17
+ - Tolerant of misspellings.&#xD;
18
+ - Search filters.&#xD;
19
+ - Increases the conversion rates.&#xD;
20
+ - No technical knowledge are required.&#xD;
21
+ - Allows the use of labels and synonyms.&#xD;
22
+ - Installs in minutes.&#xD;
23
+ - Provides statistical information.&#xD;
24
+ - Doofinder brings back the control over the searches in your site to you.&#xD;
25
+ &#xD;
26
+ A good search engine should provide relevant results. It is exactly what Doofinder does.&#xD;
27
+ &#xD;
28
+ When the user starts typing in the search box, Doofinder displays the best results for her search. If the user makes a typo, our algorithms detect it and will perform the search as if the term is correctly typed.&#xD;
29
+ &#xD;
30
+ Furthermore, Doofinder sorts the results displaying the most relevant first.&#xD;
31
+ &#xD;
32
+ # Relevant Statistics&#xD;
33
+ &#xD;
34
+ The Doofinder's backend also offers easy to understand statistics. They provide useful information about the usage, results and search trends of your users in the last 90 days.&#xD;
35
+ &#xD;
36
+ With the knowledge of what are the most used terms by your users, you can provide guided results for their queries. You can link the results with your Google Analytics profile to better evaluate and improve the contents of your website.&#xD;
37
+ &#xD;
38
+ # SaaS&#xD;
39
+ &#xD;
40
+ With Doofinder you will benefit permanently of the service updates and improvements with no additional cost. All our plans provide full technical support.&#xD;
41
+ &#xD;
42
+ If you have any question, please, contact us. Our technical support team will be happy to assist you.&#xD;
43
+ &#xD;
44
+ # More information&#xD;
45
+ &#xD;
46
+ Doofinder is fast and innovative. With no doubt, it is the best search engine for Magento. The Doofinder service is not free but it has a 30 day trial period.&#xD;
47
+ &#xD;
48
+ You can get more info and create your account visiting the Doofinder site:&#xD;
49
+ &#xD;
50
+ http://www.doofinder.com</description>
51
+ <notes>Correctly handle custom boost fields.</notes>
52
+ <authors><author><name>Carlos Escribano Rey</name><user>doofinder</user><email>carlos@doofinder.com</email></author></authors>
53
+ <date>2016-10-24</date>
54
+ <time>17:01:35</time>
55
+ <contents><target name="magecommunity"><dir name="Doofinder"><dir name="Feed"><dir name="Block"><dir name="Adminhtml"><dir name="Log"><file name="View.php" hash="a4208a50820e6b9df016314c1d4f07dc"/></dir><dir name="Map"><file name="Additional.php" hash="4b45c8bc6303097aeadc35c6d5ef81da"/></dir></dir><file name="Integration.php" hash="b42a26dce05e7b5ce6a1193b2920cc9d"/><dir name="Settings"><dir name="Buttons"><file name="Generate.php" hash="93dcc1911ca37d7f8937a3f70c4379be"/><file name="ViewLog.php" hash="ee27b184bba43f68c71f9b0b40a65b13"/></dir><dir name="Panel"><file name="Cron.php" hash="e93e0471544eef8d6cc1ea5c2a8037dd"/><file name="Crondescription.php" hash="cd421bc35644aa18df853c46e46d18fa"/><file name="Datetime.php" hash="fd91a4f4e0506ab83252eac5a3d5d502"/><file name="Description.php" hash="f0fd46fb11b5ccdbec98e68344aca3dc"/><file name="File.php" hash="addbf646a91200e9906d38fe60f51c9b"/><file name="Hashdescription.php" hash="707aa2f0eff7a1e170e6355787e09e84"/><file name="Layerdescription.php" hash="0d344a2bad97f047733fea1ad03ea214"/><file name="Message.php" hash="8026858e966d0cddd40b2b72a1bc5ab2"/></dir></dir></dir><dir name="Helper"><file name="Data.php" hash="7ff426aac405f3c183ca0e838040ddc6"/><file name="Log.php" hash="08c6835c855a9947867113b8128643a4"/><file name="Search.php" hash="507d1fd5410cd57d4ac3437b9aff1d2f"/><file name="Tax.php" hash="b0ad35c35d1aad2cca660b573a511f96"/></dir><dir name="Model"><dir name="Adminhtml"><dir name="System"><dir name="Config"><dir name="Backend"><file name="Cron.php" hash="e0c343224d9fdd3795d1d0a2b8e79da0"/></dir><dir name="Validation"><file name="Hashid.php" hash="5c84ddebe10442c48bc8bca05a9253d1"/></dir></dir></dir></dir><dir name="CatalogSearch"><dir name="Resource"><file name="Fulltext.php" hash="0e1394fcfbefb5d84d779fedc54d07f4"/></dir></dir><file name="Config.php" hash="0841738770f2981fbf8365822911d01b"/><file name="Cron.php" hash="2592919990941bcfcd51d1b82d710674"/><file name="Generator.php" hash="e244f1429862478aa2cef79f561e169e"/><file name="Log.php" hash="7119a7c51c0d60bdc4ce80ccee0fab56"/><dir name="Map"><dir name="Product"><file name="Abstract.php" hash="632a7d1656110384ad61433115826afd"/><file name="Associated.php" hash="788e01dda6cca448d4070cde193cf563"/><file name="Bundle.php" hash="372f233378744df9bf3b85c9c2280ded"/><file name="Configurable.php" hash="1539003b9da916420dbf59d35b048da6"/><file name="Downloadable.php" hash="7ae25026887f745c62e2649396456a2e"/><file name="Grouped.php" hash="aecf53b4541120404c14c3fb24ac4057"/><file name="Simple.php" hash="79ba6184149a5ed017bbf49b056d5ea4"/><file name="Virtual.php" hash="dfe0aefd4b4f6bd602bea798493491b9"/></dir></dir><dir name="Mysql4"><dir name="Cron"><file name="Collection.php" hash="e5ff5b9962152cb57eb195cc1d5e87ca"/></dir><file name="Cron.php" hash="a870fe6235fe359cf91993727e92dc67"/><dir name="Log"><file name="Collection.php" hash="85cd1039b5b14d7afc94ce0844859293"/></dir><file name="Log.php" hash="54a36a682eeffec64ea1ffe7b2e60c94"/></dir><dir name="Observers"><file name="Feed.php" hash="2aaf718ef2923ffb602a52a07440a642"/><file name="Logs.php" hash="c216bc6a29c5de41930e9614f7be22e7"/><file name="Schedule.php" hash="cbf364a83a577f871f77a838f1e3c471"/></dir><dir name="Resource"><dir name="Mysql4"><file name="Setup.php" hash="1d9cd49949d4ea44796b5df731af4d24"/></dir></dir><dir name="System"><dir name="Config"><dir name="Backend"><dir name="Map"><file name="Additional.php" hash="a4e383b9895eb7d121be8f4b5ea4e532"/></dir><dir name="Total"><file name="Limit.php" hash="a23092ea72cbe81e2779b086ad055bf6"/></dir></dir><file name="Reset.php" hash="1124da63d309e1b46aead687925b8c66"/><dir name="Source"><dir name="Product"><file name="Attributes.php" hash="bea37e6b9857dd0b8dbe7bb9c621b902"/></dir></dir></dir></dir><file name="Tools.php" hash="a13cd01a07c953f427d08f3e8f194cba"/></dir><dir name="Test"><dir name="Controller"><dir name="Index"><dir name="fixtures"><file name="testConfig.yaml" hash="0a1f21a3417389e0c0a13392c79a7a89"/><file name="testFeed.yaml" hash="694cf25a35a9a301a8ae678866937909"/><file name="testIndex.yaml" hash="0a1f21a3417389e0c0a13392c79a7a89"/></dir><dir name="providers"><file name="testConfig.yaml" hash="0a1f21a3417389e0c0a13392c79a7a89"/><file name="testFeed.yaml" hash="1ea2f638be8fdcea22ef47767ed8d7db"/><file name="testIndex.yaml" hash="0a1f21a3417389e0c0a13392c79a7a89"/></dir></dir><file name="Index.php" hash="2771de706303653d039818bd0f6590ea"/></dir><dir name="Model"><dir name="Product"><dir name="expectations"><file name="testGenerator.yaml" hash="232dda1f4fd88b8ef081393f08044731"/></dir><dir name="fixtures"><file name="testGenerator.yaml" hash="df25e3ca67fd98ab1b933c4951c599ef"/></dir><dir name="providers"><file name="testGenerator.yaml" hash="84779d5dcd8d92abdecf0cd5ee65cfb0"/></dir></dir><file name="Product.php" hash="6c45ae2b36c6cc721ef634855ed6d596"/></dir></dir><dir name="controllers"><file name="DoofinderFeedFeedController.php" hash="0e676cb464d4643befdbf0520a3ba08c"/><file name="DoofinderFeedLogController.php" hash="e31c02e2ebb2a5521a80fc952111fd42"/><file name="FeedController.php" hash="2fdc3fef31e28beeed37da732618f482"/><file name="IndexController.php" hash="d89e485848ddaf040c149898a60253f0"/></dir><dir name="etc"><file name="config.xml" hash="940d767a73891fceed5d2de577aa5608"/><file name="system.xml" hash="4c69b26ff91eedb582883e83a26b843d"/></dir><dir name="sql"><dir name="doofinder_feed_setup"><file name="mysql4-install-1.5.4.php" hash="9dc5ed4e10febbe75ab1911259a1c9fe"/><file name="mysql4-install-1.5.7.php" hash="85baa03d9c4d76f6b744ba107c21f8da"/><file name="mysql4-upgrade-1.5.4-1.5.5.php" hash="df7158f6d6cdded9bdfc5cb72c1dc8e3"/><file name="mysql4-upgrade-1.5.5-1.5.6.php" hash="0f3ca5263356a0bc83d9352b463944dc"/><file name="mysql4-upgrade-1.5.6-1.5.7.php" hash="b0180770655f36d6723483aa3bd1541f"/></dir></dir></dir></dir></target><target name="mageetc"><dir name="modules"><file name="Doofinder_Feed.xml" hash="9d3b6fbbbec12708461c33260715451c"/></dir></target><target name="magedesign"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="layout"><file name="doofinder.xml" hash="a7b9105a4e613086340b042845793d9f"/></dir></dir></dir></dir><dir name="frontend"><dir name="base"><dir name="default"><dir name="layout"><file name="doofinder.xml" hash="48a8636096950914917461260416c355"/></dir></dir></dir></dir></target><target name="mage"><dir name="js"><dir name="doofinder"><file name="admin.js" hash="ca050b0527ae101c75532fbca1c4a274"/></dir></dir></target><target name="mageskin"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="doofinder"><file name="styles.css" hash="d6ec303c3199db3ab4dffa8d2491105e"/></dir></dir></dir></dir></target><target name="magelib"><dir name="Doofinder"><file name="doofinder_api.php" hash="3a27154c61f86990f58bb719f44ccd0f"/><file name="doofinder_management_api.php" hash="71ac700c6e0d04496fd2da0bc012b84e"/><file name="errors.php" hash="6aaf93010cee28c87a6ab4ab0e2606d1"/></dir></target></contents>
56
+ <compatible/>
57
+ <dependencies><required><php><min>5.2.0</min><max>6.0.0</max></php></required></dependencies>
58
+ </package>
skin/adminhtml/default/default/doofinder/styles.css ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * This file is part of Doofinder_Feed.
3
+ */
4
+
5
+ /**
6
+ * @category CSS
7
+ * @package Doofinder_Feed
8
+ * @version 1.5.9
9
+ */
10
+
11
+ #doofinder_feed_additional_mapping_container input {
12
+ max-width: 80px;
13
+ }
14
+ #doofinder_feed_additional_mapping_container select {
15
+ max-width: 180px;
16
+ }
17
+
18
+ #row_doofinder_cron_schedule_settings_time select {
19
+ width: 80px !important;
20
+ }
21
+
22
+ #row_doofinder_cron_schedule_settings_time .df-separator:before {
23
+ content: " : "
24
+ }
25
+
26
+ #row_doofinder_search_layer_settings_script textarea {
27
+ min-width: 370px;
28
+ height: 30em;
29
+ font-family: "Courier New", monospace;
30
+ font-size: 13px;
31
+ }
32
+
33
+ .doofinder-info,
34
+ .doofinder-warning {
35
+ padding: 10px;
36
+ border: 1px solid transparent;
37
+ font-size: .95em !important;
38
+ }
39
+
40
+ .doofinder-info {
41
+ border-color: #d6d6d6;
42
+ background-color: #fafafa;
43
+ color: #333;
44
+ }
45
+
46
+ .doofinder-warning {
47
+ border-color: #EEE2BE;
48
+ background-color: #FFF9E9;
49
+ color: #5A4421;
50
+ }