· 8 years ago · Jun 27, 2018, 08:16 PM
1<?php
2
3
4
5class ISC_PRODUCT_IMAGE_EXCEPTION extends Exception { }
6
7
8
9class ISC_PRODUCT_IMAGE_INVALIDSIZE_EXCEPTION extends ISC_PRODUCT_IMAGE_EXCEPTION {
10
11
12
13 public function __construct ($size)
14
15 {
16
17 parent::__construct(sprintf(GetLang('ProductImagesInvalidSize'), $size));
18
19 }
20
21}
22
23
24
25class ISC_PRODUCT_IMAGE_INVALIDID_EXCEPTION extends ISC_PRODUCT_IMAGE_EXCEPTION { }
26
27class ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION extends ISC_PRODUCT_IMAGE_EXCEPTION { }
28
29class ISC_PRODUCT_IMAGE_RECORDNOTFOUND_EXCEPTION extends ISC_PRODUCT_IMAGE_EXCEPTION { }
30
31class ISC_PRODUCT_IMAGE_CANNOTDELETEFILE_EXCEPTION extends ISC_PRODUCT_IMAGE_EXCEPTION { }
32
33
34
35class ISC_PRODUCT_IMAGE_UNSUPPORTEDIMAGETYPE_EXCEPTION extends ISC_PRODUCT_IMAGE_EXCEPTION {
36
37 public function __construct ($imageType)
38
39 {
40
41 return parent::__construct(sprintf(GetLang('ProductImageUnsupportedImageType'), $imageType));
42
43 }
44
45}
46
47
48
49class ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION extends ISC_PRODUCT_IMAGE_EXCEPTION {
50
51 public function __construct ($filename = null)
52
53 {
54
55 if ($filename) {
56
57 return parent::__construct(sprintf(GetLang('ProductImageFileDoesNotExistSpecific'), $filename));
58
59 } else {
60
61 return parent::__construct(GetLang('ProductImageFileDoesNotExist'));
62
63 }
64
65 }
66
67}
68
69
70
71class ISC_PRODUCT_IMAGE_IMPORT_EXCEPTION extends ISC_PRODUCT_IMAGE_EXCEPTION { }
72
73
74
75class ISC_PRODUCT_IMAGE_IMPORT_INVALIDFILENAME_EXCEPTION extends ISC_PRODUCT_IMAGE_IMPORT_EXCEPTION {
76
77 public function __construct ($filename)
78
79 {
80
81 return parent::__construct(sprintf(GetLang('ProductImageInvalidFilename'), $filename));
82
83 }
84
85}
86
87
88
89class ISC_PRODUCT_IMAGE_IMPORT_INVALIDIMAGEFILE_EXCEPTION extends ISC_PRODUCT_IMAGE_IMPORT_EXCEPTION {
90
91 public function __construct ()
92
93 {
94
95 return parent::__construct(GetLang('ProductImageFileNotAnImage'));
96
97 }
98
99}
100
101
102
103class ISC_PRODUCT_IMAGE_IMPORT_NOPHPSUPPORT_EXCEPTION extends ISC_PRODUCT_IMAGE_IMPORT_EXCEPTION {
104
105 public function __construct ()
106
107 {
108
109 return parent::__construct(GetLang('ProductImageNoProcessors'));
110
111 }
112
113}
114
115
116
117class ISC_PRODUCT_IMAGE_IMPORT_EMPTYIMAGE_EXCEPTION extends ISC_PRODUCT_IMAGE_IMPORT_EXCEPTION {
118
119 public function __construct ()
120
121 {
122
123 return parent::__construct(GetLang('ProductImageNoWidthNoHeight'));
124
125 }
126
127}
128
129
130
131class ISC_PRODUCT_IMAGE_IMPORT_CANTCREATEDIR_EXCEPTION extends ISC_PRODUCT_IMAGE_IMPORT_EXCEPTION {
132
133 public function __construct ($directory)
134
135 {
136
137 return parent::__construct(sprintf(GetLang('ProductImageDestinationDirectoryError'), GetConfig('ImageDirectory')));
138
139 }
140
141}
142
143
144
145class ISC_PRODUCT_IMAGE_IMPORT_CANTMOVEFILE_EXCEPTION extends ISC_PRODUCT_IMAGE_IMPORT_EXCEPTION {
146
147 public function __construct ($filename)
148
149 {
150
151 return parent::__construct(sprintf(GetLang('ProductImageDestinationFileError'), GetConfig('ImageDirectory')));
152
153 }
154
155}
156
157
158
159class ISC_PRODUCT_IMAGE_CREATEDIRECTORY_EXCEPTION extends ISC_PRODUCT_IMAGE_IMPORT_CANTCREATEDIR_EXCEPTION { }
160
161
162
163class ISC_PRODUCT_IMAGE {
164
165
166
167 /**
168
169 * Product image id (pkey) from database
170
171 *
172
173 * @var int
174
175 */
176
177 protected $_productImageId = 0;
178
179
180
181 /**
182
183 * Path of source (non-water-marked) image file relative to product_images directory
184
185 *
186
187 * @var string
188
189 */
190
191 protected $_sourceFilePath;
192
193
194
195 /**
196
197 * Internal storage of resize results, either from resizing operations or from values saved in the database
198
199 *
200
201 * @var array
202
203 */
204
205 protected $_resizedFileDimensions = array(
206
207 ISC_PRODUCT_IMAGE_SIZE_ZOOM => null,
208
209 ISC_PRODUCT_IMAGE_SIZE_STANDARD => null,
210
211 ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL => null,
212
213 ISC_PRODUCT_IMAGE_SIZE_TINY => null,
214
215 );
216
217
218
219 /**
220
221 * Internal storage of paths to resized images, relative to the product_images directory
222
223 *
224
225 * @var string
226
227 */
228
229 protected $_resizedFilePaths = array(
230
231 ISC_PRODUCT_IMAGE_SIZE_ZOOM => null,
232
233 ISC_PRODUCT_IMAGE_SIZE_STANDARD => null,
234
235 ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL => null,
236
237 ISC_PRODUCT_IMAGE_SIZE_TINY => null,
238
239 );
240
241
242
243 /**
244
245 * Whether or not this image is selected as the thumbnail image for the product it belongs to
246
247 *
248
249 * @var bool
250
251 */
252
253 protected $_isThumbnail;
254
255
256
257 /**
258
259 * The id of the product this image belongs to
260
261 *
262
263 * @var int
264
265 */
266
267 protected $_productId = 0;
268
269
270
271 /**
272
273 * Intended for storing an object pointing to full information about the product this image belongs to but is currently unused -- see getProduct()
274
275 *
276
277 * @var stdClass
278
279 */
280
281 protected $_product;
282
283
284
285 /**
286
287 * Whether this image is visible or not -- not currently used
288
289 *
290
291 * @var mixed
292
293 */
294
295 protected $_visible;
296
297
298
299 /**
300
301 * Storage for alternate text -- not currently used
302
303 *
304
305 * @var string
306
307 */
308
309 protected $_alternateText = '';
310
311
312
313 /**
314
315 * Storage for caption text -- not currently used
316
317 *
318
319 * @var string
320
321 */
322
323 protected $_caption = '';
324
325
326
327 /**
328
329 * Storage for description text
330
331 *
332
333 * @var string
334
335 */
336
337 protected $_description = '';
338
339
340
341 /**
342
343 * Storage for the date added timestamp
344
345 *
346
347 * @var int
348
349 */
350
351 protected $_dateAdded = 0;
352
353
354
355 /**
356
357 * Stores a reference to the image library used for manipulating this product image's source image file, may be null -- see getImageLibrary()
358
359 *
360
361 * @var ISC_IMAGE_LIBRARY_INTERFACE
362
363 */
364
365 protected $_imageLibrary;
366
367
368
369 /**
370
371 * Sorting value for this product image
372
373 *
374
375 * @var int
376
377 */
378
379 protected $_sort;
380
381
382
383 /**
384
385 * Hash value of the product this image belongs to (only valid for images that belong to a product currently being added or copied)
386
387 *
388
389 * @var mixed
390
391 */
392
393 protected $_productHash = '';
394
395
396
397 /**
398
399 * The name of the product this image belongs to, only stored if the database row that populated this instance had a product name in it
400
401 *
402
403 * @var string
404
405 */
406
407 protected $_productName = '';
408
409
410
411 /**
412
413 * A shortcut to ISC_IMAGE_LIBRARY_FACTORY
414
415 *
416
417 * @param mixed $filePath
418
419 */
420
421 public static function isValidImageFile ($filePath)
422
423 {
424
425 return ISC_IMAGE_LIBRARY_FACTORY::isValidImageFile($filePath);
426
427 }
428
429
430
431 /**
432
433 * Will add $prepend before the extension of $fileName - e.g.: prependToFileExtension('a.b.c.ext', '_d') == 'a.b.c_d.ext'
434
435 *
436
437 * If no extension is found, no appending will take place
438
439 *
440
441 * @param string $fileName
442
443 * @param string $prepend
444
445 */
446
447 public static function prependToFileExtension ($fileName, $prepend)
448
449 {
450
451 return preg_replace('#^(.*)\.(.*)$#', '\1' . $prepend . '.\2', $fileName);
452
453 }
454
455
456
457 /**
458
459 * Generate a random string of characters to a specific length based on the specified selection of characters
460
461 *
462
463 * @param int $length
464
465 * @param string $selection
466
467 */
468
469 public static function randomString ($length, $selection = '0123456789abcdefghijklmnopqrstuvwxyz')
470
471 {
472
473 $output = '';
474
475
476
477 $selectionLength = strlen($selection) - 1;
478
479 while ($length) {
480
481 $output .= substr($selection, rand(0, $selectionLength), 1);
482
483 $length--;
484
485 }
486
487
488
489 return $output;
490
491 }
492
493
494
495 /**
496
497 * When a new product image is uploaded, it must be assigned a directory based on safe_mode settings -- this function will generate an appropriate path (relative to the product_images directory) to move the newly uploaded image to
498
499 *
500
501 * @param string $fileName The filename of the new image
502
503 * @param bool $safeMode Force the filename to be generated with safe mode either on or off, leave as default (null) to autodetect
504
505 */
506
507 public static function generateSourceImageRelativeFilePath ($fileName, $safeMode = null)
508
509 {
510
511 if ($safeMode === null) {
512
513 $safeMode = ini_get('safe_mode');
514
515 if ($safeMode == 1 || strtolower($safeMode) == 'on') {
516
517 $safeMode = true;
518
519 } else {
520
521 $safeMode = false;
522
523 }
524
525 }
526
527
528
529 $exists = true;
530
531
532
533 while ($exists) {
534
535 // keep generating paths until we find one that doesn't exist
536
537 $path = chr(rand(97,122)) . '/';
538
539 if (!$safeMode) {
540
541 $path .= self::randomString(3, '0123456789') . '/';
542
543 }
544
545
546
547 $randomString = self::randomString(5, '0123456789');
548
549
550
551 $path .= self::prependToFileExtension($fileName, '__' . $randomString);
552
553
554
555 $exists = file_exists($path);
556
557 }
558
559
560
561 return $path;
562
563 }
564
565
566
567 /**
568
569 * Returns the image for the given product id that would be used as the base thumbnail - if no image is marked as such, the page thumbnail will be returned, otherwise the first image according to sort order
570
571 *
572
573 * @param int|string $productId
574
575 * @param bool $hash if true, will treat $productId as a hash string of a product being added, instead of a product id
576
577 * @throws ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION If an unhandled database error occurrs while attempting to fetch product image data
578
579 * @return ISC_PRODUCT_IMAGE or false if no usable image was found
580
581 */
582
583 public static function getBaseThumbnailImageForProduct ($productId, $hash = false)
584
585 {
586
587 $db = $GLOBALS['ISC_CLASS_DB'];
588
589
590
591 if ($hash) {
592
593 $sql = "SELECT /*ISC_PRODUCT_IMAGE::getBaseThumbnailImageForProduct*/ * FROM `[|PREFIX|]product_images` WHERE imageprodhash = '" . $db->Quote($productId) . "' ORDER BY imageisthumb desc, imagesort LIMIT 1";
594
595 } else {
596
597 $sql = "SELECT /*ISC_PRODUCT_IMAGE::getBaseThumbnailImageForProduct*/ * FROM `[|PREFIX|]product_images` WHERE imageprodid = " . (int)$productId . " ORDER BY imageisthumb desc, imagesort LIMIT 1";
598
599 }
600
601
602
603 $result = $db->Query($sql);
604
605 if (!$result) {
606
607 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
608
609 }
610
611
612
613 $row = $db->Fetch($result);
614
615 if (!$row) {
616
617 return false;
618
619 }
620
621
622
623 $image = new ISC_PRODUCT_IMAGE();
624
625 $image->populateFromDatabaseRow($row);
626
627
628
629 return $image;
630
631 }
632
633
634
635 /**
636
637 * This is a maintenance method which will clean up the product_images table and any associated files to remove images which are no longer associated with any product.
638
639 *
640
641 * @return void
642
643 */
644
645 public static function deleteOrphanedProductImages ()
646
647 {
648
649 // select images where imageprodid matches no productid
650
651 $sql = "(SELECT /*ISC_PRODUCT_IMAGE::deleteOrphanedProductImages*/ pi.* FROM `[|PREFIX|]product_images` pi LEFT JOIN `[|PREFIX|]products` p ON p.productid = pi.imageprodid WHERE pi.imageprodid <> 0 AND p.productid IS NULL)";
652
653
654
655 // also select images where imageprodid is 0 and the added date is older than 24 hours
656
657 $sql .= " UNION ";
658
659 $sql .= "(SELECT * FROM `[|PREFIX|]product_images` WHERE imageprodid = 0 AND imagedateadded < " . (time() - 86400) . ")";
660
661 $sql .= " LIMIT 200"; // limit returned items to 200, any remaining images should be picked up by subsequent calls to this method
662
663
664
665 // call ->delete() for each image
666
667 $db = $GLOBALS['ISC_CLASS_DB'];
668
669 $result = $db->Query($sql);
670
671 if (!$result) {
672
673 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
674
675 }
676
677
678
679 while ($row = $db->Fetch($result)) {
680
681 $image = new ISC_PRODUCT_IMAGE();
682
683 $image->populateFromDatabaseRow($row);
684
685 try {
686
687 $image->delete();
688
689 } catch (ISC_PRODUCT_IMAGE_CANNOTDELETEFILE_EXCEPTION $exception) {
690
691 // disregard
692
693 }
694
695 }
696
697 }
698
699
700
701 /**
702
703 * Generate and return the SQL statement used by getAllProductImagesFromDatabase
704
705 *
706
707 * @return string
708
709 */
710
711 public static function generateGetAllProductImagesFromDatabaseSql ()
712
713 {
714
715 $sql = "SELECT /*ISC_PRODUCT_IMAGE::generateGetAllProductImageFromDatabaseSql*/ * FROM `[|PREFIX|]product_images`";
716
717
718
719 return $sql;
720
721 }
722
723
724
725 /**
726
727 * Retrieves all product images as an array of ISC_PRODUCT_IMAGE instances. If a large number of product images are expected use generateGetProductImagesFromDatabaseSql to directly query the db and process individual ISC_PRODUCT_IMAGE instances instead.
728
729 *
730
731 * @return array
732
733 * @throws ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION IF an unhandled database error occurrs while attempting to fetch product image data
734
735 */
736
737 public static function getAllProductImagesFromDatabase ()
738
739 {
740
741 $db = $GLOBALS['ISC_CLASS_DB'];
742
743
744
745 $result = $db->Query(self::generateGetAllProductImagesFromDatabaseSql());
746
747 if (!$result) {
748
749 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
750
751 }
752
753
754
755 $images = array();
756
757 while ($row = $db->Fetch($result)) {
758
759 $image = new ISC_PRODUCT_IMAGE();
760
761 $image->populateFromDatabaseRow($row);
762
763 $images[] = $image;
764
765 }
766
767 return $images;
768
769 }
770
771
772
773 /**
774
775 * Generate and return the SQL statement used by getProductImagesFromDatabase
776
777 *
778
779 * @param int|string $productId The product id (or hash when $hash is true) to retrieve product images for
780
781 * @param int $page Optional. Page number of product images to retrieve. If unspecified or null, all images will be returned. The page size is based on any settings for the front end.
782
783 * @param bool $hash If true, $productId is treated as a hash of an unsaved product
784
785 * @return string
786
787 */
788
789 public static function generateGetProductImagesFromDatabaseSql ($productId, $page = null, $hash = false)
790
791 {
792
793 if ($page !== null) {
794
795 throw new Exception('$page parameter for generateGetProductImagesFromDatabaseSql is not yet implemented.');
796
797 }
798
799
800
801 // nearly all columns are required for populateFromDatabaseRow, so just select *
802
803 if ($hash) {
804
805 $sql = "SELECT /*ISC_PRODUCT_IMAGE::generateGetProductImagesFromDatabaseSql*/ * FROM `[|PREFIX|]product_images` WHERE imageprodhash = '" . $GLOBALS['ISC_CLASS_DB']->Quote($productId) . "' ORDER BY imagesort";
806
807 } else {
808
809 $sql = "SELECT /*ISC_PRODUCT_IMAGE::generateGetProductImagesFromDatabaseSql*/ * FROM `[|PREFIX|]product_images` WHERE imageprodid = " . (int)$productId . " ORDER BY imagesort";
810
811 }
812
813
814
815 return $sql;
816
817 }
818
819
820
821 /**
822
823 * Retrieves images for a product as an array of ISC_PRODUCT_IMAGE instances. If a large number of product images are expected, use paging and perhaps use generateGetProductImagesFromDatabaseSql to directly query the db and process individual ISC_PRODUCT_IMAGE instances instead.
824
825 *
826
827 * @param int|string $productId The product id (or hash when $hash is true) to retrieve product images for
828
829 * @param int $page Optional. Page number of product images to retrieve. If unspecified or null, all images will be returned. The page size is based on any settings for the front end.
830
831 * @param bool $hash If true, $productId is treated as a hash of an unsaved product
832
833 * @throws ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION If an unhandled database error occurrs while attempting to fetch product image data
834
835 * @return array An array of ISC_PRODUCT_IMAGE or empty array if no images were found
836
837 */
838
839 public static function getProductImagesFromDatabase ($productId, $page = null, $hash = false)
840
841 {
842
843 $db = $GLOBALS['ISC_CLASS_DB'];
844
845
846
847 $result = $db->Query(self::generateGetProductImagesFromDatabaseSql($productId, $page, $hash));
848
849 if (!$result) {
850
851 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
852
853 }
854
855
856
857 $images = array();
858
859 while ($row = $db->Fetch($result)) {
860
861 $image = new ISC_PRODUCT_IMAGE();
862
863 $image->populateFromDatabaseRow($row);
864
865 $images[] = $image;
866
867 }
868
869 return $images;
870
871 }
872
873
874
875 /**
876
877 * Retrieves a single product image from the database as an instance of ISC_PRODUCT_IMAGE
878
879 *
880
881 * @param int $productImageId
882
883 * @return ISC_PRODUCT_IMAGE
884
885 */
886
887 public static function getProductImageFromDatabase ($productImageId)
888
889 {
890
891 return new ISC_PRODUCT_IMAGE($productImageId);
892
893 }
894
895
896
897 /**
898
899 * Returns the default JPEG compression quality defined by ISC settings (if any) as a value between 0 (worst) and 100 (best)
900
901 *
902
903 * @return float
904
905 */
906
907 public static function getDefaultJpegQuality ()
908
909 {
910
911 // if this is ever implemented on the settings page, change this to return a dynamic value
912
913 return 90;
914
915 }
916
917
918
919 /**
920
921 * Returns the default PNG compression level defined by ISC settings (if any) as a value between 0 (no compress) and 9 (maximum compression)
922
923 *
924
925 * @return int
926
927 */
928
929 public static function getDefaultPngCompression ()
930
931 {
932
933 // if this is ever implemented on the settings page, change this to return a dynamic value
934
935 return 9;
936
937 }
938
939
940
941 /**
942
943 * Returns the default PNG filters to use for product images defined by ISC settings (if any) as a bitmask value based on values of PNG_FILTER_XXX constants
944
945 *
946
947 */
948
949 public static function getDefaultPngFilters ()
950
951 {
952
953 // if this is ever implemented on the settings page, change this to return a dynamic value
954
955 return PNG_ALL_FILTERS;
956
957 }
958
959
960
961 /**
962
963 * Returns the width, in pixels, that has been configured for a given product image size
964
965 *
966
967 * @param int $size One of ISC_PRODUCT_IMAGE_SIZE_XXX
968
969 * @throws ISC_PRODUCT_IMAGE_INVALIDSIZE_EXCEPTION If an invalid size is specified
970
971 * @return int
972
973 */
974
975 public static function getSizeWidth ($size)
976
977 {
978
979 switch ($size) {
980
981 case ISC_PRODUCT_IMAGE_SIZE_STANDARD:
982
983 if(isset($GLOBALS['ISC_CFG']['ProductImagesProductPageImage_width']) && (int)$GLOBALS['ISC_CFG']['ProductImagesProductPageImage_width'] > 0) {
984
985 return min(ISC_PRODUCT_IMAGE_MAXLONGEDGE, (int)$GLOBALS['ISC_CFG']['ProductImagesProductPageImage_width']);
986
987 } else {
988
989 return ISC_PRODUCT_DEFAULT_IMAGE_SIZE_STANDARD;
990
991 }
992
993 break;
994
995
996
997 case ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL:
998
999 if(isset($GLOBALS['ISC_CFG']['ProductImagesStorewideThumbnail_width']) && (int)$GLOBALS['ISC_CFG']['ProductImagesStorewideThumbnail_width'] > 0) {
1000
1001 return min(ISC_PRODUCT_IMAGE_MAXLONGEDGE, (int)$GLOBALS['ISC_CFG']['ProductImagesStorewideThumbnail_width']);
1002
1003 } else {
1004
1005 return ISC_PRODUCT_DEFAULT_IMAGE_SIZE_THUMBNAIL;
1006
1007 }
1008
1009 break;
1010
1011
1012
1013 case ISC_PRODUCT_IMAGE_SIZE_TINY:
1014
1015 if(isset($GLOBALS['ISC_CFG']['ProductImagesGalleryThumbnail_width']) && (int)$GLOBALS['ISC_CFG']['ProductImagesGalleryThumbnail_width'] > 0) {
1016
1017 return min(ISC_PRODUCT_IMAGE_MAXLONGEDGE, (int)$GLOBALS['ISC_CFG']['ProductImagesGalleryThumbnail_width']);
1018
1019 } else {
1020
1021 return ISC_PRODUCT_DEFAULT_IMAGE_SIZE_TINY;
1022
1023 }
1024
1025 break;
1026
1027
1028
1029 case ISC_PRODUCT_IMAGE_SIZE_ZOOM:
1030
1031 if(isset($GLOBALS['ISC_CFG']['ProductImagesZoomImage_width']) && (int)$GLOBALS['ISC_CFG']['ProductImagesZoomImage_width'] > 0) {
1032
1033 return min(ISC_PRODUCT_IMAGE_MAXLONGEDGE, (int)$GLOBALS['ISC_CFG']['ProductImagesZoomImage_width']);
1034
1035 } else {
1036
1037 return ISC_PRODUCT_DEFAULT_IMAGE_SIZE_ZOOM;
1038
1039 }
1040
1041 break;
1042
1043
1044
1045 default:
1046
1047 throw new ISC_PRODUCT_IMAGE_INVALIDSIZE_EXCEPTION($size);
1048
1049 }
1050
1051
1052
1053 return $width;
1054
1055 }
1056
1057
1058
1059 /**
1060
1061 * Returns the height, in pixels, that has been configured for a given product image size
1062
1063 *
1064
1065 * @param int $size One of ISC_PRODUCT_IMAGE_SIZE_XXX
1066
1067 * @throws ISC_PRODUCT_IMAGE_INVALIDSIZE_EXCEPTION If an invalid size is specified
1068
1069 * @return int
1070
1071 */
1072
1073 public static function getSizeHeight ($size)
1074
1075 {
1076
1077 switch ($size) {
1078
1079 case ISC_PRODUCT_IMAGE_SIZE_STANDARD:
1080
1081 if(isset($GLOBALS['ISC_CFG']['ProductImagesProductPageImage_height']) && (int)$GLOBALS['ISC_CFG']['ProductImagesProductPageImage_height'] > 0) {
1082
1083 return min(ISC_PRODUCT_IMAGE_MAXLONGEDGE, (int)$GLOBALS['ISC_CFG']['ProductImagesProductPageImage_height']);
1084
1085 } else {
1086
1087 return ISC_PRODUCT_DEFAULT_IMAGE_SIZE_STANDARD;
1088
1089 }
1090
1091 break;
1092
1093
1094
1095 case ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL:
1096
1097 if(isset($GLOBALS['ISC_CFG']['ProductImagesStorewideThumbnail_height']) && (int)$GLOBALS['ISC_CFG']['ProductImagesStorewideThumbnail_height'] > 0) {
1098
1099 return min(ISC_PRODUCT_IMAGE_MAXLONGEDGE, (int)$GLOBALS['ISC_CFG']['ProductImagesStorewideThumbnail_height']);
1100
1101 } else {
1102
1103 return ISC_PRODUCT_DEFAULT_IMAGE_SIZE_THUMBNAIL;
1104
1105 }
1106
1107 break;
1108
1109
1110
1111 case ISC_PRODUCT_IMAGE_SIZE_TINY:
1112
1113 if(isset($GLOBALS['ISC_CFG']['ProductImagesGalleryThumbnail_height']) && (int)$GLOBALS['ISC_CFG']['ProductImagesGalleryThumbnail_height'] > 0) {
1114
1115 return min(ISC_PRODUCT_IMAGE_MAXLONGEDGE, (int)$GLOBALS['ISC_CFG']['ProductImagesGalleryThumbnail_height']);
1116
1117 } else {
1118
1119 return ISC_PRODUCT_DEFAULT_IMAGE_SIZE_TINY;
1120
1121 }
1122
1123 break;
1124
1125
1126
1127 case ISC_PRODUCT_IMAGE_SIZE_ZOOM:
1128
1129 if(isset($GLOBALS['ISC_CFG']['ProductImagesZoomImage_height']) && (int)$GLOBALS['ISC_CFG']['ProductImagesZoomImage_height'] > 0) {
1130
1131 return min(ISC_PRODUCT_IMAGE_MAXLONGEDGE, (int)$GLOBALS['ISC_CFG']['ProductImagesZoomImage_height']);
1132
1133 } else {
1134
1135 return ISC_PRODUCT_DEFAULT_IMAGE_SIZE_ZOOM;
1136
1137 }
1138
1139 break;
1140
1141
1142
1143 default:
1144
1145 throw new ISC_PRODUCT_IMAGE_INVALIDSIZE_EXCEPTION($size);
1146
1147 }
1148
1149
1150
1151 return $width;
1152
1153 }
1154
1155
1156
1157 /**
1158
1159 * Returns a set of default image write options based on the given image type (PNG/JPG compression settings, etc.) and any product image settings in ISC
1160
1161 *
1162
1163 * @param int $imageType
1164
1165 * @return ISC_IMAGE_WRITEOPTIONS A child class of ISC_IMAGE_WRITEOPTIONS such as ISC_IMAGE_WRITEOPTIONS_JPEG, ISC_IMAGE_WRITEOPTIONS_PNG, ISC_IMAGE_WRITEOPTIONS_GIF, etc.
1166
1167 */
1168
1169 public static function getWriteOptionsForImageType ($imageType)
1170
1171 {
1172
1173 switch ($imageType) {
1174
1175 case IMAGETYPE_JPEG:
1176
1177 $writeOptions = new ISC_IMAGE_WRITEOPTIONS_JPEG();
1178
1179 $writeOptions->setQuality(self::getDefaultJpegQuality());
1180
1181 break;
1182
1183
1184
1185 case IMAGETYPE_PNG:
1186
1187 $writeOptions = new ISC_IMAGE_WRITEOPTIONS_PNG();
1188
1189 $writeOptions->setCompression(self::getDefaultPngCompression());
1190
1191 $writeOptions->setFilters(self::getDefaultPngFilters());
1192
1193 break;
1194
1195
1196
1197 case IMAGETYPE_GIF:
1198
1199 $writeOptions = new ISC_IMAGE_WRITEOPTIONS_GIF();
1200
1201 break;
1202
1203
1204
1205 default:
1206
1207 throw new ISC_PRODUCT_IMAGE_UNSUPPORTEDIMAGETYPE_EXCEPTION($imageType);
1208
1209 break;
1210
1211 }
1212
1213
1214
1215 return $writeOptions;
1216
1217 }
1218
1219
1220
1221 /**
1222
1223 * Imports a temporary image file on the server to the given product. Performs validation, moves the file to it's final location and filename and returns an instance of ISC_PRODUCT_IMAGE.
1224
1225 *
1226
1227 * It is up to the method calling this to delete any temporary file if something goes wrong.
1228
1229 *
1230
1231 * @param string $temporaryPath Absolute path to the temporary image file stored on the server to be imported -- this file will need to be read so if it is an uploaded file and is in the tmp folder you should move it to the cache directory first since open_basedir restrictions may prevent the file being read from the tmp folder
1232
1233 * @param string $originalFilename Original intended filename (such as the name provided by the browser when uploading a file) which may differ from the temporary file at $temporaryPath -- this should not include any directory components
1234
1235 * @param int|string|bool $productId The id (or hash when $hash is true) of the product to import to, or supply as false to not save any info to the database but still return an instance of ISC_PRODUCT_IMAGE
1236
1237 * @param bool $hash If true, $productId will be treated as a hash of a product in the process of being added
1238
1239 * @param bool $moveTemporaryFile If true, the provided temporary file will be moved to it's new location, otherwise it will be copied
1240
1241 * @param bool $generateImages If true, when importing, will attempt to generate thumbnail images -- may not be desirable if importing many images at once
1242
1243 * @throws ISC_PRODUCT_IMAGE_IMPORT_INVALIDIMAGEFILE_EXCEPTION If the file is not a valid image
1244
1245 * @throws ISC_PRODUCT_IMAGE_IMPORT_NOPHPSUPPORT_EXCEPTION If the image could not be processed by any installed php extensions
1246
1247 * @throws ISC_PRODUCT_IMAGE_IMPORT_EMPTYIMAGE_EXCEPTION If the image is 'empty' - has 0 width or 0 height
1248
1249 * @throws ISC_PRODUCT_IMAGE_IMPORT_CANTCREATEDIR_EXCEPTION If an error prevented the image's destination directory from being created (usually lack of write permissions on parent directory)
1250
1251 * @throws ISC_PRODUCT_IMAGE_IMPORT_CANTMOVEFILE_EXCEPTION If an error prevented the image from being moved to the destination directory (usually lack of write permissions on parent directory)
1252
1253 * @return ISC_PRODUCT_IMAGE If everything went OK
1254
1255 */
1256
1257 public static function importImage ($temporaryPath, $originalFilename, $productId, $hash = false, $moveTemporaryFile = true, $generateImages = true)
1258
1259 {
1260
1261 if (!file_exists($temporaryPath)) {
1262
1263 throw new ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION($temporaryPath);
1264
1265 }
1266
1267
1268
1269 try {
1270
1271 $library = ISC_IMAGE_LIBRARY_FACTORY::getImageLibraryInstance($temporaryPath);
1272
1273 } catch (ISC_IMAGE_LIBRARY_FACTORY_INVALIDIMAGEFILE_EXCEPTION $ex) {
1274
1275 throw new ISC_PRODUCT_IMAGE_IMPORT_INVALIDIMAGEFILE_EXCEPTION();
1276
1277 } catch (ISC_IMAGE_LIBRARY_FACTORY_NOPHPSUPPORT_EXCEPTION $ex) {
1278
1279 throw new ISC_PRODUCT_IMAGE_IMPORT_NOPHPSUPPORT_EXCEPTION();
1280
1281 }
1282
1283
1284
1285 if ($library->getWidth() < 1 || $library->getHeight() < 1) {
1286
1287 throw new ISC_PRODUCT_IMAGE_IMPORT_EMPTYIMAGE_EXCEPTION();
1288
1289 }
1290
1291
1292
1293 $finalName = $originalFilename;
1294
1295
1296
1297
1298
1299 $finalName = basename($finalName); // remove any path components from the filename
1300
1301 $finalName = self::sanitiseFilename($finalName);
1302
1303
1304
1305 if (!self::isValidFilename($finalName, false)) {
1306
1307 throw new ISC_PRODUCT_IMAGE_IMPORT_INVALIDFILENAME_EXCEPTION($finalName);
1308
1309 }
1310
1311
1312
1313 // correct the uploaded extension
1314
1315 $correctExtension = $library->getImageTypeExtension(false);
1316
1317 if (strtolower(pathinfo($finalName, PATHINFO_EXTENSION)) != $correctExtension) {
1318
1319 // remove existing extension and trailing . if any
1320
1321 $finalName = preg_replace('#\.[^\.]*$#', '', $finalName);
1322
1323 // add correct extension
1324
1325 $finalName .= '.' . $correctExtension;
1326
1327 }
1328
1329
1330
1331 // generate a path for storing in the product_images directory
1332
1333 $finalRelativePath = self::generateSourceImageRelativeFilePath($finalName);
1334
1335
1336
1337 $image = new ISC_PRODUCT_IMAGE();
1338
1339 $image->setSourceFilePath($finalRelativePath);
1340
1341
1342
1343 $finalAbsolutePath = $image->getAbsoluteSourceFilePath();
1344
1345 $finalDirectory = dirname($finalAbsolutePath);
1346
1347
1348
1349 if (!file_exists($finalDirectory)) {
1350
1351 if (!@mkdir($finalDirectory, ISC_WRITEABLE_DIR_PERM, true)) {
1352
1353 throw new ISC_PRODUCT_IMAGE_IMPORT_CANTCREATEDIR_EXCEPTION($finalDirectory);
1354
1355 }
1356
1357 }
1358
1359
1360
1361 if ($moveTemporaryFile) {
1362
1363 if (!@rename($temporaryPath, $finalAbsolutePath)) {
1364
1365 throw new ISC_PRODUCT_IMAGE_IMPORT_CANTMOVEFILE_EXCEPTION($finalAbsolutePath);
1366
1367 }
1368
1369 } else {
1370
1371 if (!@copy($temporaryPath, $finalAbsolutePath)) {
1372
1373 throw new ISC_PRODUCT_IMAGE_IMPORT_CANTMOVEFILE_EXCEPTION($finalAbsolutePath);
1374
1375 }
1376
1377 }
1378
1379
1380
1381 // check to see if the uploaded image exceeds our internal maximum image size: ISC_PRODUCT_IMAGE_MAXLONGEDGE
1382
1383 if ($library->getWidth() > ISC_PRODUCT_IMAGE_MAXLONGEDGE || $library->getHeight() > ISC_PRODUCT_IMAGE_MAXLONGEDGE) {
1384
1385 // if it is, resize it and overwrite the uploaded source image because we only want to store images to a maximum size of ISC_PRODUCT_IMAGE_MAXLONGEDGE x ISC_PRODUCT_IMAGE_MAXLONGEDGE
1386
1387 $library->setFilePath($finalAbsolutePath);
1388
1389 $library->loadImageFileToScratch();
1390
1391 $library->resampleScratchToMaximumDimensions(ISC_PRODUCT_IMAGE_MAXLONGEDGE, ISC_PRODUCT_IMAGE_MAXLONGEDGE);
1392
1393 $library->saveScratchToFile($finalAbsolutePath, self::getWriteOptionsForImageType($library->getImageType()));
1394
1395 }
1396
1397
1398
1399 if ($productId === false) {
1400
1401 // do not assign product hash, id or save to database if $productId is false
1402
1403 if ($generateImages) {
1404
1405 // manually generate images since, normally, a call to saveToDatabase would do it
1406
1407 $image->getResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_TINY, true, false);
1408
1409 $image->getResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL, true, false);
1410
1411 $image->getResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_STANDARD, true, false);
1412
1413 $image->getResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_ZOOM, true, false);
1414
1415 }
1416
1417
1418
1419 return $image;
1420
1421 }
1422
1423
1424
1425 if ($hash) {
1426
1427 $image->setProductHash($productId);
1428
1429 } else {
1430
1431 $image->setProductId($productId);
1432
1433 }
1434
1435
1436
1437 // ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION should never really happen at this point with all the checks above so, if it does, let the exception go unhandled to bubble up to a fatal error
1438
1439 $image->saveToDatabase($generateImages);
1440
1441
1442
1443 return $image;
1444
1445 }
1446
1447
1448
1449 /**
1450
1451 * Takes a given filename and checks it against various rules created to suit windows / *nix systems running fat32, ntfs, ext3 file systems, returning a ruling on whether the provided filename can be used as a filename on all systems.
1452
1453 *
1454
1455 * @param string $filename
1456
1457 * @param bool $characterCheck If true, also check characters in filename for valid characters. Can be set to false if sanitiseFilename() was just called on the string since it uses the same rules, otherwise should be left as true.
1458
1459 * @return bool
1460
1461 */
1462
1463 public static function isValidFilename ($filename, $characterCheck = true)
1464
1465 {
1466
1467 // RESERVED NAMES
1468
1469 // Windows: CON PRN AUX NUL CLOCK$ COM(1-9)[.*] LPT(1-9)[.*]
1470
1471 // NTFS: $MFT $MFTMirr $LogFile $Volume $AttrDef $Bitmap $Boot $BadClus $Secure $Upcase $Extend
1472
1473 // Common: . (dot) .. (two dots)
1474
1475
1476
1477 if (in_array($filename, array('CON', 'PRN', 'AUX', 'CLOCK$', '$MFT', '$MFTMirr', '$LogFile', '$Volume', '$AttrDef', '$Bitmap', '$BadClus', '$Secure', '$Upcase', '$Extend', '.', '..'))) {
1478
1479 return false;
1480
1481 }
1482
1483
1484
1485 if (preg_match('#^(COM|LPT)[1-9](\.[^\.]+)?$#', $filename)) {
1486
1487 return false;
1488
1489 }
1490
1491
1492
1493 if ($characterCheck && strcmp($filename, self::sanitiseFilename($filename)) !== 0) {
1494
1495 // character check was enabled but the result of sanitisedFilename is different from provided $filename which means $filename is too long or has invalid characters
1496
1497 return false;
1498
1499 }
1500
1501
1502
1503 return true;
1504
1505 }
1506
1507
1508
1509 /**
1510
1511 * Takes a given filename and returns a name which is valid on most common operating & file systems (such as Windows / *nix using FAT32, NTFS, EXT2) in terms of length and allowed characters
1512
1513 *
1514
1515 * @param string $filename Presumed-unsafe filename without any directory components
1516
1517 * @param mixed $replacementCharacter Character to replace unsafe characters in the filename with. Typically this is an underscore and should not be more than 1 character. It can also be blank to remove invalid characters instead of replacing them.
1518
1519 * @param bool $truncate If true, will also truncate the provided filename to acceptable file system limits (after any invalid character replacements). Default is true.
1520
1521 * @return string Sanitised filename
1522
1523 */
1524
1525 public static function sanitiseFilename ($filename, $replacementCharacter = '_', $truncate = true)
1526
1527 {
1528
1529 // remove or replace any characters which are invalid on *nix and windows systems
1530
1531
1532
1533 // CHARACTERS
1534
1535 // FAT32 Includes: A-Z, 0-9, ! # $ % & ' ( ) - @ ^ _ ` { } ~, no trailing spaces on base name or extension, ascii values 128–255
1536
1537 // FAT32 Excludes: " * / : < > ? \ |, Control characters 0–31, Value 127 (DEL)
1538
1539 // NTFS Includes: Any UTF-16 code unit except
1540
1541 // NTFS Excludes: NUL (0000), / (under linux) plus \ : * ? " < > | (under windows)
1542
1543 // EXT3 Includes: All bytes
1544
1545 // EXT3 Excludes: NUL and /
1546
1547 //
1548
1549 // Worst-case support (windows + fat32 restrictions):
1550
1551 // 33 !
1552
1553 // 35-41 # $ % & ' ( )
1554
1555 // 45-46 - .
1556
1557 // 48-57 (0-9)
1558
1559 // 64 @
1560
1561 // 65-90 (A-Z)
1562
1563 // 94-123 ^ _ ` a-z {
1564
1565 // 125-126 } ~
1566
1567 // 128-255 (lots)
1568
1569
1570
1571 // replace invalid characters with placeholder character
1572
1573 $filename = preg_replace('#[^!\#-)\-\.0-9@A-Z^-{\}\~\x80-\xFF]#', $replacementCharacter, $filename);
1574
1575
1576
1577 // truncate the filename if necessary, while maintaining the file extension
1578
1579
1580
1581 // LENGTHS
1582
1583 // FAT32: 255 UTF-8 characters
1584
1585 // NTFS: 255 UTF-16 code units
1586
1587 // EXT3: 254 bytes
1588
1589 // Worst-case support: 254 bytes (EXT3 maximum)
1590
1591
1592
1593 if ($truncate) {
1594
1595 $maxLength = 254;
1596
1597
1598
1599 if (strlen($filename) > $maxLength) {
1600
1601 $pathInfo = pathinfo($filename);
1602
1603 if (isset($pathInfo['extension'])) {
1604
1605 $extension = '.' . $pathInfo['extension'];
1606
1607 $extensionLength = strlen($pathInfo['extension']) + 1;
1608
1609 } else {
1610
1611 $extension = '';
1612
1613 $extensionLength = 0;
1614
1615 }
1616
1617
1618
1619 if ($extensionLength > $maxLength) {
1620
1621 // this is probably something bogus, normally you won't want to change extensions but if the extension is this long then... too bad
1622
1623 // this will also account for ".longfilenameslikethis"
1624
1625 // truncate the whole filename
1626
1627 $filename = substr($filename, 0, $maxLength);
1628
1629 } else {
1630
1631 // the extension is small enough to be kept, truncate the basename
1632
1633 $maxBasenameLength = $maxLength - $extensionLength;
1634
1635 $basename = substr($filename, 0, $maxBasenameLength);
1636
1637 $filename = $basename . $extension;
1638
1639 }
1640
1641 }
1642
1643 }
1644
1645
1646
1647 return $filename;
1648
1649 }
1650
1651
1652
1653 /**
1654
1655 * If possible will remove the extended product image directory $directoryPath if it's empty, but will not remove the standard /product_images/[a-z]/ base directories
1656
1657 *
1658
1659 * @param mixed $directoryPath
1660
1661 * @return bool Returns true if the directory was deleted otherwise false. False may not indicate error though, the directory may just be not empty.
1662
1663 */
1664
1665 public static function removeProductImageDirectory ($directoryPath)
1666
1667 {
1668
1669 if (!preg_match('#^' . preg_quote(ISC_BASE_PATH, '#') . '/' . preg_quote(GetConfig('ImageDirectory'), '#') . '/[a-z]/[0-9]{3}$#', $directoryPath)) {
1670
1671 // given directory does not match the pattern like /product_images/a/123
1672
1673 return false;
1674
1675 }
1676
1677
1678
1679 if (!is_dir($directoryPath)) {
1680
1681 // for some reason the given path is not a directory, leave it alone
1682
1683 return false;
1684
1685 }
1686
1687
1688
1689 if (!@rmdir($directoryPath)) {
1690
1691 // rmdir internally checks for empty directories
1692
1693 return false;
1694
1695 }
1696
1697
1698
1699 return true;
1700
1701 }
1702
1703
1704
1705 /**
1706
1707 * Copies all product images from the product $productId to the temporary, being-added product $productHash - this is used mainly by the 'copy product' functionality
1708
1709 *
1710
1711 * @param int $productId
1712
1713 * @param string $productHash
1714
1715 * @return array Array of ISC_PRODUCT_IMAGE instances of the new image copies
1716
1717 */
1718
1719 public static function copyImagesToProductHash ($productId, $productHash)
1720
1721 {
1722
1723 $productId = (int)$productId;
1724
1725 $result = array();
1726
1727 $existingImages = new ISC_PRODUCT_IMAGE_ITERATOR("SELECT * FROM `[|PREFIX|]product_images` WHERE imageprodid = " . $productId . " ORDER BY imagesort");
1728
1729 foreach ($existingImages as $existingImage) {
1730
1731 /** @var $existingImage ISC_PRODUCT_IMAGE */
1732
1733 $image = $existingImage->copyToProductHash($productHash);
1734
1735
1736
1737 // perform additional work specific to copying all images assigned to a product as a set
1738
1739 $save = false;
1740
1741 if ($existingImage->getIsThumbnail()) {
1742
1743 $save = true;
1744
1745 $image->setIsThumbnail(true);
1746
1747 }
1748
1749
1750
1751 if ($save) {
1752
1753 $image->saveToDatabase(false);
1754
1755 }
1756
1757
1758
1759 $result[] = $image;
1760
1761 }
1762
1763 return $result;
1764
1765 }
1766
1767
1768
1769 /**
1770
1771 *
1772
1773 *
1774
1775 * @param int $productImageId
1776
1777 * @return ISC_PRODUCT_IMAGE
1778
1779 */
1780
1781 public function __construct ($productImageId = null)
1782
1783 {
1784
1785 if ($productImageId !== null) {
1786
1787 $productImageId = (int)$productImageId;
1788
1789 if ($productImageId) {
1790
1791 $this->setProductImageId($productImageId);
1792
1793 $this->loadFromDatabase();
1794
1795 }
1796
1797 }
1798
1799 }
1800
1801
1802
1803 public function __clone ()
1804
1805 {
1806
1807 $this->clearImageLibrary(); // don't keep image library references after cloning since they'll point to the same memory resources
1808
1809 }
1810
1811
1812
1813 public function __destruct ()
1814
1815 {
1816
1817 $this->clearImageLibrary(); // don't keep image library references after the product image object is invalid
1818
1819 }
1820
1821
1822
1823 /**
1824
1825 * Given a database row returned by PHP database functions (that is, an array of field values with named indexes), this function will populate the current instance with those values.
1826
1827 *
1828
1829 * @param array $row
1830
1831 * @return void
1832
1833 */
1834
1835 public function populateFromDatabaseRow ($row)
1836
1837 {
1838
1839 $this->setProductImageId((int)$row['imageid']);
1840
1841 $this->setProductId((int)$row['imageprodid']);
1842
1843 $this->setProductHash($row['imageprodhash']);
1844
1845 $this->setSourceFilePath($row['imagefile']);
1846
1847 $this->setIsThumbnail($row['imageisthumb'] == 1);
1848
1849 $this->setSort((int)$row['imagesort']);
1850
1851 $this->setResizedFilePath(ISC_PRODUCT_IMAGE_SIZE_STANDARD, $row['imagefilestd']);
1852
1853 $this->setResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_STANDARD, $row['imagefilestdsize']);
1854
1855 $this->setResizedFilePath(ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL, $row['imagefilethumb']);
1856
1857 $this->setResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL, $row['imagefilethumbsize']);
1858
1859 $this->setResizedFilePath(ISC_PRODUCT_IMAGE_SIZE_TINY, $row['imagefiletiny']);
1860
1861 $this->setResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_TINY, $row['imagefiletinysize']);
1862
1863 $this->setResizedFilePath(ISC_PRODUCT_IMAGE_SIZE_ZOOM, $row['imagefilezoom']);
1864
1865 $this->setResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_ZOOM, $row['imagefilezoomsize']);
1866
1867 $this->setDateAdded((int)$row['imagedateadded']);
1868
1869
1870
1871 // if the query used joins the products table, load in useful information
1872
1873 if(isset($row['prodname'])) {
1874
1875 $this->setProductName($row['prodname']);
1876
1877 }
1878
1879
1880
1881 // upgrades from previous versions will probably have null as some column values when we want blank strings instead -- set internally as a blank string so when/if it's re-edited it'll be updated
1882
1883
1884
1885 if ($row['imagedesc'] === null) {
1886
1887 $this->setDescription('');
1888
1889 } else {
1890
1891 $this->setDescription($row['imagedesc']);
1892
1893 }
1894
1895 }
1896
1897
1898
1899 /**
1900
1901 * Loads data for this product image from the database into the current instance.
1902
1903 *
1904
1905 * Throws exceptions if anything goes wrong.
1906
1907 *
1908
1909 * @param int $productImageId Optional. If specified, will load data for the given product image id. Otherwise, will use the current product image id.
1910
1911 * @throws ISC_PRODUCT_IMAGE_INVALIDID_EXCEPTION If an invalid product image id is specified
1912
1913 * @throws ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION If an unhandled database error occurred while attempting to fetch product image information
1914
1915 * @throws ISC_PRODUCT_IMAGE_RECORDNOTFOUND_EXCEPTION If on otherwise valid request for product image id resulted in 0 records being returned from the database
1916
1917 * @return int The product image id
1918
1919 */
1920
1921 public function loadFromDatabase ($productImageId = null)
1922
1923 {
1924
1925 if ($productImageId === null) {
1926
1927 $productImageId = $this->getProductImageId();
1928
1929 }
1930
1931
1932
1933 $productImageId = (int)$productImageId;
1934
1935 if (!$productImageId) {
1936
1937 throw new ISC_PRODUCT_IMAGE_INVALIDID_EXCEPTION();
1938
1939 }
1940
1941
1942
1943 $sql = "SELECT /*ISC_PRODUCT_IMAGE->loadFromDatabase*/ * FROM `[|PREFIX|]product_images` WHERE imageid = " . $productImageId;
1944
1945 $result = $GLOBALS['ISC_CLASS_DB']->Query($sql);
1946
1947
1948
1949 if (!$result) {
1950
1951 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
1952
1953 }
1954
1955
1956
1957 $row = $GLOBALS['ISC_CLASS_DB']->Fetch($result);
1958
1959 if (!$row) {
1960
1961 throw new ISC_PRODUCT_IMAGE_RECORDNOTFOUND_EXCEPTION(sprintf(GetLang("ProductImageRecordNotFound"), $productImageId));
1962
1963 }
1964
1965
1966
1967 $this->populateFromDatabaseRow($row);
1968
1969
1970
1971 return $this->getProductImageId();
1972
1973 }
1974
1975
1976
1977 /**
1978
1979 * Saves the data for the current instance to the database. If the current product image id is unspecified or 0, a new record will be created.
1980
1981 *
1982
1983 * Throws exceptions if anything goes wrong (several varieties are thrown by other methods called by saveToDatabase, but not all are listed here yet)
1984
1985 *
1986
1987 * @param bool $generateImages Default true. If true will attempt to generate all thumbnails (if necessary).
1988
1989 * @return void
1990
1991 * @throws ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION If $generateImages is true and the source image file does not exist to be processed
1992
1993 */
1994
1995 public function saveToDatabase ($generateImages = true)
1996
1997 {
1998
1999 $db = $GLOBALS['ISC_CLASS_DB'];
2000
2001 $productImageId = $this->getProductImageId();
2002
2003
2004
2005 if (!$productImageId) {
2006
2007 // checks that are performed when inserting an image
2008
2009
2010
2011 // look for existing images against the product the image is being added to
2012
2013 if ($this->getProductId()) {
2014
2015 // an existing product
2016
2017 $sql = "SELECT COUNT(*) FROM `[|PREFIX|]product_images` WHERE imageprodid = " . $this->getProductId();
2018
2019 } else {
2020
2021 // the product is being added
2022
2023 $sql = "SELECT COUNT(*) FROM `[|PREFIX|]product_images` WHERE imageprodhash = '" . $db->Quote($this->getProductHash()) . "'";
2024
2025 }
2026
2027
2028
2029 $existingImageCount = $db->FetchOne($sql);
2030
2031
2032
2033 if (!$existingImageCount) {
2034
2035 // when inserting the first image
2036
2037 $this->setSort(0);
2038
2039 $this->setIsThumbnail(true);
2040
2041 }
2042
2043
2044
2045 if ($this->getSort() === null) {
2046
2047 if ($existingImageCount) {
2048
2049 // if inserting with a null sort value, and there are existing images, discover a new sort value based on the other images
2050
2051 if ($this->getProductId()) {
2052
2053 $sql = "SELECT MAX(imagesort) + 1 FROM `[|PREFIX|]product_images` WHERE imageprodid = " . $this->getProductId();
2054
2055 } else {
2056
2057 $sql = "SELECT MAX(imagesort) + 1 FROM `[|PREFIX|]product_images` WHERE imageprodhash = '" . $db->Quote($this->getProductHash()) . "'";
2058
2059 }
2060
2061
2062
2063 $sort = $db->FetchOne($sql);
2064
2065 $this->setSort($sort);
2066
2067 } else {
2068
2069 // otherwise set the sort value to 0
2070
2071 $this->setSort(0);
2072
2073 }
2074
2075 }
2076
2077
2078
2079 if ($this->getIsThumbnail() === null) {
2080
2081 if ($existingImageCount) {
2082
2083 $this->setIsThumbnail(false);
2084
2085 } else {
2086
2087 // set the base thumbnail flag if we're inserting the first image with a null value
2088
2089 $this->setIsThumbnail(true);
2090
2091 }
2092
2093 }
2094
2095 }
2096
2097
2098
2099
2100
2101 $data = array(
2102
2103 'imageprodid' => $this->getProductId(),
2104
2105 'imageprodhash' => $this->getProductHash(),
2106
2107 'imagefile' => $this->getSourceFilePath(),
2108
2109 'imageisthumb' => '0',
2110
2111 'imagesort' => $this->getSort(),
2112
2113 'imagefilestd' => $this->getResizedFilePath(ISC_PRODUCT_IMAGE_SIZE_STANDARD, $generateImages, false),
2114
2115 'imagefilethumb' => $this->getResizedFilePath(ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL, $generateImages, false),
2116
2117 'imagefiletiny' => $this->getResizedFilePath(ISC_PRODUCT_IMAGE_SIZE_TINY, $generateImages, false),
2118
2119 'imagefilezoom' => $this->getResizedFilePath(ISC_PRODUCT_IMAGE_SIZE_ZOOM, $generateImages, false),
2120
2121 'imagefilestdsize' => $this->getResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_STANDARD, $generateImages, false),
2122
2123 'imagefilethumbsize' => $this->getResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL, $generateImages, false),
2124
2125 'imagefiletinysize' => $this->getResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_TINY, $generateImages, false),
2126
2127 'imagefilezoomsize' => $this->getResizedFileDimensions(ISC_PRODUCT_IMAGE_SIZE_ZOOM, $generateImages, false),
2128
2129 'imagedesc' => $this->getDescription(),
2130
2131 'imagedateadded' => $this->getDateAdded(),
2132
2133 );
2134
2135
2136
2137 if ($this->getIsThumbnail()) {
2138
2139 $data['imageisthumb'] = '1';
2140
2141 }
2142
2143
2144
2145 // the results of getResizedFileDimensions may be null or blank string but if it's an array it needs to be serialized to the db (popoulateFromDatabaseRow does the opposite)
2146
2147 if (is_array($data['imagefilestdsize'])) {
2148
2149 $data['imagefilestdsize'] = implode('x', $data['imagefilestdsize']);
2150
2151 }
2152
2153
2154
2155 if (is_array($data['imagefilethumbsize'])) {
2156
2157 $data['imagefilethumbsize'] = implode('x', $data['imagefilethumbsize']);
2158
2159 }
2160
2161
2162
2163 if (is_array($data['imagefiletinysize'])) {
2164
2165 $data['imagefiletinysize'] = implode('x', $data['imagefiletinysize']);
2166
2167 }
2168
2169
2170
2171 if (is_array($data['imagefilezoomsize'])) {
2172
2173 $data['imagefilezoomsize'] = implode('x', $data['imagefilezoomsize']);
2174
2175 }
2176
2177
2178
2179 if (!$productImageId) {
2180
2181 // record when the image was inserted so that uploads during the product add stage can be cleaned up later if the product itself is never added
2182
2183 $data['imagedateadded'] = time();
2184
2185
2186
2187 $result = $db->InsertQuery('product_images', $data);
2188
2189
2190
2191 if ($result === false) {
2192
2193 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
2194
2195 } else {
2196
2197 $this->setProductImageId($result);
2198
2199 $this->setDateAdded($data['imagedateadded']);
2200
2201 }
2202
2203 } else {
2204
2205 if (!$db->UpdateQuery('product_images', $data, "imageid = " . $productImageId)) {
2206
2207 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
2208
2209 }
2210
2211
2212
2213 if ($this->getIsThumbnail()) {
2214
2215 // this image is the thumbnail, make sure others are not
2216
2217 if ($this->getProductHash()) {
2218
2219 $db->Query("UPDATE /*ISC_PRODUCT_IMAGE->saveToDatabase*/ `[|PREFIX|]product_images` SET imageisthumb = 0 WHERE imageid <> " . $this->getProductImageId() . " AND imageprodhash = '" . $db->Quote($this->getProductHash()) . "'");
2220
2221 } else if ($this->getProductId()) {
2222
2223 $db->Query("UPDATE /*ISC_PRODUCT_IMAGE->saveToDatabase*/ `[|PREFIX|]product_images` SET imageisthumb = 0 WHERE imageid <> " . $this->getProductImageId() . " AND imageprodid = " . $this->getProductId());
2224
2225 }
2226
2227 }
2228
2229 }
2230
2231 }
2232
2233
2234
2235 /**
2236
2237 * Sets the id of this product image
2238
2239 *
2240
2241 * @param int $productImageId
2242
2243 */
2244
2245 public function setProductImageId ($productImageId)
2246
2247 {
2248
2249 $this->_productImageId = (int)$productImageId;
2250
2251 }
2252
2253
2254
2255 /**
2256
2257 * Returns the id of this product image
2258
2259 *
2260
2261 * @return int
2262
2263 */
2264
2265 public function getProductImageId ()
2266
2267 {
2268
2269 return $this->_productImageId;
2270
2271 }
2272
2273
2274
2275 /**
2276
2277 * Sets the path to the source image file for the product image, relative to the product_images directory
2278
2279 *
2280
2281 * @param string $sourceFilePath
2282
2283 */
2284
2285 public function setSourceFilePath ($sourceFilePath)
2286
2287 {
2288
2289 if ($this->_sourceFilePath !== $sourceFilePath) {
2290
2291 $this->_sourceFilePath = $sourceFilePath;
2292
2293
2294
2295 // clear any generated size paths
2296
2297 $this->_resizedFilePaths = array(
2298
2299 ISC_PRODUCT_IMAGE_SIZE_ZOOM => null,
2300
2301 ISC_PRODUCT_IMAGE_SIZE_STANDARD => null,
2302
2303 ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL => null,
2304
2305 ISC_PRODUCT_IMAGE_SIZE_TINY => null,
2306
2307 );
2308
2309
2310
2311 $this->_resizedFileDimensions = array(
2312
2313 ISC_PRODUCT_IMAGE_SIZE_ZOOM => null,
2314
2315 ISC_PRODUCT_IMAGE_SIZE_STANDARD => null,
2316
2317 ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL => null,
2318
2319 ISC_PRODUCT_IMAGE_SIZE_TINY => null,
2320
2321 );
2322
2323
2324
2325 // as the file has changed, the image library to handle it may also change -- remove any existing library object
2326
2327 $this->_imageLibrary = null;
2328
2329 }
2330
2331 }
2332
2333
2334
2335 /**
2336
2337 * Returns the path to the source image for this product image, relative to the product_imags directory
2338
2339 *
2340
2341 * @return string
2342
2343 */
2344
2345 public function getSourceFilePath ()
2346
2347 {
2348
2349 return $this->_sourceFilePath;
2350
2351 }
2352
2353
2354
2355 /**
2356
2357 * Returns the absolute path to the source image file on the filesystem, calculated based on the ISC base directory and the image's relative directory
2358
2359 *
2360
2361 */
2362
2363 public function getAbsoluteSourceFilePath ()
2364
2365 {
2366
2367 return ISC_BASE_PATH . '/' . GetConfig('ImageDirectory') . '/' . $this->getSourceFilePath();
2368
2369 }
2370
2371
2372
2373 /**
2374
2375 * Returns only the filename of the source image for this product image. To get the full path, use getSourceFilePath or getAbsoluteSourceFilePath instead.
2376
2377 *
2378
2379 * @return string The source image filename, not including path
2380
2381 */
2382
2383 public function getFileName ()
2384
2385 {
2386
2387 return basename($this->getAbsoluteSourceFilePath());
2388
2389 }
2390
2391
2392
2393 /**
2394
2395 * Flags the current image as the preferred thumbnail for the product it's related to. Does not automatically update other images
2396
2397 *
2398
2399 * @param boolean $isThumbnail
2400
2401 */
2402
2403 public function setIsThumbnail ($isThumbnail)
2404
2405 {
2406
2407 $this->_isThumbnail = !!$isThumbnail;
2408
2409 }
2410
2411
2412
2413 /**
2414
2415 * Returns whether the current image is the preferred thumbnail for this product or not
2416
2417 *
2418
2419 * @return bool True if the image is the preferred thumbnail.
2420
2421 */
2422
2423 public function getIsThumbnail ()
2424
2425 {
2426
2427 return $this->_isThumbnail;
2428
2429 }
2430
2431
2432
2433 /**
2434
2435 * Sets the product id value for this product image
2436
2437 *
2438
2439 * @param int $productId
2440
2441 */
2442
2443 public function setProductId ($productId)
2444
2445 {
2446
2447 $productId = (int)$productId;
2448
2449 if ($this->_productId !== $productId) {
2450
2451 $this->_productId = $productId;
2452
2453
2454
2455 // as the product id has changed, remove any information about the product
2456
2457 $this->_product = null;
2458
2459 }
2460
2461 }
2462
2463
2464
2465 /**
2466
2467 * Returns the product id for this product image
2468
2469 *
2470
2471 * @return int
2472
2473 */
2474
2475 public function getProductId ()
2476
2477 {
2478
2479 return $this->_productId;
2480
2481 }
2482
2483
2484
2485 /**
2486
2487 * Returns information about the product this image is attached to
2488
2489 *
2490
2491 */
2492
2493 public function getProduct ()
2494
2495 {
2496
2497 throw new Exception('Not Yet Implemented');
2498
2499 }
2500
2501
2502
2503 /**
2504
2505 * Sets whether or not this image is visible on the front end
2506
2507 *
2508
2509 * @param bool $visible
2510
2511 */
2512
2513 public function setVisible ($visible)
2514
2515 {
2516
2517 $this->_visible = !!$visible;
2518
2519 }
2520
2521
2522
2523 /**
2524
2525 * Gets whether or not this image is visible on the front end
2526
2527 *
2528
2529 * @return bool
2530
2531 */
2532
2533 public function getVisible ()
2534
2535 {
2536
2537 return $this->_visible;
2538
2539 }
2540
2541
2542
2543 /**
2544
2545 * Sets the product name for the product that uses the current image
2546
2547 *
2548
2549 * @param string $name
2550
2551 */
2552
2553 public function setProductName ($name)
2554
2555 {
2556
2557 $this->_productName = $name;
2558
2559 }
2560
2561
2562
2563 /**
2564
2565 * Returns the name of the product that owns the current product image
2566
2567 *
2568
2569 * @return string
2570
2571 */
2572
2573 public function getProductName ()
2574
2575 {
2576
2577 return $this->_productName;
2578
2579 }
2580
2581
2582
2583 /**
2584
2585 * Sets the alternate text for this image -- intended for use in ALT or TITLE attributes.
2586
2587 *
2588
2589 * @param string $alternateText
2590
2591 */
2592
2593 public function setAlternateText ($alternateText)
2594
2595 {
2596
2597 $this->_alternateText = $alternateText;
2598
2599 }
2600
2601
2602
2603 /**
2604
2605 * Returns the alternate text for this image -- intended for use in ALT or TITLE attributes.
2606
2607 *
2608
2609 * @return string
2610
2611 */
2612
2613 public function getAlternateText ()
2614
2615 {
2616
2617 return $this->_alternateText;
2618
2619 }
2620
2621
2622
2623 /**
2624
2625 * Sets the caption for this image -- intended for use as a short / one-line description shown below or beside the image
2626
2627 *
2628
2629 * @param mixed $caption
2630
2631 */
2632
2633 public function setCaption ($caption)
2634
2635 {
2636
2637 $this->_caption = $caption;
2638
2639 }
2640
2641
2642
2643 /**
2644
2645 * Returns the caption for this image -- intended for use as a short / one-line description shown below or beside the image
2646
2647 *
2648
2649 * @return string
2650
2651 */
2652
2653 public function getCaption ()
2654
2655 {
2656
2657 return $this->_caption;
2658
2659 }
2660
2661
2662
2663 /**
2664
2665 * Sets the description for this image -- intended for use as a long image description
2666
2667 *
2668
2669 * @param string $description
2670
2671 */
2672
2673 public function setDescription ($description)
2674
2675 {
2676
2677 $this->_description = $description;
2678
2679 }
2680
2681
2682
2683 /**
2684
2685 * Return the description for this image -- intended for use as a long image description
2686
2687 *
2688
2689 * @return string
2690
2691 */
2692
2693 public function getDescription ()
2694
2695 {
2696
2697 return $this->_description;
2698
2699 }
2700
2701
2702
2703 /**
2704
2705 * Sets the date added timestamp value
2706
2707 *
2708
2709 * @param int $dateAdded
2710
2711 */
2712
2713 public function setDateAdded ($dateAdded)
2714
2715 {
2716
2717 $this->_dateAdded = $dateAdded;
2718
2719 }
2720
2721
2722
2723 /**
2724
2725 * Returns timestamp value representing the date this image was added to the database
2726
2727 *
2728
2729 * @return int
2730
2731 */
2732
2733 public function getDateAdded ()
2734
2735 {
2736
2737 return $this->_dateAdded;
2738
2739 }
2740
2741
2742
2743 /**
2744
2745 * Sets the display order for this image -- will not update the display order for other images for this product
2746
2747 *
2748
2749 * @param int $sort
2750
2751 */
2752
2753 public function setSort ($sort)
2754
2755 {
2756
2757 $this->_sort = (int)$sort;
2758
2759 }
2760
2761
2762
2763 /**
2764
2765 * Gets the display order for this image
2766
2767 *
2768
2769 * @return int
2770
2771 */
2772
2773 public function getSort ()
2774
2775 {
2776
2777 return $this->_sort;
2778
2779 }
2780
2781
2782
2783 /**
2784
2785 * Creates and returns an image manipulation library depending on the current image type and the libraries available on the server.
2786
2787 *
2788
2789 * @return ISC_IMAGE_LIBRARY_INTERFACE
2790
2791 */
2792
2793 public function getImageLibrary ()
2794
2795 {
2796
2797 if (!$this->_imageLibrary) {
2798
2799 $this->_imageLibrary = ISC_IMAGE_LIBRARY_FACTORY::getImageLibraryInstance($this->getAbsoluteSourceFilePath());
2800
2801 }
2802
2803
2804
2805 return $this->_imageLibrary;
2806
2807 }
2808
2809
2810
2811 /**
2812
2813 * Wipes the current image manipulation library and it's resources, if any
2814
2815 *
2816
2817 * @return void
2818
2819 */
2820
2821 public function clearImageLibrary ()
2822
2823 {
2824
2825 $this->_imageLibrary = null;
2826
2827 }
2828
2829
2830
2831 /**
2832
2833 * Generate resized version of this product image according to ISC settings, returning the absolute file path of the newly created image. If the resized image already exists and is up to date, a new one will not be generated but the file path will still be returned.
2834
2835 *
2836
2837 * @param int $size One of ISC_PRODUCT_IMAGE_SIZE_XXX
2838
2839 * @param bool $save If necessary, update image in database with the correct path
2840
2841 * @param bool $force Set to true to bypass existing file and mtime checks to force the resized image to be generated
2842
2843 * @return string Absolute file path pointing to resized image
2844
2845 * @throws ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION If the source file does not exist
2846
2847 */
2848
2849 public function generateResizedFile ($size, $save = true, $force = false)
2850
2851 {
2852
2853 $resizedFilePath = $this->getAbsoluteResizedFilePath($size, false, $save);
2854
2855
2856
2857 if (!file_exists($this->getAbsoluteSourceFilePath())) {
2858
2859 throw new ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION($this->getAbsoluteSourceFilePath());
2860
2861 }
2862
2863
2864
2865 $sourceImageModifiedTime = filemtime($this->getAbsoluteSourceFilePath());
2866
2867
2868
2869 if (!$force && file_exists($resizedFilePath) && $sourceImageModifiedTime < filemtime($resizedFilePath)) {
2870
2871 // resized file exists and source file is hasn't changed since it was made
2872
2873 return $resizedFilePath;
2874
2875 }
2876
2877
2878
2879 $width = self::getSizeWidth($size);
2880
2881 $height = self::getSizeHeight($size);
2882
2883
2884
2885 // clamp the width and height to maximum internal sizes
2886
2887 if ($width > ISC_PRODUCT_IMAGE_MAXLONGEDGE) {
2888
2889 $width = ISC_PRODUCT_IMAGE_MAXLONGEDGE;
2890
2891 }
2892
2893
2894
2895 if ($height > ISC_PRODUCT_IMAGE_MAXLONGEDGE) {
2896
2897 $height = ISC_PRODUCT_IMAGE_MAXLONGEDGE;
2898
2899 }
2900
2901
2902
2903 // it's possible that the destination directory may not exist yet if safe mode is off -- attempt to create it
2904
2905 $resizedDirectoryPath = dirname($resizedFilePath);
2906
2907 if (!file_exists($resizedDirectoryPath)) {
2908
2909 if (!@mkdir($resizedDirectoryPath, ISC_WRITEABLE_DIR_PERM)) {
2910
2911 throw new ISC_PRODUCT_IMAGE_CREATEDIRECTORY_EXCEPTION();
2912
2913 }
2914
2915 }
2916
2917
2918
2919 // since the image library is cached we need to clear it just incase a resized image is already in memory
2920
2921 $this->clearImageLibrary();
2922
2923
2924
2925 $image = $this->getImageLibrary();
2926
2927 $writeOptions = self::getWriteOptionsForImageType($image->getImageType());
2928
2929
2930
2931 if ($image->getWidth() > $width || $image->getHeight() > $height) {
2932
2933 // the source image is larger than the specified resize, so scale it down
2934
2935 $image->loadImageFileToScratch();
2936
2937 $image->resampleScratchToMaximumDimensions($width, $height);
2938
2939 $image->saveScratchToFile($resizedFilePath, $writeOptions);
2940
2941 } else {
2942
2943 // the source image is smaller or equal to the specified resize, make a copy only
2944
2945 copy($image->getFilePath(), $resizedFilePath);
2946
2947 isc_chmod($image->getFilePath(), ISC_WRITEABLE_FILE_PERM);
2948
2949 }
2950
2951
2952
2953 $relativePath = $this->getResizedFilePath($size);
2954
2955 $this->_resizedFilePaths[$size] = $relativePath;
2956
2957 $this->_resizedFileDimensions[$size] = array($image->getWidth(), $image->getHeight());
2958
2959
2960
2961 if ($save) {
2962
2963 $this->saveToDatabase(false);
2964
2965 }
2966
2967
2968
2969 // remove resizing resources
2970
2971 $this->clearImageLibrary();
2972
2973
2974
2975 return $resizedFilePath;
2976
2977 }
2978
2979
2980
2981 /**
2982
2983 * Remove all files associated with this image, including the original source file
2984
2985 *
2986
2987 * @throws ISC_PRODUCT_IMAGE_CANNOTDELETEFILE_EXCEPTION If any of the image files existed but could not be deleted
2988
2989 * @return void
2990
2991 */
2992
2993 public function removeFiles ()
2994
2995 {
2996
2997 $this->removeResizedFiles();
2998
2999 $this->removeSourceFile();
3000
3001 }
3002
3003
3004
3005 /**
3006
3007 * Removes the source image file from the file system for this image
3008
3009 *
3010
3011 * @throws ISC_PRODUCT_IMAGE_CANNOTDELETEFILE_EXCEPTION If the image file exists but could not be deleted
3012
3013 * @return void
3014
3015 */
3016
3017 public function removeSourceFile ()
3018
3019 {
3020
3021 $filePath = $this->getAbsoluteSourceFilePath();
3022
3023 if (file_exists($filePath) && is_file($filePath)) {
3024
3025 if (!@unlink($filePath)) {
3026
3027 $error = error_get_last();
3028
3029 throw new ISC_PRODUCT_IMAGE_CANNOTDELETEFILE_EXCEPTION(GetLang('ProductImageDeleteSourceFileError'));
3030
3031 }
3032
3033 }
3034
3035
3036
3037 self::removeProductImageDirectory(dirname($filePath));
3038
3039 }
3040
3041
3042
3043 /**
3044
3045 * Removes all resized files for the current product image from the filesystem but does not remove their paths from the database record
3046
3047 *
3048
3049 * @throws ISC_PRODUCT_IMAGE_CANNOTDELETEFILE_EXCEPTION If any of the image files existed but could not be deleted
3050
3051 * @return void
3052
3053 */
3054
3055 public function removeResizedFiles ()
3056
3057 {
3058
3059 $this->removeResizedFile(ISC_PRODUCT_IMAGE_SIZE_ZOOM);
3060
3061 $this->removeResizedFile(ISC_PRODUCT_IMAGE_SIZE_STANDARD);
3062
3063 $this->removeResizedFile(ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL);
3064
3065 $this->removeResizedFile(ISC_PRODUCT_IMAGE_SIZE_TINY);
3066
3067 }
3068
3069
3070
3071 /**
3072
3073 * Removes the resized file for the specified size for the current product image from the filesystem but does not remove it's path from the database record
3074
3075 *
3076
3077 * @param int $size One of the defined ISC_PRODUCT_IMAGE_SIZE_? constants
3078
3079 * @throws ISC_PRODUCT_IMAGE_CANNOTDELETEFILE_EXCEPTION If the image file exists but could not be deleted
3080
3081 * @return void
3082
3083 */
3084
3085 public function removeResizedFile ($size)
3086
3087 {
3088
3089 $filePath = $this->getAbsoluteResizedFilePath($size, false);
3090
3091 if (file_exists($filePath) && is_file($filePath)) {
3092
3093 if (!@unlink($filePath)) {
3094
3095 $error = error_get_last();
3096
3097 throw new ISC_PRODUCT_IMAGE_CANNOTDELETEFILE_EXCEPTION(GetLang('ProductImageDeleteResizedFileError'));
3098
3099 }
3100
3101 }
3102
3103
3104
3105 self::removeProductImageDirectory(dirname($filePath));
3106
3107 }
3108
3109
3110
3111 /**
3112
3113 * Returns the width and height of the actual resized version of a product image in the form of an arrray where index 0 is width, index 1 is height.
3114
3115 *
3116
3117 * @param int $size One of the defined ISC_PRODUCT_IMAGE_SIZE_? constants
3118
3119 * @param bool $generate If necessary, generate the resized image to determine it's size otherwise will rely purely on any internally cached values. If this is called with $generate as false and no value has been stored yet, null will be returned.
3120
3121 * @param bool $save If necessary, update image in the database with the correct sizes
3122
3123 * @throws ISC_PRODUCT_IMAGE_INVALIDSIZE_EXCEPTION If an invalid size is specified
3124
3125 * @return array index 0 is width, index 1 is height
3126
3127 */
3128
3129 public function getResizedFileDimensions ($size, $generate = true, $save = true)
3130
3131 {
3132
3133 if ($generate || $this->_resizedFileDimensions[$size] === null) {
3134
3135 $imageFilePath = $this->getAbsoluteResizedFilePath($size, $generate, $save);
3136
3137
3138
3139 // if the call above actually generated the file then the size will now be cached and we don't need to calculate it again
3140
3141 if ($this->_resizedFileDimensions[$size] === null) {
3142
3143 try {
3144
3145 $library = ISC_IMAGE_LIBRARY_FACTORY::getImageLibraryInstance($imageFilePath);
3146
3147 $this->_resizedFileDimensions[$size] = array($library->getWidth(), $library->getHeight());
3148
3149 } catch (ISC_IMAGE_LIBRARY_FACTORY_FILEDOESNTEXIST_EXCEPTION $exception) {
3150
3151 // do nothing, keep size as null
3152
3153 }
3154
3155 }
3156
3157 }
3158
3159
3160
3161 return $this->_resizedFileDimensions[$size];
3162
3163 }
3164
3165
3166
3167 /**
3168
3169 * Sets the dimensions of a resized file. This sets the value only and does not actually perform any resizing. This method is primarily used when populating an ISC_PRODUCT_IMAGE instance based on database data.
3170
3171 *
3172
3173 * If no valid input is specified the internal value will change to null.
3174
3175 *
3176
3177 * @param int $size One of the defined ISC_PRODUCT_IMAGE_SIZE_? constants
3178
3179 * @param array|string $dimensions Either an array(width,height) or a string in the format of "widthxheight" (i.e. "400x300")
3180
3181 * @return void
3182
3183 */
3184
3185 public function setResizedFileDimensions ($size, $dimensions)
3186
3187 {
3188
3189 if (is_array($dimensions)) {
3190
3191 $this->_resizedFileDimensions[$size] = $dimensions;
3192
3193 return;
3194
3195 }
3196
3197
3198
3199 if (is_string($dimensions) && $dimensions && strpos($dimensions, 'x') !== false) {
3200
3201 $this->_resizedFileDimensions[$size] = explode('x', $dimensions, 2);
3202
3203 $this->_resizedFileDimensions[$size][0] = (int)$this->_resizedFileDimensions[$size][0];
3204
3205 $this->_resizedFileDimensions[$size][1] = (int)$this->_resizedFileDimensions[$size][1];
3206
3207 return;
3208
3209 }
3210
3211
3212
3213 $this->_resizedFileDimensions[$size] = null;
3214
3215 }
3216
3217
3218
3219 /**
3220
3221 * Returns the file path that a resized version of this image would use, relative to the configured product_images directory. The resized image may not exist, this function simply predicts the path to the generated file. You can supply $generate as true to ensure the image exists.
3222
3223 *
3224
3225 * @param int $size One of the defined ISC_PRODUCT_IMAGE_SIZE_? constants
3226
3227 * @param bool $generate Set to true to attempt to generate the resized image if it does not exist or it's out of date
3228
3229 * @param bool $save If necessary, update image in database with the correct path
3230
3231 * @throws ISC_PRODUCT_IMAGE_INVALIDSIZE_EXCEPTION If an invalid size is specified
3232
3233 * @throws ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION If the source file does not exist
3234
3235 * @return string
3236
3237 */
3238
3239 public function getResizedFilePath ($size, $generate = false, $save = true)
3240
3241 {
3242
3243 if ($generate) {
3244
3245 // the call to generateResizedFile will also call getResizedFilePath again with $generate as false - we can return here without calculating the path twice since generateResizedFile returns the file path
3246
3247 $filePath = $this->generateResizedFile($size, $save);
3248
3249 // generateResizedFile will return an absolute path but we require the relative path
3250
3251 $filePath = substr($filePath, strlen(ISC_BASE_PATH . '/' . GetConfig('ImageDirectory') . '/'));
3252
3253 return $filePath;
3254
3255 }
3256
3257
3258
3259 // the resize type affects the filename
3260
3261 switch ($size) {
3262
3263 case ISC_PRODUCT_IMAGE_SIZE_STANDARD:
3264
3265 $sizeTag = 'std';
3266
3267 break;
3268
3269
3270
3271 case ISC_PRODUCT_IMAGE_SIZE_THUMBNAIL:
3272
3273 $sizeTag = 'thumb';
3274
3275 break;
3276
3277
3278
3279 case ISC_PRODUCT_IMAGE_SIZE_TINY:
3280
3281 $sizeTag = 'tiny';
3282
3283 break;
3284
3285
3286
3287 case ISC_PRODUCT_IMAGE_SIZE_ZOOM:
3288
3289 $sizeTag = 'zoom';
3290
3291 break;
3292
3293
3294
3295 default:
3296
3297 throw new ISC_PRODUCT_IMAGE_INVALIDSIZE_EXCEPTION($size);
3298
3299 }
3300
3301
3302
3303 if ($this->_resizedFilePaths[$size]) {
3304
3305 return $this->_resizedFilePaths[$size];
3306
3307 }
3308
3309
3310
3311 $sourceFilePath = $this->getSourceFilePath();
3312
3313 $sourceFileName = basename($sourceFilePath);
3314
3315
3316
3317 // the end of source files should be __##### -- strip it and let ::generateSourceImage... do it again
3318
3319 $resizedFileName = preg_replace('#__([0-9]{5})\.([^\.]+)$#', '.\\2', $sourceFileName);
3320
3321
3322
3323 // generate a new path to place the resized image in
3324
3325 $resizedFilePath = self::generateSourceImageRelativeFilePath($resizedFileName);
3326
3327
3328
3329 // insert the size tag before the file extension
3330
3331 $resizedFilePath = self::prependToFileExtension($resizedFilePath, '_' . $sizeTag);
3332
3333
3334
3335 // cache it
3336
3337 $this->_resizedFilePaths[$size] = $resizedFilePath;
3338
3339
3340
3341 return $resizedFilePath;
3342
3343 }
3344
3345
3346
3347 /**
3348
3349 * Returns the absolute file path that a resized version of this image would use. The resized image may not exist, this function simply predicts the path to the generated file. You can supply $generate as true to ensure the image exists.
3350
3351 *
3352
3353 * @param int $size One of the defined ISC_PRODUCT_IMAGE_SIZE_? constants
3354
3355 * @param bool $generate Set to true to attempt to generate the resized image if it does not exist or it's out of date
3356
3357 * @param bool $save If necessary, update image in database with the correct path
3358
3359 * @return string
3360
3361 * @throws ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION If the source file does not exist
3362
3363 */
3364
3365 public function getAbsoluteResizedFilePath ($size, $generate = false, $save = true)
3366
3367 {
3368
3369 $path = $this->getResizedFilePath($size, $generate, $save);
3370
3371 $path = ISC_BASE_PATH . '/' . GetConfig('ImageDirectory') . '/' . $path;
3372
3373 return $path;
3374
3375 }
3376
3377
3378
3379 /**
3380
3381 * Sets the file path to a resized version of this image -- does not perform any validation so it assumes the image is present and correct.
3382
3383 *
3384
3385 * @param int $size One of the defined ISC_PRODUCT_IMAGE_SIZE_? constants
3386
3387 * @param string $filePath Path to the resized version of this image, relative to the product_images directory
3388
3389 */
3390
3391 public function setResizedFilePath ($size, $filePath)
3392
3393 {
3394
3395 $this->_resizedFilePaths[$size] = $filePath;
3396
3397 }
3398
3399
3400
3401 /**
3402
3403 * Returns a URL path to the specified resized version of this product image based on the current website settings. If the resized version doesn't exist yet it will be created if $generate is true.
3404
3405 *
3406
3407 * @param int $size One of the defined ISC_PRODUCT_IMAGE_SIZE_? constants
3408
3409 * @param bool $generate Set to true to attempt to generate the resized image if it does not exist or it's out of date
3410
3411 * @param bool $save If necessary, update image in database with the correct path
3412
3413 * @param bool $ssl You may provide this option as either true or false to force the link returned by this function to either HTTPS (true) or HTTP (false), otherwise leave as the default (null) to auto-detect
3414
3415 * @return string
3416
3417 * @throws ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION If the source file does not exist
3418
3419 */
3420
3421 public function getResizedUrl ($size, $generate = false, $save = true, $ssl = null)
3422
3423 {
3424
3425 $filePath = $this->getAbsoluteResizedFilePath($size, $generate, $save);
3426
3427
3428
3429 // cut the left portion of the path off, which will be the ISC_BASE_PATH - the remaining directory should be the URL relative to the installation
3430
3431 $filePath = substr($filePath, strlen(ISC_BASE_PATH));
3432
3433
3434
3435 // as this function should return a usable url, we need encode the path incase the filename contains brackets such as "(1)" after images copied in Windows
3436
3437 $filePath = explode('/', $filePath);
3438
3439 foreach ($filePath as &$filePathComponent) {
3440
3441 $filePathComponent = rawurlencode($filePathComponent);
3442
3443 }
3444
3445 $filePath = implode('/', $filePath);
3446
3447
3448
3449 if ($ssl === null) {
3450
3451 $shopPath = GetConfig('ShopPath');
3452
3453 } else if ($ssl === true) {
3454
3455 $shopPath = GetConfig('ShopPathSSL');
3456
3457 } else {
3458
3459 $shopPath = GetConfig('ShopPathNormal');
3460
3461 }
3462
3463
3464
3465 $filePath = $shopPath . $filePath;
3466
3467
3468
3469 return $filePath;
3470
3471 }
3472
3473
3474
3475 public function setProductHash ($productHash)
3476
3477 {
3478
3479 $this->_productHash = $productHash;
3480
3481 }
3482
3483
3484
3485 public function getProductHash ()
3486
3487 {
3488
3489 return $this->_productHash;
3490
3491 }
3492
3493
3494
3495 /**
3496
3497 * Removes the image from the database as well as removes any files recorded against it, updates sorting values of other images in the database and sets new thumbnails if the current image was the default thumbnail
3498
3499 *
3500
3501 * @param bool $loadFirst If specified as true (default) the latest data will be loaded from the database first before deleting to ensure all other resources being deleted are up to date, instead of relying on in-memory values
3502
3503 * @param bool $deleteFiles If specified as true (default) will also attempt to delete all files on the file system associated with the image
3504
3505 * @param int &$newThumbnailId By reference variable will be populated with the id of the new thumbnail image id if the image being deleted was the current thumbnail, if no new thumbnail was chosen or the current image is not a thumbnail the value set will be null
3506
3507 * @throws ISC_PRODUCT_IMAGE_INVALIDID_EXCEPTION If the current product image id is invalid
3508
3509 * @throws ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION If an unhandled database error occurred while attempting to delete product image data
3510
3511 * @throws ISC_PRODUCT_IMAGE_CANNOTDELETEFILE_EXCEPTION If an error occurred while attempting to delete any files from the file system
3512
3513 * @return void
3514
3515 */
3516
3517 public function delete ($loadFirst = true, $deleteFiles = true, &$newThumbnailId = null)
3518
3519 {
3520
3521 $newThumbnailId = null;
3522
3523 $imageId = $this->getProductImageId();
3524
3525 if (!$imageId) {
3526
3527 throw new ISC_PRODUCT_IMAGE_INVALIDID_EXCEPTION();
3528
3529 }
3530
3531
3532
3533 // to properly delete an image, it's files and to update other images accordingly, we need to make sure we have the latest data
3534
3535 if ($loadFirst) {
3536
3537 $this->loadFromDatabase();
3538
3539 }
3540
3541
3542
3543 $db = $GLOBALS['ISC_CLASS_DB'];
3544
3545 $db->Query("SET /*ISC_PRODUCT_IMAGE->delete*/ autocommit = 0 ");
3546
3547 $db->Query("LOCK TABLES /*ISC_PRODUCT_IMAGE->delete*/ `[|PREFIX|]product_images` WRITE");
3548
3549
3550
3551 // delete the image record
3552
3553 if (!$db->Query("DELETE FROM /*ISC_PRODUCT_IMAGE->delete*/ `[|PREFIX|]product_images` WHERE imageid = " . $imageId)) {
3554
3555 $db->Query("ROLLBACK /*ISC_PRODUCT_IMAGE->delete*/");
3556
3557 $db->Query("UNLOCK TABLES /*ISC_PRODUCT_IMAGE->delete*/");
3558
3559 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
3560
3561 }
3562
3563
3564
3565 // shift remaining image sorting values
3566
3567 if ($this->getProductId()) {
3568
3569 $sql = "UPDATE /*ISC_PRODUCT_IMAGE->delete*/ `[|PREFIX|]product_images` SET imagesort = imagesort - 1 WHERE imageprodid = " . $this->getProductId() . " AND imagesort > " . $this->getSort();
3570
3571 } else {
3572
3573 $sql = "UPDATE /*ISC_PRODUCT_IMAGE->delete*/ `[|PREFIX|]product_images` SET imagesort = imagesort - 1 WHERE imageprodhash = '" . $GLOBALS['ISC_CLASS_DB']->Quote($this->getProductHash()) . "' AND imagesort > " . $this->getSort();
3574
3575 }
3576
3577
3578
3579 if (!$db->Query($sql)) {
3580
3581 $db->Query("ROLLBACK /*ISC_PRODUCT_IMAGE->delete*/");
3582
3583 $db->Query("UNLOCK TABLES /*ISC_PRODUCT_IMAGE->delete*/");
3584
3585 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
3586
3587 }
3588
3589
3590
3591 // if necessary, set another image as the thumbnail
3592
3593 if ($this->getIsThumbnail()) {
3594
3595 // find the next thumbnail candidate
3596
3597 if ($this->getProductId()) {
3598
3599 $sql = "SELECT /*ISC_PRODUCT_IMAGE->delete*/ imageid FROM `[|PREFIX|]product_images` WHERE imageprodid = " . $this->getProductId() . " ORDER BY imagesort ASC LIMIT 1";
3600
3601 } else {
3602
3603 $sql = "SELECT /*ISC_PRODUCT_IMAGE->delete*/ imageid FROM `[|PREFIX|]product_images` WHERE imageprodhash = '" . $GLOBALS['ISC_CLASS_DB']->Quote($this->getProductHash()) . "' ORDER BY imagesort ASC LIMIT 1";
3604
3605 }
3606
3607
3608
3609 $result = $db->Query($sql);
3610
3611 if (!$result) {
3612
3613 $db->Query("ROLLBACK /*ISC_PRODUCT_IMAGE->delete*/");
3614
3615 $db->Query("UNLOCK TABLES /*ISC_PRODUCT_IMAGE->delete*/");
3616
3617 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
3618
3619 }
3620
3621
3622
3623 $row = $db->Fetch($result);
3624
3625 if ($row !== false) {
3626
3627 // update it to be the new thumbnail
3628
3629 if (!$db->Query("UPDATE /*ISC_PRODUCT_IMAGE->delete*/ `[|PREFIX|]product_images` SET imageisthumb = 1 WHERE imageid = " . (int)$row['imageid'])) {
3630
3631 $db->Query("ROLLBACK /*ISC_PRODUCT_IMAGE->delete*/");
3632
3633 $db->Query("UNLOCK TABLES /*ISC_PRODUCT_IMAGE->delete*/");
3634
3635 throw new ISC_PRODUCT_IMAGE_DBERROR_EXCEPTION(sprintf(GetLang('ProductImageDatabaseError'), __CLASS__, __METHOD__, $db->GetErrorMsg()));
3636
3637 }
3638
3639
3640
3641 $newThumbnailId = (int)$row['imageid'];
3642
3643 }
3644
3645 }
3646
3647
3648
3649 $db->Query("COMMIT /*ISC_PRODUCT_IMAGE->delete*/");
3650
3651 $db->Query("UNLOCK TABLES /*ISC_PRODUCT_IMAGE->delete*/");
3652
3653
3654
3655 if ($deleteFiles) {
3656
3657 $this->removeFiles();
3658
3659 }
3660
3661 }
3662
3663
3664
3665 /**
3666
3667 * Copies the currently loaded image record to an in-progress product $productHash
3668
3669 *
3670
3671 * @param string $productHash
3672
3673 * @return ISC_PRODUCT_IMAGE New instance of ISC_PRODUCT_IMAGE representing the copied database record
3674
3675 */
3676
3677 public function copyToProductHash ($productHash)
3678
3679 {
3680
3681 // this is easiest done by 'importing' based on the current source image
3682
3683 // if copying a product with many images is slow, this can be optimised by directly copying records and files because the import process will do all sorts of validation and may resize files which are already valid and sized
3684
3685
3686
3687 $existingSourceFilePath = $this->getAbsoluteSourceFilePath();
3688
3689 if (!file_exists($existingSourceFilePath)) {
3690
3691 throw new ISC_PRODUCT_IMAGE_SOURCEFILEDOESNTEXIST_EXCEPTION($existingSourceFilePath);
3692
3693 }
3694
3695
3696
3697 // base the new filename off the old one but remove random numbering so the import process can randomise it again
3698
3699 $newSourceFileName = basename($existingSourceFilePath);
3700
3701 $newSourceFileName = preg_replace('#__([0-9]{5})\.([^\.]+)$#', '.\\2', $newSourceFileName);
3702
3703
3704
3705 $image = self::importImage($existingSourceFilePath, $newSourceFileName, $productHash, true, false, false);
3706
3707
3708
3709 // perform additional work to inherit image properties that aren't carried over by a raw file import
3710
3711 $save = false;
3712
3713
3714
3715 if ($this->getDescription()) {
3716
3717 $save = true;
3718
3719 $image->setDescription($this->getDescription());
3720
3721 }
3722
3723
3724
3725 // note: probably don't want to copy thumbnail and sort values, but alternate text and caption will have to go here if they are implemented in future
3726
3727
3728
3729 if ($save) {
3730
3731 $image->saveToDatabase(false);
3732
3733 }
3734
3735
3736
3737 return $image;
3738
3739 }
3740
3741}