Printful Integration for WooCommerce - Version 2.0

Version Description

New major plugin version * All new Printful dashboard * Connect to Printful with a single click * View your Printful profits and latest Printful product orders in WordPress admin * Edit your shipping carriers from Printful dashboard * Improved sales tax compatibility with existing tax rates * New status page - see if your integration is running smoothly * New support page - all info about finding help in one place * Size chart tab - when pushing products from Printful, the size chart will be placed in a separate tab * Improved logging of API requests coming to and from Printful

Download this release

Release Info

Developer printful
Plugin Icon 128x128 Printful Integration for WooCommerce
Version 2.0
Comparing to
See all releases

Code changes from version 1.2.8 to 2.0

Files changed (42) hide show
  1. assets/css/dashboard.css +181 -0
  2. assets/css/global.css +19 -0
  3. assets/css/settings.css +40 -0
  4. assets/css/status.css +18 -0
  5. assets/css/support.css +62 -0
  6. assets/images/connect.svg +74 -0
  7. assets/images/printful-logo-footer.png +0 -0
  8. assets/images/printful-menu-icon.png +0 -0
  9. assets/js/block-loader.js +22 -0
  10. assets/js/connect.js +51 -0
  11. assets/js/intercom.min.js +12 -0
  12. assets/js/settings.js +42 -0
  13. includes/class-printful-admin-dashboard.php +267 -0
  14. includes/class-printful-admin-settings.php +250 -0
  15. includes/class-printful-admin-status.php +541 -0
  16. includes/class-printful-admin-support.php +235 -0
  17. includes/class-printful-admin.php +167 -0
  18. includes/class-printful-api-resource.php +109 -22
  19. includes/class-printful-carriers.php +88 -0
  20. includes/class-printful-client.php +110 -91
  21. includes/class-printful-integration.php +147 -238
  22. includes/class-printful-request-log.php +142 -0
  23. includes/class-printful-shipping.php +308 -266
  24. includes/class-printful-size-chart-tab.php +132 -0
  25. includes/class-printful-taxes.php +188 -0
  26. includes/templates/ajax-loader.php +11 -0
  27. includes/templates/connect.php +40 -0
  28. includes/templates/error.php +1 -0
  29. includes/templates/footer.php +1 -0
  30. includes/templates/header.php +20 -0
  31. includes/templates/inline-script.php +3 -0
  32. includes/templates/order-table.php +69 -0
  33. includes/templates/quick-links.php +32 -0
  34. includes/templates/setting-group.php +59 -0
  35. includes/templates/setting-submit.php +21 -0
  36. includes/templates/shipping-notification.php +4 -0
  37. includes/templates/stats.php +44 -0
  38. includes/templates/status-report.php +19 -0
  39. includes/templates/status-table.php +54 -0
  40. includes/templates/support-info.php +27 -0
  41. printful-shipping.php +114 -56
  42. readme.txt +159 -132
