PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.19
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.19
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
← All changes | includes/Templates.php +194 -43 1.10.41.10.19 View file →
@@ -41,8 +41,13 @@
41 41 */
42 42 const OFFLINE_CAPABLE_ENGINES = array( 'logicless', 'thermal' );
43 43
44 44 /**
45 + * File extensions a bundled gallery template's content file can use.
46 + */
47 + const GALLERY_CONTENT_EXTENSIONS = array( 'html', 'php', 'xml' );
48 +
49 + /**
45 50 * Per-request cache of installed gallery template preview data profiles.
46 51 *
47 52 * @var array<string,string|null>
48 53 */
@@ -48,8 +53,22 @@
48 53 */
49 54 private static $gallery_preview_data_cache = array();
50 55
51 56 /**
57 + * Bump when the default term set below changes; a lower stored value reseeds.
58 + *
59 + * This class is constructed on EVERY request (Init::init_common), and the
60 + * seeding used to run nine `term_exists()` checks each time — 18 of the 31
61 + * queries the plugin added to every storefront page (measured 2026-09-03
62 + * on dev-next, see .claude/research/2026-09-03-online-store-footprint.md).
63 + * Behind the latch the whole registration costs no queries.
64 + */
65 + public const DEFAULT_TERMS_VERSION = 1;
66 +
67 + /** Autoloaded latch: read on every request, so it must ride in alloptions. */
68 + public const DEFAULT_TERMS_OPTION = 'woocommerce_pos_template_default_terms_version';
69 +
70 + /**
52 71 * Constructor.
53 72 */
54 73 public function __construct() {
55 74 // Register immediately since this is already being called during 'init'.
@@ -54,11 +73,68 @@
54 73 public function __construct() {
55 74 // Register immediately since this is already being called during 'init'.
56 75 $this->register_post_type();
57 76 $this->register_taxonomy();
77 + $this->maybe_seed_default_terms();
58 78 }
59 79
60 80 /**
81 + * Make sure the template post type and taxonomies exist on this request.
82 + *
83 + * A storefront request constructs Templates only on its first order write
84 + * (see Init::ensure_order_services()), but the static readers below are
85 + * also reached from a plain page — the My Account order actions read the
86 + * active receipt template. A tax_query against an unregistered taxonomy
87 + * matches nothing, and get_active_template_id() would then treat the
88 + * merchant's custom template as gone and delete the active-template
89 + * option. Registration itself costs no queries (the default terms are
90 + * behind an autoloaded latch), so every static reader calls this first.
91 + */
92 + public static function ensure_registered(): void {
93 + if ( taxonomy_exists( 'wcpos_template_type' ) && post_type_exists( 'wcpos_template' ) ) {
94 + return;
95 + }
96 + new self();
97 + }
98 +
99 + /**
100 + * Seed the default template types and categories once per DEFAULT_TERMS_VERSION.
101 + *
102 + * The latch is set only once every default term verifiably exists, so a
103 + * failed `wp_insert_term()` (a filter returning WP_Error, a transient DB
104 + * fault) leaves seeding armed for the next request instead of marking it
105 + * done. A term deleted by hand is restored on the next version bump or
106 + * plugin (re)activation, not the next request; the taxonomies are hidden
107 + * from menus and the defaults exist for the receipt UI, so that is the
108 + * right trade for a free page load.
109 + */
110 + private function maybe_seed_default_terms(): void {
111 + if ( (int) get_option( self::DEFAULT_TERMS_OPTION, 0 ) >= self::DEFAULT_TERMS_VERSION ) {
112 + return;
113 + }
114 + $this->register_default_template_types();
115 + $this->register_default_template_categories();
116 + if ( $this->default_terms_present() ) {
117 + update_option( self::DEFAULT_TERMS_OPTION, self::DEFAULT_TERMS_VERSION, true );
118 + }
119 + }
120 +
121 + /** Whether every default type and category term exists. */
122 + private function default_terms_present(): bool {
123 + foreach ( array( 'receipt', 'report' ) as $slug ) {
124 + if ( ! term_exists( $slug, 'wcpos_template_type' ) ) {
125 + return false;
126 + }
127 + }
128 + foreach ( array_keys( self::default_template_categories() ) as $slug ) {
129 + if ( ! term_exists( $slug, 'wcpos_template_category' ) ) {
130 + return false;
131 + }
132 + }
133 + return true;
134 + }
135 +
136 + /**
61 137 * Register the custom post type for templates.
62 138 * Only custom user-created templates are stored in the database.
63 139 *
64 140 * @return void
@@ -177,10 +253,9 @@
177 253 );
178 254
179 255 register_taxonomy( 'wcpos_template_type', array( 'wcpos_template' ), $args );
180 256
181 - // Register default template types.
182 - $this->register_default_template_types();
257 + // Default terms are seeded by maybe_seed_default_terms(), behind a latch.
183 258
184 259 // Register category taxonomy for gallery filtering.
185 260 register_taxonomy(
186 261 'wcpos_template_category',
@@ -204,10 +279,8 @@
204 279 'assign_terms' => 'manage_woocommerce_pos',
205 280 ),
206 281 )
207 282 );
208 -
209 - $this->register_default_template_categories();
210 283 }
211 284
212 285 /**
213 286 * Save raw post content directly to the database, bypassing wp_kses.
@@ -316,8 +389,9 @@
316 389 *
317 390 * @return null|array Template data or null if not found.
318 391 */
319 392 public static function get_template( int $template_id ): ?array {
393 + self::ensure_registered();
320 394 $post = get_post( $template_id );
321 395
322 396 if ( ! $post || 'wcpos_template' !== $post->post_type ) {
323 397 return null;
@@ -334,10 +408,10 @@
334 408 $gallery_key = get_post_meta( $template_id, '_template_gallery_key', true );
335 409 $preview_data = null;
336 410 if ( \is_string( $gallery_key ) && '' !== $gallery_key ) {
337 411 if ( ! array_key_exists( $gallery_key, self::$gallery_preview_data_cache ) ) {
338 - $gallery_template = self::get_gallery_template_by_key( $gallery_key );
339 - self::$gallery_preview_data_cache[ $gallery_key ] = $gallery_template['preview_data'] ?? null;
412 + $gallery_metadata = self::get_gallery_template_metadata( $gallery_key );
413 + self::$gallery_preview_data_cache[ $gallery_key ] = $gallery_metadata['preview_data'] ?? null;
340 414 }
341 415
342 416 $preview_data = self::$gallery_preview_data_cache[ $gallery_key ];
343 417 }
@@ -888,8 +962,9 @@
888 962 *
889 963 * @return array Array of template data arrays.
890 964 */
891 965 public static function get_enabled_templates( string $type = 'receipt' ): array {
966 + self::ensure_registered();
892 967 $disabled_virtual = self::get_disabled_virtual_templates( $type );
893 968 $order = self::get_template_order( $type );
894 969 $templates = array();
895 970
@@ -1008,10 +1083,9 @@
1008 1083 if ( ! is_dir( $gallery_dir ) ) {
1009 1084 return array();
1010 1085 }
1011 1086
1012 - $templates = array();
1013 - $extensions = array( 'html', 'php', 'xml' );
1087 + $templates = array();
1014 1088
1015 1089 foreach ( Gallery_Registry::all() as $key => $metadata ) {
1016 1090 if ( $type && ( $metadata['type'] ?? '' ) !== $type ) {
1017 1091 continue;
@@ -1019,36 +1093,17 @@
1019 1093 if ( $category && ( $metadata['category'] ?? '' ) !== $category ) {
1020 1094 continue;
1021 1095 }
1022 1096
1023 - $content_file = null;
1024 - foreach ( $extensions as $ext ) {
1025 - $candidate = $gallery_dir . $key . '.' . $ext;
1026 - if ( file_exists( $candidate ) ) {
1027 - $content_file = $candidate;
1028 - break;
1029 - }
1030 - }
1097 + $content_file = self::find_gallery_content_file( $key );
1031 1098
1032 - if ( ! $content_file ) {
1099 + if ( '' === $content_file ) {
1033 1100 continue;
1034 1101 }
1035 1102
1036 - $metadata['key'] = $key;
1037 - $metadata['direction'] = isset( $metadata['direction'] ) && 'rtl' === $metadata['direction']
1038 - ? 'rtl'
1039 - : 'ltr';
1040 -
1041 - $templates[] = array_merge(
1042 - $metadata,
1043 - array(
1044 - 'content' => file_get_contents( $content_file ),
1045 - 'content_file' => $content_file,
1046 - 'is_premade' => true,
1047 - 'is_virtual' => true,
1048 - 'source' => 'gallery',
1049 - 'offline_capable' => in_array( $metadata['engine'] ?? 'logicless', self::OFFLINE_CAPABLE_ENGINES, true ),
1050 - )
1103 + $templates[] = self::build_gallery_template(
1104 + self::prepare_gallery_metadata( $key, $metadata ),
1105 + $content_file
1051 1106 );
1052 1107 }
1053 1108
1054 1109 usort(
@@ -1061,24 +1116,113 @@
1061 1116 return $templates;
1062 1117 }
1063 1118
1064 1119 /**
1120 + * Locate the content file for a bundled gallery template key.
1121 + *
1122 + * Only stats the candidate paths for the given key — it never reads file
1123 + * contents, and never touches the other bundled gallery templates.
1124 + *
1125 + * @param string $key Gallery template key (e.g. "standard-receipt").
1126 + *
1127 + * @return string Absolute path to the content file, or '' when none exists.
1128 + */
1129 + private static function find_gallery_content_file( string $key ): string {
1130 + $gallery_dir = \WCPOS\WooCommercePOS\PLUGIN_PATH . 'templates/gallery/';
1131 +
1132 + foreach ( self::GALLERY_CONTENT_EXTENSIONS as $ext ) {
1133 + $candidate = $gallery_dir . $key . '.' . $ext;
1134 + if ( file_exists( $candidate ) ) {
1135 + return $candidate;
1136 + }
1137 + }
1138 +
1139 + return '';
1140 + }
1141 +
1142 + /**
1143 + * Normalize a raw registry entry into gallery template metadata.
1144 + *
1145 + * @param string $key Gallery template key.
1146 + * @param array<string,mixed> $metadata Raw registry entry.
1147 + *
1148 + * @return array<string,mixed> Normalized metadata.
1149 + */
1150 + private static function prepare_gallery_metadata( string $key, array $metadata ): array {
1151 + $metadata['key'] = $key;
1152 + $metadata['direction'] = isset( $metadata['direction'] ) && 'rtl' === $metadata['direction']
1153 + ? 'rtl'
1154 + : 'ltr';
1155 +
1156 + return $metadata;
1157 + }
1158 +
1159 + /**
1160 + * Build a full gallery template record by reading its content file.
1161 + *
1162 + * @param array<string,mixed> $metadata Normalized metadata for the key.
1163 + * @param string $content_file Absolute path to the content file.
1164 + *
1165 + * @return array<string,mixed> Gallery template record.
1166 + */
1167 + private static function build_gallery_template( array $metadata, string $content_file ): array {
1168 + return array_merge(
1169 + $metadata,
1170 + array(
1171 + 'content' => file_get_contents( $content_file ),
1172 + 'content_file' => $content_file,
1173 + 'is_premade' => true,
1174 + 'is_virtual' => true,
1175 + 'source' => 'gallery',
1176 + 'offline_capable' => in_array( $metadata['engine'] ?? 'logicless', self::OFFLINE_CAPABLE_ENGINES, true ),
1177 + )
1178 + );
1179 + }
1180 +
1181 + /**
1182 + * Get a single gallery template's metadata without reading any template file.
1183 + *
1184 + * Metadata (title, description, engine, preview_data, ...) lives in
1185 + * Gallery_Registry, so callers that only need metadata never pay for reading
1186 + * the bundled template content — which is why get_template() uses this rather
1187 + * than get_gallery_template_by_key().
1188 + *
1189 + * @param string $key Gallery template key (e.g. "standard-receipt").
1190 + *
1191 + * @return null|array Gallery template metadata, or null when the key is unknown
1192 + * or has no bundled content file.
1193 + */
1194 + public static function get_gallery_template_metadata( string $key ): ?array {
1195 + $registry = Gallery_Registry::all();
1196 +
1197 + if ( ! isset( $registry[ $key ] ) ) {
1198 + return null;
1199 + }
1200 +
1201 + if ( '' === self::find_gallery_content_file( $key ) ) {
1202 + return null;
1203 + }
1204 +
1205 + return self::prepare_gallery_metadata( $key, $registry[ $key ] );
1206 + }
1207 +
1208 + /**
1065 1209 * Get a single gallery template by its key.
1066 1210 *
1211 + * Reads only the requested template's content file.
1212 + *
1067 1213 * @param string $key Gallery template key (e.g. "standard-receipt").
1068 1214 *
1069 1215 * @return null|array Gallery template data or null if not found.
1070 1216 */
1071 1217 public static function get_gallery_template_by_key( string $key ): ?array {
1072 - $templates = self::get_gallery_templates();
1218 + $metadata = self::get_gallery_template_metadata( $key );
1073 1219
1074 - foreach ( $templates as $template ) {
1075 - if ( ( $template['key'] ?? '' ) === $key ) {
1076 - return $template;
1077 - }
1220 + if ( null === $metadata ) {
1221 + return null;
1078 1222 }
1079 1223
1080 - return null;
1224 + return self::build_gallery_template( $metadata, self::find_gallery_content_file( $key ) );
1081 1225 }
1082 1226
1083 1227 /**
1084 1228 * Install a starter template as a custom (database) template.
@@ -1246,14 +1390,14 @@
1246 1390 }
1247 1391 }
1248 1392
1249 1393 /**
1250 - * Register default template categories.
1394 + * The default template categories: slug => label.
1251 1395 *
1252 - * @return void
1396 + * @return array<string, string>
1253 1397 */
1254 - private function register_default_template_categories(): void {
1255 - $categories = array(
1398 + private static function default_template_categories(): array {
1399 + return array(
1256 1400 'receipt' => /* translators: Receipt template post type or template option label. */ __( 'Receipt', 'woocommerce-pos' ),
1257 1401 'invoice' => /* translators: Receipt template post type or template option label. */ __( 'Invoice', 'woocommerce-pos' ),
1258 1402 'gift-receipt' => /* translators: Receipt template post type or template option label. */ __( 'Gift Receipt', 'woocommerce-pos' ),
1259 1403 'credit-note' => /* translators: Receipt template post type or template option label. */ __( 'Credit Note', 'woocommerce-pos' ),
@@ -1260,10 +1404,17 @@
1260 1404 'purchase-order' => /* translators: Receipt template post type or template option label. */ __( 'Purchase Order', 'woocommerce-pos' ),
1261 1405 'kitchen-ticket' => /* translators: Receipt template post type or template option label. */ __( 'Kitchen Ticket', 'woocommerce-pos' ),
1262 1406 'bar-ticket' => /* translators: Receipt template post type or template option label. */ __( 'Bar Ticket', 'woocommerce-pos' ),
1263 1407 );
1408 + }
1264 1409
1265 - foreach ( $categories as $slug => $name ) {
1410 + /**
1411 + * Register default template categories.
1412 + *
1413 + * @return void
1414 + */
1415 + private function register_default_template_categories(): void {
1416 + foreach ( self::default_template_categories() as $slug => $name ) {
1266 1417 if ( ! term_exists( $slug, 'wcpos_template_category' ) ) {
1267 1418 wp_insert_term( $name, 'wcpos_template_category', array( 'slug' => $slug ) );
1268 1419 }
1269 1420 }