assets/css/dashboard.css ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .printful-connect {
2
+ position: relative;
3
+ height: 100%;
4
+ }
5
+
6
+ .printful-connect-inner {
7
+ max-width: 600px;
8
+ margin: 100px auto 0 auto;
9
+ text-align: center;
10
+ position: relative;
11
+ }
12
+ .printful-connect .connect-image {
13
+ max-width: 100%;
14
+ }
15
+ .printful-connect h1 {
16
+ margin-bottom: 30px;
17
+ font-size: 34px;
18
+ }
19
+ .printful-connect .connect-description {
20
+ margin: 20px 0 30px 0;
21
+ }
22
+
23
+ /*noinspection CssUnusedSymbol*/
24
+ .printful-connect .printful-connect-button {
25
+ height: 53px;
26
+ line-height: 53px;
27
+ padding: 0 40px;
28
+ font-size: 18px;
29
+ }
30
+
31
+ .printful-stats {
32
+ position: relative;
33
+ margin: 20px 0 50px 0;
34
+ background-color: #fff;
35
+ padding: 30px 20px;
36
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
37
+ border-left: #0073aa 4px solid;
38
+ }
39
+ .printful-stats .printful-stats-item {
40
+ width: 49%;
41
+ display: inline-block;
42
+ margin-bottom: 25px;
43
+ box-sizing: border-box;
44
+ vertical-align: top;
45
+ padding: 0 8px;
46
+ }
47
+
48
+ .printful-stats .printful-stats-item h4 {
49
+ margin: 0 0 10px 0;
50
+ font-size: 24px;
51
+ line-height: 26px;
52
+ }
53
+ .printful-stats .printful-stats-item h4 .dashicons {
54
+ font-size: 26px;
55
+ width: 26px;
56
+ height: 26px;
57
+ }
58
+
59
+ /*noinspection ALL*/
60
+ .printful-stats .printful-stats-item h4 .dashicons-arrow-up-alt {
61
+ color: green;
62
+ }
63
+ .printful-stats .printful-stats-item h4 .dashicons-arrow-down-alt {
64
+ color: #d54e21;
65
+ }
66
+
67
+ @media (min-width: 768px) {
68
+ .printful-stats .printful-stats-item {
69
+ width: 24%;
70
+ margin-bottom: 0;
71
+ padding: 0 25px;
72
+ }
73
+ .printful-stats .printful-stats-item h4 {
74
+ font-size: 32px;
75
+ line-height: 36px;
76
+ }
77
+ .printful-stats .printful-stats-item h4 .dashicons {
78
+ padding-top: 4px;
79
+ font-size: 30px;
80
+ width: 30px;
81
+ height: 30px;
82
+ }
83
+ }
84
+
85
+ .printful-latest-orders {
86
+ display: table;
87
+ width: 100%;
88
+ margin-bottom: 50px;
89
+ }
90
+ .printful-latest-orders .row {
91
+ display: table-row;
92
+ }
93
+ .printful-latest-orders .row .cell {
94
+ display: table-cell;
95
+ }
96
+
97
+ .wrap h2.nav-tab-wrapper.printful-tabs {
98
+ margin-bottom: 15px;
99
+ }
100
+
101
+
102
+ .printful-quick-links {
103
+ position: relative;
104
+ margin-bottom: 30px;
105
+ }
106
+ .printful-quick-links .printful-quick-links-item {
107
+ width: 120px;
108
+ background: #fff;
109
+ border: solid 1px #e1e1e1;
110
+ float: left;
111
+ padding: 20px;
112
+ margin-right: 25px;
113
+ margin-bottom: 25px;
114
+ text-align: center;
115
+ text-decoration: none;
116
+ }
117
+ .printful-quick-links .printful-quick-links-item .dashicons {
118
+ font-size: 40px;
119
+ width: 40px;
120
+ height: 40px;
121
+ text-decoration: none;
122
+ color: #444;
123
+ }
124
+ .printful-quick-links .printful-quick-links-item h4 {
125
+ text-decoration: none;
126
+ color: #444;
127
+ margin: 5px 0 0 0;
128
+ }
129
+ .printful-quick-links .printful-quick-links-item:hover {
130
+ cursor: pointer;
131
+ }
132
+ .printful-quick-links .printful-quick-links-item:hover .dashicons,
133
+ .printful-quick-links .printful-quick-links-item:hover h4 {
134
+ color: #0D72B2;
135
+ }
136
+
137
+ .printful-error {
138
+ margin-top: 20px;
139
+ }
140
+
141
+ .printful-notice {
142
+ background: #fff;
143
+ border-left: 4px solid #dc3232;
144
+ padding: 1px 12px;
145
+ margin: 10px 0 20px 0;
146
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
147
+ text-align: left;
148
+ }
149
+ .printful-notice li {
150
+ font-weight: bold;
151
+ margin-left: 20px;
152
+ list-style: disc;
153
+ }
154
+
155
+ .block-loader {
156
+ text-align: center;
157
+ padding: 20px 0;
158
+ line-height: 20px;
159
+ }
160
+ .block-loader .message {
161
+ padding-left: 10px;
162
+ line-height: 20px;
163
+ display: inline-block;
164
+ font-size: 16px;
165
+ }
166
+ .block-loader .loader {
167
+ vertical-align: top;
168
+ }
169
+
170
+ #wpfooter::after {
171
+ box-sizing: border-box;
172
+ content: "";
173
+ width: 64px;
174
+ left: 50%;
175
+ margin-left: -32px;
176
+ height: 28px;
177
+ position: absolute;
178
+ bottom: 0;
179
+ background: url("../images/printful-logo-footer.png") no-repeat bottom center;
180
+ background-size: 64px;
181
+ }
assets/css/global.css ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .printful-toolbar .printful-toolbar-issues {
2
+ background-color: #d54e21;
3
+ display: inline-block;
4
+ padding: 0 5px !important;
5
+ border-radius: 10px !important;
6
+ line-height: 16px !important;
7
+ font-size: 11px !important;
8
+ color: #fff !important;
9
+ }
10
+ .printful-toolbar:hover .printful-toolbar-issues,
11
+ .printful-toolbar:active .printful-toolbar-issues,
12
+ .printful-toolbar.hover .printful-toolbar-issues
13
+ {
14
+ color: #fff !important;
15
+ }
16
+
17
+ #adminmenu .toplevel_page_printful-dashboard img {
18
+ width: 28px;
19
+ }
assets/css/settings.css ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .printful-setting-group {
2
+ overflow: hidden;
3
+ margin-bottom: 20px;
4
+ }
5
+
6
+ .printful-setting-group .input-text {
7
+ width: 400px;
8
+ }
9
+
10
+ .printful-setting-group .checkbox-group-item i {
11
+ color: #777;
12
+ }
13
+
14
+ .printful-submit .woocommerce-save-button {
15
+ float: left;
16
+ }
17
+
18
+ .printful-submit .loader-wrap {
19
+ display: inline-block;
20
+ padding: 4px 10px;
21
+ float: left;
22
+ }
23
+
24
+ .printful-submit .loader-wrap .loader {
25
+ display: none;
26
+ float: left;
27
+ }
28
+
29
+ .printful-submit .loader-wrap .dashicons {
30
+ font-size: 24px;
31
+ width: 24px;
32
+ height: 24px;
33
+ }
34
+
35
+ .printful-submit .loader-wrap .pass,
36
+ .printful-submit .loader-wrap .fail {
37
+ display: none;
38
+ float: left;
39
+ line-height: 24px;
40
+ }
assets/css/status.css ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .pass {
2
+ font-weight: bold;
3
+ color: green;
4
+ }
5
+ .fail {
6
+ font-weight: bold;
7
+ color: red;
8
+ }
9
+ .warning {
10
+ font-weight: bold;
11
+ color: #cab827;
12
+ }
13
+ .printful-status .col-status {
14
+ width: 10%;
15
+ }
16
+ .printful-status .col-desc {
17
+ width: 57%;
18
+ }
assets/css/support.css ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .support-report-wrap {
2
+ background-color: #fff;
3
+ padding: 20px;
4
+ margin: 20px 0;
5
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
6
+ border-left: #0073aa 4px solid;
7
+ }
8
+
9
+ .support-report-wrap p {
10
+ margin-top: 0;
11
+ }
12
+
13
+ .support-report-wrap .support-report {
14
+ width: 100%;
15
+ height: 100px;
16
+ font-family: monospace;
17
+ margin-bottom: 10px;
18
+ background: #efefef;
19
+ }
20
+
21
+ .support-info-wrap {
22
+ margin-top: 40px;
23
+ box-sizing: border-box;
24
+
25
+ }
26
+
27
+ .support-info-wrap .support-info-block {
28
+ box-sizing: border-box;
29
+ padding: 20px;
30
+ background: #fff;
31
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
32
+ margin-bottom: 20px;
33
+ }
34
+
35
+ .support-info-wrap .support-info-block h3 {
36
+ text-align: center;
37
+ margin-top: 10px;
38
+ }
39
+
40
+ .support-info-wrap .support-info-block .btn-wrap {
41
+ margin-top: 20px;
42
+ text-align: center;
43
+ }
44
+
45
+ @media (min-width: 768px) {
46
+
47
+ .support-info-wrap {
48
+ justify-content: space-between;
49
+ display: flex;
50
+ }
51
+
52
+ .support-info-wrap .support-info-block {
53
+ width: 32%;
54
+ }
55
+
56
+ .support-info-wrap .support-info-block:first-child {
57
+
58
+ }
59
+ .support-info-wrap .support-info-block:last-child {
60
+
61
+ }
62
+ }
assets/images/connect.svg ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="408" height="177" viewBox="0 0 408 177">
2
+ <g fill="none" fill-rule="evenodd" transform="translate(1 1)">
3
+ <path fill="#FFF" fill-rule="nonzero" stroke="#231F20" stroke-width="2" d="M299 120h107v8H299zM0 120h107v8H0z"/>
4
+ <path stroke="#D1D3D4" d="M197.5 21V10.2c0-5.5-4.1-9.8-9.6-9.8h-124c-5.5 0-10.4 4.2-10.4 9.8V87M208.5 21V10.2c0-5.5 4.9-9.8 10.4-9.8h124c5.5 0 9.6 4.2 9.6 9.8V87"/>
5
+ <path fill="#FFF" fill-rule="nonzero" stroke="#231F20" stroke-width="2" d="M5 48h97v72H5z"/>
6
+ <g fill-rule="nonzero">
7
+ <path fill="#9B5C8F" d="M27.104 70h44.77C74.706 70 77 72.312 77 75.168v17.225c0 2.856-2.293 5.168-5.127 5.168H55.82L58.022 103l-9.691-5.44H27.127C24.294 97.56 22 95.25 22 92.394V75.168C21.978 72.334 24.27 70 27.104 70z"/>
8
+ <path fill="#FFF" d="M25.389 74.664c.316-.431.79-.658 1.422-.703 1.152-.091 1.807.453 1.965 1.633.7 4.743 1.468 8.76 2.28 12.05l4.946-9.463c.451-.862 1.016-1.316 1.693-1.361.994-.068 1.603.567 1.852 1.906.565 3.017 1.287 5.582 2.145 7.76.587-5.763 1.58-9.916 2.98-12.48.34-.635.836-.953 1.491-.998.52-.046.994.113 1.423.453.429.34.655.772.7 1.294.022.408-.045.749-.226 1.09-.88 1.633-1.603 4.378-2.19 8.19-.565 3.699-.768 6.58-.633 8.646.046.567-.045 1.066-.27 1.497-.272.5-.678.772-1.197.817-.588.046-1.197-.227-1.784-.84-2.1-2.155-3.771-5.377-4.99-9.666a425.409 425.409 0 0 0-3.252 6.535c-1.333 2.564-2.462 3.88-3.41 3.948-.61.046-1.13-.476-1.58-1.565-1.152-2.973-2.394-8.714-3.726-17.223-.09-.59.044-1.112.36-1.52zm48.526 3.563c-.813-1.43-2.01-2.292-3.613-2.633a5.904 5.904 0 0 0-1.22-.136c-2.167 0-3.928 1.135-5.306 3.404-1.174 1.929-1.76 4.062-1.76 6.399 0 1.747.36 3.244 1.083 4.493.813 1.43 2.01 2.291 3.613 2.632.429.09.835.136 1.22.136 2.19 0 3.95-1.135 5.306-3.404 1.174-1.951 1.761-4.084 1.761-6.421.023-1.77-.361-3.245-1.084-4.47zm-2.845 6.285c-.316 1.498-.88 2.61-1.716 3.358-.655.59-1.265.84-1.83.726-.541-.113-.993-.59-1.331-1.475a5.778 5.778 0 0 1-.407-2.065c0-.567.045-1.134.158-1.656a7.357 7.357 0 0 1 1.197-2.7c.745-1.112 1.535-1.566 2.348-1.407.542.113.994.59 1.333 1.475.27.703.406 1.407.406 2.065 0 .59-.045 1.157-.158 1.679zm-11.29-6.285c-.813-1.43-2.033-2.292-3.613-2.633a5.902 5.902 0 0 0-1.22-.136c-2.168 0-3.929 1.135-5.306 3.404-1.174 1.929-1.762 4.062-1.762 6.399 0 1.747.362 3.244 1.084 4.493.813 1.43 2.01 2.291 3.613 2.632.43.09.835.136 1.22.136 2.19 0 3.951-1.135 5.306-3.404 1.174-1.951 1.761-4.084 1.761-6.421 0-1.77-.361-3.245-1.084-4.47zm-2.868 6.285c-.316 1.498-.881 2.61-1.716 3.358-.655.59-1.265.84-1.83.726-.541-.113-.993-.59-1.332-1.475a5.787 5.787 0 0 1-.406-2.065c0-.567.045-1.134.158-1.656a7.354 7.354 0 0 1 1.197-2.7c.745-1.112 1.535-1.566 2.348-1.407.542.113.994.59 1.332 1.475.272.703.407 1.407.407 2.065a6.726 6.726 0 0 1-.158 1.679z"/>
9
+ </g>
10
+ <path fill="#FFF" fill-rule="nonzero" stroke="#231F20" stroke-width="2" d="M304 48h97v72h-97z"/>
11
+ <g fill-rule="nonzero">
12
+ <path fill="#F2C994" d="M320 98l17.5-30L355 98z"/>
13
+ <path fill="#ED4642" d="M336 98l17.5-30L371 98z"/>
14
+ <path fill="#17BCB5" d="M352 98l17.5-30L387 98z"/>
15
+ <path fill="#16342F" d="M352 98h19l-9.5-16z"/>
16
+ <path fill="#DF392F" d="M336 98h19l-9.5-16z"/>
17
+ <path fill="#15291B" d="M352 98h3l-1.5-3z"/>
18
+ </g>
19
+ <path fill="#FFF" fill-rule="nonzero" stroke="#231F20" stroke-width="2" d="M155 21h97v124h-97z"/>
20
+ <path fill="#FFF" fill-rule="nonzero" stroke="#231F20" stroke-width="2" d="M150 142h107v33H150z"/>
21
+ <path stroke="#D1D3D4" d="M162.5 161.8l1.3-3.4 2 3.6 5.3-9.2 4 9.2 2.7-4.6 3.3 2.6"/>
22
+ <path stroke="#231F20" d="M161.5 148.5h20v20h-20zM187.5 148.5h61v20h-61z"/>
23
+ <circle cx="191.2" cy="33.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
24
+ <path stroke="#FFF" d="M191.2 33.8l.7-3.8"/>
25
+ <circle cx="191.2" cy="44.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
26
+ <path stroke="#FFF" d="M191.2 44.8l2.3 3"/>
27
+ <circle cx="191.2" cy="55.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
28
+ <path stroke="#FFF" d="M191.2 55.8l-3 2.3"/>
29
+ <circle cx="191.2" cy="66.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
30
+ <path stroke="#FFF" d="M191.2 66.8l2.3 3"/>
31
+ <circle cx="191.2" cy="77.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
32
+ <path stroke="#FFF" d="M191.2 77.8l3.6 1.2"/>
33
+ <path fill="#F1F2F2" fill-rule="nonzero" stroke="#231F20" d="M183.5 87.5h7v3h-7zM193.5 87.5h7v3h-7zM203.5 87.5h7v3h-7zM213.5 87.5h7v3h-7zM223.5 87.5h7v3h-7z"/>
34
+ <path fill="#F2C994" fill-rule="nonzero" stroke="#231F20" d="M233.5 87.5h7v3h-7z"/>
35
+ <path stroke="#231F20" stroke-width="2" d="M184 30v52"/>
36
+ <circle cx="214.2" cy="33.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
37
+ <path stroke="#FFF" d="M214.2 33.8l2.3 3"/>
38
+ <circle cx="214.2" cy="44.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
39
+ <path stroke="#FFF" d="M214.2 44.8l3-2.4"/>
40
+ <circle cx="214.2" cy="55.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
41
+ <path stroke="#FFF" d="M214.2 55.8l2.3 3"/>
42
+ <circle cx="214.2" cy="66.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
43
+ <path stroke="#FFF" d="M214.2 66.8l-3 2.3"/>
44
+ <circle cx="214.2" cy="77.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
45
+ <path stroke="#FFF" d="M214.2 77.8l2.3 3"/>
46
+ <path stroke="#231F20" stroke-width="2" d="M207 30v52"/>
47
+ <circle cx="237.2" cy="33.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
48
+ <path stroke="#FFF" d="M237.2 33.8l3.7.8"/>
49
+ <circle cx="237.2" cy="44.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
50
+ <path stroke="#FFF" d="M237.2 44.8l2.3 3"/>
51
+ <circle cx="237.2" cy="55.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
52
+ <path stroke="#FFF" d="M237.2 55.8l.3 3.8"/>
53
+ <circle cx="237.2" cy="66.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
54
+ <path stroke="#FFF" d="M237.2 66.8l3.4-1.7"/>
55
+ <circle cx="237.2" cy="77.8" r="4" fill="#D1D3D4" fill-rule="nonzero"/>
56
+ <path stroke="#FFF" d="M237.2 77.8l2.3 3"/>
57
+ <path stroke="#231F20" stroke-width="2" d="M230 30v52M184 96v34M207 96v34"/>
58
+ <path fill="#F1F2F2" fill-rule="nonzero" stroke="#231F20" d="M179.5 109.5h9v3h-9zM202.5 125.5h9v4h-9z"/>
59
+ <path stroke="#231F20" stroke-width="2" d="M230 96v34"/>
60
+ <path fill="#F1F2F2" fill-rule="nonzero" stroke="#231F20" d="M225.5 114.5h9v4h-9z"/>
61
+ <circle cx="169.5" cy="35.8" r="5" fill="#F2C994" fill-rule="nonzero" stroke="#231F20" stroke-width="2"/>
62
+ <circle cx="169.5" cy="48.8" r="3" fill="#D1D3D4" fill-rule="nonzero"/>
63
+ <circle cx="169.5" cy="59.8" r="3" fill="#D1D3D4" fill-rule="nonzero"/>
64
+ <circle cx="169.5" cy="70.8" r="3" fill="#D1D3D4" fill-rule="nonzero"/>
65
+ <circle cx="169.5" cy="81.8" r="3" fill="#D1D3D4" fill-rule="nonzero"/>
66
+ <circle cx="169.5" cy="99.8" r="3" fill="#D1D3D4" fill-rule="nonzero"/>
67
+ <circle cx="169.5" cy="113.8" r="3" fill="#D1D3D4" fill-rule="nonzero"/>
68
+ <circle cx="169.5" cy="127.8" r="3" fill="#D1D3D4" fill-rule="nonzero"/>
69
+ <circle cx="194.2" cy="152.8" r="2" fill="#F2C994" fill-rule="nonzero"/>
70
+ <circle cx="210.2" cy="162.8" r="2" fill="#F2C994" fill-rule="nonzero"/>
71
+ <path stroke="#231F20" d="M187 156.5h61"/>
72
+ <circle cx="239.2" cy="159.8" r="2" fill="#F2C994" fill-rule="nonzero"/>
73
+ </g>
74
+ </svg>
assets/images/printful-logo-footer.png ADDED
Binary file
assets/images/printful-menu-icon.png ADDED
Binary file
assets/js/block-loader.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Printful_Block_Loader;
2
+
3
+ (function () {
4
+ 'use strict';
5
+
6
+ Printful_Block_Loader = {
7
+ load: function (ajax_url, block) {
8
+
9
+ block = jQuery('#' + block);
10
+ if (block.length > 0) {
11
+
12
+ jQuery.ajax({
13
+ type: "GET",
14
+ url: ajax_url,
15
+ success: function (response) {
16
+ block.html(response);
17
+ }
18
+ });
19
+ }
20
+ }
21
+ };
22
+ })();
assets/js/connect.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Printful_Connect;
2
+
3
+ (function () {
4
+ 'use strict';
5
+
6
+ Printful_Connect = {
7
+ interval: 0,
8
+ ajax_url: '',
9
+ init: function (ajax_url) {
10
+ this.ajax_url = ajax_url;
11
+ this.loader();
12
+ this.listen_status();
13
+ this.listen_auth_return();
14
+ },
15
+ loader: function () {
16
+ jQuery('.printful-connect-button').click(function () {
17
+ jQuery(this).hide();
18
+ jQuery(this).siblings('.loader').removeClass('hidden');
19
+ });
20
+ },
21
+ listen_status: function () {
22
+ this.interval = setInterval(this.get_status.bind(this), 10000); //check status every 10 secs
23
+ },
24
+ get_status: function () {
25
+ var interval = this.interval;
26
+ jQuery.ajax( {
27
+ type: "GET",
28
+ url: this.ajax_url,
29
+ success: function( response ) {
30
+ if (response === 'OK') {
31
+ clearInterval(interval);
32
+ Printful_Connect.send_return_message();
33
+ }
34
+ }
35
+ });
36
+ },
37
+ listen_auth_return: function () {
38
+ var intercom = Intercom.getInstance();
39
+ intercom.on('printful-auth', function (data) {
40
+ if (data.success === true) {
41
+ location.reload();
42
+ }
43
+ });
44
+ },
45
+ send_return_message: function () {
46
+ var intercom = Intercom.getInstance();
47
+ intercom.emit('printful-auth', {success: true});
48
+ window.top.close();
49
+ }
50
+ };
51
+ })();
assets/js/intercom.min.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*! intercom.js | https://github.com/diy/intercom.js | Apache License (v2) */
2
+ var Intercom=function(){var g=function(){};g.createInterface=function(b){return{on:function(a,c){"undefined"===typeof this[b]&&(this[b]={});this[b].hasOwnProperty(a)||(this[b][a]=[]);this[b][a].push(c)},off:function(a,c){"undefined"!==typeof this[b]&&this[b].hasOwnProperty(a)&&i.removeItem(c,this[b][a])},trigger:function(a){if("undefined"!==typeof this[b]&&this[b].hasOwnProperty(a))for(var c=Array.prototype.slice.call(arguments,1),e=0;e<this[b][a].length;e++)this[b][a][e].apply(this[b][a][e],c)}}};
3
+ var m=g.createInterface("_handlers");g.prototype._on=m.on;g.prototype._off=m.off;g.prototype._trigger=m.trigger;var n=g.createInterface("handlers");g.prototype.on=function(){n.on.apply(this,arguments);Array.prototype.unshift.call(arguments,"on");this._trigger.apply(this,arguments)};g.prototype.off=n.off;g.prototype.trigger=n.trigger;var f=window.localStorage;"undefined"===typeof f&&(f={getItem:function(){},setItem:function(){},removeItem:function(){}});var i={},h=function(){return(65536*(1+Math.random())|
4
+ 0).toString(16).substring(1)};i.guid=function(){return h()+h()+"-"+h()+"-"+h()+"-"+h()+"-"+h()+h()+h()};i.throttle=function(b,a){var c=0;return function(){var e=(new Date).getTime();e-c>b&&(c=e,a.apply(this,arguments))}};i.extend=function(b,a){if("undefined"===typeof b||!b)b={};if("object"===typeof a)for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b};i.removeItem=function(b,a){for(var c=a.length-1;0<=c;c--)a[c]===b&&a.splice(c,1);return a};var d=function(){var b=this,a=(new Date).getTime();
5
+ this.origin=i.guid();this.lastMessage=a;this.bindings=[];this.receivedIDs={};this.previousValues={};a=function(){b._onStorageEvent.apply(b,arguments)};window.attachEvent?document.attachEvent("onstorage",a):window.addEventListener("storage",a,!1)};d.prototype._transaction=function(b){var a=this,c=!1,e=!1,p=null,d=function(){if(!c){var g=(new Date).getTime(),s=parseInt(f.getItem(l)||0);s&&1E3>g-s?(e||(a._on("storage",d),e=!0),p=window.setTimeout(d,20)):(c=!0,f.setItem(l,g),b(),e&&a._off("storage",d),
6
+ p&&window.clearTimeout(p),f.removeItem(l))}};d()};d.prototype._cleanup_emit=i.throttle(100,function(){this._transaction(function(){for(var b=(new Date).getTime()-t,a=0,c=JSON.parse(f.getItem(j)||"[]"),e=c.length-1;0<=e;e--)c[e].timestamp<b&&(c.splice(e,1),a++);0<a&&f.setItem(j,JSON.stringify(c))})});d.prototype._cleanup_once=i.throttle(100,function(){var b=this;this._transaction(function(){var a,c=JSON.parse(f.getItem(k)||"{}");(new Date).getTime();var e=0;for(a in c)b._once_expired(a,c)&&(delete c[a],
7
+ e++);0<e&&f.setItem(k,JSON.stringify(c))})});d.prototype._once_expired=function(b,a){if(!a||!a.hasOwnProperty(b)||"object"!==typeof a[b])return!0;var c=a[b].ttl||u,e=(new Date).getTime();return a[b].timestamp<e-c};d.prototype._localStorageChanged=function(b,a){if(b&&b.key)return b.key===a;var c=f.getItem(a);if(c===this.previousValues[a])return!1;this.previousValues[a]=c;return!0};d.prototype._onStorageEvent=function(b){var b=b||window.event,a=this;this._localStorageChanged(b,j)&&this._transaction(function(){for(var b=
8
+ (new Date).getTime(),e=f.getItem(j),e=JSON.parse(e||"[]"),d=0;d<e.length;d++)if(e[d].origin!==a.origin&&!(e[d].timestamp<a.lastMessage)){if(e[d].id){if(a.receivedIDs.hasOwnProperty(e[d].id))continue;a.receivedIDs[e[d].id]=!0}a.trigger(e[d].name,e[d].payload)}a.lastMessage=b});this._trigger("storage",b)};d.prototype._emit=function(b,a,c){if((c="string"===typeof c||"number"===typeof c?String(c):null)&&c.length){if(this.receivedIDs.hasOwnProperty(c))return;this.receivedIDs[c]=!0}var e={id:c,name:b,origin:this.origin,
9
+ timestamp:(new Date).getTime(),payload:a},d=this;this._transaction(function(){var c=f.getItem(j)||"[]",c=[c.substring(0,c.length-1),"[]"===c?"":",",JSON.stringify(e),"]"].join("");f.setItem(j,c);d.trigger(b,a);window.setTimeout(function(){d._cleanup_emit()},50)})};d.prototype.bind=function(b,a){for(var c=0;c<d.bindings.length;c++){var e=d.bindings[c].factory(b,a||null,this);e&&this.bindings.push(e)}};d.prototype.emit=function(b,a){this._emit.apply(this,arguments);this._trigger("emit",b,a)};d.prototype.once=
10
+ function(b,a,c){if(d.supported){var e=this;this._transaction(function(){var d=JSON.parse(f.getItem(k)||"{}");e._once_expired(b,d)&&(d[b]={},d[b].timestamp=(new Date).getTime(),"number"===typeof c&&(d[b].ttl=1E3*c),f.setItem(k,JSON.stringify(d)),a(),window.setTimeout(function(){e._cleanup_once()},50))})}};i.extend(d.prototype,g.prototype);d.bindings=[];d.supported="undefined"!==typeof f;var j="intercom",k="intercom_once",l="intercom_lock",t=5E4,u=36E5;d.destroy=function(){f.removeItem(l);f.removeItem(j);
11
+ f.removeItem(k)};var q=null;d.getInstance=function(){q||(q=new d);return q};var r=function(b,a,c){a=i.extend({id:null,send:!0,receive:!0},a);if(a.receive){var d=[],f=function(f){-1===d.indexOf(f)&&(d.push(f),b.on(f,function(b){var d="function"===typeof a.id?a.id(f,b):null,e="function"===typeof a.receive?a.receive(f,b):!0;(e||"boolean"!==typeof e)&&c._emit(f,b,d)}))},g;for(g in c.handlers)for(var h=0;h<c.handlers[g].length;h++)f(g,c.handlers[g][h]);c._on("on",f)}a.send&&c._on("emit",function(c,d){var e=
12
+ "function"===typeof a.send?a.send(c,d):!0;(e||"boolean"!==typeof e)&&b.emit(c,d)})};r.factory=function(b,a,c){return"undefined"===typeof b.socket?!1:new r(b,a,c)};d.bindings.push(r);return d}();
assets/js/settings.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Printful_Settings;
2
+
3
+ (function () {
4
+ 'use strict';
5
+
6
+ Printful_Settings = {
7
+ init_submit: function () {
8
+
9
+ var form = jQuery('form[name=printful_settings]');
10
+ var submit_button = form.find('.woocommerce-save-button');
11
+ var loader = form.find('.loader');
12
+ var pass = form.find('.loader-wrap .pass');
13
+ var fail = form.find('.loader-wrap .fail');
14
+
15
+ submit_button.click(function (e) {
16
+
17
+ e.preventDefault();
18
+ submit_button.attr('disabled', 'disabled');
19
+ loader.show();
20
+
21
+ jQuery.ajax({
22
+ type: "POST",
23
+ url: form.attr('action'),
24
+ data: form.serialize(),
25
+ success: function (response) {
26
+ submit_button.removeAttr('disabled');
27
+ loader.hide();
28
+
29
+ if (response === 'OK') {
30
+ pass.show(0).delay(3000).hide(0);
31
+ } else {
32
+ fail.show(0).delay(3000).hide(0);
33
+ }
34
+ }
35
+ });
36
+ });
37
+ },
38
+ enable_submit_btn: function () {
39
+ jQuery('.printful-submit input[type=submit]').removeClass('disabled').prop('disabled', false);
40
+ }
41
+ };
42
+ })();
includes/class-printful-admin-dashboard.php ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin_Dashboard {
5
+
6
+ const API_KEY_SEARCH_STRING = 'Printful';
7
+
8
+ public static $_instance;
9
+
10
+ /**
11
+ * @return Printful_Admin_Dashboard
12
+ */
13
+ public static function instance() {
14
+
15
+ if ( is_null( self::$_instance ) ) {
16
+ self::$_instance = new self();
17
+ }
18
+
19
+ return self::$_instance;
20
+ }
21
+
22
+ /**
23
+ * Printful_Admin_Dashboard constructor.
24
+ */
25
+ function __construct() {
26
+
27
+ }
28
+
29
+ /**
30
+ * Show the view
31
+ */
32
+ public static function view() {
33
+
34
+ $dashboard = self::instance();
35
+ $api_key = Printful_Integration::instance()->get_option('printful_key');
36
+ $connect_status = Printful_Integration::instance()->is_connected();
37
+
38
+ if ( $connect_status ) {
39
+ $dashboard->render_dashboard();
40
+ } else if(!$connect_status && strlen($api_key) > 0) {
41
+ $dashboard->render_connect_error();
42
+ } else {
43
+ $dashboard->render_connect();
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Display the Printful connect page
49
+ */
50
+ public function render_connect() {
51
+
52
+ $status = Printful_Admin_Status::instance();
53
+ $issues = array();
54
+
55
+ $api_enabled = $status->run_single_test('check_WC_API_enabled');
56
+ $permalinks_set = $status->run_single_test('check_permalinks');
57
+
58
+ if ( $permalinks_set == Printful_Admin_Status::PF_STATUS_FAIL ) {
59
+ $message = 'WooCommerce API will not work unless your permalinks are set up correctly. Go to <a href="%s">Permalinks settings</a> and make sure that they are NOT set to "plain".';
60
+ $settings_url = admin_url( 'options-permalink.php' );
61
+ $issues[] = sprintf( $message, $settings_url );
62
+ }
63
+
64
+ if ( $api_enabled == Printful_Admin_Status::PF_STATUS_FAIL ) {
65
+ $message = 'Your site needs to enable WooCommerce API so that Printful can connect to it. Go to <a href="%s">API settings</a> and enable it.';
66
+ $settings_url = admin_url( 'admin.php?page=wc-settings&tab=api' );
67
+ $issues[] = sprintf( $message, $settings_url );
68
+ }
69
+
70
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
71
+
72
+ Printful_Admin::load_template( 'connect', array(
73
+ 'consumer_key' => $this->_get_consumer_key(),
74
+ 'waiting_sync' => isset( $_GET['sync-in-progress'] ),
75
+ 'consumer_key_error' => isset( $_GET['consumer-key-error'] ),
76
+ 'issues' => $issues,
77
+ )
78
+ );
79
+
80
+ if ( isset( $_GET['sync-in-progress'] ) ) {
81
+ $emit_auth_response = 'Printful_Connect.send_return_message();';
82
+ Printful_Admin::load_template( 'inline-script', array( 'script' => $emit_auth_response ) );
83
+ }
84
+
85
+ Printful_Admin::load_template('footer');
86
+ }
87
+
88
+ /**
89
+ * Display the Printful connect error page
90
+ */
91
+ public function render_connect_error() {
92
+
93
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
94
+
95
+ $connect_error = Printful_Integration::instance()->get_connect_error();
96
+ if ( $connect_error ) {
97
+ Printful_Admin::load_template('error', array('error' => $connect_error));
98
+ }
99
+
100
+ Printful_Admin::load_template('footer');
101
+ }
102
+
103
+ /**
104
+ * Display the dashboard
105
+ */
106
+ public function render_dashboard() {
107
+
108
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
109
+
110
+ $stats = $this->_get_stats(true);
111
+ $orders = $this->_get_orders(true);
112
+ $error = false;
113
+
114
+ if ( is_wp_error( $stats ) ) {
115
+ $error = $stats;
116
+ }
117
+ if ( is_wp_error( $orders ) ) {
118
+ $error = $orders;
119
+ }
120
+
121
+ if ( ! $error ) {
122
+
123
+ if ( $stats ) {
124
+ Printful_Admin::load_template( 'stats', array( 'stats' => $stats ) );
125
+ } else {
126
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_stats', 'message' => 'Loading your stats...' ) );
127
+ }
128
+
129
+ if ($orders) {
130
+ Printful_Admin::load_template( 'order-table', array( 'orders' => $orders ) );
131
+ } else {
132
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_orders', 'message' => 'Loading your orders...' ) );
133
+ }
134
+
135
+ } else {
136
+ Printful_Admin::load_template( 'error', array( 'error' => $error->get_error_message('printful') ) );
137
+ }
138
+
139
+ Printful_Admin::load_template( 'quick-links' );
140
+
141
+ if ( isset( $_GET['sync-in-progress'] ) ) {
142
+ $emit_auth_response = 'Printful_Connect.send_return_message();';
143
+ Printful_Admin::load_template( 'inline-script', array( 'script' => $emit_auth_response ) );
144
+ }
145
+
146
+ Printful_Admin::load_template( 'footer' );
147
+ }
148
+
149
+ /**
150
+ * Ajax response for stats block
151
+ */
152
+ public static function render_stats_ajax() {
153
+
154
+ $stats = self::instance()->_get_stats();
155
+
156
+ if ( ! empty( $stats ) && ! is_wp_error( $stats ) ) {
157
+ Printful_Admin::load_template( 'stats', array( 'stats' => $stats ) );
158
+ } else {
159
+ Printful_Admin::load_template( 'error', array( 'error' => $stats->get_error_message( 'printful' ) ) );
160
+ }
161
+
162
+ exit;
163
+ }
164
+
165
+ /**
166
+ * Ajax response for stats block
167
+ */
168
+ public static function render_orders_ajax() {
169
+
170
+ $orders = self::instance()->_get_orders();
171
+
172
+ if ( ! empty( $orders ) && is_wp_error( $orders ) ) {
173
+ Printful_Admin::load_template( 'error', array( 'error' => $orders->get_error_message('printful') ) );
174
+ } else {
175
+ Printful_Admin::load_template( 'order-table', array( 'orders' => $orders ) );
176
+ }
177
+
178
+ exit;
179
+ }
180
+
181
+ /**
182
+ * Get store statistics from API
183
+ * @param bool $only_cached_results
184
+ * @return mixed
185
+ */
186
+ private function _get_stats($only_cached_results = false) {
187
+
188
+ $stats = get_transient( 'printful_stats' );
189
+ if ( $only_cached_results || $stats ) {
190
+ return $stats;
191
+ }
192
+
193
+ try {
194
+ $stats = Printful_Integration::instance()->get_client()->get( 'store/statistics' );
195
+ if ( ! empty( $stats['store_statistics'] ) ) {
196
+ $stats = $stats['store_statistics'];
197
+ }
198
+ set_transient( 'printful_stats', $stats, MINUTE_IN_SECONDS * 5 ); //cache for 5 minute
199
+ } catch (PrintfulApiException $e) {
200
+ return new WP_Error('printful', 'Could not connect to Printful API. Please try again later!');
201
+ } catch (PrintfulException $e) {
202
+ return new WP_Error('printful', 'Could not connect to Printful API. Please try again later!');
203
+ }
204
+
205
+ return $stats;
206
+ }
207
+
208
+ /**
209
+ * Get Printful orders from the API
210
+ * @param bool $only_cached_results
211
+ * @return mixed
212
+ */
213
+ private function _get_orders($only_cached_results = false) {
214
+
215
+ $orders = get_transient( 'printful_orders' );
216
+
217
+ if ( $only_cached_results || $orders ) {
218
+ return $orders;
219
+ }
220
+
221
+ try {
222
+ $order_data = Printful_Integration::instance()->get_client()->get( 'orders' );
223
+
224
+ if ( ! empty( $order_data ) ) {
225
+
226
+ foreach ( $order_data as $key => $order ) {
227
+
228
+ if($order['status'] == 'pending') {
229
+ $order_data[$key]['status'] = 'Waiting for fulfillment';
230
+ }
231
+ }
232
+ }
233
+
234
+ $orders = array( 'count' => count( $order_data ), 'results' => $order_data );
235
+ set_transient( 'printful_orders', $orders, MINUTE_IN_SECONDS * 5 ); //cache for 5 minute
236
+ } catch (PrintfulApiException $e) {
237
+ return new WP_Error('printful', 'Could not connect to Printful API. Please try again later!');
238
+ } catch (PrintfulException $e) {
239
+ return new WP_Error('printful', 'Could not connect to Printful API. Please try again later!');
240
+ }
241
+
242
+ return $orders;
243
+ }
244
+
245
+ /**
246
+ * Get the last used consumer key fragment and use it for validating the address
247
+ * @return null|string
248
+ */
249
+ private function _get_consumer_key() {
250
+
251
+ global $wpdb;
252
+
253
+ // Get the API key
254
+ $search = "AND description LIKE '%" . esc_sql( $wpdb->esc_like( wc_clean( self::API_KEY_SEARCH_STRING ) ) ) . "%' ";
255
+ $query = "SELECT truncated_key FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search} ORDER BY key_id DESC LIMIT 1";
256
+ $consumer_key = $wpdb->get_var( $query );
257
+
258
+ //if not found by description, it was probably manually created. try the last used key instead
259
+ if ( ! $consumer_key ) {
260
+ $query = "SELECT truncated_key FROM {$wpdb->prefix}woocommerce_api_keys ORDER BY key_id DESC LIMIT 1";
261
+ $consumer_key = $wpdb->get_var( $query );
262
+ }
263
+
264
+ return $consumer_key;
265
+ }
266
+
267
+ }
includes/class-printful-admin-settings.php ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin_Settings {
5
+
6
+ public static $_instance;
7
+ const CARRIER_TYPE_STANDARD = 'standard';
8
+ const CARRIER_TYPE_EXPEDITED = 'expedited';
9
+
10
+ public static $integration_fields = array(
11
+ 'printful_key' => array(
12
+ 'title' => 'Printful store API key',
13
+ 'type' => 'text',
14
+ 'desc_tip' => true,
15
+ 'description' => 'Your store\'s Printful API key. Create it in the Prinful dashboard',
16
+ 'default' => false,
17
+ ),
18
+ 'calculate_tax' => array(
19
+ 'title' => 'Calculate sales tax',
20
+ 'type' => 'checkbox',
21
+ 'label' => 'Calculated for all products shipped to North Carolina and California',
22
+ 'default' => 'no',
23
+ ),
24
+ 'disable_ssl' => array(
25
+ 'title' => 'Disable SSL',
26
+ 'type' => 'checkbox',
27
+ 'label' => 'Use HTTP instead of HTTPS to connect to the Printful API (may be required if the plugin does not work for some hosting configurations)',
28
+ 'default' => 'no',
29
+ ),
30
+ );
31
+
32
+ /**
33
+ * @return Printful_Admin_Settings
34
+ */
35
+ public static function instance() {
36
+
37
+ if ( is_null( self::$_instance ) ) {
38
+ self::$_instance = new self();
39
+ }
40
+
41
+ return self::$_instance;
42
+ }
43
+
44
+ /**
45
+ * Setup the view
46
+ */
47
+ public static function view() {
48
+
49
+ $settings = self::instance();
50
+ $settings->render();
51
+ }
52
+
53
+ /**
54
+ * Display the view
55
+ */
56
+ public function render() {
57
+
58
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
59
+
60
+ echo '<form method="post" name="printful_settings" action="' . admin_url( 'admin-ajax.php?action=save_printful_settings' ) . '">';
61
+
62
+ //integration settings
63
+ $integration_settings = $this->setup_integration_fields();
64
+ Printful_Admin::load_template( 'setting-group', $integration_settings );
65
+
66
+ Printful_Admin::load_template( 'shipping-notification' );
67
+
68
+ //carriers settings
69
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_carriers', 'message' => 'Loading your carriers...' ) );
70
+
71
+ Printful_Admin::load_template( 'setting-submit', array( 'nonce' => wp_create_nonce( 'printful_settings' ), 'disabled' => true ) );
72
+
73
+ echo '</from>';
74
+
75
+ Printful_Admin::load_template( 'footer' );
76
+ }
77
+
78
+ /**
79
+ * Display the ajax content for carrier settings
80
+ */
81
+ public static function render_carriers_ajax() {
82
+
83
+ $carrier_settings = self::instance()->setup_carrier_fields();
84
+ Printful_Admin::load_template( 'setting-group', $carrier_settings );
85
+ $enable_submit = 'Printful_Settings.enable_submit_btn();';
86
+ Printful_Admin::load_template( 'inline-script', array('script' => $enable_submit) );
87
+ exit;
88
+ }
89
+
90
+ /**
91
+ * @return mixed
92
+ * @internal param $integration_settings
93
+ */
94
+ public function setup_integration_fields() {
95
+
96
+ $integration_settings = array(
97
+ 'title' => 'Integration settings',
98
+ 'description' => '',
99
+ 'settings' => self::$integration_fields,
100
+ );
101
+
102
+ foreach ( $integration_settings['settings'] as $key => $setting ) {
103
+ if ( $setting['type'] !== 'title' ) {
104
+ $integration_settings['settings'][ $key ]['value'] = Printful_Integration::instance()->get_option( $key, $setting['default'] );
105
+ }
106
+ }
107
+
108
+ return $integration_settings;
109
+ }
110
+
111
+ /**
112
+ * @internal param $carrier_settings
113
+ */
114
+ public function setup_carrier_fields() {
115
+
116
+ $carrier_settings = array(
117
+ 'title' => 'Carriers & Services',
118
+ 'description' => "You can specify here which shipping carriers are available for this store.\n
119
+ Uncheck the ones you want to disable. If you leave “Flat rate” checked it means that any order where flat rate was selected can still ship with any carrier and service.",
120
+ 'settings' => array(),
121
+ );
122
+
123
+ if ( ! Printful_Integration::instance()->is_connected() ) {
124
+ $carrier_settings['description'] = 'You need to be connected to Printful API to edit carrier settings!';
125
+ return $carrier_settings;
126
+ }
127
+
128
+ $carriers = Printful_Carriers::instance()->carriers;
129
+ if ( empty( $carriers ) ) {
130
+ return false;
131
+ }
132
+
133
+ $standard = array();
134
+ $expedited = array();
135
+
136
+ foreach ( $carriers as $carrier ) {
137
+
138
+ $item = array(
139
+ 'title' => false,
140
+ 'type' => 'checkbox',
141
+ 'label' => $carrier['title'] . ' <i>' . $carrier['subtitle'] . '</i>',
142
+ 'default' => 'yes',
143
+ 'value' => ( $carrier['status'] == 'on' ? 'yes' : 'no' ),
144
+ );
145
+
146
+ if ( $carrier['type'] == self::CARRIER_TYPE_STANDARD ) {
147
+ $standard[ $carrier['carrier_id'] ] = $item;
148
+ } else {
149
+ $expedited[ $carrier['carrier_id'] ] = $item;
150
+ }
151
+ }
152
+
153
+ $carrier_settings['settings'][ self::CARRIER_TYPE_STANDARD ] = array(
154
+ 'title' => 'Standard shipping',
155
+ 'type' => 'checkbox-group',
156
+ 'items' => $standard,
157
+ );
158
+
159
+ $carrier_settings['settings'][ self::CARRIER_TYPE_EXPEDITED ] = array(
160
+ 'title' => 'Expedited shipping',
161
+ 'type' => 'checkbox-group',
162
+ 'items' => $expedited,
163
+ );
164
+
165
+ return $carrier_settings;
166
+ }
167
+
168
+ /**
169
+ * Prepare carrier data for posting to Printful API
170
+ * @return array
171
+ */
172
+ public function prepare_carriers() {
173
+
174
+ $carriers = Printful_Carriers::instance()->carriers;
175
+
176
+ if ( empty( $carriers ) ) {
177
+ return false;
178
+ }
179
+
180
+ $standard = ( ! empty( $_POST[ self::CARRIER_TYPE_STANDARD ] ) ? $_POST[ self::CARRIER_TYPE_STANDARD ] : array() );
181
+ $expedited = ( ! empty( $_POST[ self::CARRIER_TYPE_EXPEDITED ] ) ? $_POST[ self::CARRIER_TYPE_EXPEDITED ] : array() );
182
+
183
+ $saved_carriers = array_merge( $standard, $expedited );
184
+
185
+ if ( empty( $saved_carriers ) ) {
186
+ return false;
187
+ }
188
+
189
+ $request_body = array();
190
+
191
+ foreach ( $carriers as $carrier ) {
192
+ $status = 'off';
193
+ if ( in_array( $carrier['carrier_id'], $saved_carriers ) ) {
194
+ $status = 'on';
195
+ }
196
+ $request_body[] = array(
197
+ 'carrier_id' => $carrier['carrier_id'],
198
+ 'status' => $status,
199
+ );
200
+ }
201
+
202
+ return $request_body;
203
+ }
204
+
205
+ /**
206
+ * Ajax endpoint for saving the settings
207
+ */
208
+ public static function save_printful_settings() {
209
+
210
+ if ( ! empty( $_POST ) ) {
211
+
212
+ check_admin_referer( 'printful_settings' );
213
+
214
+ //save carriers first, so API key change does not affect this
215
+ if ( Printful_Integration::instance()->is_connected(true) ) {
216
+
217
+ //save remote carrier settings
218
+ $request_body = Printful_Admin_Settings::instance()->prepare_carriers();
219
+ $result = Printful_Carriers::instance()->post_carriers( $request_body );
220
+
221
+ if ( ! $result ) {
222
+ die( 'Error: failed to save carriers' );
223
+ }
224
+ }
225
+
226
+ $options = array();
227
+
228
+ //build save options list
229
+ foreach ( self::$integration_fields as $key => $field ) {
230
+
231
+ if ( $field['type'] == 'checkbox' ) {
232
+ if ( isset( $_POST[ $key ] ) ) {
233
+ $options[ $key ] = 'yes';
234
+ } else {
235
+ $options[ $key ] = 'no';
236
+ }
237
+ } else {
238
+ if ( isset( $_POST[ $key ] ) ) {
239
+ $options[ $key ] = $_POST[ $key ];
240
+ }
241
+ }
242
+ }
243
+
244
+ //save integration settings
245
+ Printful_Integration::instance()->update_settings( $options );
246
+
247
+ die('OK');
248
+ }
249
+ }
250
+ }
includes/class-printful-admin-status.php ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin_Status {
5
+
6
+ const PF_STATUS_OK = 1;
7
+ const PF_STATUS_WARNING = 0;
8
+ const PF_STATUS_FAIL = -1;
9
+
10
+ const API_KEY_SEARCH_STRING = 'Printful';
11
+ const PF_WEBHOOK_NAME = 'Printful Integration';
12
+ const PF_REMOTE_REQUEST_URL = 'webhook/woocommerce?store=1';
13
+ const PF_REMOTE_REQUEST_TOPIC = 'woo.plugin.test';
14
+ const PF_STATUS_ISSUE_COUNT = 'printful_status_issue_count';
15
+ const PF_CACHED_CHECKLIST = 'printful_cached_checklist';
16
+
17
+ public static $_instance;
18
+ public static $checklist_items = array(
19
+ array(
20
+ 'name' => 'WordPress Permalinks',
21
+ 'description' => 'WooCommerce API will not work unless your permalinks in Settings > Permalinks are set up correctly. Make sure you that they are NOT set to "plain".',
22
+ 'method' => 'check_permalinks',
23
+ ),
24
+ array(
25
+ 'name' => 'WordPress version',
26
+ 'description' => 'WordPress should always be updated to the latest version. Updates can be installed from your WordPress admin dashboard.',
27
+ 'method' => 'check_WP_version',
28
+ ),
29
+ array(
30
+ 'name' => 'WooCommerce API enabled',
31
+ 'description' => 'Your site needs to enbable WooCommerce API so that Printful can connect to it',
32
+ 'method' => 'check_WC_API_enabled',
33
+ ),
34
+ array(
35
+ 'name' => 'WooCommerce Webhooks',
36
+ 'description' => 'Printful requires WooCommerce webhooks to quickly capture you incoming orders, products updates etc.',
37
+ 'method' => 'check_PF_webhooks',
38
+ ),
39
+ array(
40
+ 'name' => 'WooCommerce API keys are set',
41
+ 'description' => 'Printful needs access to your WooCommerce API for the integration to work - otherwise we can\'t sync your store, push or pull your products etc.',
42
+ 'method' => 'check_WC_API_access',
43
+ ),
44
+ array(
45
+ 'name' => 'WooCommerce authentication URL access',
46
+ 'description' => 'Some hosts have unnecessarily intrusive security checks in place that prevent WooCommerce API authentication from working (mod_security rule #1234234). If this check fails, you will not be able authorize Printful app.',
47
+ 'method' => 'check_WC_auth_url_access',
48
+ ),
49
+ array(
50
+ 'name' => 'WordPress remote requests',
51
+ 'description' => 'WordPress needs to be able to connect to Printful server to call webhooks. If this check fails, contact your hosting support.',
52
+ 'method' => 'check_remote_requests',
53
+ ),
54
+ array(
55
+ 'name' => 'Printful API key is set',
56
+ 'description' => 'Your store needs access to Printful API to use most of it\'s features like shipping rates, tax rates and other settings.',
57
+ 'method' => 'check_PF_API_key',
58
+ ),
59
+ array(
60
+ 'name' => 'Connection to Printful API',
61
+ 'description' => 'Is your store successfully connected to Printful API',
62
+ 'method' => 'check_PF_API_connect',
63
+ ),
64
+ array(
65
+ 'name' => 'Recent store sync errors',
66
+ 'description' => 'Printful will connect to your store\'s API regularly and sync your latest products, orders etc. If there have been any recent issues with sync, this check will fail.',
67
+ 'method' => 'check_PF_sync_errors',
68
+ ),
69
+ array(
70
+ 'name' => 'Write permissions',
71
+ 'description' => 'Make the uploads directory writable. This is required for mockup generator product push to work correctly. Contact your hosting provider if you need help with this.',
72
+ 'method' => 'check_uploads_write',
73
+ ),
74
+ array(
75
+ 'name' => 'PHP memory limit',
76
+ 'description' => 'Set PHP allocated memory limit to at least 128mb. Contact your hosting provider if you need help with this.',
77
+ 'method' => 'check_PHP_memory_limit',
78
+ ),
79
+ array(
80
+ 'name' => 'PHP script time limit',
81
+ 'description' => 'Set PHP script execution time limit to at least 30 seconds. This is required to successfully push products with many variants. Contact your hosting provider if you need help with this.',
82
+ 'method' => 'check_PHP_time_limit',
83
+ ),
84
+ // array(
85
+ // 'name' => 'Error logs',
86
+ // 'description' => 'Your WordPress site needs to have it\'s error logging set up correctly so in case of issues you can figure out what\'s wrong. Note: your hosting might already have an independent log from WordPress.',
87
+ // 'method' => 'check_WP_error_logs',
88
+ // ),
89
+ array(
90
+ 'name' => 'W3 Total Cache DB Cache',
91
+ 'description' => 'If you are using W3 Total Cache, the database caching feature needs to be disabled since it can cause issues with product push to store.',
92
+ 'method' => 'check_W3_db_cache',
93
+ 'silent' => true,
94
+ ),
95
+ array(
96
+ 'name' => 'Remove Print Aura plugin',
97
+ 'description' => 'Print Aura plugin is known to cause issues so it needs to be removed',
98
+ 'method' => 'check_printaura_plugin',
99
+ 'silent' => true,
100
+ ),
101
+ );
102
+
103
+ /**
104
+ * @return Printful_Admin_Status
105
+ */
106
+ public static function instance() {
107
+
108
+ if ( is_null( self::$_instance ) ) {
109
+ self::$_instance = new self();
110
+ }
111
+
112
+ return self::$_instance;
113
+ }
114
+
115
+ /**
116
+ * Setup the view variables
117
+ */
118
+ public static function view() {
119
+
120
+ $status = self::instance();
121
+ $status->render();
122
+ }
123
+
124
+ /**
125
+ * Render the view
126
+ */
127
+ public function render() {
128
+
129
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
130
+
131
+ $checklist = self::get_checklist( true );
132
+ if ( $checklist ) {
133
+ Printful_Admin::load_template( 'status-table', array( 'checklist' => $checklist ) );
134
+ } else {
135
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_status_checklist', 'message' => 'Testing your store (this may take up to 30 seconds)...' ) );
136
+ }
137
+
138
+ Printful_Admin::load_template( 'footer' );
139
+ }
140
+
141
+ /**
142
+ * Build the content for status page
143
+ */
144
+ public static function render_status_table_ajax() {
145
+
146
+ $checklist = self::get_checklist();
147
+ Printful_Admin::load_template( 'status-table', array( 'checklist' => $checklist ) );
148
+
149
+ exit;
150
+ }
151
+
152
+ /**
153
+ * Run the tests
154
+ * @param bool $only_cached_results
155
+ * @return array
156
+ */
157
+ public static function get_checklist($only_cached_results = false) {
158
+
159
+ $status = self::instance();
160
+
161
+ $list = get_transient(Printful_Admin_Status::PF_CACHED_CHECKLIST);
162
+
163
+ if ( $only_cached_results || $list ) {
164
+ return $list;
165
+ }
166
+
167
+ $list = array();
168
+ $list['overall_status'] = true;
169
+ $issueCount = 0;
170
+
171
+ foreach ( self::$checklist_items as $item ) {
172
+ $list_item = array();
173
+ $list_item['name'] = $item['name'];
174
+ $list_item['description'] = $item['description'];
175
+
176
+ if ( method_exists( $status, $item['method'] ) ) {
177
+ $list_item['status'] = $status->{$item['method']}();
178
+
179
+ if ( $status->should_result_be_visible( $list_item['status'], $item ) ) {
180
+ $list['items'][] = $list_item;
181
+ }
182
+
183
+ if ( $list_item['status'] == self::PF_STATUS_FAIL) {
184
+ $list['overall_status'] = false;
185
+ $issueCount ++;
186
+ }
187
+ }
188
+ }
189
+
190
+ set_transient( Printful_Admin_Status::PF_CACHED_CHECKLIST, $list, MINUTE_IN_SECONDS );
191
+ set_transient( Printful_Admin_Status::PF_STATUS_ISSUE_COUNT, $issueCount, HOUR_IN_SECONDS );
192
+
193
+ return $list;
194
+ }
195
+
196
+ /**
197
+ * Execute only one test
198
+ * @param $method
199
+ * @return mixed
200
+ */
201
+ public function run_single_test( $method ) {
202
+ if ( method_exists( $this, $method ) ) {
203
+ return $this->{$method}();
204
+ }
205
+ return false;
206
+ }
207
+
208
+ /**
209
+ * @param $status
210
+ * @param bool $item
211
+ *
212
+ * @return int
213
+ */
214
+ private function should_result_be_visible( $status, $item = false ) {
215
+
216
+ if ( ! isset( $item['silent'] ) || ( $item['silent'] === true && $status === self::PF_STATUS_FAIL ) ) { //silent items are only shown on FAIL
217
+ return true;
218
+ }
219
+
220
+ return false;
221
+ }
222
+
223
+ /**
224
+ * Function for checking if thumbnails are resized
225
+ */
226
+ private function check_uploads_write() {
227
+
228
+ $upload_dir = wp_upload_dir();
229
+ if ( is_writable( $upload_dir['basedir'] ) ) {
230
+ return self::PF_STATUS_OK;
231
+ }
232
+
233
+ return self::PF_STATUS_FAIL;
234
+ }
235
+
236
+ /**
237
+ * @return int
238
+ */
239
+ private function check_PHP_memory_limit() {
240
+
241
+ $memory_limit = ini_get( 'memory_limit' );
242
+
243
+ if ( preg_match( '/^(\d+)(.)$/', $memory_limit, $matches ) ) {
244
+ if ( $matches[2] == 'M' ) {
245
+ $memory_limit = $matches[1] * 1024 * 1024; // nnnM -> nnn MB
246
+ } else if ( $matches[2] == 'K' ) {
247
+ $memory_limit = $matches[1] * 1024; // nnnK -> nnn KB
248
+ }
249
+ }
250
+
251
+ $ok = ( $memory_limit >= 128 * 1024 * 1024 ); // at least 128M?
252
+
253
+ if ( $ok ) {
254
+ return self::PF_STATUS_OK;
255
+ }
256
+
257
+ return self::PF_STATUS_FAIL;
258
+ }
259
+
260
+ /**
261
+ * @return int
262
+ */
263
+ private function check_WP_version() {
264
+
265
+ $current = get_bloginfo( 'version' );
266
+
267
+ try {
268
+ $url = 'https://api.wordpress.org/core/version-check/1.7/';
269
+ $response = wp_remote_get( $url );
270
+
271
+ if ( ! is_wp_error( $response ) ) {
272
+ $json = $response['body'];
273
+ $obj = json_decode( $json );
274
+ }
275
+
276
+ if ( empty( $obj ) ) {
277
+ return self::PF_STATUS_FAIL;
278
+ }
279
+
280
+ $version = $obj->offers[0];
281
+ $latest = $version->version;
282
+
283
+ } catch ( Exception $e ) {
284
+ return self::PF_STATUS_FAIL;
285
+ }
286
+
287
+ if ( ! $latest ) {
288
+ return self::PF_STATUS_FAIL;
289
+ }
290
+
291
+ if ( version_compare( $current, $latest, '==' ) ) {
292
+ return self::PF_STATUS_OK;
293
+ }
294
+
295
+ return self::PF_STATUS_FAIL;
296
+ }
297
+
298
+ /**
299
+ * @return int
300
+ */
301
+ private function check_PF_webhooks() {
302
+
303
+ // Query args
304
+ $args = array(
305
+ 'post_type' => 'shop_webhook',
306
+ 'nopaging' => true,
307
+ 'ignore_sticky_posts' => true,
308
+ 's' => self::PF_WEBHOOK_NAME,
309
+ 'post_status' => 'published',
310
+ );
311
+
312
+ // Get the webhooks
313
+ $webhook_results = new WP_Query( $args );
314
+ $webhooks = $webhook_results->posts;
315
+
316
+ if ( count( $webhooks ) > 0 ) {
317
+ return self::PF_STATUS_OK;
318
+ }
319
+
320
+ return self::PF_STATUS_FAIL;
321
+ }
322
+
323
+ /**
324
+ * @return int
325
+ */
326
+ private function check_WC_API_access() {
327
+
328
+ global $wpdb;
329
+
330
+ //if any keys are set
331
+ $query = "SELECT COUNT(*) as key_count FROM {$wpdb->prefix}woocommerce_api_keys";
332
+ $count = $wpdb->get_var( $query );
333
+
334
+ if ( $count == 0 ) {
335
+ return self::PF_STATUS_FAIL;
336
+ }
337
+
338
+ // Get the API key with matching description
339
+ $search = "AND description LIKE '%" . esc_sql( $wpdb->esc_like( wc_clean( self::API_KEY_SEARCH_STRING ) ) ) . "%' ";
340
+ $query = "SELECT * FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search} ORDER BY last_access DESC LIMIT 1";
341
+ $key = $wpdb->get_row( $query );
342
+
343
+ if ( ! empty( $key ) && $key->permissions == 'read_write' ) {
344
+ return self::PF_STATUS_OK;
345
+ }
346
+
347
+ return self::PF_STATUS_WARNING;
348
+ }
349
+
350
+ /**
351
+ * @return int
352
+ */
353
+ private function check_PF_API_key() {
354
+
355
+ $option = get_option( 'woocommerce_printful_settings', array() );
356
+ if ( ! empty( $option['printful_key'] ) && strlen( $option['printful_key'] ) == 36 ) {
357
+ return self::PF_STATUS_OK;
358
+ }
359
+
360
+ return self::PF_STATUS_FAIL;
361
+ }
362
+
363
+ /**
364
+ * @return int
365
+ */
366
+ private function check_PF_API_connect() {
367
+
368
+ if ( Printful_Integration::instance()->is_connected(true) ) {
369
+ return self::PF_STATUS_OK;
370
+ }
371
+
372
+ return self::PF_STATUS_FAIL;
373
+ }
374
+
375
+ /**
376
+ * @return int
377
+ */
378
+ private function check_PHP_time_limit() {
379
+ $time_limit = ini_get( 'max_execution_time' );
380
+
381
+ if ( $time_limit >= 30 ) {
382
+ return self::PF_STATUS_OK;
383
+ }
384
+
385
+ return self::PF_STATUS_FAIL;
386
+ }
387
+
388
+ /**
389
+ * @return int
390
+ */
391
+ private function check_PF_sync_errors() {
392
+
393
+ $sync_log = get_option( Printful_Request_log::PF_OPTION_INCOMING_API_REQUEST_LOG, array() );
394
+ if ( empty( $sync_log ) ) {
395
+ return self::PF_STATUS_OK; //no results means no errors
396
+ }
397
+
398
+ $sync_log = array_reverse( $sync_log );
399
+ $sync_log = array_slice( $sync_log, 0, 6 ); //we only care about last to syncs
400
+
401
+ foreach ( $sync_log as $sl ) {
402
+ if ( ! empty( $sl['result'] ) && $sl['result'] == 'ERROR' ) {
403
+ return self::PF_STATUS_FAIL;
404
+ }
405
+ }
406
+
407
+ return self::PF_STATUS_OK;
408
+ }
409
+
410
+ /**
411
+ * @return int
412
+ */
413
+ private function check_W3_db_cache() {
414
+
415
+ if ( ! is_plugin_active( 'w3-total-cache/w3-total-cache.php' ) ) {
416
+ return self::PF_STATUS_OK;
417
+ }
418
+
419
+ $w3tc_config_file = get_home_path() . 'wp-content/w3tc-config/master.php';
420
+ if ( file_exists( $w3tc_config_file ) && is_readable( $w3tc_config_file ) ) {
421
+ $content = @file_get_contents( $w3tc_config_file );
422
+ $config = @json_decode( substr( $content, 14 ), true );
423
+
424
+ if ( is_array( $config ) && ! empty( $config['dbcache.enabled'] ) ) {
425
+ return ! $config['dbcache.enabled'];
426
+ }
427
+ }
428
+
429
+ return self::PF_STATUS_OK;
430
+ }
431
+
432
+ /**
433
+ * @return int
434
+ */
435
+ private function check_WC_API_enabled() {
436
+
437
+ $enabled = get_option('woocommerce_api_enabled', false);
438
+
439
+ if($enabled == 'yes') {
440
+ return self::PF_STATUS_OK;
441
+ }
442
+
443
+ return self::PF_STATUS_FAIL;
444
+ }
445
+
446
+ /**
447
+ * @return int
448
+ */
449
+ private function check_permalinks() {
450
+
451
+ $permalinks = get_option( 'permalink_structure', false );
452
+
453
+ if ( $permalinks && strlen( $permalinks ) > 0 ) {
454
+ return self::PF_STATUS_OK;
455
+ }
456
+
457
+ return self::PF_STATUS_FAIL;
458
+ }
459
+
460
+ /**
461
+ * @return int
462
+ */
463
+ private function check_printaura_plugin() {
464
+
465
+ if ( ! is_plugin_active( 'printaura-woocommerce-api/printaura-woocommerce-api.php' ) ) {
466
+ return self::PF_STATUS_OK;
467
+ }
468
+
469
+ return self::PF_STATUS_FAIL;
470
+ }
471
+
472
+ /**
473
+ * @return int
474
+ */
475
+ private function check_remote_requests() {
476
+
477
+ // Setup request args.
478
+ $http_args = array(
479
+ 'method' => 'POST',
480
+ 'timeout' => MINUTE_IN_SECONDS,
481
+ 'redirection' => 0,
482
+ 'httpversion' => '1.0',
483
+ 'blocking' => true,
484
+ 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ),
485
+ 'body' => trim( json_encode( array( 'test' => true ) ) ),
486
+ 'headers' => array( 'Content-Type' => 'application/json' ),
487
+ 'cookies' => array(),
488
+ );
489
+
490
+ // Add custom headers.
491
+ $http_args['headers']['X-WC-Webhook-Source'] = home_url( '/' ); // Since 2.6.0.
492
+ $http_args['headers']['X-WC-Webhook-Topic'] = self::PF_REMOTE_REQUEST_TOPIC;
493
+
494
+ // Webhook away!
495
+ $response = wp_safe_remote_request( Printful_Base::get_printful_host() . self::PF_REMOTE_REQUEST_URL, $http_args );
496
+
497
+ if ( is_wp_error( $response ) ) {
498
+ return self::PF_STATUS_FAIL;
499
+ }
500
+
501
+ return self::PF_STATUS_OK;
502
+ }
503
+
504
+ /**
505
+ * @return int
506
+ */
507
+ private function check_WC_auth_url_access() {
508
+
509
+ $url = home_url( '/' ) . 'wc-auth/v1/authorize?app_name=Printful&scope=read_write&user_id=1&return_url=https%3A%2F%2Fwww.printful.com%2Fdashboard%2Fwoocommerce%2Freturn&callback_url=https%3A%2F%2Fwww.printful.com%2Fwebhook%2Fwoocommerce-auth-callback';
510
+ $http_args = array(
511
+ 'timeout' => 60,
512
+ 'method' => 'GET',
513
+ 'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36',
514
+ );
515
+
516
+ $response = wp_safe_remote_get( $url, $http_args );
517
+ $code = $response['response']['code'];
518
+
519
+ if ( $code == 200 ) {
520
+ return self::PF_STATUS_OK;
521
+ }
522
+
523
+ return self::PF_STATUS_FAIL;
524
+ }
525
+
526
+ /**
527
+ * @return int
528
+ */
529
+ private function check_WP_error_logs() {
530
+
531
+ if (
532
+ ( defined( 'WP_DEBUG' ) && WP_DEBUG == true )
533
+ &&
534
+ ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG == true )
535
+ ) {
536
+ return self::PF_STATUS_OK;
537
+ }
538
+
539
+ return self::PF_STATUS_FAIL;
540
+ }
541
+ }
includes/class-printful-admin-support.php ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin_Support {
5
+
6
+ public static $_instance;
7
+
8
+ /**
9
+ * @return Printful_Admin_Support
10
+ */
11
+ public static function instance() {
12
+
13
+ if ( is_null( self::$_instance ) ) {
14
+ self::$_instance = new self();
15
+ }
16
+
17
+ return self::$_instance;
18
+ }
19
+
20
+ /**
21
+ * Setup the view
22
+ */
23
+ public static function view() {
24
+
25
+ $support = self::instance();
26
+ $support->render();
27
+ }
28
+
29
+ /**
30
+ * Display support report
31
+ */
32
+ public function render() {
33
+
34
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
35
+
36
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_status_report', 'message' => 'Building support report (this may take up to 30 seconds)...' ) );
37
+
38
+ Printful_Admin::load_template( 'support-info' );
39
+
40
+ Printful_Admin::load_template( 'footer' );
41
+ }
42
+
43
+ /**
44
+ * Build the content for status page
45
+ */
46
+ public static function render_status_report_ajax() {
47
+
48
+ $status_report = self::instance()->generate_report();
49
+ Printful_Admin::load_template( 'status-report', array( 'status_report' => $status_report ) );
50
+
51
+ exit;
52
+ }
53
+
54
+ /**
55
+ * Create system status report
56
+ * @return string
57
+ */
58
+ public function generate_report() {
59
+
60
+ if ( ! class_exists( 'WC_REST_System_Status_Controller' ) ) {
61
+ return false;
62
+ }
63
+
64
+ $system_status = new WC_REST_System_Status_Controller; //make use of the woocommerce system status report
65
+
66
+ ob_start();
67
+
68
+ echo "##### Printful Checklist #####\n";
69
+ $checklist = Printful_Admin_Status::get_checklist();
70
+ foreach ( $checklist['items'] as $item ) {
71
+
72
+ $status = 'OK';
73
+ if($item['status'] == Printful_Admin_Status::PF_STATUS_WARNING) {
74
+ $status = 'WARNING';
75
+ } else if($item['status'] == Printful_Admin_Status::PF_STATUS_FAIL) {
76
+ $status = 'FAIL';
77
+ }
78
+ echo str_pad( esc_html($item['name']), 30 ) . "=> " . esc_html($status) . "\n";
79
+ }
80
+
81
+ echo "\n\n##### Printful Last Sync's #####\n";
82
+ $syncReport = $this->get_sync_report();
83
+ if ( ! empty( $syncReport ) ) {
84
+
85
+ echo str_pad( 'Date', 30 );
86
+ echo str_pad( 'Request', 30 );
87
+ echo str_pad( 'Message', 30 );
88
+ echo "\n";
89
+
90
+ foreach ( $syncReport as $sr ) {
91
+
92
+ echo str_pad( esc_html($sr['date']) . ';', 30 );
93
+ echo str_pad( esc_html($sr['path']) . ';', 30 );
94
+ echo str_pad( esc_html($sr['message']) . ';', 30 );
95
+
96
+ echo "\n";
97
+ }
98
+ }
99
+
100
+ echo "\n\n##### Environment #####\n";
101
+ $this->output_report_block( $system_status->get_environment_info() );
102
+
103
+ echo "\n\n##### Database #####\n";
104
+ $this->output_report_block( $system_status->get_database_info() );
105
+
106
+ echo "\n\n##### Active Plugins #####\n";
107
+ foreach ( $system_status->get_active_plugins() as $plugin ) {
108
+ if ( ! empty( $plugin['name'] ) ) {
109
+ echo esc_html( $plugin['name'] ) . " (" . $plugin['version'] . ")\n";
110
+ }
111
+ }
112
+
113
+ echo "\n\n##### Theme #####\n";
114
+ $this->output_report_block( $system_status->get_theme_info() );
115
+
116
+ echo "\n\n##### WooCommerce settings #####\n";
117
+ $this->output_report_block( $system_status->get_settings() );
118
+
119
+ if (
120
+ ( defined( 'WP_DEBUG' ) && WP_DEBUG == true )
121
+ &&
122
+ ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG == true )
123
+ ) {
124
+ echo "\n\n##### Wordpress Error log (last 50 entries) #####\n";
125
+ $contents = $this->get_error_log_contents();
126
+ if ( $contents ) {
127
+ print_r( $contents );
128
+ }
129
+ }
130
+
131
+ $report = ob_get_contents();
132
+ ob_end_clean();
133
+
134
+ return $report;
135
+ }
136
+
137
+ /**
138
+ * Get last 50 lines of error log
139
+ * @return bool|string
140
+ */
141
+ public function get_error_log_contents() {
142
+
143
+ if ( ! function_exists( 'fopen' ) ) {
144
+ return false;
145
+ }
146
+
147
+ return $this->file_tail( WP_CONTENT_DIR . '/debug.log', 50 );
148
+ }
149
+
150
+ /**
151
+ * source: https://gist.github.com/lorenzos/1711e81a9162320fde20
152
+ * @param $filepath
153
+ * @param int $lines
154
+ * @param bool $adaptive
155
+ *
156
+ * @return bool|string
157
+ */
158
+ function file_tail( $filepath, $lines = 1, $adaptive = true ) {
159
+
160
+ $f = @fopen( $filepath, "rb" );
161
+ if ( $f === false ) {
162
+ return false;
163
+ }
164
+
165
+ // Sets buffer size, according to the number of lines to retrieve.
166
+ if ( ! $adaptive ) {
167
+ $buffer = 4096;
168
+ } else {
169
+ $buffer = ( $lines < 2 ? 64 : ( $lines < 10 ? 512 : 4096 ) );
170
+ }
171
+
172
+ // Jump to last character
173
+ fseek( $f, - 1, SEEK_END );
174
+ if ( fread( $f, 1 ) != "\n" ) {
175
+ $lines -= 1;
176
+ }
177
+
178
+ $output = '';
179
+ $chunk = '';
180
+ while ( ftell( $f ) > 0 && $lines >= 0 ) {
181
+ // Figure out how far back we should jump
182
+ $seek = min( ftell( $f ), $buffer );
183
+ // Do the jump (backwards, relative to where we are)
184
+ fseek( $f, - $seek, SEEK_CUR );
185
+ $output = ( $chunk = fread( $f, $seek ) ) . $output;
186
+ fseek( $f, - mb_strlen( $chunk, '8bit' ), SEEK_CUR );
187
+ $lines -= substr_count( $chunk, "\n" );
188
+ }
189
+ while ( $lines ++ < 0 ) {
190
+ $output = substr( $output, strpos( $output, "\n" ) + 1 );
191
+ }
192
+ fclose( $f );
193
+
194
+ return trim( $output );
195
+ }
196
+
197
+
198
+ /**
199
+ * Displays the data
200
+ * @param $data
201
+ */
202
+ public function output_report_block( $data ) {
203
+
204
+ foreach ( $data as $key => $item ) {
205
+ if ( is_string( $item ) ) {
206
+ echo str_pad( $key, 30 ) . "=> " . $item . "\n";
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Returns log of last incoming API requests from Printful
213
+ * @return array
214
+ */
215
+ public function get_sync_report() {
216
+
217
+ $report = array();
218
+ $request_log = get_option( Printful_Request_log::PF_OPTION_INCOMING_API_REQUEST_LOG, array() );
219
+ $request_log = array_reverse( $request_log );
220
+
221
+ if ( empty( $request_log ) ) {
222
+ return $report;
223
+ }
224
+
225
+ foreach ( $request_log as $log ) {
226
+ $report[] = array(
227
+ 'date' => $log['date'],
228
+ 'path' => $log['request'],
229
+ 'message' => $log['result'],
230
+ );
231
+ }
232
+
233
+ return $report;
234
+ }
235
+ }
includes/class-printful-admin.php ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin {
5
+
6
+ const MENU_TITLE_TOP = 'Printful';
7
+ const PAGE_TITLE_DASHBOARD = 'Dashboard';
8
+ const MENU_TITLE_DASHBOARD = 'Dashboard';
9
+ const MENU_SLUG_DASHBOARD = 'printful-dashboard';
10
+ const CAPABILITY = 'manage_options';
11
+
12
+ public static function init() {
13
+ $admin = new self;
14
+ $admin->register_admin();
15
+ }
16
+
17
+ /**
18
+ * Register admin scripts
19
+ */
20
+ public function register_admin() {
21
+
22
+ add_action( 'admin_menu', array($this, 'register_admin_menu_page'));
23
+ add_action( 'admin_enqueue_scripts', array($this, 'add_admin_styles'));
24
+ add_action( 'admin_enqueue_scripts', array($this, 'add_admin_scripts'));
25
+ add_action( 'wp_enqueue_scripts', array($this, 'add_global_style'));
26
+ add_action( 'admin_bar_menu', array($this, 'add_printful_status_toolbar'), 999 );
27
+ }
28
+
29
+ /**
30
+ * Loads stylesheets used in printful admin pages
31
+ * @param $hook
32
+ */
33
+ public function add_admin_styles($hook) {
34
+
35
+ wp_enqueue_style( 'printful-global', plugins_url( '../assets/css/global.css', __FILE__ ) );
36
+
37
+ if ( strpos( $hook, 'printful-dashboard' ) !== false ) {
38
+ wp_enqueue_style( 'printful-dashboard', plugins_url( '../assets/css/dashboard.css', __FILE__ ) );
39
+ wp_enqueue_style( 'printful-status', plugins_url( '../assets/css/status.css', __FILE__ ) );
40
+ wp_enqueue_style( 'printful-support', plugins_url( '../assets/css/support.css', __FILE__ ) );
41
+ wp_enqueue_style( 'printful-settings', plugins_url( '../assets/css/settings.css', __FILE__ ) );
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Loads stylesheet for printful toolbar element
47
+ */
48
+ public function add_global_style() {
49
+ wp_enqueue_style( 'printful-global', plugins_url( '../assets/css/global.css', __FILE__ ) );
50
+ }
51
+
52
+ /**
53
+ * Loads scripts used in printful admin pages
54
+ * @param $hook
55
+ */
56
+ public function add_admin_scripts($hook) {
57
+ if ( strpos( $hook, 'printful-dashboard' ) !== false ) {
58
+ wp_enqueue_script( 'printful-settings', plugins_url( '../assets/js/settings.js', __FILE__ ) );
59
+ wp_enqueue_script( 'printful-connect', plugins_url( '../assets/js/connect.js', __FILE__ ) );
60
+ wp_enqueue_script( 'printful-block-loader', plugins_url( '../assets/js/block-loader.js', __FILE__ ) );
61
+ wp_enqueue_script( 'printful-intercom', plugins_url( '../assets/js/intercom.min.js', __FILE__ ) );
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Register admin menu pages
67
+ */
68
+ public function register_admin_menu_page() {
69
+
70
+ add_menu_page(
71
+ self::PAGE_TITLE_DASHBOARD,
72
+ self::MENU_TITLE_TOP,
73
+ self::CAPABILITY,
74
+ self::MENU_SLUG_DASHBOARD,
75
+ array( 'Printful_Admin', 'route' ),
76
+ Printful_Base::get_asset_url() . 'images/printful-menu-icon.png',
77
+ 58
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Route the tabs
83
+ */
84
+ public static function route() {
85
+
86
+ $tabs = array(
87
+ 'dashboard' => 'Printful_Admin_Dashboard',
88
+ 'settings' => 'Printful_Admin_Settings',
89
+ 'status' => 'Printful_Admin_Status',
90
+ 'support' => 'Printful_Admin_Support',
91
+ );
92
+
93
+ $tab = ( ! empty( $_GET['tab'] ) ? $_GET['tab'] : 'dashboard' );
94
+ if ( ! empty( $tabs[ $tab ] ) ) {
95
+ call_user_func( array( $tabs[ $tab ], 'view' ) );
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Get the tabs used in printful admin pages
101
+ * @return array
102
+ */
103
+ public static function get_tabs() {
104
+
105
+ $tabs = array(
106
+ array( 'name' => 'Settings', 'tab_url' => 'settings' ),
107
+ array( 'name' => 'Status', 'tab_url' => 'status' ),
108
+ array( 'name' => 'Support', 'tab_url' => 'support' ),
109
+ );
110
+
111
+ if ( Printful_Integration::instance()->is_connected() ) {
112
+ array_unshift( $tabs, array( 'name' => 'Dashboard', 'tab_url' => false ) );
113
+ } else {
114
+ array_unshift( $tabs, array( 'name' => 'Connect', 'tab_url' => false ) );
115
+ }
116
+
117
+ return $tabs;
118
+ }
119
+
120
+ /**
121
+ * Create the printful toolbar
122
+ * @param $wp_admin_bar
123
+ */
124
+ public function add_printful_status_toolbar( $wp_admin_bar ) {
125
+
126
+ $issueCount = get_transient( Printful_Admin_Status::PF_STATUS_ISSUE_COUNT );
127
+
128
+ if ( $issueCount ) {
129
+ //Add top level menu item
130
+ $args = array(
131
+ 'id' => 'printful_toolbar',
132
+ 'title' => 'Printful Integration' . ( $issueCount > 0 ? ' <span class="printful-toolbar-issues">' . esc_attr( $issueCount ) . '</span>' : '' ),
133
+ 'href' => get_admin_url( null, 'admin.php?page=' . Printful_Admin::MENU_SLUG_DASHBOARD ),
134
+ 'meta' => array( 'class' => 'printful-toolbar' ),
135
+ );
136
+ $wp_admin_bar->add_node( $args );
137
+
138
+ //Add status
139
+ $args = array(
140
+ 'id' => 'printful_toolbar_status',
141
+ 'parent' => 'printful_toolbar',
142
+ 'title' => 'Integration status' . ( $issueCount > 0 ? ' (' . esc_attr( $issueCount ) . _n( ' issue', ' issues', $issueCount ) . ')' : '' ),
143
+ 'href' => get_admin_url( null, 'admin.php?page=' . Printful_Admin::MENU_SLUG_DASHBOARD . '&tab=status' ),
144
+ 'meta' => array( 'class' => 'printful-toolbar-status' ),
145
+ );
146
+ $wp_admin_bar->add_node( $args );
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Load a template file. Extract any variables that are passed
152
+ * @param $name
153
+ * @param array $variables
154
+ */
155
+ public static function load_template( $name, $variables = array() ) {
156
+
157
+ if ( ! empty( $variables ) ) {
158
+ extract( $variables );
159
+ }
160
+
161
+ $filename = plugin_dir_path( __FILE__ ) . 'templates/' . $name . '.php';
162
+ if ( file_exists( $filename ) ) {
163
+ include( $filename );
164
+ }
165
+ }
166
+
167
+ }
includes/class-printful-api-resource.php CHANGED
@@ -6,32 +6,119 @@ class Printful_API_Resource extends WC_API_Resource {
6
  /** @var string $base the route base */
7
  protected $base = '/printful';
8
 
9
- public function register_routes( $routes ) {
10
 
11
- $routes[ $this->base.'/version' ] = array(
12
- array( array( $this, 'get_status' ), WC_API_Server::READABLE | WC_API_Server::HIDDEN_ENDPOINT),
13
- );
14
- return $routes;
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  /**
18
  * Allow remotely get plugin version for debug purposes
19
  */
20
- public function get_status(){
21
- $error = false;
22
- try {
23
- $client = Printful_Integration::instance()->get_client();
24
- $storeData = $client->get('store');
25
- }
26
- catch(Exception $e){
27
- $error = $e->getMessage();
28
- }
29
- return array(
30
- 'version' => Printful_Base::VERSION,
31
- 'api_key' => !empty(Printful_Integration::instance()->settings['printful_key']),
32
- 'store_id' => !empty($storeData['id']) ? $storeData['id'] : false,
33
- 'error' => $error,
34
- );
35
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  }
6
  /** @var string $base the route base */
7
  protected $base = '/printful';
8
 
9
+ public function register_routes( $routes ) {
10
 
11
+ $routes[ $this->base . '/version' ] = array(
12
+ array( array( $this, 'get_status' ), WC_API_Server::READABLE | WC_API_Server::ACCEPT_DATA ),
13
+ );
14
+
15
+ $routes[ $this->base . '/access' ] = array(
16
+ array( array( $this, 'put_access_data' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
17
+ );
18
+
19
+ $routes[ $this->base . '/products/(?P<product_id>\d+)/size-chart' ] = array(
20
+ array( array( $this, 'post_size_chart' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
21
+ );
22
+
23
+ return $routes;
24
+ }
25
+
26
+ /**
27
+ * Push size chart to meta property
28
+ * @param $product_id
29
+ * @param $data
30
+ * @return array|WP_Error
31
+ */
32
+ public function post_size_chart( $product_id, $data ) {
33
+
34
+ if ( empty( $data['size_chart'] ) ) {
35
+ return new WP_Error( 'printful_api_size_chart_empty', __( 'No size chart was provided', 'printful' ), array( 'status' => 400 ) );
36
+ }
37
+
38
+ //product id is valid
39
+ $product_id = intval( $product_id );
40
+ if ( $product_id < 1 ) {
41
+ return new WP_Error( 'printful_api_product_not_found', __( 'The product ID is invalid', 'printful' ), array( 'status' => 400 ) );
42
+ }
43
+
44
+ //product exists
45
+ $product = get_post( $product_id );
46
+ if ( empty( $product ) || $product->post_type != 'product' ) {
47
+ return new WP_Error( 'printful_api_product_not_found', __( 'The product is not found', 'printful' ), array( 'status' => 400 ) );
48
+ }
49
+
50
+ //how about permissions?
51
+ $post_type = get_post_type_object( $product->post_type );
52
+ if ( ! current_user_can( $post_type->cap->edit_post, $product->ID ) ) {
53
+ return new WP_Error( 'printful_api_user_cannot_edit_product_size_chart', __( 'You do not have permission to edit the size chart', 'printful' ), array( 'status' => 401 ) );
54
+ }
55
+
56
+ //lets do this
57
+ update_post_meta( $product_id, 'pf_size_chart', htmlspecialchars( $data['size_chart'] ) );
58
+
59
+ return array(
60
+ 'product' => $product,
61
+ 'size_chart' => $data['size_chart'],
62
+ );
63
+ }
64
 
65
  /**
66
  * Allow remotely get plugin version for debug purposes
67
  */
68
+ public function get_status() {
69
+
70
+ $error = false;
71
+ try {
72
+ $client = Printful_Integration::instance()->get_client();
73
+ $storeData = $client->get( 'store' );
74
+ } catch ( Exception $e ) {
75
+ $error = $e->getMessage();
76
+ }
77
+
78
+ $checklist = Printful_Admin_Status::get_checklist();
79
+ $checklist['overall_status'] = ( $checklist['overall_status'] ? 'OK' : 'FAIL' );
80
+
81
+ foreach ( $checklist['items'] as $key => $item ) {
82
+ $item['status'] = ( $item['status'] ? 'OK' : 'FAIL' );
83
+ $checklist['items'][ $key ] = $item;
84
+ }
85
+
86
+ return array(
87
+ 'version' => Printful_Base::VERSION,
88
+ 'api_key' => Printful_Integration::instance()->get_option('printful_key'),
89
+ 'store_id' => ! empty( $storeData['id'] ) ? $storeData['id'] : false,
90
+ 'error' => $error,
91
+ 'status_checklist' => $checklist,
92
+ );
93
+ }
94
+
95
+ /**
96
+ * @param $data
97
+ *
98
+ * @return array
99
+ */
100
+ public function put_access_data( $data ) {
101
+
102
+ $error = false;
103
+
104
+ $apiKey = $data['accessKey'];
105
+ $storeId = $data['storeId'];
106
+
107
+ $option = Printful_Integration::instance()->get_option( 'printful_settings', array() );
108
+ $storeId = intval( $storeId );
109
+
110
+ if ( ! is_string( $apiKey ) || strlen( $apiKey ) == 0 || $storeId == 0 ) {
111
+ $error = 'Failed to update access data';
112
+ }
113
+
114
+ $option['printful_key'] = $apiKey;
115
+ $option['printful_store_id'] = $storeId;
116
+
117
+ Printful_Integration::instance()->update_settings( $option );
118
+
119
+ return array(
120
+ 'error' => $error,
121
+ );
122
+ }
123
 
124
  }
includes/class-printful-carriers.php ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Carriers {
5
+
6
+ public $carriers;
7
+ public static $_instance;
8
+
9
+ public static function instance() {
10
+
11
+ if ( is_null( self::$_instance ) ) {
12
+ self::$_instance = new self();
13
+ }
14
+
15
+ return self::$_instance;
16
+ }
17
+
18
+ public function __construct() {
19
+
20
+ $this->carriers = $this->get_carriers();
21
+ }
22
+
23
+ /**
24
+ * Get carrier data
25
+ * @return mixed
26
+ */
27
+ public function get_carriers() {
28
+
29
+ $carriers = get_transient( 'printful_carriers' );
30
+ if ( ! $carriers ) {
31
+ $carriers = $this->refresh_carriers();
32
+ }
33
+
34
+ return $carriers;
35
+ }
36
+
37
+ /**
38
+ * Refresh carrier data from Printful
39
+ * @return mixed
40
+ */
41
+ public function refresh_carriers() {
42
+
43
+ try {
44
+ $carriers = Printful_Integration::instance()->get_client()->get( 'store/carriers-services' );
45
+ $this->update_carrier_cache( $carriers );
46
+ } catch (PrintfulApiException $e) {
47
+ $carriers = array();
48
+ } catch (PrintfulException $e) {
49
+ $carriers = array();
50
+ }
51
+
52
+ return $carriers;
53
+ }
54
+
55
+ /**
56
+ * Update carrier transient
57
+ * @param $carriers
58
+ */
59
+ public function update_carrier_cache($carriers) {
60
+
61
+ set_transient( 'printful_carriers', $carriers, MINUTE_IN_SECONDS * 5); //5mins
62
+ }
63
+
64
+ /**
65
+ * Post carrier settings to printful
66
+ * @param $data
67
+ * @return mixed
68
+ */
69
+ public function post_carriers( $data ) {
70
+
71
+ if ( empty( $data ) ) {
72
+ return false;
73
+ }
74
+
75
+ $shipping = new self;
76
+ try {
77
+ $carriers = Printful_Integration::instance()->get_client()->post( 'store/carriers-services', $data );
78
+ $shipping->update_carrier_cache( $carriers );
79
+ } catch ( PrintfulApiException $e ) {
80
+ $carriers = false;
81
+ } catch (PrintfulException $e) {
82
+ $carriers = false;
83
+ }
84
+
85
+ return $carriers;
86
+ }
87
+
88
+ }
includes/class-printful-client.php CHANGED
@@ -1,47 +1,54 @@
1
  <?php
 
 
2
  /**
3
  * Printful API client
4
  */
5
-
6
  class Printful_Client {
7
- private $key = false;
8
- private $lastResponseRaw;
9
- private $lastResponse;
10
- private $userAgent = 'Printful WooCommerce Plugin';
11
- private $apiUrl = 'https://api.theprintful.com/';
12
-
13
- /**
14
- * @param string $key Printful Store API key
15
- * @param string $disable_ssl Force HTTP instead of HTTPS for API requests
16
- * @throws PrintfulException if the library failed to initialize
17
- */
18
- public function __construct($key = '', $disable_ssl = false){
19
- $key = (string)$key;
20
 
21
- $this->userAgent .= ' '.Printful_Base::VERSION.' (WP '. get_bloginfo( 'version' ) . ' + WC ' . WC()->version.')';
22
-
23
- if(!function_exists('json_decode') || !function_exists('json_encode')){
24
- throw new PrintfulException('PHP JSON extension is required for the Printful API library to work!');
25
- }
26
- if(strlen($key) < 32){
27
- throw new PrintfulException('Missing or invalid Printful store key!');
28
- }
29
- $this->key = $key;
30
-
31
- if($disable_ssl)
32
- {
33
- $this->apiUrl = str_replace('https://','http://', $this->apiUrl);
34
- }
35
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  /**
38
  * Returns total available item count from the last request if it supports paging (e.g order list) or null otherwise.
39
  *
40
  * @return int|null Item count
41
  */
42
- public function getItemCount(){
43
- return isset($this->lastResponse['paging']['total']) ? $this->lastResponse['paging']['total'] : null;
44
- }
45
 
46
  /**
47
  * Perform a GET request to the API
@@ -51,9 +58,9 @@ class Printful_Client {
51
  * @throws PrintfulApiException if the API call status code is not in the 2xx range
52
  * @throws PrintfulException if the API call has failed or the response is invalid
53
  */
54
- public function get($path, $params = array()){
55
- return $this->request('GET', $path, $params);
56
- }
57
 
58
  /**
59
  * Perform a DELETE request to the API
@@ -63,9 +70,9 @@ class Printful_Client {
63
  * @throws PrintfulApiException if the API call status code is not in the 2xx range
64
  * @throws PrintfulException if the API call has failed or the response is invalid
65
  */
66
- public function delete($path, $params = array()){
67
- return $this->request('DELETE', $path, $params);
68
- }
69
 
70
  /**
71
  * Perform a POST request to the API
@@ -76,9 +83,9 @@ class Printful_Client {
76
  * @throws PrintfulApiException if the API call status code is not in the 2xx range
77
  * @throws PrintfulException if the API call has failed or the response is invalid
78
  */
79
- public function post($path, $data = array(), $params = array()){
80
- return $this->request('POST', $path, $params, $data);
81
- }
82
  /**
83
  * Perform a PUT request to the API
84
  * @param string $path Request path (e.g. 'orders' or 'orders/123')
@@ -88,65 +95,77 @@ class Printful_Client {
88
  * @throws PrintfulApiException if the API call status code is not in the 2xx range
89
  * @throws PrintfulException if the API call has failed or the response is invalid
90
  */
91
- public function put($path, $data = array(), $params = array()){
92
- return $this->request('PUT', $path, $params, $data);
93
- }
94
 
95
  /**
96
  * Return raw response data from the last request
97
  * @return string|null Response data
98
  */
99
- public function getLastResponseRaw(){
100
- return $this->lastResponseRaw;
101
- }
102
  /**
103
  * Return decoded response data from the last request
104
  * @return array|null Response data
105
  */
106
- public function getLastResponse(){
107
- return $this->lastResponse;
108
- }
109
-
110
- /**
111
- * Internal request implementation
112
- */
113
- private function request($method, $path, array $params = array(), $data = null){
114
-
115
- $this->lastResponseRaw = null;
116
- $this->lastResponse = null;
117
-
118
- $url = trim($path,'/');
119
-
120
- if(!empty($params)){
121
- $url .= '?'.http_build_query($params);
122
- }
123
-
124
- $result = wp_remote_get($this->apiUrl . $url, array(
125
- 'timeout' => 10,
126
- 'user-agent' => $this->userAgent,
127
- 'method' => $method,
128
- 'headers' => array(
129
- 'Authorization' => 'Basic ' . base64_encode($this->key)
130
- ),
131
- 'body' => $data !== null ? json_encode($data) : null
132
- ));
133
-
134
- if (is_wp_error($result))
135
- {
136
- throw new PrintfulException("API request failed - ". $result->get_error_message());
137
- }
138
- $this->lastResponseRaw = $result['body'];
139
- $this->lastResponse = $response = json_decode($result['body'], true);
140
-
141
- if(!isset($response['code'], $response['result'])){
142
- throw new PrintfulException('Invalid API response');
143
- }
144
- $status = (int)$response['code'];
145
- if($status < 200 || $status >= 300){
146
- throw new PrintfulApiException((string)$response['result'], $status);
147
- }
148
- return $response['result'];
149
- }
 
 
 
 
 
 
 
 
 
 
 
 
150
  }
151
 
152
  /**
1
  <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
  /**
5
  * Printful API client
6
  */
 
7
  class Printful_Client {
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ private $key = false;
10
+ private $lastResponseRaw;
11
+ private $lastResponse;
12
+ private $userAgent = 'Printful WooCommerce Plugin';
13
+ private $apiUrl = 'https://api.printful.com/';
14
+
15
+ /**
16
+ * @param string $key Printful Store API key
17
+ * @param bool|string $disable_ssl Force HTTP instead of HTTPS for API requests
18
+ *
19
+ * @throws PrintfulException if the library failed to initialize
20
+ */
21
+ public function __construct( $key = '', $disable_ssl = false ) {
22
+
23
+ $key = (string) $key;
24
+
25
+ $this->userAgent .= ' ' . Printful_Base::VERSION . ' (WP ' . get_bloginfo( 'version' ) . ' + WC ' . WC()->version . ')';
26
+
27
+ if ( ! function_exists( 'json_decode' ) || ! function_exists( 'json_encode' ) ) {
28
+ throw new PrintfulException( 'PHP JSON extension is required for the Printful API library to work!' );
29
+ }
30
+ if ( strlen( $key ) < 32 ) {
31
+ throw new PrintfulException( 'Missing or invalid Printful store key!' );
32
+ }
33
+ $this->key = $key;
34
+
35
+ if ( $disable_ssl ) {
36
+ $this->apiUrl = str_replace( 'https://', 'http://', $this->apiUrl );
37
+ }
38
+
39
+ if ( defined( 'PF_DEV_API_HOST' ) ) {
40
+ $this->apiUrl = PF_DEV_API_HOST;
41
+ }
42
+ }
43
 
44
  /**
45
  * Returns total available item count from the last request if it supports paging (e.g order list) or null otherwise.
46
  *
47
  * @return int|null Item count
48
  */
49
+ public function getItemCount() {
50
+ return isset( $this->lastResponse['paging']['total'] ) ? $this->lastResponse['paging']['total'] : null;
51
+ }
52
 
53
  /**
54
  * Perform a GET request to the API
58
  * @throws PrintfulApiException if the API call status code is not in the 2xx range
59
  * @throws PrintfulException if the API call has failed or the response is invalid
60
  */
61
+ public function get( $path, $params = array() ) {
62
+ return $this->request( 'GET', $path, $params );
63
+ }
64
 
65
  /**
66
  * Perform a DELETE request to the API
70
  * @throws PrintfulApiException if the API call status code is not in the 2xx range
71
  * @throws PrintfulException if the API call has failed or the response is invalid
72
  */
73
+ public function delete( $path, $params = array() ) {
74
+ return $this->request( 'DELETE', $path, $params );
75
+ }
76
 
77
  /**
78
  * Perform a POST request to the API
83
  * @throws PrintfulApiException if the API call status code is not in the 2xx range
84
  * @throws PrintfulException if the API call has failed or the response is invalid
85
  */
86
+ public function post( $path, $data = array(), $params = array() ) {
87
+ return $this->request( 'POST', $path, $params, $data );
88
+ }
89
  /**
90
  * Perform a PUT request to the API
91
  * @param string $path Request path (e.g. 'orders' or 'orders/123')
95
  * @throws PrintfulApiException if the API call status code is not in the 2xx range
96
  * @throws PrintfulException if the API call has failed or the response is invalid
97
  */
98
+ public function put( $path, $data = array(), $params = array() ) {
99
+ return $this->request( 'PUT', $path, $params, $data );
100
+ }
101
 
102
  /**
103
  * Return raw response data from the last request
104
  * @return string|null Response data
105
  */
106
+ public function getLastResponseRaw() {
107
+ return $this->lastResponseRaw;
108
+ }
109
  /**
110
  * Return decoded response data from the last request
111
  * @return array|null Response data
112
  */
113
+ public function getLastResponse() {
114
+ return $this->lastResponse;
115
+ }
116
+
117
+ /**
118
+ * Internal request implementation
119
+ *
120
+ * @param $method
121
+ * @param $path
122
+ * @param array $params
123
+ * @param null $data
124
+ *
125
+ * @return
126
+ * @throws PrintfulApiException
127
+ * @throws PrintfulException
128
+ */
129
+ private function request( $method, $path, array $params = array(), $data = null ) {
130
+
131
+ $this->lastResponseRaw = null;
132
+ $this->lastResponse = null;
133
+
134
+ $url = trim( $path, '/' );
135
+
136
+ if ( ! empty( $params ) ) {
137
+ $url .= '?' . http_build_query( $params );
138
+ }
139
+
140
+ $request = array(
141
+ 'timeout' => 10,
142
+ 'user-agent' => $this->userAgent,
143
+ 'method' => $method,
144
+ 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( $this->key ) ),
145
+ 'body' => $data !== null ? json_encode( $data ) : null,
146
+ );
147
+
148
+ $result = wp_remote_get( $this->apiUrl . $url, $request );
149
+
150
+ //allow other methods to hook in on the api result
151
+ $result = apply_filters( 'printful_api_result', $result, $method, $this->apiUrl . $url, $request );
152
+
153
+ if ( is_wp_error( $result ) ) {
154
+ throw new PrintfulException( "API request failed - " . $result->get_error_message() );
155
+ }
156
+ $this->lastResponseRaw = $result['body'];
157
+ $this->lastResponse = $response = json_decode( $result['body'], true );
158
+
159
+ if ( ! isset( $response['code'], $response['result'] ) ) {
160
+ throw new PrintfulException( 'Invalid API response' );
161
+ }
162
+ $status = (int) $response['code'];
163
+ if ( $status < 200 || $status >= 300 ) {
164
+ throw new PrintfulApiException( (string) $response['result'], $status );
165
+ }
166
+
167
+ return $response['result'];
168
+ }
169
  }
170
 
171
  /**
includes/class-printful-integration.php CHANGED
@@ -1,243 +1,152 @@
1
  <?php
2
  if ( ! defined( 'ABSPATH' ) ) exit;
3
 
4
- class Printful_Integration extends WC_Integration
5
  {
6
-
7
- public static $_instance;
8
-
9
- public static function instance() {
10
- if ( is_null( self::$_instance ) )
11
- self::$_instance = new self();
12
- return self::$_instance;
13
- }
14
-
15
- public function __construct()
16
- {
17
- $this->id = 'printful';
18
- $this->method_title = 'Printful Integration';
19
- $this->method_description = 'Enable integration with Printful fulfillment service';
20
-
21
- add_action('woocommerce_update_options_integration_' . $this->id, array($this, 'process_admin_options'));
22
-
23
- $this->init_form_fields();
24
- $this->init_settings();
25
-
26
- if ($this->get_option('calculate_tax') == 'yes')
27
- {
28
- //Update tax options if taxes are enabled
29
- if (get_option('woocommerce_calc_taxes') != 'yes')
30
- {
31
- update_option('woocommerce_calc_taxes', 'yes');
32
- }
33
- if (get_option('woocommerce_tax_based_on') != 'shipping')
34
- {
35
- update_option('woocommerce_tax_based_on', 'shipping');
36
- }
37
-
38
- //Show warning in the tax settings section
39
- add_action('woocommerce_settings_tax_options', array($this, 'show_tax_warning'));
40
-
41
- //Override tax rates calculated by Woocommerce
42
- add_filter('woocommerce_matched_tax_rates', array($this, 'calculate_tax'), 10, 6);
43
- }
44
-
45
- self::$_instance = $this;
46
- }
47
-
48
- public function get_client()
49
- {
50
- require_once 'class-printful-client.php';
51
- $client = new Printful_Client($this->get_option('printful_key'), $this->get_option('disable_ssl') == 'yes');
52
- return $client;
53
- }
54
-
55
- public function init_settings()
56
- {
57
- parent::init_settings();
58
-
59
- //Copy settings from old plugin settings location if upgraded from plugin version 1.0.2
60
- if ($this->get_option('printful_key') === false)
61
- {
62
- $oldsettings = get_option( $this->plugin_id . 'printful_shipping_settings', null );
63
- $this->settings['printful_key'] = '';
64
-
65
- if(!empty($oldsettings['printful_key']))
66
- {
67
- $this->settings['printful_key'] = $oldsettings['printful_key'];
68
- }
69
- if(!empty($oldsettings['disable_ssl']))
70
- {
71
- $this->settings['disable_ssl'] = $oldsettings['disable_ssl'];
72
- }
73
-
74
- update_option($this->plugin_id . 'printful_settings', $this->settings);
75
- }
76
- }
77
-
78
- public function init_form_fields()
79
- {
80
- $this->form_fields = array(
81
- 'printful_key' => array(
82
- 'title' => 'Printful store API key',
83
- 'type' => 'text',
84
- 'desc_tip' => true,
85
- 'description'=> 'Your store\'s Printful API key. Create it in the Prinful dashboard',
86
- 'default' => false,
87
- ),
88
- 'calculate_tax' => array(
89
- 'title' => 'Calculate sales tax',
90
- 'type' => 'checkbox',
91
- 'label' => 'Calculate sales tax for locations where it is required for Printful orders',
92
- 'default' => 'no'
93
- ),
94
- 'disable_ssl' => array(
95
- 'title' => 'Disable SSL',
96
- 'type' => 'checkbox',
97
- 'label' => 'Use HTTP instead of HTTPS to connect to the Printful API (may be required if the plugin does not work for some hosting configurations)',
98
- 'default' => 'no'
99
- ),
100
- 'info' => array(
101
- 'type' => 'title',
102
- 'description' => 'Go to <b><a href="' . admin_url('admin.php?page=wc-settings&tab=shipping&section=printful_shipping') . '">Shipping → Printful Shipping</a></b> to enable Printful shipping rate calculation'
103
- )
104
-
105
- );
106
- }
107
-
108
- public function calculate_tax($matched_tax_rates, $country, $state, $postcode, $city, $tax_class)
109
- {
110
- $countries = $this->get_tax_countries();
111
- if(isset($countries[$country][$state]))
112
- {
113
- $key = 'printful_tax_rate_' . $country.'-'.$state.'-'.$city.'-'.$postcode;
114
- $rate = get_transient($key);
115
- if ($rate === false) {
116
- try {
117
- $client = $this->get_client();
118
- $response = $client->post('tax/rates', array(
119
- 'recipient' => array(
120
- 'country_code' => $country,
121
- 'state_code' => $state,
122
- 'city' => $city,
123
- 'zip' => $postcode,
124
- )
125
- ));
126
- }
127
- catch(Exception $e)
128
- {}
129
-
130
- if(isset($response['rate']))
131
- {
132
- $rate = $response;
133
- } else {
134
- $rate = array(
135
- 'required' => false,
136
- 'rate' => 0,
137
- 'shipping_taxable' => false,
138
- );
139
- }
140
- set_transient($key, $rate, 1800);
141
- }
142
-
143
- if ($rate['required'])
144
- {
145
- $id = $this->get_printful_rate_id($country, $state, $rate['shipping_taxable']);
146
- return array(
147
- $id => array(
148
- 'rate' => $rate['rate'] * 100,
149
- 'label' => 'Sales Tax',
150
- 'shipping' => $rate['shipping_taxable'] ? 'yes' : 'no',
151
- 'compound' => 'no',
152
- )
153
- );
154
- }
155
- }
156
- //Return no taxes
157
- return array();
158
- }
159
-
160
- /**
161
- * Gets list of countries and states where Printful needs to calculate sales tax
162
- */
163
- private function get_tax_countries()
164
- {
165
- $countries = get_transient('printful_tax_countries');
166
- if (!$countries)
167
- {
168
- $countries = array();
169
- try {
170
- $client = $this->get_client();
171
- $list = $client->get('tax/countries');
172
-
173
- foreach($list as $country)
174
- {
175
- foreach($country['states'] as $state){
176
- $countries[$country['code']][$state['code']] = 1;
177
- }
178
- }
179
- if(!empty($countries))
180
- {
181
- set_transient('printful_tax_countries', $countries, 6 * 3600);
182
- }
183
- }
184
- catch(Exception $e)
185
- {
186
- //Default to CA if can't get the actual state list
187
- return array('US' => array('CA' => 1));
188
- }
189
- }
190
- return $countries;
191
- }
192
-
193
- /**
194
- * Creates dummy tax rate ID to display Printful tax rates in the cart summary.
195
- */
196
- private function get_printful_rate_id($cc, $state, $includeShipping = false)
197
- {
198
- global $wpdb;
199
-
200
- $includeShipping = (int)$includeShipping;
201
-
202
- $states = WC()->countries->get_states($cc);
203
- $tax_title = (isset($states[$state]) ? $states[$state] .' ': '' ). 'Sales Tax';
204
- $id = $wpdb->get_var(
205
- $wpdb->prepare("SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class='printful'
206
- and tax_rate_country = %s AND tax_rate_state = %s AND tax_rate_shipping = %s LIMIT 1",
207
- $cc,
208
- $state,
209
- $includeShipping
210
- ));
211
- if(empty($id))
212
- {
213
- $wpdb->insert(
214
- $wpdb->prefix . "woocommerce_tax_rates",
215
- array(
216
- 'tax_rate_country' => $cc,
217
- 'tax_rate_state' => $state,
218
- 'tax_rate' => 0,
219
- 'tax_rate_name' => $tax_title,
220
- 'tax_rate_priority' => 1,
221
- 'tax_rate_compound' => 0,
222
- 'tax_rate_shipping' => $includeShipping,
223
- 'tax_rate_class' => 'printful'
224
- )
225
- );
226
- $id = $wpdb->insert_id;
227
- }
228
- return $id;
229
- }
230
-
231
- public function show_tax_warning(){
232
- ?>
233
- <div class="error below-h2">
234
- <p>
235
- Warning: Tax rates are overriden by Printful Integration plugin. Go to
236
- <a href="<?php echo admin_url('admin.php?page=wc-settings&tab=integration&section=printful') ?>">Printful Integration settings</a>
237
- to disable automatic tax calculation if you want to use your own settings.
238
- </p>
239
- </div>
240
- <?php
241
- }
242
-
243
  }
1
  <?php
2
  if ( ! defined( 'ABSPATH' ) ) exit;
3
 
4
+ class Printful_Integration
5
  {
6
+ const PF_API_CONNECT_STATUS = 'printful_api_connect_status';
7
+ const PF_CONNECT_ERROR = 'printful_connect_error';
8
+
9
+ public static $_instance;
10
+
11
+ public static function instance() {
12
+ if ( is_null( self::$_instance ) ) {
13
+ self::$_instance = new self();
14
+ }
15
+
16
+ return self::$_instance;
17
+ }
18
+
19
+ public function __construct() {
20
+ self::$_instance = $this;
21
+ }
22
+
23
+ /**
24
+ * @return Printful_Client
25
+ */
26
+ public function get_client() {
27
+
28
+ require_once 'class-printful-client.php';
29
+ $client = new Printful_Client( $this->get_option( 'printful_key' ), $this->get_option( 'disable_ssl' ) == 'yes' );
30
+
31
+ return $client;
32
+ }
33
+
34
+ /**
35
+ * Check if the connection to printful is working
36
+ * @param bool $force
37
+ * @return bool
38
+ */
39
+ public function is_connected($force = false) {
40
+
41
+ $api_key = $this->get_option( 'printful_key' );
42
+
43
+ //dont need to show error - the plugin is simply not setup
44
+ if ( empty( $api_key )) {
45
+ return false;
46
+ }
47
+
48
+ //validate length, show error
49
+ if ( strlen( $api_key ) != 36 ) {
50
+ $message = 'Invalid API key - the key must be 36 characters long. Please ensure that your API key in <a href="%s">Settings</a> matches the one in your <a href="%s">Printful dashboard</a>.';
51
+ $settings_url = admin_url( 'admin.php?page=printful-dashboard&tab=settings' );
52
+ $printful_url = Printful_Base::get_printful_host() . 'dashboard/';
53
+ $this->set_connect_error(sprintf( $message, $settings_url, $printful_url ) );
54
+ return false;
55
+ }
56
+
57
+ //show connect status from cache
58
+ if ( ! $force ) {
59
+ $connected = get_transient( self::PF_API_CONNECT_STATUS );
60
+ if ( $connected && $connected['status'] == 1 ) {
61
+ $this->clear_connect_error();
62
+ return true;
63
+ } else if ( $connected && $connected['status'] == 0 ) { //try again in a minute
64
+ return false;
65
+ }
66
+ }
67
+
68
+ $client = $this->get_client();
69
+ $response = false;
70
+
71
+ //attempt to connect to printful to verify the API key
72
+ try {
73
+ $storeData = $client->get( 'store' );
74
+ if ( ! empty( $storeData ) ) {
75
+ $response = true;
76
+ $this->clear_connect_error();
77
+ set_transient( self::PF_API_CONNECT_STATUS, array( 'status' => 1 ) ); //no expiry
78
+ }
79
+ } catch ( Exception $e ) {
80
+
81
+ if ( $e->getCode() == 401 ) {
82
+ $message = 'Invalid API key. Please ensure that your API key in <a href="%s">Settings</a> matches the one in your <a href="%s">Printful dashboard</a>.';
83
+ $settings_url = admin_url( 'admin.php?page=printful-dashboard&tab=settings' );
84
+ $printful_url = Printful_Base::get_printful_host() . 'dashboard/';
85
+ $this->set_connect_error( sprintf( $message, $settings_url, $printful_url ) );
86
+ set_transient( self::PF_API_CONNECT_STATUS, array( 'status' => 0 ), MINUTE_IN_SECONDS ); //try again in 1 minute
87
+ } else {
88
+ $this->set_connect_error( 'Could not connect to Printful API. Please try again later.' );
89
+ }
90
+
91
+ //do nothing
92
+ set_transient( self::PF_API_CONNECT_STATUS, array( 'status' => 0 ), MINUTE_IN_SECONDS ); //try again in 1 minute
93
+ }
94
+
95
+ return $response;
96
+ }
97
+
98
+ /**
99
+ * Update connect error message
100
+ * @param string $error
101
+ */
102
+ public function set_connect_error($error = '') {
103
+ update_option(self::PF_CONNECT_ERROR, $error);
104
+ }
105
+
106
+ /**
107
+ * Get current connect error message
108
+ */
109
+ public function get_connect_error() {
110
+ return get_option(self::PF_CONNECT_ERROR, false);
111
+ }
112
+
113
+ /**
114
+ * Remove option used for storing current connect error
115
+ */
116
+ public function clear_connect_error() {
117
+ delete_option(self::PF_CONNECT_ERROR);
118
+ }
119
+
120
+ /**
121
+ * AJAX call endpoint for connect status check
122
+ */
123
+ public static function ajax_force_check_connect_status() {
124
+ if ( Printful_Integration::instance()->is_connected( true ) ) {
125
+ die( 'OK' );
126
+ }
127
+ die( 'FAIL' );
128
+ }
129
+
130
+ /**
131
+ * Wrapper method for getting an option
132
+ * @param $name
133
+ * @param array $default
134
+ * @return bool
135
+ */
136
+ public function get_option($name, $default = array()) {
137
+ $options = get_option( 'woocommerce_printful_settings', $default );
138
+ if ( ! empty( $options[ $name ] ) ) {
139
+ return $options[ $name ];
140
+ }
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * Save the setting
146
+ * @param $settings
147
+ */
148
+ public function update_settings( $settings ) {
149
+ delete_transient( self::PF_API_CONNECT_STATUS ); //remove the successful API status since API key could have changed
150
+ update_option( 'woocommerce_printful_settings', $settings );
151
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
includes/class-printful-request-log.php ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Request_log {
5
+
6
+ const PF_USER_AGENT = 'Printful WooCommerce Integration';
7
+ const PF_OPTION_LAST_API_RESPONSE = 'printful_last_api_response';
8
+ const PF_OPTION_INCOMING_API_REQUEST_LOG = 'printful_incoming_api_request_log';
9
+ const PF_OPTION_OUTGOING_API_REQUEST_LOG = 'printful_outgoing_api_request_log';
10
+ const PF_INCOMING_API_ERRORS = 'printful-incoming-api-errors';
11
+ const PF_OUTGOING_API_ERRORS = 'printful-outgoing-api-errors';
12
+
13
+ public static function init() {
14
+ $printful_log = new self;
15
+ add_filter( 'woocommerce_api_serve_request', array( $printful_log, 'log_incoming_printful_api_requests' ), 10, 3 );
16
+ add_filter ('printful_api_result', array($printful_log, 'log_outgoing_printful_api_requests'), 10, 4);
17
+ }
18
+
19
+ /**
20
+ * Log printful API errors and save the last 20 API responses
21
+ * @param $served
22
+ * @param $result
23
+ * @param $request
24
+ *
25
+ * @return mixed
26
+ */
27
+ public function log_incoming_printful_api_requests( $served, $result, $request ) {
28
+
29
+ if ( ! $this->isPrintfulApiRequest( $request ) ) {
30
+ return $served;
31
+ }
32
+
33
+ $last_api_response = get_option( self::PF_OPTION_LAST_API_RESPONSE, false );
34
+ $response_hash = md5( serialize( array( 'request' => $request, 'results' => $result ) ) );
35
+
36
+ if ( $last_api_response == $response_hash ) { //do not allow the same response to be logged twice
37
+ return $served;
38
+ }
39
+
40
+ //save full error to WC logs
41
+ if ( ! empty( $result['errors'] ) ) {
42
+ $this->save_to_wc_log( $request, $result, self::PF_INCOMING_API_ERRORS );
43
+ }
44
+
45
+ //save summary in database to be easily accessible for status page
46
+ $this->save_to_printful_log( $request->method . ' ' . $request->path, $result, $response_hash, self::PF_OPTION_INCOMING_API_REQUEST_LOG );
47
+
48
+ return $served; //we avoid changing the result
49
+ }
50
+
51
+
52
+ /**
53
+ * @param $result
54
+ * @param $method
55
+ * @param $url
56
+ * @param $request
57
+ * @return array|mixed|object
58
+ */
59
+ public function log_outgoing_printful_api_requests($result, $method, $url, $request) {
60
+
61
+ $original_result = $result;
62
+ $request['path'] = $url;
63
+
64
+ if ( ! is_wp_error( $result ) ) {
65
+ $result = json_decode( $result['body'], true );
66
+ $params_set = ! isset( $result['code'], $result['result'] );
67
+ $status = (int) $result['code'];
68
+ $code_success = ( $status < 200 || $status >= 300 );
69
+ }
70
+
71
+ //if the request contains error, log it
72
+ if ( is_wp_error($result) || $params_set || $code_success ) {
73
+ $this->save_to_wc_log( $request, $result, self::PF_OUTGOING_API_ERRORS );
74
+ }
75
+
76
+ $response_hash = md5( serialize( array( 'request' => $request, 'results' => $result ) ) );
77
+
78
+ //save summary in database to be easily accessible for status page
79
+ $this->save_to_printful_log( $method . ' ' . $url, $result, $response_hash, self::PF_OPTION_OUTGOING_API_REQUEST_LOG);
80
+
81
+ return $original_result; //don't change the result
82
+ }
83
+
84
+ /**
85
+ * Write Printful API request errors to log
86
+ *
87
+ * @param $request
88
+ * @param $result
89
+ * @param string $context
90
+ */
91
+ private function save_to_wc_log( $request, $result, $context) {
92
+
93
+ $logger = wc_get_logger();
94
+ $context = array( 'source' => $context );
95
+ $log_item = array(
96
+ 'request' => (array) $request,
97
+ 'results' => (array) $result,
98
+ );
99
+ $logger->error( wc_print_r( $log_item, true ), $context );
100
+ }
101
+
102
+ /**
103
+ * Save 20 lasts requests in easily accessible location
104
+ *
105
+ * @param $request_title
106
+ * @param $result
107
+ * @param $response_hash
108
+ * @param $log
109
+ */
110
+ private function save_to_printful_log( $request_title, $result, $response_hash, $log ) {
111
+
112
+ $request_log = get_option( $log, array() );
113
+ if ( count( $request_log ) > 20 ) {
114
+ $request_log = array_slice( $request_log, 1, 19 ); //if there are more than 20, remove the first entry
115
+ }
116
+
117
+ $is_error = is_wp_error($result) || !empty( $result['errors'] );
118
+
119
+ $request_log[] = array(
120
+ 'date' => date( 'Y-m-d H:i:s' ),
121
+ 'request' => $request_title,
122
+ 'result' => ( $is_error ? 'ERROR' : 'OK' ),
123
+ );
124
+
125
+ update_option( $log, $request_log );
126
+ update_option( self::PF_OPTION_LAST_API_RESPONSE, $response_hash );
127
+ }
128
+
129
+ /**
130
+ * Check requests header for indications that this is a printful api request
131
+ * @param $request
132
+ * @return bool
133
+ */
134
+ private function isPrintfulApiRequest( $request ) {
135
+
136
+ if ( ! empty( $request->headers ) && ! empty( $request->headers['USER_AGENT'] ) && $request->headers['USER_AGENT'] == self::PF_USER_AGENT ) {
137
+ return true;
138
+ }
139
+
140
+ return false;
141
+ }
142
+ }
includes/class-printful-shipping.php CHANGED
@@ -11,272 +11,314 @@ class Printful_Shipping extends WC_Shipping_Method
11
  //Store whether currently processed package contains Printful products (for WC<2.6)
12
  private $printful_package = true;
13
 
14
- public function __construct()
15
- {
16
- $this->id = 'printful_shipping';
17
- $this->method_title = $this->title = 'Printful Shipping';
18
- $this->method_description = 'Calculate live shipping rates based on actual Printful shipping costs.';
19
-
20
- $this->init_form_fields();
21
- $this->init_settings();
22
-
23
- add_action('woocommerce_update_options_shipping_' . $this->id, array(&$this, 'process_admin_options'));
24
-
25
- $this->enabled = $this->get_option('enabled');
26
- $this->show_warnings = $this->get_option('show_warnings') == 'yes';
27
- $this->override_defaults = $this->get_option('override_defaults') == 'yes';
28
-
29
- //Initialize shipping methods for specific package (or no package)
30
- add_filter('woocommerce_load_shipping_methods', array($this, 'woocommerce_load_shipping_methods'), 10000);
31
-
32
- //Remove other shipping methods for Printful package on WC < 2.6
33
- add_filter('woocommerce_shipping_methods', array($this, 'woocommerce_shipping_methods'), 10000);
34
-
35
- add_filter('woocommerce_cart_shipping_packages', array($this, 'woocommerce_cart_shipping_packages'), 10000);
36
-
37
- }
38
- public function init_form_fields()
39
- {
40
- $this->form_fields = array(
41
- 'enabled' => array(
42
- 'title' => __( 'Enable/Disable', 'woocommerce' ),
43
- 'type' => 'checkbox',
44
- 'label' => __( 'Enable this shipping method', 'woocommerce' ),
45
- 'default' => 'no'
46
- ),
47
- 'override_defaults' => array(
48
- 'title' => 'Disable Woocommerce rates',
49
- 'type' => 'checkbox',
50
- 'label' => 'Disable standard Woocommerce rates for products fulfilled by Printful',
51
- 'default' => 'yes'
52
- ),
53
- 'show_warnings' => array(
54
- 'title' => 'Show Printful warnings',
55
- 'type' => 'checkbox',
56
- 'label' => 'Display Printful status messages if rate API request fails',
57
- 'default' => 'yes'
58
- )
59
- );
60
-
61
- if (empty(Printful_Integration::instance()->settings['printful_key'])) {
62
- $this->form_fields['info'] = array(
63
- 'type' => 'title',
64
- 'description' => 'Please add Printful API key to the
65
- <a href="'. admin_url('admin.php?page=wc-settings&tab=integration&section=printful') .'">Printful Integration settings section</a>
66
- to enable rate calculation.'
67
- );
68
- }
69
- }
70
-
71
- //Enable only Printful shipping method for Printful packages
72
- public function woocommerce_load_shipping_methods( $package = array() ) {
73
- $this->printful_package = false;
74
- if($package && !empty($package['printful'])) {
75
- if($this->enabled == 'yes') {
76
- $this->printful_package = true;
77
- if($this->override_defaults) {
78
- //Remove default methods if we process Printful package
79
- WC()->shipping()->unregister_shipping_methods();
80
- }
81
- WC()->shipping()->register_shipping_method($this);
82
- }
83
- } else if(!$package) {
84
- //Show Printful tab on Shipping rate settings
85
- WC()->shipping()->register_shipping_method($this);
86
- }
87
  }
88
 
89
- //Remove non-Printful methods for Printful packages on WC < 2.6
90
- public function woocommerce_shipping_methods($methods) {
91
- if($this->override_defaults && $this->printful_package && version_compare(WC()->version, '2.6', '<')) {
92
- //For WC < 2.6 woocommerce_shipping_methods is executed after woocommerce_load_shipping_methods
93
- //So we need to clean up unnecessary methods from there
94
- return array();
95
- }
96
- return $methods;
97
- }
98
-
99
- //Split Printul products to a separate package if there are any
100
- public function woocommerce_cart_shipping_packages($packages = array()) {
101
-
102
- //Printful rates are turned off, do not split products
103
- if ($this->enabled !== 'yes') {
104
- return $packages;
105
- }
106
-
107
- $return_packages = array();
108
-
109
- foreach ($packages as $package) {
110
-
111
- $ids = array();
112
- foreach ($package['contents'] as $key => $item) {
113
- $ids[$key]= $item['variation_id'] ? $item['variation_id'] : $item['product_id'];
114
- }
115
-
116
- $printful_ids = array();
117
- if($ids) {
118
- asort($ids);
119
- $values = implode(',', array_unique($ids));
120
- $key = 'printful_productids_'.md5($values);
121
- $printful_ids = get_transient($key);
122
- if (!is_array($printful_ids)) {
123
- $printful_ids = array();
124
- try {
125
- $client = Printful_Integration::instance()->get_client();
126
- $status = $client->get('sync/variants', array(
127
- 'external_ids' => $values
128
- ));
129
- if(!empty($status['sync_variants'])) {
130
- foreach($status['sync_variants'] as $variant) {
131
- if($variant['synced']) {
132
- $printful_ids[]= $variant['external_id'];
133
- }
134
- }
135
- }
136
- set_transient($key, $printful_ids, 1800);
137
- } catch (PrintfulException $e) {
138
- $this->set_error($e);
139
- //Failed to get Printful status, return default packages
140
- return $packages;
141
- }
142
- }
143
- }
144
- $new_contents = array(
145
- 'printful' => array(),
146
- 'virtual' => array(),
147
- 'woocommerce' => array()
148
- );
149
-
150
- foreach($ids as $key => $external_id) {
151
- $item = $package['contents'][$key];
152
- if(in_array($external_id,$printful_ids)) {
153
- $new_contents['printful'][$key] = $item;
154
- } else if($item['data']->is_virtual() || $item['data']->is_downloadable()) {
155
- $new_contents['virtual'][$key] = $item;
156
- } else {
157
- $new_contents['woocommerce'][$key] = $item;
158
- }
159
- }
160
-
161
- //Put virtual products together with any other package
162
- if($new_contents['virtual']) {
163
- if($new_contents['printful'] && !$new_contents['woocommerce']) {
164
- $new_contents['printful'] += $new_contents['virtual'];
165
- } else {
166
- $new_contents['woocommerce'] += $new_contents['virtual'];
167
- }
168
- unset ($new_contents['virtual']);
169
- }
170
-
171
- foreach ($new_contents as $key => $contents) {
172
- if($contents) {
173
- $new_package = $package;
174
- $new_package['contents_cost'] = 0;
175
- $new_package['contents'] = $contents;
176
- foreach ($contents as $item ) {
177
- if ( $item['data']->needs_shipping() ) {
178
- if ( isset( $item['line_total'] ) ) {
179
- $new_package['contents_cost'] += $item['line_total'];
180
- }
181
- }
182
- }
183
- if ($key == 'printful') {
184
- $new_package['printful'] = true;
185
- }
186
- $return_packages[]= $new_package;
187
- }
188
- }
189
- }
190
- return $return_packages;
191
- }
192
-
193
-
194
- public function calculate_shipping($package = array())
195
- {
196
- $request = array(
197
- 'recipient' => array(
198
- 'address1' => $package['destination']['address'],
199
- 'address2' => $package['destination']['address_2'],
200
- 'city' => $package['destination']['city'],
201
- 'state_code' => $package['destination']['state'],
202
- 'country_code' => $package['destination']['country'],
203
- 'zip' => $package['destination']['postcode'],
204
- ),
205
- 'items' => array(),
206
- 'currency' => get_woocommerce_currency()
207
- );
208
-
209
-
210
- if ($request['recipient']['country_code'] == 'US' &&
211
- (empty($request['recipient']['zip']) ||
212
- empty($request['recipient']['state_code']))
213
- ) {
214
- return false;
215
- }
216
-
217
- foreach ($package['contents'] as $item) {
218
- if (!empty($item['data']) && ($item['data']->is_virtual() || $item['data']->is_downloadable())) {
219
- continue;
220
- }
221
- $request['items'] []= array(
222
- 'external_variant_id' => $item['variation_id'] ? $item['variation_id'] : $item['product_id'],
223
- 'quantity' => $item['quantity'],
224
- 'value' => $item['line_total'] / $item['quantity']
225
- );
226
- }
227
-
228
- if (!$request['items']) {
229
- return false;
230
- }
231
-
232
- try {
233
- $client = Printful_Integration::instance()->get_client();
234
- } catch( PrintfulException $e) {
235
- $this->set_error($e);
236
- return false;
237
- }
238
-
239
- try {
240
- $key = 'printful_rates_' . md5(json_encode($request));
241
- $response = get_transient($key);
242
- if($response === false) {
243
- $response = $client->post('shipping/rates', $request, array(
244
- 'expedited' => true,
245
- ));
246
- //Cache locally, since WC < 2.6 had problems with caching rates form multiple packages internally
247
- set_transient($key, $response, 1800);
248
- }
249
-
250
- foreach ($response as $rate) {
251
- $rateData = array(
252
- 'id' => $this->id . '_' . $rate['id'],
253
- 'label' => $rate['name'],
254
- 'cost' => $rate['rate'],
255
- 'calc_tax' => 'per_order'
256
- );
257
- $this->add_rate($rateData);
258
- }
259
- } catch ( PrintfulException $e) {
260
- $this->set_error($e);
261
- return false;
262
- }
263
- }
264
-
265
- private function set_error($error)
266
- {
267
- if ($this->show_warnings){
268
- $this->last_error = $error;
269
- add_filter('woocommerce_cart_no_shipping_available_html', array($this, 'show_error'));
270
- add_filter('woocommerce_no_shipping_available_html', array($this, 'show_error'));
271
- }
272
- }
273
- public function show_error($data)
274
- {
275
- $error = $this->last_error;
276
- $message = $error->getMessage();
277
- if($error instanceof PrintfulApiException && $error->getCode() == 401){
278
- $message = 'Invalid API key';
279
- }
280
- return '<p>ERROR: '.htmlspecialchars($message).'</p>';
281
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  }
11
  //Store whether currently processed package contains Printful products (for WC<2.6)
12
  private $printful_package = true;
13
 
14
+ public static function init() {
15
+ new self;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
 
18
+ public function __construct() {
19
+
20
+ $this->id = 'printful_shipping';
21
+ $this->method_title = $this->title = 'Printful Shipping';
22
+ $this->method_description = 'Calculate live shipping rates based on actual Printful shipping costs.';
23
+
24
+ $this->init_form_fields();
25
+ $this->init_settings();
26
+
27
+ add_action( 'woocommerce_update_options_shipping_' . $this->id, array( &$this, 'process_admin_options' ) );
28
+
29
+ $this->enabled = $this->get_option( 'enabled' );
30
+ $this->show_warnings = $this->get_option( 'show_warnings' ) == 'yes';
31
+ $this->override_defaults = $this->get_option( 'override_defaults' ) == 'yes';
32
+
33
+ //Initialize shipping methods for specific package (or no package)
34
+ add_filter( 'woocommerce_load_shipping_methods', array( $this, 'woocommerce_load_shipping_methods' ), 10000 );
35
+
36
+ //Remove other shipping methods for Printful package on WC < 2.6
37
+ add_filter( 'woocommerce_shipping_methods', array( $this, 'woocommerce_shipping_methods' ), 10000 );
38
+
39
+ add_filter( 'woocommerce_cart_shipping_packages', array( $this, 'woocommerce_cart_shipping_packages' ), 10000 );
40
+ }
41
+
42
+ /**
43
+ * Init fields for Printful Shipping form
44
+ */
45
+ public function init_form_fields() {
46
+
47
+ $this->form_fields = array(
48
+ 'enabled' => array(
49
+ 'title' => __( 'Enable/Disable', 'woocommerce' ),
50
+ 'type' => 'checkbox',
51
+ 'label' => __( 'Enable this shipping method', 'woocommerce' ),
52
+ 'default' => 'no',
53
+ ),
54
+ 'override_defaults' => array(
55
+ 'title' => 'Disable Woocommerce rates',
56
+ 'type' => 'checkbox',
57
+ 'label' => 'Disable standard Woocommerce rates for products fulfilled by Printful',
58
+ 'default' => 'yes',
59
+ ),
60
+ 'show_warnings' => array(
61
+ 'title' => 'Show Printful warnings',
62
+ 'type' => 'checkbox',
63
+ 'label' => 'Display Printful status messages if rate API request fails',
64
+ 'default' => 'yes',
65
+ ),
66
+ );
67
+
68
+ if ( !Printful_Integration::instance()->get_option('printful_key') ) {
69
+ $this->form_fields['info'] = array(
70
+ 'type' => 'title',
71
+ 'description' => 'Please add Printful API key to the
72
+ <a href="' . admin_url( 'admin.php?page=printful-dashboard&tab=settings' ) . '">Printful Integration settings section</a>
73
+ to enable rate calculation.',
74
+ );
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Enable only Printful shipping method for Printful packages
80
+ * @param array $package
81
+ */
82
+ public function woocommerce_load_shipping_methods( $package = array() ) {
83
+
84
+ $this->printful_package = false;
85
+ if ( $package && ! empty( $package['printful'] ) ) {
86
+ if ( $this->enabled == 'yes' ) {
87
+ $this->printful_package = true;
88
+ if ( $this->override_defaults ) {
89
+ //Remove default methods if we process Printful package
90
+ WC()->shipping()->unregister_shipping_methods();
91
+ }
92
+ WC()->shipping()->register_shipping_method( $this );
93
+ }
94
+ } else if ( ! $package ) {
95
+ //Show Printful tab on Shipping rate settings
96
+ WC()->shipping()->register_shipping_method( $this );
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Remove non-Printful methods for Printful packages on WC < 2.6
102
+ * @param $methods
103
+ *
104
+ * @return array
105
+ */
106
+ public function woocommerce_shipping_methods( $methods ) {
107
+
108
+ if ( $this->override_defaults && $this->printful_package && version_compare( WC()->version, '2.6', '<' ) ) {
109
+ //For WC < 2.6 woocommerce_shipping_methods is executed after woocommerce_load_shipping_methods
110
+ //So we need to clean up unnecessary methods from there
111
+ return array();
112
+ }
113
+
114
+ return $methods;
115
+ }
116
+
117
+ /**
118
+ * Split Printul products to a separate package if there are any
119
+ * @param array $packages
120
+ *
121
+ * @return array
122
+ */
123
+ public function woocommerce_cart_shipping_packages( $packages = array() ) {
124
+
125
+ //Printful rates are turned off, do not split products
126
+ if ( $this->enabled !== 'yes' ) {
127
+ return $packages;
128
+ }
129
+
130
+ $return_packages = array();
131
+
132
+ foreach ( $packages as $package ) {
133
+
134
+ $ids = array();
135
+ foreach ( $package['contents'] as $key => $item ) {
136
+ $ids[ $key ] = $item['variation_id'] ? $item['variation_id'] : $item['product_id'];
137
+ }
138
+
139
+ $printful_ids = array();
140
+ if ( $ids ) {
141
+ asort( $ids );
142
+ $values = implode( ',', array_unique( $ids ) );
143
+ $key = 'printful_productids_' . md5( $values );
144
+ $printful_ids = get_transient( $key );
145
+ if ( ! is_array( $printful_ids ) ) {
146
+ $printful_ids = array();
147
+ try {
148
+ $client = Printful_Integration::instance()->get_client();
149
+ $status = $client->get( 'sync/variants', array(
150
+ 'external_ids' => $values,
151
+ ) );
152
+ if ( ! empty( $status['sync_variants'] ) ) {
153
+ foreach ( $status['sync_variants'] as $variant ) {
154
+ if ( $variant['synced'] ) {
155
+ $printful_ids[] = $variant['external_id'];
156
+ }
157
+ }
158
+ }
159
+ set_transient( $key, $printful_ids, 1800 );
160
+ } catch ( PrintfulException $e ) {
161
+ $this->set_error( $e );
162
+
163
+ //Failed to get Printful status, return default packages
164
+ return $packages;
165
+ }
166
+ }
167
+ }
168
+ $new_contents = array(
169
+ 'printful' => array(),
170
+ 'virtual' => array(),
171
+ 'woocommerce' => array(),
172
+ );
173
+
174
+ foreach ( $ids as $key => $external_id ) {
175
+ $item = $package['contents'][ $key ];
176
+ if ( in_array( $external_id, $printful_ids ) ) {
177
+ $new_contents['printful'][ $key ] = $item;
178
+ } else if ( $item['data']->is_virtual() || $item['data']->is_downloadable() ) {
179
+ $new_contents['virtual'][ $key ] = $item;
180
+ } else {
181
+ $new_contents['woocommerce'][ $key ] = $item;
182
+ }
183
+ }
184
+
185
+ //Put virtual products together with any other package
186
+ if ( $new_contents['virtual'] ) {
187
+ if ( $new_contents['printful'] && ! $new_contents['woocommerce'] ) {
188
+ $new_contents['printful'] += $new_contents['virtual'];
189
+ } else {
190
+ $new_contents['woocommerce'] += $new_contents['virtual'];
191
+ }
192
+ unset ( $new_contents['virtual'] );
193
+ }
194
+
195
+ foreach ( $new_contents as $key => $contents ) {
196
+ if ( $contents ) {
197
+ $new_package = $package;
198
+ $new_package['contents_cost'] = 0;
199
+ $new_package['contents'] = $contents;
200
+ foreach ( $contents as $item ) {
201
+ if ( $item['data']->needs_shipping() ) {
202
+ if ( isset( $item['line_total'] ) ) {
203
+ $new_package['contents_cost'] += $item['line_total'];
204
+ }
205
+ }
206
+ }
207
+ if ( $key == 'printful' ) {
208
+ $new_package['printful'] = true;
209
+ }
210
+ $return_packages[] = $new_package;
211
+ }
212
+ }
213
+ }
214
+
215
+ return $return_packages;
216
+ }
217
+
218
+ /**
219
+ * @param array $package
220
+ *
221
+ * @return bool
222
+ */
223
+ public function calculate_shipping( $package = array() ) {
224
+
225
+ $request = array(
226
+ 'recipient' => array(
227
+ 'address1' => $package['destination']['address'],
228
+ 'address2' => $package['destination']['address_2'],
229
+ 'city' => $package['destination']['city'],
230
+ 'state_code' => $package['destination']['state'],
231
+ 'country_code' => $package['destination']['country'],
232
+ 'zip' => $package['destination']['postcode'],
233
+ ),
234
+ 'items' => array(),
235
+ 'currency' => get_woocommerce_currency(),
236
+ );
237
+
238
+
239
+ if ( $request['recipient']['country_code'] == 'US' &&
240
+ ( empty( $request['recipient']['zip'] ) ||
241
+ empty( $request['recipient']['state_code'] ) )
242
+ ) {
243
+ return false;
244
+ }
245
+
246
+ foreach ( $package['contents'] as $item ) {
247
+ if ( ! empty( $item['data'] ) && ( $item['data']->is_virtual() || $item['data']->is_downloadable() ) ) {
248
+ continue;
249
+ }
250
+ $request['items'] [] = array(
251
+ 'external_variant_id' => $item['variation_id'] ? $item['variation_id'] : $item['product_id'],
252
+ 'quantity' => $item['quantity'],
253
+ 'value' => $item['line_total'] / $item['quantity'],
254
+ );
255
+ }
256
+
257
+ if ( ! $request['items'] ) {
258
+ return false;
259
+ }
260
+
261
+ try {
262
+ $client = Printful_Integration::instance()->get_client();
263
+ } catch ( PrintfulException $e ) {
264
+ $this->set_error( $e );
265
+
266
+ return false;
267
+ }
268
+
269
+ try {
270
+ $key = 'printful_rates_' . md5( json_encode( $request ) );
271
+ $response = get_transient( $key );
272
+ if ( $response === false ) {
273
+ $response = $client->post( 'shipping/rates', $request, array(
274
+ 'expedited' => true,
275
+ ) );
276
+ //Cache locally, since WC < 2.6 had problems with caching rates form multiple packages internally
277
+ set_transient( $key, $response, 1800 );
278
+ }
279
+
280
+ foreach ( $response as $rate ) {
281
+ $rateData = array(
282
+ 'id' => $this->id . '_' . $rate['id'],
283
+ 'label' => $rate['name'],
284
+ 'cost' => $rate['rate'],
285
+ 'calc_tax' => 'per_order',
286
+ );
287
+ $this->add_rate( $rateData );
288
+ }
289
+ } catch ( PrintfulException $e ) {
290
+ $this->set_error( $e );
291
+ return false;
292
+ }
293
+
294
+ return false;
295
+ }
296
+
297
+ /**
298
+ * @param $error
299
+ */
300
+ private function set_error( $error ) {
301
+
302
+ if ( $this->show_warnings ) {
303
+ $this->last_error = $error;
304
+ add_filter( 'woocommerce_cart_no_shipping_available_html', array( $this, 'show_error' ) );
305
+ add_filter( 'woocommerce_no_shipping_available_html', array( $this, 'show_error' ) );
306
+ }
307
+ }
308
+
309
+ /**
310
+ * @param $data
311
+ *
312
+ * @return string
313
+ */
314
+ public function show_error( $data ) {
315
+
316
+ $error = $this->last_error;
317
+ $message = $error->getMessage();
318
+ if ( $error instanceof PrintfulApiException && $error->getCode() == 401 ) {
319
+ $message = 'Invalid API key';
320
+ }
321
+
322
+ return '<p>ERROR: ' . htmlspecialchars( $message ) . '</p>';
323
+ }
324
  }
includes/class-printful-size-chart-tab.php ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Size_Chart_Tab {
5
+
6
+ /**
7
+ * Is meta boxes saved once?
8
+ *
9
+ * @var boolean
10
+ */
11
+ private static $saved_meta_boxes = false;
12
+
13
+ /**
14
+ * Printful_Size_Chart_Tab constructor.
15
+ */
16
+ public static function init() {
17
+ $size_chart = new self;
18
+ add_filter( 'woocommerce_product_tabs', array( $size_chart, 'init_size_chart_tab' ) );
19
+ add_action( 'add_meta_boxes', array($size_chart, 'init_metabox'));
20
+ add_action( 'save_post', array( $size_chart, 'save_size_chart' ), 1, 2 );
21
+ }
22
+
23
+ /**
24
+ * Initialize meta boxes
25
+ */
26
+ public function init_metabox() {
27
+ add_meta_box( 'pf_size_chart', __( 'Size chart', 'printful' ), array($this, 'size_chart_metabox'), 'product', 'normal' );
28
+ }
29
+
30
+ /**
31
+ * @param $tabs
32
+ *
33
+ * @return mixed
34
+ */
35
+ public function init_size_chart_tab( $tabs ) {
36
+
37
+ if ( strlen( $this->get_size_chart_content() ) > 0 ) {
38
+ $tabs['size_chart'] = array(
39
+ 'title' => __( 'Size Chart', 'printful' ),
40
+ 'priority' => 50,
41
+ 'callback' => array( $this, 'size_chart_tab_content' ),
42
+ );
43
+ }
44
+
45
+ return $tabs;
46
+ }
47
+
48
+ /**
49
+ * Display the size chart content
50
+ */
51
+ public function size_chart_tab_content() {
52
+
53
+ echo '<h2>' . __( 'Size Chart', 'printful' ) . '</h2>';
54
+ echo $this->get_size_chart_content();
55
+ }
56
+
57
+ /**
58
+ * @return mixed
59
+ */
60
+ public function get_size_chart_content() {
61
+
62
+ global $post;
63
+ return htmlspecialchars_decode(get_post_meta( $post->ID, 'pf_size_chart', true ));
64
+ }
65
+
66
+ /**
67
+ * @param $meta_id
68
+ */
69
+ public function size_chart_metabox( $meta_id ) {
70
+
71
+ $settings = array(
72
+ 'textarea_name' => 'pf_size_chart',
73
+ 'tinymce' => array(
74
+ 'theme_advanced_buttons1' => 'bold,italic,strikethrough,separator,bullist,numlist,separator,blockquote,separator,justifyleft,justifycenter,justifyright,separator,link,unlink,separator,undo,redo,separator',
75
+ 'theme_advanced_buttons2' => '',
76
+ ),
77
+ 'editor_css' => '<style>#wp-pf_size_chart-editor-container .wp-editor-area{height:175px; width:100%;} .wp-editor-area{height:175px; width:100%;}</style>',
78
+ );
79
+
80
+ $content = get_post_meta( $meta_id->ID, 'pf_size_chart', true );
81
+
82
+ wp_editor( htmlspecialchars_decode( $content ), 'pf_size_chart_editor', apply_filters( 'woocommerce_product_short_description_editor_settings', $settings ) );
83
+ }
84
+
85
+ /**
86
+ * @param $post_id
87
+ * @param $post
88
+ */
89
+ public function save_size_chart($post_id, $post ) {
90
+
91
+ // $post_id and $post are required
92
+ if ( empty( $post_id ) || empty( $post ) || self::$saved_meta_boxes ) {
93
+ return;
94
+ }
95
+
96
+ // Dont' save meta boxes for revisions or autosaves
97
+ if ( defined( 'DOING_AUTOSAVE' ) || is_int( wp_is_post_revision( $post ) ) || is_int( wp_is_post_autosave( $post ) ) ) {
98
+ return;
99
+ }
100
+
101
+ // Check the nonce
102
+ if ( empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) ) {
103
+ return;
104
+ }
105
+
106
+ // Check the post being saved == the $post_id to prevent triggering this call for other save_post events
107
+ if ( empty( $_POST['post_ID'] ) || $_POST['post_ID'] != $post_id ) {
108
+ return;
109
+ }
110
+
111
+ // Check user has permission to edit
112
+ if ( ! current_user_can( 'edit_post', $post_id ) ) {
113
+ return;
114
+ }
115
+
116
+ // Check the post type
117
+ if ( $post->post_type != 'product' ) {
118
+ return;
119
+ }
120
+
121
+ //check if not empty
122
+ if ( empty( $_POST['pf_size_chart'] ) ) {
123
+ return;
124
+ }
125
+
126
+ // We need this save event to run once to avoid potential endless loops.
127
+ self::$saved_meta_boxes = true;
128
+
129
+ //save
130
+ update_post_meta($post_id, 'pf_size_chart', htmlspecialchars($_POST['pf_size_chart']));
131
+ }
132
+ }
includes/class-printful-taxes.php ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Taxes {
5
+
6
+ /**
7
+ * Initialize the values, hooks and actions
8
+ */
9
+ public static function init() {
10
+
11
+ if ( Printful_Integration::instance()->get_option( 'calculate_tax' ) == 'yes' ) {
12
+ //Update tax options if taxes are enabled
13
+ if ( get_option( 'woocommerce_calc_taxes' ) != 'yes' ) {
14
+ update_option( 'woocommerce_calc_taxes', 'yes' );
15
+ }
16
+ if ( get_option( 'woocommerce_tax_based_on' ) != 'shipping' ) {
17
+ update_option( 'woocommerce_tax_based_on', 'shipping' );
18
+ }
19
+
20
+ //Override tax rates calculated by Woocommerce
21
+ $taxes = new self;
22
+ add_filter( 'woocommerce_matched_tax_rates', array($taxes, 'calculate_tax' ), 10, 6 );
23
+ }
24
+ }
25
+
26
+ /**
27
+ * @param $matched_tax_rates
28
+ * @param $country
29
+ * @param $state
30
+ * @param $postcode
31
+ * @param $city
32
+ * @param $tax_class
33
+ *
34
+ * @return mixed
35
+ */
36
+ public function calculate_tax( $matched_tax_rates, $country, $state, $postcode, $city, $tax_class ) {
37
+
38
+ //if a tax rate is already matched, avoid adding extra one
39
+ if ( ! empty( $matched_tax_rates ) ) {
40
+ return $matched_tax_rates;
41
+ }
42
+
43
+ $countries = $this->get_tax_countries();
44
+ if ( isset( $countries[ $country ][ $state ] ) && !empty($postcode) ) { //only make the request if country, state and zip are set
45
+ $key = 'printful_tax_rate_' . $country . '-' . $state . '-' . $city . '-' . $postcode;
46
+ $rate = get_transient( $key );
47
+ if ( $rate === false ) {
48
+ try {
49
+ $client = Printful_Integration::instance()->get_client();
50
+ $response = $client->post( 'tax/rates', array(
51
+ 'recipient' => array(
52
+ 'country_code' => $country,
53
+ 'state_code' => $state,
54
+ 'city' => $city,
55
+ 'zip' => $postcode,
56
+ ),
57
+ ) );
58
+ } catch ( Exception $e ) {
59
+ }
60
+
61
+ if ( isset( $response['rate'] ) ) {
62
+ $rate = $response;
63
+ } else {
64
+ $rate = array(
65
+ 'required' => false,
66
+ 'rate' => 0,
67
+ 'shipping_taxable' => false,
68
+ );
69
+ }
70
+ set_transient( $key, $rate, 1800 );
71
+ }
72
+
73
+ if ( $rate['required'] ) {
74
+ $rate_item = array(
75
+ 'rate' => $rate['rate'] * 100,
76
+ 'label' => 'Sales Tax',
77
+ 'shipping' => $rate['shipping_taxable'] ? 'yes' : 'no',
78
+ 'compound' => 'no',
79
+ );
80
+
81
+ if ( $this->isRateUnique( $rate_item, $matched_tax_rates ) ) {
82
+ $id = $this->get_printful_rate_id( $country, $state, $rate['shipping_taxable'] );
83
+ $matched_tax_rates[ $id ] = $rate_item;
84
+ }
85
+ }
86
+ }
87
+
88
+ return $matched_tax_rates;
89
+ }
90
+
91
+ /**
92
+ * Checks if a equal tax rate is not already set
93
+ *
94
+ * @param $rate
95
+ * @param $matched_tax_rates
96
+ *
97
+ * @return bool
98
+ */
99
+ private function isRateUnique( $rate, $matched_tax_rates ) {
100
+
101
+ if ( empty( $matched_tax_rates ) ) {
102
+ return true;
103
+ }
104
+
105
+ foreach ( $matched_tax_rates as $mr ) {
106
+ if ( floatval( $mr['rate'] ) == floatval( $rate['rate'] ) && $mr['shipping'] == $rate['shipping'] ) {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ return true;
112
+ }
113
+
114
+ /**
115
+ * Gets list of countries and states where Printful needs to calculate sales tax
116
+ * @return array|mixed
117
+ */
118
+ private function get_tax_countries() {
119
+
120
+ $countries = get_transient( 'printful_tax_countries' );
121
+ if ( ! $countries ) {
122
+ $countries = array();
123
+ try {
124
+ $client = Printful_Integration::instance()->get_client();
125
+ $list = $client->get( 'tax/countries' );
126
+
127
+ foreach ( $list as $country ) {
128
+ foreach ( $country['states'] as $state ) {
129
+ $countries[ $country['code'] ][ $state['code'] ] = 1;
130
+ }
131
+ }
132
+ if ( ! empty( $countries ) ) {
133
+ set_transient( 'printful_tax_countries', $countries, 6 * 3600 );
134
+ }
135
+ } catch ( Exception $e ) {
136
+ //Default to CA if can't get the actual state list
137
+ return array( 'US' => array( 'CA' => 1 ) );
138
+ }
139
+ }
140
+
141
+ return $countries;
142
+ }
143
+
144
+ /**
145
+ * Creates dummy tax rate ID to display Printful tax rates in the cart summary.
146
+ *
147
+ * @param $cc
148
+ * @param $state
149
+ * @param bool $includeShipping
150
+ *
151
+ * @return int|null|string
152
+ */
153
+ private function get_printful_rate_id( $cc, $state, $includeShipping = false ) {
154
+ global $wpdb;
155
+
156
+ $includeShipping = (int) $includeShipping;
157
+
158
+ $states = WC()->countries->get_states( $cc );
159
+ $tax_title = ( isset( $states[ $state ] ) ? $states[ $state ] . ' ' : '' ) . 'Sales Tax';
160
+ $id = $wpdb->get_var(
161
+ $wpdb->prepare( "SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class='printful'
162
+ and tax_rate_country = %s AND tax_rate_state = %s AND tax_rate_shipping = %s LIMIT 1",
163
+ $cc,
164
+ $state,
165
+ $includeShipping
166
+ ) );
167
+ if ( empty( $id ) ) {
168
+ $wpdb->insert(
169
+ $wpdb->prefix . "woocommerce_tax_rates",
170
+ array(
171
+ 'tax_rate_country' => $cc,
172
+ 'tax_rate_state' => $state,
173
+ 'tax_rate' => 0,
174
+ 'tax_rate_name' => $tax_title,
175
+ 'tax_rate_priority' => 1,
176
+ 'tax_rate_compound' => 0,
177
+ 'tax_rate_shipping' => $includeShipping,
178
+ 'tax_rate_class' => 'printful',
179
+ )
180
+ );
181
+ $id = $wpdb->insert_id;
182
+ }
183
+
184
+ return $id;
185
+ }
186
+
187
+
188
+ }
includes/templates/ajax-loader.php ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="loader-block-<?php echo esc_attr($action); ?>">
2
+ <div class="block-loader loader-wrap">
3
+ <img src="<?php echo esc_url( admin_url( 'images/spinner-2x.gif' ) ) ?>" class="loader" width="20px" height="20px" alt="loader"/>
4
+ <span class="message"><?php echo esc_html($message); ?></span>
5
+ </div>
6
+ </div>
7
+ <script type="text/javascript">
8
+ jQuery(document).ready(function () {
9
+ Printful_Block_Loader.load('<?php echo esc_url(admin_url( 'admin-ajax.php?action=' . $action )); ?>', 'loader-block-<?php echo esc_attr($action); ?>');
10
+ });
11
+ </script>
includes/templates/connect.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="printful-connect">
2
+
3
+ <div class="printful-connect-inner">
4
+
5
+ <h1>Connect to Printful</h1>
6
+
7
+ <img src=" <?php echo esc_url(Printful_Base::get_asset_url() . 'images/connect.svg'); ?>" class="connect-image" alt="connect to printful">
8
+
9
+ <?php
10
+ if ( ! empty( $issues ) ) {
11
+ ?>
12
+ <p>To connect your store to Printful, fix the following errors:</p>
13
+ <div class="printful-notice">
14
+ <ul>
15
+ <?php
16
+ foreach ( $issues as $issue ) {
17
+ echo '<li>' . $issue . '</li>';
18
+ }
19
+ ?>
20
+ </ul>
21
+ </div>
22
+ <?php
23
+ $url = '#';
24
+ } else {
25
+ ?><p class="connect-description">You're almost done! Just 2 more steps to have your WooCommerce store connected to Printful for automatic order fulfillment.</p><?php
26
+ $url = Printful_Base::get_printful_host() . 'dashboard/woocommerce/plugin-connect?website=' . urlencode( trailingslashit( get_site_url() ) ) . '&key=' . urlencode( $consumer_key ) . '&returnUrl=' . urlencode( get_admin_url( null,'admin.php?page=' . Printful_Admin::MENU_SLUG_DASHBOARD ) );
27
+ }
28
+
29
+ echo '<a href="' . esc_url($url) . '" class="button button-primary printful-connect-button ' . ( ! empty( $issues ) ? 'disabled' : '' ) . '" target="_blank">Connect</a>';
30
+ ?>
31
+
32
+ <img src="<?php echo esc_url( admin_url( 'images/spinner-2x.gif' ) ) ?>" class="loader hidden" width="20px" height="20px" alt="loader"/>
33
+
34
+ <script type="text/javascript">
35
+ jQuery(document).ready(function () {
36
+ Printful_Connect.init('<?php echo admin_url( 'admin-ajax.php?action=ajax_force_check_connect_status' ); ?>');
37
+ });
38
+ </script>
39
+ </div>
40
+ </div>
includes/templates/error.php ADDED
@@ -0,0 +1 @@
 
1
+ <p class="printful-error"><b>Error:</b> <?php echo wp_kses_post($error); ?></p>
includes/templates/footer.php ADDED
@@ -0,0 +1 @@
 
1
+ </div>
includes/templates/header.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="wrap">
2
+
3
+ <?php
4
+ $base_url = '?page=printful-dashboard';
5
+ ?>
6
+
7
+ <h2 class="nav-tab-wrapper printful-tabs">
8
+ <?php foreach ($tabs as $tab) : ?>
9
+ <?php
10
+ $active = '';
11
+ if ( ! empty( $_GET['tab'] ) && $_GET['tab'] == $tab['tab_url'] ) {
12
+ $active = 'nav-tab-active';
13
+ }
14
+ if ( empty( $_GET['tab'] ) && $tab['tab_url'] == '' ) {
15
+ $active = 'nav-tab-active';
16
+ }
17
+ ?>
18
+ <a href="<?php echo esc_url($base_url . ($tab['tab_url'] ? '&tab=' . $tab['tab_url'] : '') ); ?>" class="nav-tab <?php echo esc_attr($active);?>"><?php echo esc_html($tab['name']); ?></a>
19
+ <?php endforeach; ?>
20
+ </h2>
includes/templates/inline-script.php ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <script type="text/javascript">
2
+ <?php echo $script; ?>
3
+ </script>
includes/templates/order-table.php ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <h2>Printful product orders</h2>
3
+
4
+ <?php if ( ! empty( $orders ) && $orders['count'] > 0 ): ?>
5
+
6
+ <table class="wp-list-table widefat fixed striped printful-latest-orders">
7
+ <thead>
8
+ <tr>
9
+ <th class="col-order">Order</th>
10
+ <th class="col-date">Date</th>
11
+ <th class="col-from">From</th>
12
+ <th class="col-status">Status</th>
13
+ <th class="col-total">Total</th>
14
+ <th class="col-actions">Actions</th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+
19
+ <?php foreach ( $orders['results'] as $order ) : ?>
20
+
21
+ <tr>
22
+ <td>
23
+ <?php
24
+ if ( $order['external_id'] ) {
25
+ echo '<a href="' . get_edit_post_link( $order['external_id'] ) . '">';
26
+ echo '#' . esc_html( $order['external_id'] );
27
+ echo '</a>';
28
+ } else {
29
+ echo '#' . esc_html( $order['id'] );
30
+ }
31
+ ?>
32
+ </td>
33
+ <td>
34
+ <?php echo esc_html( date('Y-m-d', $order['created']) ); ?>
35
+ </td>
36
+ <td>
37
+ <?php echo esc_html( $order['recipient']['name'] ); ?>
38
+ </td>
39
+ <td>
40
+ <?php echo esc_html( ucfirst($order['status']) ); ?>
41
+ </td>
42
+ <td>
43
+ $<?php echo esc_html( $order['costs']['total'] ); ?>
44
+ </td>
45
+ <td>
46
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard?order_id=<?php echo esc_attr($order['id']); ?>" target="_blank">Open in Printful</a>
47
+ </td>
48
+ </tr>
49
+
50
+ <?php endforeach; ?>
51
+
52
+ </tbody>
53
+ <tfoot>
54
+ <tr>
55
+ <th class="col-order">Order</th>
56
+ <th class="col-date">Date</th>
57
+ <th class="col-from">From</th>
58
+ <th class="col-status">Status</th>
59
+ <th class="col-total">Total</th>
60
+ <th class="col-actions">Actions</th>
61
+ </tr>
62
+ </tfoot>
63
+ </table>
64
+
65
+ <?php else: ?>
66
+ <div class="printful-latest-orders">
67
+ <p>Once your store gets some Printful product orders, they will be shown here!</p>
68
+ </div>
69
+ <?php endif; ?>
includes/templates/quick-links.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <h2>Quick links</h2>
2
+
3
+ <div class="printful-quick-links">
4
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard" target="_blank" class="printful-quick-links-item">
5
+ <span class="dashicons dashicons-cart"></span>
6
+ <h4>Orders</h4>
7
+ </a>
8
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/library" target="_blank" class="printful-quick-links-item">
9
+ <span class="dashicons dashicons-images-alt"></span>
10
+ <h4>File library</h4>
11
+ </a>
12
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/store" target="_blank" class="printful-quick-links-item">
13
+ <span class="dashicons dashicons-store"></span>
14
+ <h4>Stores</h4>
15
+ </a>
16
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/reports" target="_blank" class="printful-quick-links-item">
17
+ <span class="dashicons dashicons-chart-area"></span>
18
+ <h4>Reports</h4>
19
+ </a>
20
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/customer" target="_blank" class="printful-quick-links-item">
21
+ <span class="dashicons dashicons-admin-users"></span>
22
+ <h4>My Account</h4>
23
+ </a>
24
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/billing" target="_blank" class="printful-quick-links-item">
25
+ <span class="dashicons dashicons-portfolio"></span>
26
+ <h4>Billing</h4>
27
+ </a>
28
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/notifications" target="_blank" class="printful-quick-links-item">
29
+ <span class="dashicons dashicons-megaphone"></span>
30
+ <h4>Notifications</h4>
31
+ </a>
32
+ </div>
includes/templates/setting-group.php ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="printful-setting-group">
2
+
3
+ <h2><?php echo esc_html($title); ?></h2>
4
+ <p><?php echo esc_html($description); ?></p>
5
+
6
+ <?php if ( !empty( $settings ) ) : ?>
7
+
8
+ <table class="form-table">
9
+ <tbody>
10
+ <?php foreach($settings as $key => $setting) : ?>
11
+
12
+ <?php
13
+ if($setting['type'] == 'title') {
14
+ continue;
15
+ }
16
+ ?>
17
+
18
+ <tr valign="top">
19
+
20
+ <th scope="row" class="titledesc">
21
+ <span class="woocommerce-help-tip"></span>
22
+ <label for="<?php echo esc_attr($key); ?>"><?php echo esc_html($setting['title']); ?></label>
23
+ </th>
24
+
25
+ <td class="forminp">
26
+ <fieldset>
27
+ <legend class="screen-reader-text"><span><?php echo esc_html($setting['title']); ?></span></legend>
28
+
29
+ <?php if ( $setting['type'] == 'text' ) : ?>
30
+
31
+ <input class="input-text regular-input" type="text" name="<?php echo esc_attr($key); ?>" id="<?php echo esc_attr($key); ?>" value="<?php echo esc_html($setting['value']); ?>" placeholder="">
32
+
33
+ <?php elseif ($setting['type'] == 'checkbox') : ?>
34
+
35
+ <label for="<?php echo esc_attr($key); ?>">
36
+ <input class="" type="checkbox" name="<?php echo esc_attr($key); ?>" id="<?php echo esc_attr($key); ?>" value="<?php echo esc_attr($setting['value']); ?>" <?php if($setting['value'] == 'yes') { echo 'checked="checked"'; } ?>><?php echo esc_html($setting['label']); ?></label><br>
37
+
38
+ <?php elseif ($setting['type'] == 'checkbox-group') : ?>
39
+
40
+ <?php foreach ( $setting['items'] as $checkbox_key => $item ) : ?>
41
+
42
+ <label for="<?php echo esc_attr($checkbox_key); ?>" class="checkbox-group-item">
43
+ <input class="" type="checkbox" name="<?php echo esc_attr($key) . '[]'; ?>" id="<?php echo esc_attr($checkbox_key); ?>" value="<?php echo esc_attr($checkbox_key); ?>" <?php if ( $item['value'] == 'yes' ) {echo 'checked="checked"';} ?>><?php echo wp_kses_post( $item['label'] ); ?></label><br>
44
+
45
+ <?php endforeach; ?>
46
+
47
+ <?php endif; ?>
48
+
49
+ </fieldset>
50
+ </td>
51
+ </tr>
52
+
53
+ <?php endforeach; ?>
54
+ </tbody>
55
+ </table>
56
+
57
+ <?php endif; ?>
58
+
59
+ </div>
includes/templates/setting-submit.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p class="printful-submit">
2
+ <input name="save" class="button-primary woocommerce-save-button <?php if($disabled) { echo 'disabled'; } ?>" type="submit" value="Save changes" <?php if($disabled) { echo 'disabled'; } ?>/>
3
+ <input type="hidden" id="_wpnonce" name="_wpnonce" value="<?php echo esc_attr($nonce); ?>">
4
+ <?php wp_referer_field(true); ?>
5
+ <span class="loader-wrap">
6
+ <img src="<?php echo esc_url( admin_url( 'images/spinner-2x.gif' ) ) ?>" class="loader" width="20px" height="20px" alt="loader"/>
7
+ <span class="pass">
8
+ <span class="dashicons dashicons-yes"></span>
9
+ Saved successfully
10
+ </span>
11
+ <span class="fail">
12
+ <span class="dashicons dashicons-no"></span>
13
+ Saving settings failed
14
+ </span>
15
+ </span>
16
+ </p>
17
+ <script type="text/javascript">
18
+ jQuery(document).ready(function () {
19
+ Printful_Settings.init_submit();
20
+ });
21
+ </script>
includes/templates/shipping-notification.php ADDED
@@ -0,0 +1,4 @@
 
 
 
 
1
+ <div class="printful-setting-group">
2
+ <h2>Printful Shipping</h2>
3
+ <p>To enable/disable Printful shipping for your store go to <a href="<?php echo admin_url('admin.php?page=wc-settings&tab=shipping&section=printful_shipping'); ?>">WooCommerce Shipping settings</a>.</p>
4
+ </div>
includes/templates/stats.php ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="printful-stats">
2
+ <div class="printful-stats-item">
3
+ <h4>$<?php echo esc_html($stats['orders_today']['total']); ?></h4>
4
+ <b>
5
+ <?php
6
+ echo esc_html($stats['orders_today']['orders']);
7
+ echo ' ' . _n('ORDER', 'ORDERS', $stats['orders_today']['orders']);
8
+ ?>
9
+ </b>
10
+ today
11
+ </div>
12
+ <div class="printful-stats-item">
13
+ <h4>
14
+ $<?php echo esc_html($stats['orders_last_7_days']['total']); ?>
15
+ <?php echo '<span class="dashicons dashicons-arrow-' . esc_attr($stats['orders_last_7_days']['trend']) .'-alt"></span>'; ?>
16
+ </h4>
17
+ <b>
18
+ <?php
19
+ echo esc_html($stats['orders_last_7_days']['orders']);
20
+ echo ' ' . _n('ORDER', 'ORDERS', $stats['orders_last_7_days']['orders']);
21
+ ?>
22
+ </b>
23
+ last 7 days
24
+ </div>
25
+ <div class="printful-stats-item">
26
+ <h4>
27
+ $<?php echo esc_html($stats['orders_last_28_days']['total']); ?>
28
+ <?php echo '<span class="dashicons dashicons-arrow-' . esc_attr($stats['orders_last_28_days']['trend']) .'-alt"></span>'; ?>
29
+ </h4>
30
+ <b>
31
+ <?php
32
+ echo esc_html($stats['orders_last_28_days']['orders']);
33
+ echo ' ' . _n('ORDER', 'ORDERS', $stats['orders_last_28_days']['orders']);
34
+ ?>
35
+ </b> last 28 days
36
+ </div>
37
+ <div class="printful-stats-item">
38
+ <h4>
39
+ $<?php echo esc_attr($stats['profit_last_28_days']); ?>
40
+ <?php echo '<span class="dashicons dashicons-arrow-' . esc_attr($stats['profit_trend_last_28_days']) .'-alt"></span>'; ?>
41
+ </h4>
42
+ <b>PROFIT</b> last 28 days
43
+ </div>
44
+ </div>
includes/templates/status-report.php ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="support-report-wrap">
2
+ <p>Copy the box content below and add it to your support message</p>
3
+ <textarea class="support-report"><?php echo esc_html($status_report); ?></textarea>
4
+ <button class="button button-primary button-large support-report-btn">Copy</button>
5
+ <script type="text/javascript">
6
+ var copyTextareaBtn = document.querySelector('.support-report-btn');
7
+
8
+ copyTextareaBtn.addEventListener('click', function() {
9
+ var copyTextarea = document.querySelector('.support-report');
10
+ copyTextarea.select();
11
+
12
+ try {
13
+ document.execCommand('copy');
14
+ } catch (err) {
15
+ //do nothing
16
+ }
17
+ });
18
+ </script>
19
+ </div>
includes/templates/status-table.php ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php if ( $checklist['overall_status'] ) {
2
+ ?>
3
+ <div class="notice notice-success">
4
+ <p>Looks like the everything is set up correctly and Printful integration should work as intended.</p>
5
+ </div>
6
+ <?php
7
+ } else {
8
+ ?>
9
+ <div class="notice notice-error">
10
+ <p>There are errors with your store setup that may cause the Printful integration to not work as intended!</p>
11
+ </div>
12
+ <?php
13
+ }
14
+ ?>
15
+
16
+ <table class="wp-list-table widefat fixed striped printful-status">
17
+ <thead>
18
+ <tr>
19
+ <td class="col-name">Name</td>
20
+ <td class="col-desc">Description</td>
21
+ <td class="col-status">Status</td>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <?php
26
+ foreach ( $checklist['items'] as $item ) : ?>
27
+ <tr>
28
+ <td><?php echo esc_html( $item['name'] ); ?></td>
29
+ <td><?php echo esc_html( $item['description'] ); ?></td>
30
+ <td>
31
+ <?php
32
+ $status = 'OK';
33
+ if ( $item['status'] == 1 ) {
34
+ echo '<span class="pass">OK</span>';
35
+ } else if ( $item['status'] == 0 ) {
36
+ echo '<span class="warning">WARNING&#42;</span>';
37
+ } else {
38
+ echo '<span class="fail">FAIL</span>';
39
+ }
40
+ ?>
41
+ </td>
42
+ </tr>
43
+ <?php endforeach; ?>
44
+ </tbody>
45
+ <tfoot>
46
+ <tr>
47
+ <td class="col-name">Name</td>
48
+ <td class="col-desc">Description</td>
49
+ <td class="col-status">Status</td>
50
+ </tr>
51
+ </tfoot>
52
+ </table>
53
+
54
+ <p class="asterisk">&#42; Warnings are issued when the test was unable to come to a definite conclusion or if the result was passable, but not ideal.</p>
includes/templates/support-info.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="support-info-wrap">
2
+
3
+ <div class="support-info-block">
4
+ <h3>Need help? Get in touch!</h3>
5
+ <p>Email, call, text, or visit us. We've got support teams in the USA and Europe to make sure we're there when you need us.<br/>We'd love to hear from you.</p>
6
+ <div class="btn-wrap">
7
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>support" class="button button-primary button-large" target="_blank">Contact support</a>
8
+ </div>
9
+ </div>
10
+
11
+ <div class="support-info-block">
12
+ <h3>Read our FAQs</h3>
13
+ <p>Getting started made easy – read the FAQs to jumpstart your business.<br/>Whether you're a video tutorial fan or prefer the written answers – we've got it covered!</p>
14
+ <div class="btn-wrap">
15
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>faq" class="button button-primary button-large" target="_blank">See Printful FAQ</a>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="support-info-block">
20
+ <h3>Integration help</h3>
21
+ <p>Are you experiencing technical issues? Solve them on your own by reading these helpful guides and video tutorials.</p>
22
+ <div class="btn-wrap">
23
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>faq/integrations/woocommerce" class="button button-primary button-large" target="_blank">Integration help</a>
24
+ </div>
25
+ </div>
26
+
27
+ </div>
printful-shipping.php CHANGED
@@ -1,56 +1,114 @@
1
- <?php
2
- /**
3
- Plugin Name: Printful Integration for WooCommerce
4
- Plugin URI: https://wordpress.org/plugins/printful-shipping-for-woocommerce/
5
- Description: Calculate correct shipping and tax rates for your Printful-Woocommerce integration.
6
- Version: 1.2.8
7
- Author: Printful
8
- Author URI: http://www.theprintful.com
9
- License: GPL2 http://www.gnu.org/licenses/gpl-2.0.html
10
- */
11
-
12
- if ( ! defined( 'ABSPATH' ) ) exit;
13
-
14
- new Printful_Base();
15
-
16
-
17
- class Printful_Base {
18
-
19
- const VERSION = '1.2.8';
20
-
21
- /**
22
- * Construct the plugin.
23
- */
24
- public function __construct() {
25
- add_action( 'plugins_loaded', array( $this, 'init' ) );
26
- }
27
- /**
28
- * Initialize the plugin.
29
- */
30
- public function init() {
31
- if (!class_exists('WC_Integration')) {
32
- return;
33
- }
34
- //Register integration section
35
- add_filter('woocommerce_integrations', array($this, 'add_integration'));
36
-
37
- //Register API endpoint
38
- add_filter('woocommerce_api_classes', array($this, 'add_api_resource'));
39
- require_once 'includes/class-printful-integration.php';
40
- require_once 'includes/class-printful-shipping.php';
41
- new Printful_Shipping();
42
- }
43
-
44
- public function add_integration( $integrations ) {
45
- require_once 'includes/class-printful-integration.php';
46
- $integrations[] = 'Printful_Integration';
47
- return $integrations;
48
- }
49
-
50
- public function add_api_resource($endpoints)
51
- {
52
- require_once 'includes/class-printful-api-resource.php';
53
- $endpoints[]= 'Printful_API_Resource';
54
- return $endpoints;
55
- }
56
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ Plugin Name: Printful Integration for WooCommerce
4
+ Plugin URI: https://wordpress.org/plugins/printful-shipping-for-woocommerce/
5
+ Description: Calculate correct shipping and tax rates for your Printful-Woocommerce integration.
6
+ Version: 2.0
7
+ Author: Printful
8
+ Author URI: http://www.printful.com
9
+ License: GPL2 http://www.gnu.org/licenses/gpl-2.0.html
10
+ */
11
+
12
+ if ( ! defined( 'ABSPATH' ) ) exit;
13
+
14
+ class Printful_Base {
15
+
16
+ const VERSION = '2.0';
17
+ const PF_HOST = 'https://www.printful.com/';
18
+
19
+ /**
20
+ * Construct the plugin.
21
+ */
22
+ public function __construct() {
23
+ add_action( 'plugins_loaded', array( $this, 'init' ) );
24
+ }
25
+
26
+ /**
27
+ * Initialize the plugin.
28
+ */
29
+ public function init() {
30
+
31
+ if (!class_exists('WC_Integration')) {
32
+ return;
33
+ }
34
+
35
+ //Register API endpoint
36
+ add_filter('woocommerce_api_classes', array($this, 'add_api_resource'));
37
+
38
+ //Add settings to WooCommerce Index response (legacy - v2)
39
+ add_filter('woocommerce_api_index', array($this, 'add_extra_info_to_api_index'));
40
+
41
+ //load required classes
42
+ require_once 'includes/class-printful-integration.php';
43
+ require_once 'includes/class-printful-carriers.php';
44
+ require_once 'includes/class-printful-taxes.php';
45
+ require_once 'includes/class-printful-shipping.php';
46
+ require_once 'includes/class-printful-request-log.php';
47
+ require_once 'includes/class-printful-admin.php';
48
+ require_once 'includes/class-printful-admin-dashboard.php';
49
+ require_once 'includes/class-printful-admin-settings.php';
50
+ require_once 'includes/class-printful-admin-status.php';
51
+ require_once 'includes/class-printful-admin-support.php';
52
+ require_once 'includes/class-printful-size-chart-tab.php';
53
+ require_once 'includes/class-printful-size-chart-tab.php';
54
+
55
+ //launch init
56
+ Printful_Taxes::init();
57
+ Printful_Shipping::init();
58
+ Printful_Request_log::init();
59
+ Printful_Admin::init();
60
+ Printful_Size_Chart_Tab::init();
61
+
62
+ //hook ajax callbacks
63
+ add_action( 'wp_ajax_save_printful_settings', array( 'Printful_Admin_Settings', 'save_printful_settings' ) );
64
+ add_action( 'wp_ajax_ajax_force_check_connect_status', array( 'Printful_Integration', 'ajax_force_check_connect_status' ) );
65
+ add_action( 'wp_ajax_get_printful_stats', array( 'Printful_Admin_Dashboard', 'render_stats_ajax' ) );
66
+ add_action( 'wp_ajax_get_printful_orders', array( 'Printful_Admin_Dashboard', 'render_orders_ajax' ) );
67
+ add_action( 'wp_ajax_get_printful_status_checklist', array( 'Printful_Admin_Status', 'render_status_table_ajax' ) );
68
+ add_action( 'wp_ajax_get_printful_status_report', array( 'Printful_Admin_Support', 'render_status_report_ajax' ) );
69
+ add_action( 'wp_ajax_get_printful_carriers', array( 'Printful_Admin_Settings', 'render_carriers_ajax' ) );
70
+ }
71
+
72
+ /**
73
+ * Added API endpoints
74
+ * @param $endpoints
75
+ * @return array
76
+ */
77
+ public function add_api_resource($endpoints) {
78
+ require_once 'includes/class-printful-api-resource.php';
79
+ $endpoints[]= 'Printful_API_Resource';
80
+ return $endpoints;
81
+ }
82
+
83
+ /**
84
+ * @param $available
85
+ * Include plugin version in WC API Index
86
+ * @return mixed
87
+ */
88
+ public static function add_extra_info_to_api_index($available) {
89
+ $available['printful_plugin_version'] = self::VERSION;
90
+ $available['locale'] = get_locale();
91
+ return $available;
92
+ }
93
+
94
+ /**
95
+ * @return string
96
+ */
97
+ public static function get_asset_url() {
98
+ return trailingslashit(plugin_dir_url(__FILE__)) . 'assets/';
99
+ }
100
+
101
+ /**
102
+ * @return string
103
+ */
104
+ public static function get_printful_host() {
105
+ if ( defined( 'PF_DEV_HOST' ) ) {
106
+ return PF_DEV_HOST;
107
+ }
108
+
109
+ return self::PF_HOST;
110
+ }
111
+
112
+ }
113
+
114
+ new Printful_Base(); //let's go
readme.txt CHANGED
@@ -1,142 +1,169 @@
1
- === Printful Integration for WooCommerce ===
2
- Contributors: girts_u
3
- Tags: woocommerce, printful, drop shipping, shipping, shipping rates, fulfillment, printing, fedex, carriers, checkout, t-shirts
4
- Requires at least: 3.8
5
- Tested up to: 4.8
6
- Stable tag: 1.2.8
7
- License: GPLv2 or later
8
- License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
-
10
- Calculate live shipping rates and tax rates based on actual Printful shipping costs
11
-
12
- == Description ==
13
-
14
- Display actual live shipping rates from carriers like FedEx on your WooCommerce checkout page. This plugin will return a list of available shipping rates specific to the shipping address your customer provides when checking out. These rates are identical to the list you get when you submit an order manually via Printful dashboard.
15
-
16
- This plugin will also automatically calculate taxes where it is required for Printful so that your originally intended profit margin stays intact.
17
-
18
- = Known Limitations =
19
-
20
- * Works with WooCommmerce 2.1 and up
21
-
22
- == Installation ==
23
- 1. Upload 'printful-shipping' to the '/wp-content/plugins/' directory
24
- 1. Activate the plugin through the 'Plugins' menu in WordPress
25
- 1. Add your Printful API key to WooCommerce->Settings->Integration->Integration tab
26
- 1. Enable shipping rate calculation in WooCommerce->Settings->Shipping->Printful Shipping tab
27
- 1. To automatically calculate taxes please check 'Enable taxes and tax calculations' under WooCommerce Tax settings.
28
- 1. Then go to 'Integration' tab and check 'Calculate sales tax for locations where it is required for Printful orders'.
29
-
30
- == Frequently Asked Questions ==
31
-
32
- = How do I get Printful API key? =
33
-
34
- Go to https://www.theprintful.com/dashboard/store , select your WooCommerce store, click "Edit" and then click "Enable API Access". Your API key will be generated and displayed there.
35
-
36
- == Screenshots ==
37
-
38
- 1. Plugin settings dialog
39
- 2. Shipping rate dialog
40
- 3. Shipping rate selection
41
-
42
- == Upgrade Notice ==
43
-
44
- = 1.2.8 =
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  Fixed bug that caused tax rates to become invisible on checkout since WC 3.0
46
 
47
- = 1.2.7 =
48
  Do not calculate shipping rates for US addresses while ZIP or state is not entered
49
 
50
- = 1.2.6 =
51
  Include shipping rates in tax calculation for states that require that
52
-
53
- = 1.2.5 =
54
- Added option to allow Woocommerce default rates together with Printful rates for Printful products
55
-
56
- = 1.2.4 =
57
- Prevent virtual products from requiring shipping rate when bought together with Printful products
58
-
59
- = 1.2.3 =
60
- Fixed issue introduced in 1.2.2
61
-
62
- = 1.2.2 =
63
- Fixed PHP warning on Woocommerce 2.6
64
-
65
- = 1.2.1 =
66
- Fixed bug
67
-
68
- = 1.2 =
69
- Support calculating shipping rates for both Printful and non-Printful products at the same time
70
-
71
- = 1.1.2 =
72
- Removed check for Curl extension
73
-
74
- = 1.1.1 =
75
- Ignore virtual and downloadable products when calculating shipping rates
76
-
77
- = 1.1 =
78
- Added tax rate calculation
79
-
80
- = 1.0.2 =
81
- Added option to disable SSL
82
-
83
- = 1.0.1 =
84
- Minor improvements
85
-
86
- = 1.0 =
87
- First release
88
-
89
- == Changelog ==
90
-
91
- = 1.2.8 =
 
 
 
 
 
 
 
 
 
 
 
 
92
  * Fixed bug that caused tax rates to become invisible on checkout since WC 3.0
93
 
94
- = 1.2.7 =
95
  * Do not calculate shipping rates for US addresses while ZIP or state is not entered
96
 
97
- = 1.2.6 =
98
  * Include shipping rates in tax calculation for states that require that
99
-
100
- = 1.2.5 =
101
- * Added option to allow Woocommerce default rates together with Printful rates for Printful products
102
-
103
- = 1.2.4 =
104
- * Prevent virtual products from requiring shipping rate when bought together with Printful products
105
-
106
- = 1.2.3 =
107
- * Fixed issue introduced in 1.2.2
108
-
109
- = 1.2.2 =
110
- * Fixed PHP warning on Woocommerce 2.6 due to changed method signature
111
- * Fixed conflict with "Multiple Packages for WooCommerce" plugin
112
-
113
- = 1.2.1 =
114
- * Fixed bug that could have show error message when calculating shipping rates
115
-
116
- = 1.2 =
117
- * Support calculating shipping rates for both Printful and non-Printful products at the same time (non-Printful
118
- products will get default rates provided by Woocommerce)
119
- * Added caching to tax rates
120
- * Improved compatibility with Woocommerce 2.6
121
-
122
- = 1.1.2 =
123
- * Removed check for Curl extension (since we already used wp_remote_get and it is no longer necessary)
124
-
125
- = 1.1.1 =
126
- * Ignore virtual and downloadable products when calculating shipping rates
127
-
128
- = 1.1 =
129
- * Added option to calculate sales tax rates for locations where it is required for Printful orders
130
- * Added automatic conversion of shipping rates to the currency used by Woocommerce
131
- * Printful API client library updated to use Wordpress internal wp_remote_get method instead of CURL directly
132
- * Changed plugin code structure for easier implementation of new features in the future
133
-
134
- = 1.0.2 =
135
- * Added option to disable SSL for users that do not have a valid CA certificates in their PHP installation
136
-
137
- = 1.0.1 =
138
- * Removed CURLOPT_FOLLOWLOCATION that caused problems on some hosting environments
139
- * Added option to display reason status messages if the rate API request has failed
140
-
141
- = 1.0 =
142
- * First release
1
+ === Printful Integration for WooCommerce ===
2
+ Contributors: girts_u, kievins
3
+ Tags: woocommerce, printful, drop shipping, shipping, shipping rates, fulfillment, printing, fedex, carriers, checkout, t-shirts
4
+ Requires at least: 3.8
5
+ Tested up to: 4.8
6
+ Stable tag: 2.0
7
+ License: GPLv2 or later
8
+ License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
+
10
+ Calculate live shipping rates and tax rates based on actual Printful shipping costs
11
+
12
+ == Description ==
13
+
14
+ Display actual live shipping rates from carriers like FedEx on your WooCommerce checkout page. This plugin will return a list of available shipping rates specific to the shipping address your customer provides when checking out. These rates are identical to the list you get when you submit an order manually via Printful dashboard.
15
+
16
+ This plugin will also automatically calculate taxes where it is required for Printful so that your originally intended profit margin stays intact.
17
+
18
+ = Known Limitations =
19
+
20
+ * Works with WooCommmerce 2.1 and up
21
+
22
+ == Installation ==
23
+ 1. Upload 'printful-shipping' to the '/wp-content/plugins/' directory
24
+ 1. Activate the plugin through the 'Plugins' menu in WordPress
25
+ 1. Click the "Connect" button or add your Printful API key manually to Printful->Settings tab
26
+ 1. Enable shipping rate calculation in WooCommerce->Settings->Shipping->Printful Shipping tab
27
+ 1. To automatically calculate taxes please check 'Enable taxes and tax calculations' under WooCommerce Tax settings.
28
+ 1. Then go to Printful->Settings tab and check 'Calculated for all products shipped to North Carolina and California'.
29
+
30
+ == Frequently Asked Questions ==
31
+
32
+ = How do I get Printful API key? =
33
+
34
+ Go to https://www.printful.com/dashboard/store , select your WooCommerce store, click "Edit" and then click "Enable API Access". Your API key will be generated and displayed there.
35
+
36
+ == Screenshots ==
37
+
38
+ 1. Connect to Printful
39
+ 2. Printful dashboard
40
+ 3. Integration settings
41
+ 4. Plugin status page
42
+ 5. Support page
43
+ 6. Shipping live rates in cart
44
+
45
+ == Upgrade Notice ==
46
+
47
+ = 2.0 =
48
+ New major plugin version
49
+ * All new Printful dashboard
50
+ * Connect to Printful with a single click
51
+ * View your Printful profits and latest Printful product orders in WordPress admin
52
+ * Edit your shipping carriers from Printful dashboard
53
+ * Improved sales tax compatibility with existing tax rates
54
+ * New status page - see if your integration is running smoothly
55
+ * New support page - all info about finding help in one place
56
+ * Size chart tab - when pushing products from Printful, the size chart will be placed in a separate tab
57
+ * Improved logging of API requests coming to and from Printful
58
+
59
+ = 1.2.8 =
60
  Fixed bug that caused tax rates to become invisible on checkout since WC 3.0
61
 
62
+ = 1.2.7 =
63
  Do not calculate shipping rates for US addresses while ZIP or state is not entered
64
 
65
+ = 1.2.6 =
66
  Include shipping rates in tax calculation for states that require that
67
+
68
+ = 1.2.5 =
69
+ Added option to allow Woocommerce default rates together with Printful rates for Printful products
70
+
71
+ = 1.2.4 =
72
+ Prevent virtual products from requiring shipping rate when bought together with Printful products
73
+
74
+ = 1.2.3 =
75
+ Fixed issue introduced in 1.2.2
76
+
77
+ = 1.2.2 =
78
+ Fixed PHP warning on Woocommerce 2.6
79
+
80
+ = 1.2.1 =
81
+ Fixed bug
82
+
83
+ = 1.2 =
84
+ Support calculating shipping rates for both Printful and non-Printful products at the same time
85
+
86
+ = 1.1.2 =
87
+ Removed check for Curl extension
88
+
89
+ = 1.1.1 =
90
+ Ignore virtual and downloadable products when calculating shipping rates
91
+
92
+ = 1.1 =
93
+ Added tax rate calculation
94
+
95
+ = 1.0.2 =
96
+ Added option to disable SSL
97
+
98
+ = 1.0.1 =
99
+ Minor improvements
100
+
101
+ = 1.0 =
102
+ First release
103
+
104
+ == Changelog ==
105
+
106
+ = 2.0 =
107
+ New major plugin version
108
+ * All new Printful dashboard
109
+ * Connect to Printful with a single click
110
+ * View your Printful profits and latest Printful product orders in WordPress admin
111
+ * Edit your shipping carriers from Printful dashboard
112
+ * Improved sales tax compatibility with existing tax rates
113
+ * New status page - see if your integration is running smoothly
114
+ * New support page - all info about finding help in one place
115
+ * Size chart tab - when pushing products from Printful, the size chart will be placed in a separate tab
116
+ * Improved logging of API requests coming to and from Printful
117
+
118
+ = 1.2.8 =
119
  * Fixed bug that caused tax rates to become invisible on checkout since WC 3.0
120
 
121
+ = 1.2.7 =
122
  * Do not calculate shipping rates for US addresses while ZIP or state is not entered
123
 
124
+ = 1.2.6 =
125
  * Include shipping rates in tax calculation for states that require that
126
+
127
+ = 1.2.5 =
128
+ * Added option to allow Woocommerce default rates together with Printful rates for Printful products
129
+
130
+ = 1.2.4 =
131
+ * Prevent virtual products from requiring shipping rate when bought together with Printful products
132
+
133
+ = 1.2.3 =
134
+ * Fixed issue introduced in 1.2.2
135
+
136
+ = 1.2.2 =
137
+ * Fixed PHP warning on Woocommerce 2.6 due to changed method signature
138
+ * Fixed conflict with "Multiple Packages for WooCommerce" plugin
139
+
140
+ = 1.2.1 =
141
+ * Fixed bug that could have show error message when calculating shipping rates
142
+
143
+ = 1.2 =
144
+ * Support calculating shipping rates for both Printful and non-Printful products at the same time (non-Printful
145
+ products will get default rates provided by Woocommerce)
146
+ * Added caching to tax rates
147
+ * Improved compatibility with Woocommerce 2.6
148
+
149
+ = 1.1.2 =
150
+ * Removed check for Curl extension (since we already used wp_remote_get and it is no longer necessary)
151
+
152
+ = 1.1.1 =
153
+ * Ignore virtual and downloadable products when calculating shipping rates
154
+
155
+ = 1.1 =
156
+ * Added option to calculate sales tax rates for locations where it is required for Printful orders
157
+ * Added automatic conversion of shipping rates to the currency used by Woocommerce
158
+ * Printful API client library updated to use Wordpress internal wp_remote_get method instead of CURL directly
159
+ * Changed plugin code structure for easier implementation of new features in the future
160
+
161
+ = 1.0.2 =
162
+ * Added option to disable SSL for users that do not have a valid CA certificates in their PHP installation
163
+
164
+ = 1.0.1 =
165
+ * Removed CURLOPT_FOLLOWLOCATION that caused problems on some hosting environments
166
+ * Added option to display reason status messages if the rate API request has failed
167
+
168
+ = 1.0 =
169
+ * First release