WP Job Manager - Version 1.32.1

Version Description

  • Fix: Adds compatibility with PHP 7.3
  • Fix: Restores original site search functionality.
Download this release

Release Info

Developer jakeom
Plugin Icon 128x128 WP Job Manager
Version 1.32.1
Comparing to
See all releases

Code changes from version 1.32.0 to 1.32.1

changelog.txt CHANGED
@@ -1,7 +1,11 @@
 
 
 
 
1
  = 1.32.0 =
2
  * Enhancement: Switched from Chosen to Select2 for enhanced dropdown handling and better mobile support. May require theme update.
3
  * Enhancement: Draft and unsubmitted job listings now appear in `[job_dashboard]`, allowing users to complete their submission.
4
- * Enhancement: Filled and expired positions are now hidden from WordPress search. (@felipeelia)
5
  * Enhancement: Adds additional support for the new block editor. Restricted to classic block for compatibility with frontend editor.
6
  * Enhancement: Job types can be preselected in `[jobs]` shortcode with `?search_job_type=term-slug`. (@felipeelia)
7
  * Enhancement: Author selection in WP admin now uses a searchable dropdown.
1
+ = 1.32.1 =
2
+ * Fix: Adds compatibility with PHP 7.3
3
+ * Fix: Restores original site search functionality.
4
+
5
  = 1.32.0 =
6
  * Enhancement: Switched from Chosen to Select2 for enhanced dropdown handling and better mobile support. May require theme update.
7
  * Enhancement: Draft and unsubmitted job listings now appear in `[job_dashboard]`, allowing users to complete their submission.
8
+ * Enhancement: [REVERTED IN 1.32.1] Filled and expired positions are now hidden from WordPress search. (@felipeelia)
9
  * Enhancement: Adds additional support for the new block editor. Restricted to classic block for compatibility with frontend editor.
10
  * Enhancement: Job types can be preselected in `[jobs]` shortcode with `?search_job_type=term-slug`. (@felipeelia)
11
  * Enhancement: Author selection in WP admin now uses a searchable dropdown.
includes/admin/class-wp-job-manager-writepanels.php CHANGED
@@ -701,7 +701,7 @@ class WP_Job_Manager_Writepanels {
701
  break;
702
  default:
703
  if ( ! isset( $_POST[ $key ] ) ) {
704
- continue;
705
  } elseif ( is_array( $_POST[ $key ] ) ) {
706
  update_post_meta( $post_id, $key, array_filter( array_map( 'sanitize_text_field', $_POST[ $key ] ) ) );
707
  } else {
701
  break;
702
  default:
703
  if ( ! isset( $_POST[ $key ] ) ) {
704
+ break;
705
  } elseif ( is_array( $_POST[ $key ] ) ) {
706
  update_post_meta( $post_id, $key, array_filter( array_map( 'sanitize_text_field', $_POST[ $key ] ) ) );
707
  } else {
includes/class-wp-job-manager-email-notifications.php CHANGED
@@ -125,7 +125,7 @@ final class WP_Job_Manager_Email_Notifications {
125
  include_once JOB_MANAGER_PLUGIN_DIR . '/includes/emails/class-wp-job-manager-email-employer-expiring-job.php';
126
  include_once JOB_MANAGER_PLUGIN_DIR . '/includes/emails/class-wp-job-manager-email-admin-expiring-job.php';
127
 
128
- if ( ! class_exists( 'Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
129
  include_once JOB_MANAGER_PLUGIN_DIR . '/lib/emogrifier/class-emogrifier.php';
130
  }
131
  }
125
  include_once JOB_MANAGER_PLUGIN_DIR . '/includes/emails/class-wp-job-manager-email-employer-expiring-job.php';
126
  include_once JOB_MANAGER_PLUGIN_DIR . '/includes/emails/class-wp-job-manager-email-admin-expiring-job.php';
127
 
128
+ if ( ! class_exists( 'Emogrifier' ) && class_exists( 'DOMDocument' ) && version_compare( PHP_VERSION, '5.5', '>=' ) ) {
129
  include_once JOB_MANAGER_PLUGIN_DIR . '/lib/emogrifier/class-emogrifier.php';
130
  }
131
  }
includes/class-wp-job-manager-post-types.php CHANGED
@@ -69,7 +69,6 @@ class WP_Job_Manager_Post_Types {
69
  add_action( 'update_post_meta', array( $this, 'update_post_meta' ), 10, 4 );
70
  add_action( 'wp_insert_post', array( $this, 'maybe_add_default_meta_data' ), 10, 2 );
71
 
72
- add_action( 'parse_query', array( $this, 'public_search_handler' ) );
73
  add_action( 'parse_query', array( $this, 'add_feed_query_args' ) );
74
 
75
  // Single job content.
@@ -372,7 +371,7 @@ class WP_Job_Manager_Post_Types {
372
  'expired',
373
  array(
374
  'label' => _x( 'Expired', 'post status', 'wp-job-manager' ),
375
- 'public' => ! isset( $_GET['s'] ),
376
  'protected' => true,
377
  'exclude_from_search' => true,
378
  'show_in_admin_all_list' => true,
@@ -532,35 +531,6 @@ class WP_Job_Manager_Post_Types {
532
  remove_filter( 'posts_search', 'get_job_listings_keyword_search' );
533
  }
534
 
535
- /**
536
- * Modifies WordPress Query of public search.
537
- *
538
- * @param WP_Query $query Query being processed.
539
- */
540
- public function public_search_handler( $query ) {
541
- if ( ! $query->is_search() ) {
542
- return;
543
- }
544
-
545
- // Remove filled positions, if necessary.
546
- if ( 1 === absint( get_option( 'job_manager_hide_filled_positions' ) ) ) {
547
- $meta_query = $query->get( 'meta_query' );
548
- if ( ! is_array( $meta_query ) ) {
549
- $meta_query = array();
550
- }
551
-
552
- $meta_query[] = array(
553
- 'key' => '_filled',
554
- 'value' => '1',
555
- 'compare' => '!=',
556
- );
557
-
558
- if ( ! empty( $meta_query ) ) {
559
- $query->set( 'meta_query', $meta_query );
560
- }
561
- }
562
- }
563
-
564
  /**
565
  * Adds query arguments in order to make sure that the feed properly queries the 'job_listing' type.
566
  *
69
  add_action( 'update_post_meta', array( $this, 'update_post_meta' ), 10, 4 );
70
  add_action( 'wp_insert_post', array( $this, 'maybe_add_default_meta_data' ), 10, 2 );
71
 
 
72
  add_action( 'parse_query', array( $this, 'add_feed_query_args' ) );
73
 
74
  // Single job content.
371
  'expired',
372
  array(
373
  'label' => _x( 'Expired', 'post status', 'wp-job-manager' ),
374
+ 'public' => true,
375
  'protected' => true,
376
  'exclude_from_search' => true,
377
  'show_in_admin_all_list' => true,
531
  remove_filter( 'posts_search', 'get_job_listings_keyword_search' );
532
  }
533
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  /**
535
  * Adds query arguments in order to make sure that the feed properly queries the 'job_listing' type.
536
  *
includes/class-wp-job-manager-usage-tracking.php CHANGED
@@ -121,7 +121,7 @@ class WP_Job_Manager_Usage_Tracking extends WP_Job_Manager_Usage_Tracking_Base {
121
  * @return bool
122
  */
123
  protected function do_track_plugin( $plugin_slug ) {
124
- if ( 1 === preg_match( '/^wp-job-manager/', $plugin_slug ) ) {
125
  return true;
126
  }
127
  $third_party_plugins = array(
121
  * @return bool
122
  */
123
  protected function do_track_plugin( $plugin_slug ) {
124
+ if ( 1 === preg_match( '/^wp\-job\-manager/', $plugin_slug ) ) {
125
  return true;
126
  }
127
  $third_party_plugins = array(
languages/wp-job-manager.pot CHANGED
@@ -1,14 +1,14 @@
1
- # Copyright (C) 2018 Automattic
2
  # This file is distributed under the GPL2+.
3
  msgid ""
4
  msgstr ""
5
- "Project-Id-Version: WP Job Manager 1.32.0\n"
6
  "Report-Msgid-Bugs-To: https://github.com/Automattic/WP-Job-Manager/issues\n"
7
- "POT-Creation-Date: 2018-12-17 17:26:39+00:00\n"
8
  "MIME-Version: 1.0\n"
9
  "Content-Type: text/plain; charset=utf-8\n"
10
  "Content-Transfer-Encoding: 8bit\n"
11
- "PO-Revision-Date: 2018-MO-DA HO:MI+ZONE\n"
12
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
  "Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"
14
  "X-Generator: grunt-wp-i18n1.0.2\n"
@@ -278,7 +278,7 @@ msgid "View"
278
  msgstr ""
279
 
280
  #: includes/admin/class-wp-job-manager-cpt.php:624
281
- #: includes/class-wp-job-manager-post-types.php:317
282
  #: templates/job-dashboard.php:52 templates/job-dashboard.php:70
283
  msgid "Edit"
284
  msgstr ""
@@ -349,8 +349,8 @@ msgid ""
349
  msgstr ""
350
 
351
  #: includes/admin/class-wp-job-manager-settings.php:117
352
- #: includes/class-wp-job-manager-post-types.php:311
353
- #: includes/class-wp-job-manager-post-types.php:413
354
  msgid "Job Listings"
355
  msgstr ""
356
 
@@ -741,7 +741,7 @@ msgstr ""
741
  #: includes/admin/class-wp-job-manager-taxonomy-meta.php:78
742
  #: includes/admin/class-wp-job-manager-taxonomy-meta.php:101
743
  #: includes/admin/class-wp-job-manager-taxonomy-meta.php:120
744
- #: includes/class-wp-job-manager-post-types.php:271
745
  #: includes/rest-api/class-wp-job-manager-models-job-types-custom-fields.php:36
746
  msgid "Employment Type"
747
  msgstr ""
@@ -1111,7 +1111,7 @@ msgid "WP Job Manager"
1111
  msgstr ""
1112
 
1113
  #: includes/class-wp-job-manager-data-exporter.php:52
1114
- #: includes/class-wp-job-manager-post-types.php:334
1115
  msgid "Company Logo"
1116
  msgstr ""
1117
 
@@ -1124,13 +1124,13 @@ msgid "Job title"
1124
  msgstr ""
1125
 
1126
  #: includes/class-wp-job-manager-email-notifications.php:243
1127
- #: includes/class-wp-job-manager-post-types.php:206
1128
  #: includes/forms/class-wp-job-manager-form-submit-job.php:192
1129
  msgid "Job type"
1130
  msgstr ""
1131
 
1132
  #: includes/class-wp-job-manager-email-notifications.php:253
1133
- #: includes/class-wp-job-manager-post-types.php:142
1134
  #: includes/forms/class-wp-job-manager-form-submit-job.php:201
1135
  msgid "Job category"
1136
  msgstr ""
@@ -1185,13 +1185,13 @@ msgstr ""
1185
  msgid "Employer"
1186
  msgstr ""
1187
 
1188
- #: includes/class-wp-job-manager-post-types.php:143
1189
  msgid "Job categories"
1190
  msgstr ""
1191
 
1192
- #: includes/class-wp-job-manager-post-types.php:171
1193
- #: includes/class-wp-job-manager-post-types.php:234
1194
- #: includes/class-wp-job-manager-post-types.php:327
1195
  #. translators: Placeholder %s is the plural label of the job listing category
1196
  #. taxonomy type.
1197
  #. translators: Placeholder %s is the plural label of the job listing job type
@@ -1201,9 +1201,9 @@ msgstr ""
1201
  msgid "Search %s"
1202
  msgstr ""
1203
 
1204
- #: includes/class-wp-job-manager-post-types.php:173
1205
- #: includes/class-wp-job-manager-post-types.php:236
1206
- #: includes/class-wp-job-manager-post-types.php:313
1207
  #. translators: Placeholder %s is the plural label of the job listing category
1208
  #. taxonomy type.
1209
  #. translators: Placeholder %s is the plural label of the job listing job type
@@ -1213,9 +1213,9 @@ msgstr ""
1213
  msgid "All %s"
1214
  msgstr ""
1215
 
1216
- #: includes/class-wp-job-manager-post-types.php:175
1217
- #: includes/class-wp-job-manager-post-types.php:238
1218
- #: includes/class-wp-job-manager-post-types.php:333
1219
  #. translators: Placeholder %s is the singular label of the job listing
1220
  #. category taxonomy type.
1221
  #. translators: Placeholder %s is the singular label of the job listing job
@@ -1225,8 +1225,8 @@ msgstr ""
1225
  msgid "Parent %s"
1226
  msgstr ""
1227
 
1228
- #: includes/class-wp-job-manager-post-types.php:177
1229
- #: includes/class-wp-job-manager-post-types.php:240
1230
  #. translators: Placeholder %s is the singular label of the job listing
1231
  #. category taxonomy type.
1232
  #. translators: Placeholder %s is the singular label of the job listing job
@@ -1234,9 +1234,9 @@ msgstr ""
1234
  msgid "Parent %s:"
1235
  msgstr ""
1236
 
1237
- #: includes/class-wp-job-manager-post-types.php:179
1238
- #: includes/class-wp-job-manager-post-types.php:242
1239
- #: includes/class-wp-job-manager-post-types.php:319
1240
  #. translators: Placeholder %s is the singular label of the job listing
1241
  #. category taxonomy type.
1242
  #. translators: Placeholder %s is the singular label of the job listing job
@@ -1246,8 +1246,8 @@ msgstr ""
1246
  msgid "Edit %s"
1247
  msgstr ""
1248
 
1249
- #: includes/class-wp-job-manager-post-types.php:181
1250
- #: includes/class-wp-job-manager-post-types.php:244
1251
  #. translators: Placeholder %s is the singular label of the job listing
1252
  #. category taxonomy type.
1253
  #. translators: Placeholder %s is the singular label of the job listing job
@@ -1255,8 +1255,8 @@ msgstr ""
1255
  msgid "Update %s"
1256
  msgstr ""
1257
 
1258
- #: includes/class-wp-job-manager-post-types.php:183
1259
- #: includes/class-wp-job-manager-post-types.php:246
1260
  #. translators: Placeholder %s is the singular label of the job listing
1261
  #. category taxonomy type.
1262
  #. translators: Placeholder %s is the singular label of the job listing job
@@ -1264,8 +1264,8 @@ msgstr ""
1264
  msgid "Add New %s"
1265
  msgstr ""
1266
 
1267
- #: includes/class-wp-job-manager-post-types.php:185
1268
- #: includes/class-wp-job-manager-post-types.php:248
1269
  #. translators: Placeholder %s is the singular label of the job listing
1270
  #. category taxonomy type.
1271
  #. translators: Placeholder %s is the singular label of the job listing job
@@ -1273,79 +1273,79 @@ msgstr ""
1273
  msgid "New %s Name"
1274
  msgstr ""
1275
 
1276
- #: includes/class-wp-job-manager-post-types.php:207
1277
  msgid "Job types"
1278
  msgstr ""
1279
 
1280
- #: includes/class-wp-job-manager-post-types.php:280
1281
  msgid "Job"
1282
  msgstr ""
1283
 
1284
- #: includes/class-wp-job-manager-post-types.php:281
1285
  msgid "Jobs"
1286
  msgstr ""
1287
 
1288
- #: includes/class-wp-job-manager-post-types.php:314
1289
  msgid "Add New"
1290
  msgstr ""
1291
 
1292
- #: includes/class-wp-job-manager-post-types.php:316
1293
  #. translators: Placeholder %s is the singular label of the job listing post
1294
  #. type.
1295
  msgid "Add %s"
1296
  msgstr ""
1297
 
1298
- #: includes/class-wp-job-manager-post-types.php:321
1299
  #. translators: Placeholder %s is the singular label of the job listing post
1300
  #. type.
1301
  msgid "New %s"
1302
  msgstr ""
1303
 
1304
- #: includes/class-wp-job-manager-post-types.php:323
1305
- #: includes/class-wp-job-manager-post-types.php:325
1306
  #. translators: Placeholder %s is the singular label of the job listing post
1307
  #. type.
1308
  msgid "View %s"
1309
  msgstr ""
1310
 
1311
- #: includes/class-wp-job-manager-post-types.php:329
1312
  #. translators: Placeholder %s is the singular label of the job listing post
1313
  #. type.
1314
  msgid "No %s found"
1315
  msgstr ""
1316
 
1317
- #: includes/class-wp-job-manager-post-types.php:331
1318
  #. translators: Placeholder %s is the plural label of the job listing post
1319
  #. type.
1320
  msgid "No %s found in trash"
1321
  msgstr ""
1322
 
1323
- #: includes/class-wp-job-manager-post-types.php:335
1324
  msgid "Set company logo"
1325
  msgstr ""
1326
 
1327
- #: includes/class-wp-job-manager-post-types.php:336
1328
  msgid "Remove company logo"
1329
  msgstr ""
1330
 
1331
- #: includes/class-wp-job-manager-post-types.php:337
1332
  msgid "Use as company logo"
1333
  msgstr ""
1334
 
1335
- #: includes/class-wp-job-manager-post-types.php:340
1336
  #. translators: Placeholder %s is the plural label of the job listing post
1337
  #. type.
1338
  msgid "This is where you can create and manage %s."
1339
  msgstr ""
1340
 
1341
- #: includes/class-wp-job-manager-post-types.php:381
1342
  #. translators: Placeholder %s is the number of expired posts of this type.
1343
  msgid "Expired <span class=\"count\">(%s)</span>"
1344
  msgid_plural "Expired <span class=\"count\">(%s)</span>"
1345
  msgstr[0] ""
1346
  msgstr[1] ""
1347
 
1348
- #: includes/class-wp-job-manager-post-types.php:393
1349
  #. translators: Placeholder %s is the number of posts in a preview state.
1350
  msgid "Preview <span class=\"count\">(%s)</span>"
1351
  msgid_plural "Preview <span class=\"count\">(%s)</span>"
@@ -2229,15 +2229,15 @@ msgstr ""
2229
  msgid "Standard REST API implementation from WP core"
2230
  msgstr ""
2231
 
2232
- #: wp-job-manager.php:328
2233
  msgid "Load previous listings"
2234
  msgstr ""
2235
 
2236
- #: wp-job-manager.php:429
2237
  msgid "Invalid file type. Accepted types:"
2238
  msgstr ""
2239
 
2240
- #: wp-job-manager.php:444
2241
  msgid "Are you sure you want to delete this listing?"
2242
  msgstr ""
2243
 
@@ -2294,19 +2294,19 @@ msgid "yy-mm-dd"
2294
  msgstr ""
2295
 
2296
  #: includes/admin/class-wp-job-manager-permalink-settings.php:104
2297
- #: includes/class-wp-job-manager-post-types.php:853
2298
  msgctxt "Job permalink - resave permalinks after changing this"
2299
  msgid "job"
2300
  msgstr ""
2301
 
2302
  #: includes/admin/class-wp-job-manager-permalink-settings.php:113
2303
- #: includes/class-wp-job-manager-post-types.php:854
2304
  msgctxt "Job category slug - resave permalinks after changing this"
2305
  msgid "job-category"
2306
  msgstr ""
2307
 
2308
  #: includes/admin/class-wp-job-manager-permalink-settings.php:122
2309
- #: includes/class-wp-job-manager-post-types.php:855
2310
  msgctxt "Job type slug - resave permalinks after changing this"
2311
  msgid "job-type"
2312
  msgstr ""
@@ -2326,13 +2326,13 @@ msgctxt "Default page title (wizard)"
2326
  msgid "Jobs"
2327
  msgstr ""
2328
 
2329
- #: includes/class-wp-job-manager-post-types.php:374
2330
  #: wp-job-manager-functions.php:320
2331
  msgctxt "post status"
2332
  msgid "Expired"
2333
  msgstr ""
2334
 
2335
- #: includes/class-wp-job-manager-post-types.php:387
2336
  #: wp-job-manager-functions.php:321
2337
  msgctxt "post status"
2338
  msgid "Preview"
@@ -2358,7 +2358,7 @@ msgctxt "post status"
2358
  msgid "Active"
2359
  msgstr ""
2360
 
2361
- #: includes/class-wp-job-manager-post-types.php:837
2362
  msgctxt "Post type archive slug - resave permalinks after changing this"
2363
  msgid "jobs"
2364
  msgstr ""
1
+ # Copyright (C) 2019 Automattic
2
  # This file is distributed under the GPL2+.
3
  msgid ""
4
  msgstr ""
5
+ "Project-Id-Version: WP Job Manager 1.32.1\n"
6
  "Report-Msgid-Bugs-To: https://github.com/Automattic/WP-Job-Manager/issues\n"
7
+ "POT-Creation-Date: 2019-01-28 10:55:14+00:00\n"
8
  "MIME-Version: 1.0\n"
9
  "Content-Type: text/plain; charset=utf-8\n"
10
  "Content-Transfer-Encoding: 8bit\n"
11
+ "PO-Revision-Date: 2019-MO-DA HO:MI+ZONE\n"
12
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
  "Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"
14
  "X-Generator: grunt-wp-i18n1.0.2\n"
278
  msgstr ""
279
 
280
  #: includes/admin/class-wp-job-manager-cpt.php:624
281
+ #: includes/class-wp-job-manager-post-types.php:316
282
  #: templates/job-dashboard.php:52 templates/job-dashboard.php:70
283
  msgid "Edit"
284
  msgstr ""
349
  msgstr ""
350
 
351
  #: includes/admin/class-wp-job-manager-settings.php:117
352
+ #: includes/class-wp-job-manager-post-types.php:310
353
+ #: includes/class-wp-job-manager-post-types.php:412
354
  msgid "Job Listings"
355
  msgstr ""
356
 
741
  #: includes/admin/class-wp-job-manager-taxonomy-meta.php:78
742
  #: includes/admin/class-wp-job-manager-taxonomy-meta.php:101
743
  #: includes/admin/class-wp-job-manager-taxonomy-meta.php:120
744
+ #: includes/class-wp-job-manager-post-types.php:270
745
  #: includes/rest-api/class-wp-job-manager-models-job-types-custom-fields.php:36
746
  msgid "Employment Type"
747
  msgstr ""
1111
  msgstr ""
1112
 
1113
  #: includes/class-wp-job-manager-data-exporter.php:52
1114
+ #: includes/class-wp-job-manager-post-types.php:333
1115
  msgid "Company Logo"
1116
  msgstr ""
1117
 
1124
  msgstr ""
1125
 
1126
  #: includes/class-wp-job-manager-email-notifications.php:243
1127
+ #: includes/class-wp-job-manager-post-types.php:205
1128
  #: includes/forms/class-wp-job-manager-form-submit-job.php:192
1129
  msgid "Job type"
1130
  msgstr ""
1131
 
1132
  #: includes/class-wp-job-manager-email-notifications.php:253
1133
+ #: includes/class-wp-job-manager-post-types.php:141
1134
  #: includes/forms/class-wp-job-manager-form-submit-job.php:201
1135
  msgid "Job category"
1136
  msgstr ""
1185
  msgid "Employer"
1186
  msgstr ""
1187
 
1188
+ #: includes/class-wp-job-manager-post-types.php:142
1189
  msgid "Job categories"
1190
  msgstr ""
1191
 
1192
+ #: includes/class-wp-job-manager-post-types.php:170
1193
+ #: includes/class-wp-job-manager-post-types.php:233
1194
+ #: includes/class-wp-job-manager-post-types.php:326
1195
  #. translators: Placeholder %s is the plural label of the job listing category
1196
  #. taxonomy type.
1197
  #. translators: Placeholder %s is the plural label of the job listing job type
1201
  msgid "Search %s"
1202
  msgstr ""
1203
 
1204
+ #: includes/class-wp-job-manager-post-types.php:172
1205
+ #: includes/class-wp-job-manager-post-types.php:235
1206
+ #: includes/class-wp-job-manager-post-types.php:312
1207
  #. translators: Placeholder %s is the plural label of the job listing category
1208
  #. taxonomy type.
1209
  #. translators: Placeholder %s is the plural label of the job listing job type
1213
  msgid "All %s"
1214
  msgstr ""
1215
 
1216
+ #: includes/class-wp-job-manager-post-types.php:174
1217
+ #: includes/class-wp-job-manager-post-types.php:237
1218
+ #: includes/class-wp-job-manager-post-types.php:332
1219
  #. translators: Placeholder %s is the singular label of the job listing
1220
  #. category taxonomy type.
1221
  #. translators: Placeholder %s is the singular label of the job listing job
1225
  msgid "Parent %s"
1226
  msgstr ""
1227
 
1228
+ #: includes/class-wp-job-manager-post-types.php:176
1229
+ #: includes/class-wp-job-manager-post-types.php:239
1230
  #. translators: Placeholder %s is the singular label of the job listing
1231
  #. category taxonomy type.
1232
  #. translators: Placeholder %s is the singular label of the job listing job
1234
  msgid "Parent %s:"
1235
  msgstr ""
1236
 
1237
+ #: includes/class-wp-job-manager-post-types.php:178
1238
+ #: includes/class-wp-job-manager-post-types.php:241
1239
+ #: includes/class-wp-job-manager-post-types.php:318
1240
  #. translators: Placeholder %s is the singular label of the job listing
1241
  #. category taxonomy type.
1242
  #. translators: Placeholder %s is the singular label of the job listing job
1246
  msgid "Edit %s"
1247
  msgstr ""
1248
 
1249
+ #: includes/class-wp-job-manager-post-types.php:180
1250
+ #: includes/class-wp-job-manager-post-types.php:243
1251
  #. translators: Placeholder %s is the singular label of the job listing
1252
  #. category taxonomy type.
1253
  #. translators: Placeholder %s is the singular label of the job listing job
1255
  msgid "Update %s"
1256
  msgstr ""
1257
 
1258
+ #: includes/class-wp-job-manager-post-types.php:182
1259
+ #: includes/class-wp-job-manager-post-types.php:245
1260
  #. translators: Placeholder %s is the singular label of the job listing
1261
  #. category taxonomy type.
1262
  #. translators: Placeholder %s is the singular label of the job listing job
1264
  msgid "Add New %s"
1265
  msgstr ""
1266
 
1267
+ #: includes/class-wp-job-manager-post-types.php:184
1268
+ #: includes/class-wp-job-manager-post-types.php:247
1269
  #. translators: Placeholder %s is the singular label of the job listing
1270
  #. category taxonomy type.
1271
  #. translators: Placeholder %s is the singular label of the job listing job
1273
  msgid "New %s Name"
1274
  msgstr ""
1275
 
1276
+ #: includes/class-wp-job-manager-post-types.php:206
1277
  msgid "Job types"
1278
  msgstr ""
1279
 
1280
+ #: includes/class-wp-job-manager-post-types.php:279
1281
  msgid "Job"
1282
  msgstr ""
1283
 
1284
+ #: includes/class-wp-job-manager-post-types.php:280
1285
  msgid "Jobs"
1286
  msgstr ""
1287
 
1288
+ #: includes/class-wp-job-manager-post-types.php:313
1289
  msgid "Add New"
1290
  msgstr ""
1291
 
1292
+ #: includes/class-wp-job-manager-post-types.php:315
1293
  #. translators: Placeholder %s is the singular label of the job listing post
1294
  #. type.
1295
  msgid "Add %s"
1296
  msgstr ""
1297
 
1298
+ #: includes/class-wp-job-manager-post-types.php:320
1299
  #. translators: Placeholder %s is the singular label of the job listing post
1300
  #. type.
1301
  msgid "New %s"
1302
  msgstr ""
1303
 
1304
+ #: includes/class-wp-job-manager-post-types.php:322
1305
+ #: includes/class-wp-job-manager-post-types.php:324
1306
  #. translators: Placeholder %s is the singular label of the job listing post
1307
  #. type.
1308
  msgid "View %s"
1309
  msgstr ""
1310
 
1311
+ #: includes/class-wp-job-manager-post-types.php:328
1312
  #. translators: Placeholder %s is the singular label of the job listing post
1313
  #. type.
1314
  msgid "No %s found"
1315
  msgstr ""
1316
 
1317
+ #: includes/class-wp-job-manager-post-types.php:330
1318
  #. translators: Placeholder %s is the plural label of the job listing post
1319
  #. type.
1320
  msgid "No %s found in trash"
1321
  msgstr ""
1322
 
1323
+ #: includes/class-wp-job-manager-post-types.php:334
1324
  msgid "Set company logo"
1325
  msgstr ""
1326
 
1327
+ #: includes/class-wp-job-manager-post-types.php:335
1328
  msgid "Remove company logo"
1329
  msgstr ""
1330
 
1331
+ #: includes/class-wp-job-manager-post-types.php:336
1332
  msgid "Use as company logo"
1333
  msgstr ""
1334
 
1335
+ #: includes/class-wp-job-manager-post-types.php:339
1336
  #. translators: Placeholder %s is the plural label of the job listing post
1337
  #. type.
1338
  msgid "This is where you can create and manage %s."
1339
  msgstr ""
1340
 
1341
+ #: includes/class-wp-job-manager-post-types.php:380
1342
  #. translators: Placeholder %s is the number of expired posts of this type.
1343
  msgid "Expired <span class=\"count\">(%s)</span>"
1344
  msgid_plural "Expired <span class=\"count\">(%s)</span>"
1345
  msgstr[0] ""
1346
  msgstr[1] ""
1347
 
1348
+ #: includes/class-wp-job-manager-post-types.php:392
1349
  #. translators: Placeholder %s is the number of posts in a preview state.
1350
  msgid "Preview <span class=\"count\">(%s)</span>"
1351
  msgid_plural "Preview <span class=\"count\">(%s)</span>"
2229
  msgid "Standard REST API implementation from WP core"
2230
  msgstr ""
2231
 
2232
+ #: wp-job-manager.php:331
2233
  msgid "Load previous listings"
2234
  msgstr ""
2235
 
2236
+ #: wp-job-manager.php:455
2237
  msgid "Invalid file type. Accepted types:"
2238
  msgstr ""
2239
 
2240
+ #: wp-job-manager.php:470
2241
  msgid "Are you sure you want to delete this listing?"
2242
  msgstr ""
2243
 
2294
  msgstr ""
2295
 
2296
  #: includes/admin/class-wp-job-manager-permalink-settings.php:104
2297
+ #: includes/class-wp-job-manager-post-types.php:823
2298
  msgctxt "Job permalink - resave permalinks after changing this"
2299
  msgid "job"
2300
  msgstr ""
2301
 
2302
  #: includes/admin/class-wp-job-manager-permalink-settings.php:113
2303
+ #: includes/class-wp-job-manager-post-types.php:824
2304
  msgctxt "Job category slug - resave permalinks after changing this"
2305
  msgid "job-category"
2306
  msgstr ""
2307
 
2308
  #: includes/admin/class-wp-job-manager-permalink-settings.php:122
2309
+ #: includes/class-wp-job-manager-post-types.php:825
2310
  msgctxt "Job type slug - resave permalinks after changing this"
2311
  msgid "job-type"
2312
  msgstr ""
2326
  msgid "Jobs"
2327
  msgstr ""
2328
 
2329
+ #: includes/class-wp-job-manager-post-types.php:373
2330
  #: wp-job-manager-functions.php:320
2331
  msgctxt "post status"
2332
  msgid "Expired"
2333
  msgstr ""
2334
 
2335
+ #: includes/class-wp-job-manager-post-types.php:386
2336
  #: wp-job-manager-functions.php:321
2337
  msgctxt "post status"
2338
  msgid "Preview"
2358
  msgid "Active"
2359
  msgstr ""
2360
 
2361
+ #: includes/class-wp-job-manager-post-types.php:807
2362
  msgctxt "Post type archive slug - resave permalinks after changing this"
2363
  msgid "jobs"
2364
  msgstr ""
lib/emogrifier/class-emogrifier.php CHANGED
@@ -4,17 +4,15 @@
4
  *
5
  * For more information, please see the README.md file.
6
  *
7
- * @version 1.2.0
8
  *
9
  * @author Cameron Brooks
10
  * @author Jaime Prado
11
- * @author Oliver Klee <typo3-coding@oliverklee.de>
12
  * @author Roman Ožana <ozana@omdesign.cz>
13
  * @author Sander Kruger <s.kruger@invessel.com>
14
- *
15
- * @see https://raw.githubusercontent.com/MyIntervals/emogrifier/V1.2.0/Classes/Emogrifier.php
16
  */
17
- // @codingStandardsIgnoreFile
18
  class Emogrifier
19
  {
20
  /**
@@ -89,35 +87,35 @@ class Emogrifier
89
  /**
90
  * @var bool[]
91
  */
92
- private $excludedSelectors = array();
93
 
94
  /**
95
  * @var string[]
96
  */
97
- private $unprocessableHtmlTags = array( 'wbr' );
98
 
99
  /**
100
  * @var bool[]
101
  */
102
- private $allowedMediaTypes = array( 'all' => true, 'screen' => true, 'print' => true );
103
 
104
  /**
105
  * @var mixed[]
106
  */
107
- private $caches = array(
108
- self::CACHE_KEY_CSS => array(),
109
- self::CACHE_KEY_SELECTOR => array(),
110
- self::CACHE_KEY_XPATH => array(),
111
- self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => array(),
112
- self::CACHE_KEY_COMBINED_STYLES => array(),
113
- );
114
 
115
  /**
116
  * the visited nodes with the XPath paths as array keys
117
  *
118
  * @var \DOMElement[]
119
  */
120
- private $visitedNodes = array();
121
 
122
  /**
123
  * the styles to apply to the nodes with the XPath paths as array keys for the outer array
@@ -125,7 +123,7 @@ class Emogrifier
125
  *
126
  * @var string[][]
127
  */
128
- private $styleAttributesForNodes = array();
129
 
130
  /**
131
  * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved.
@@ -158,37 +156,38 @@ class Emogrifier
158
  /**
159
  * @var string[]
160
  */
161
- private $xPathRules = array(
162
- // child
163
- '/\\s*>\\s*/' => '/',
164
- // adjacent sibling
165
- '/\\s+\\+\\s+/' => '/following-sibling::*[1]/self::',
166
- // descendant
167
- '/\\s+(?=.*[^\\]]{1}$)/' => '//',
168
- // :first-child
169
- '/([^\\/]+):first-child/i' => '*[1]/self::\\1',
170
- // :last-child
171
- '/([^\\/]+):last-child/i' => '*[last()]/self::\\1',
172
- // attribute only
173
- '/^\\[(\\w+|\\w+\\=[\'"]?\\w+[\'"]?)\\]/' => '*[@\\1]',
174
- // attribute
175
- '/(\\w)\\[(\\w+)\\]/' => '\\1[@\\2]',
176
- // exact attribute
177
  '/(\\w)\\[(\\w+)\\=[\'"]?([\\w\\s]+)[\'"]?\\]/' => '\\1[@\\2="\\3"]',
178
- // element attribute~=
179
- '/([\\w\\*]+)\\[(\\w+)[\\s]*\\~\\=[\\s]*[\'"]?([\\w-_\\/]+)[\'"]?\\]/'
180
  => '\\1[contains(concat(" ", @\\2, " "), concat(" ", "\\3", " "))]',
181
- // element attribute^=
182
- '/([\\w\\*]+)\\[(\\w+)[\\s]*\\^\\=[\\s]*[\'"]?([\\w-_\\/]+)[\'"]?\\]/' => '\\1[starts-with(@\\2, "\\3")]',
183
- // element attribute*=
184
- '/([\\w\\*]+)\\[(\\w+)[\\s]*\\*\\=[\\s]*[\'"]?([\\w-_\\s\\/:;]+)[\'"]?\\]/' => '\\1[contains(@\\2, "\\3")]',
185
- // element attribute$=
186
- '/([\\w\\*]+)\\[(\\w+)[\\s]*\\$\\=[\\s]*[\'"]?([\\w-_\\s\\/]+)[\'"]?\\]/'
187
- => '\\1[substring(@\\2, string-length(@\\2) - string-length("\\3") + 1) = "\\3"]',
188
- // element attribute|=
189
- '/([\\w\\*]+)\\[(\\w+)[\\s]*\\|\\=[\\s]*[\'"]?([\\w-_\\s\\/]+)[\'"]?\\]/'
190
  => '\\1[@\\2="\\3" or starts-with(@\\2, concat("\\3", "-"))]',
191
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
  /**
194
  * Determines whether CSS styles that have an equivalent HTML attribute
@@ -206,27 +205,32 @@ class Emogrifier
206
  *
207
  * @var mixed[][]
208
  */
209
- private $cssToHtmlMap = array(
210
- 'background-color' => array(
211
  'attribute' => 'bgcolor',
212
- ),
213
- 'text-align' => array(
214
  'attribute' => 'align',
215
- 'nodes' => array('p', 'div', 'td'),
216
- 'values' => array('left', 'right', 'center', 'justify'),
217
- ),
218
- 'float' => array(
219
  'attribute' => 'align',
220
- 'nodes' => array('table', 'img'),
221
- 'values' => array('left', 'right'),
222
- ),
223
- 'border-spacing' => array(
224
  'attribute' => 'cellspacing',
225
- 'nodes' => array('table'),
226
- ),
227
- );
228
 
229
- public static $_media = '';
 
 
 
 
 
230
 
231
  /**
232
  * The constructor.
@@ -284,16 +288,7 @@ class Emogrifier
284
  */
285
  public function emogrify()
286
  {
287
- if ($this->html === '') {
288
- throw new BadMethodCallException('Please set some HTML first before calling emogrify.', 1390393096);
289
- }
290
-
291
- self::$_media = ''; // reset.
292
-
293
- $xmlDocument = $this->createXmlDocument();
294
- $this->process($xmlDocument);
295
-
296
- return $xmlDocument->saveHTML();
297
  }
298
 
299
  /**
@@ -307,20 +302,31 @@ class Emogrifier
307
  * @throws \BadMethodCallException
308
  */
309
  public function emogrifyBodyContent()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  {
311
  if ($this->html === '') {
312
- throw new BadMethodCallException('Please set some HTML first before calling emogrify.', 1390393096);
313
  }
314
 
315
- $xmlDocument = $this->createXmlDocument();
 
316
  $this->process($xmlDocument);
317
 
318
- $innerDocument = new DOMDocument();
319
- foreach ($xmlDocument->documentElement->getElementsByTagName('body')->item(0)->childNodes as $childNode) {
320
- $innerDocument->appendChild($innerDocument->importNode($childNode, true));
321
- }
322
-
323
- return html_entity_decode($innerDocument->saveHTML());
324
  }
325
 
326
  /**
@@ -334,35 +340,18 @@ class Emogrifier
334
  *
335
  * @throws \InvalidArgumentException
336
  */
337
- protected function process(DOMDocument $xmlDocument)
338
  {
339
- $xPath = new DOMXPath($xmlDocument);
340
  $this->clearAllCaches();
341
-
342
- // Before be begin processing the CSS file, parse the document and normalize all existing CSS attributes.
343
- // This changes 'DISPLAY: none' to 'display: none'.
344
- // We wouldn't have to do this if DOMXPath supported XPath 2.0.
345
- // Also store a reference of nodes with existing inline styles so we don't overwrite them.
346
  $this->purgeVisitedNodes();
 
347
 
348
- set_error_handler(array($this, 'handleXpathError'), E_WARNING);
349
-
350
- $nodesWithStyleAttributes = $xPath->query('//*[@style]');
351
- if ($nodesWithStyleAttributes !== false) {
352
- /** @var \DOMElement $node */
353
- foreach ($nodesWithStyleAttributes as $node) {
354
- if ($this->isInlineStyleAttributesParsingEnabled) {
355
- $this->normalizeStyleAttributes($node);
356
- } else {
357
- $node->removeAttribute('style');
358
- }
359
- }
360
- }
361
 
362
  // grab any existing style blocks from the html and append them to the existing CSS
363
  // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
364
  $allCss = $this->css;
365
-
366
  if ($this->isStyleBlocksParsingEnabled) {
367
  $allCss .= $this->getCssFromAllStyleNodes($xPath);
368
  }
@@ -371,10 +360,17 @@ class Emogrifier
371
  $excludedNodes = $this->getNodesToExclude($xPath);
372
  $cssRules = $this->parseCssRules($cssParts['css']);
373
  foreach ($cssRules as $cssRule) {
374
- // query the body for the xpath selector
375
- $nodesMatchingCssSelectors = $xPath->query($this->translateCssToXpath($cssRule['selector']));
376
- // ignore invalid selectors
377
- if ($nodesMatchingCssSelectors === false) {
 
 
 
 
 
 
 
378
  continue;
379
  }
380
 
@@ -383,18 +379,14 @@ class Emogrifier
383
  if (in_array($node, $excludedNodes, true)) {
384
  continue;
385
  }
386
-
387
  // if it has a style attribute, get it, process it, and append (overwrite) new stuff
388
  if ($node->hasAttribute('style')) {
389
  // break it up into an associative array
390
  $oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
391
  } else {
392
- $oldStyleDeclarations = array();
393
  }
394
  $newStyleDeclarations = $this->parseCssDeclarationsBlock($cssRule['declarationsBlock']);
395
- if ($this->shouldMapCssToHtml) {
396
- $this->mapCssToHtmlAttributes($newStyleDeclarations, $node);
397
- }
398
  $node->setAttribute(
399
  'style',
400
  $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations)
@@ -402,17 +394,101 @@ class Emogrifier
402
  }
403
  }
404
 
405
- restore_error_handler();
406
-
407
  if ($this->isInlineStyleAttributesParsingEnabled) {
408
  $this->fillStyleAttributesWithMergedStyles();
409
  }
410
 
 
 
 
 
411
  if ($this->shouldKeepInvisibleNodes) {
412
  $this->removeInvisibleNodes($xPath);
413
  }
414
 
 
 
415
  $this->copyCssWithMediaToStyleNode($xmlDocument, $xPath, $cssParts['media']);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  }
417
 
418
  /**
@@ -422,11 +498,11 @@ class Emogrifier
422
  * node.
423
  *
424
  * @param string[] $styles the new CSS styles taken from the global styles to be applied to this node
425
- * @param \DOMNode $node node to apply styles to
426
  *
427
  * @return void
428
  */
429
- private function mapCssToHtmlAttributes(array $styles, DOMNode $node)
430
  {
431
  foreach ($styles as $property => $value) {
432
  // Strip !important indicator
@@ -441,12 +517,12 @@ class Emogrifier
441
  * This method maps a CSS rule to HTML attributes and adds those to the node.
442
  *
443
  * @param string $property the name of the CSS property to map
444
- * @param string $value the value of the style rule to map
445
- * @param \DOMNode $node node to apply styles to
446
  *
447
  * @return void
448
  */
449
- private function mapCssToHtmlAttribute($property, $value, DOMNode $node)
450
  {
451
  if (!$this->mapSimpleCssProperty($property, $value, $node)) {
452
  $this->mapComplexCssProperty($property, $value, $node);
@@ -457,12 +533,12 @@ class Emogrifier
457
  * Looks up the CSS property in the mapping table and maps it if it matches the conditions.
458
  *
459
  * @param string $property the name of the CSS property to map
460
- * @param string $value the value of the style rule to map
461
- * @param \DOMNode $node node to apply styles to
462
  *
463
  * @return bool true if the property cab be mapped using the simple mapping table
464
  */
465
- private function mapSimpleCssProperty($property, $value, DOMNode $node)
466
  {
467
  if (!isset($this->cssToHtmlMap[$property])) {
468
  return false;
@@ -484,12 +560,12 @@ class Emogrifier
484
  * Maps CSS properties that need special transformation to an HTML attribute.
485
  *
486
  * @param string $property the name of the CSS property to map
487
- * @param string $value the value of the style rule to map
488
- * @param \DOMNode $node node to apply styles to
489
  *
490
  * @return void
491
  */
492
- private function mapComplexCssProperty($property, $value, DOMNode $node)
493
  {
494
  $nodeName = $node->nodeName;
495
  $isTable = $nodeName === 'table';
@@ -501,7 +577,7 @@ class Emogrifier
501
  // Parse out the color, if any
502
  $styles = explode(' ', $value);
503
  $first = $styles[0];
504
- if (!is_numeric(substr($first, 0, 1)) && substr($first, 0, 3) !== 'url') {
505
  // This is not a position or image, assume it's a color
506
  $node->setAttribute('bgcolor', $first);
507
  }
@@ -549,7 +625,7 @@ class Emogrifier
549
  {
550
  $values = preg_split('/\\s+/', $value);
551
 
552
- $css = array();
553
  $css['top'] = $values[0];
554
  $css['right'] = (count($values) > 1) ? $values[1] : $css['top'];
555
  $css['bottom'] = (count($values) > 2) ? $values[2] : $css['top'];
@@ -574,9 +650,10 @@ class Emogrifier
574
  $cssKey = md5($css);
575
  if (!isset($this->caches[self::CACHE_KEY_CSS][$cssKey])) {
576
  // process the CSS file for selectors and definitions
577
- preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mis', $css, $matches, PREG_SET_ORDER);
578
 
579
- $cssRules = array();
 
580
  /** @var string[] $cssRule */
581
  foreach ($matches as $key => $cssRule) {
582
  $cssDeclaration = trim($cssRule[2]);
@@ -589,22 +666,25 @@ class Emogrifier
589
  // don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
590
  // only allow structural pseudo-classes
591
  $hasPseudoElement = strpos($selector, '::') !== false;
592
- $hasAnyPseudoClass = (bool) preg_match('/:[a-zA-Z]/', $selector);
593
- $hasSupportedPseudoClass = (bool) preg_match('/:\\S+\\-(child|type\\()/i', $selector);
 
 
 
594
  if ($hasPseudoElement || ($hasAnyPseudoClass && !$hasSupportedPseudoClass)) {
595
  continue;
596
  }
597
 
598
- $cssRules[] = array(
599
  'selector' => trim($selector),
600
  'declarationsBlock' => $cssDeclaration,
601
  // keep track of where it appears in the file, since order is important
602
  'line' => $key,
603
- );
604
  }
605
  }
606
 
607
- usort($cssRules, array($this, 'sortBySelectorPrecedence'));
608
 
609
  $this->caches[self::CACHE_KEY_CSS][$cssKey] = $cssRules;
610
  }
@@ -679,18 +759,18 @@ class Emogrifier
679
  */
680
  private function clearCache($key)
681
  {
682
- $allowedCacheKeys = array(
683
  self::CACHE_KEY_CSS,
684
  self::CACHE_KEY_SELECTOR,
685
  self::CACHE_KEY_XPATH,
686
  self::CACHE_KEY_CSS_DECLARATIONS_BLOCK,
687
  self::CACHE_KEY_COMBINED_STYLES,
688
- );
689
  if (!in_array($key, $allowedCacheKeys, true)) {
690
- throw new InvalidArgumentException('Invalid cache key: ' . $key, 1391822035);
691
  }
692
 
693
- $this->caches[$key] = array();
694
  }
695
 
696
  /**
@@ -700,8 +780,8 @@ class Emogrifier
700
  */
701
  private function purgeVisitedNodes()
702
  {
703
- $this->visitedNodes = array();
704
- $this->styleAttributesForNodes = array();
705
  }
706
 
707
  /**
@@ -801,7 +881,7 @@ class Emogrifier
801
  *
802
  * @return void
803
  */
804
- private function removeInvisibleNodes(DOMXPath $xPath)
805
  {
806
  $nodesWithStyleDisplayNone = $xPath->query(
807
  '//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]'
@@ -814,14 +894,35 @@ class Emogrifier
814
  // we don't try to call removeChild on a nonexistent child node
815
  /** @var \DOMNode $node */
816
  foreach ($nodesWithStyleDisplayNone as $node) {
817
- if ($node->parentNode && is_callable(array($node->parentNode, 'removeChild'))) {
818
  $node->parentNode->removeChild($node);
819
  }
820
  }
821
  }
822
 
823
- private function normalizeStyleAttributes_callback( $m ) {
824
- return strtolower( $m[0] );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
  }
826
 
827
  /**
@@ -831,11 +932,13 @@ class Emogrifier
831
  *
832
  * @return void
833
  */
834
- private function normalizeStyleAttributes(DOMElement $node)
835
  {
836
  $normalizedOriginalStyle = preg_replace_callback(
837
  '/[A-z\\-]+(?=\\:)/S',
838
- array( $this, 'normalizeStyleAttributes_callback' ),
 
 
839
  $node->getAttribute('style')
840
  );
841
 
@@ -896,7 +999,7 @@ class Emogrifier
896
 
897
  $newAttributeValue = $newStyles[$attributeName];
898
  if ($this->attributeValueIsImportant($attributeValue)
899
- && !$this->attributeValueIsImportant($newAttributeValue)
900
  ) {
901
  $combinedStyles[$attributeName] = $attributeValue;
902
  }
@@ -913,6 +1016,18 @@ class Emogrifier
913
  return $trimmedStyle;
914
  }
915
 
 
 
 
 
 
 
 
 
 
 
 
 
916
  /**
917
  * Checks whether $attributeValue is marked as !important.
918
  *
@@ -934,13 +1049,13 @@ class Emogrifier
934
  *
935
  * @return void
936
  */
937
- private function copyCssWithMediaToStyleNode(DOMDocument $xmlDocument, DOMXPath $xPath, $css)
938
  {
939
  if ($css === '') {
940
  return;
941
  }
942
 
943
- $mediaQueriesRelevantForDocument = array();
944
 
945
  foreach ($this->extractMediaQueriesFromCss($css) as $mediaQuery) {
946
  foreach ($this->parseCssRules($mediaQuery['css']) as $selector) {
@@ -964,14 +1079,15 @@ class Emogrifier
964
  private function extractMediaQueriesFromCss($css)
965
  {
966
  preg_match_all('/@media\\b[^{]*({((?:[^{}]+|(?1))*)})/', $css, $rawMediaQueries, PREG_SET_ORDER);
967
- $parsedQueries = array();
968
 
 
969
  foreach ($rawMediaQueries as $mediaQuery) {
970
  if ($mediaQuery[2] !== '') {
971
- $parsedQueries[] = array(
972
- 'css' => $mediaQuery[2],
973
  'query' => $mediaQuery[0],
974
- );
975
  }
976
  }
977
 
@@ -980,15 +1096,26 @@ class Emogrifier
980
 
981
  /**
982
  * Checks whether there is at least one matching element for $cssSelector.
 
 
983
  *
984
  * @param \DOMXPath $xPath
985
  * @param string $cssSelector
986
  *
987
  * @return bool
 
 
988
  */
989
- private function existsMatchForCssSelector(DOMXPath $xPath, $cssSelector)
990
  {
991
- $nodesMatchingSelector = $xPath->query($this->translateCssToXpath($cssSelector));
 
 
 
 
 
 
 
992
 
993
  return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0;
994
  }
@@ -1000,7 +1127,7 @@ class Emogrifier
1000
  *
1001
  * @return string
1002
  */
1003
- private function getCssFromAllStyleNodes(DOMXPath $xPath)
1004
  {
1005
  $styleNodes = $xPath->query('//style');
1006
 
@@ -1030,35 +1157,55 @@ class Emogrifier
1030
  *
1031
  * @return void
1032
  */
1033
- protected function addStyleElementToDocument(DOMDocument $document, $css)
1034
  {
1035
  $styleElement = $document->createElement('style', $css);
1036
  $styleAttribute = $document->createAttribute('type');
1037
  $styleAttribute->value = 'text/css';
1038
  $styleElement->appendChild($styleAttribute);
1039
 
1040
- $head = $this->getOrCreateHeadElement($document);
1041
- $head->appendChild($styleElement);
1042
  }
1043
 
1044
  /**
1045
- * Returns the existing or creates a new head element in $document.
1046
  *
1047
  * @param \DOMDocument $document
1048
- *
1049
- * @return \DOMNode the head element
1050
  */
1051
- private function getOrCreateHeadElement(DOMDocument $document)
1052
  {
1053
- $head = $document->getElementsByTagName('head')->item(0);
 
 
1054
 
1055
- if ($head === null) {
1056
- $head = $document->createElement('head');
1057
- $html = $document->getElementsByTagName('html')->item(0);
1058
- $html->insertBefore($head, $document->getElementsByTagName('body')->item(0));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1059
  }
1060
 
1061
- return $head;
1062
  }
1063
 
1064
  /**
@@ -1091,25 +1238,24 @@ class Emogrifier
1091
  $mediaTypesExpression = '|' . implode('|', array_keys($this->allowedMediaTypes));
1092
  }
1093
 
 
1094
  $cssForAllowedMediaTypes = preg_replace_callback(
1095
- '#@media\\s+(?:only\\s)?(?:[\\s{\\(]' . $mediaTypesExpression . ')\\s?[^{]+{.*}\\s*}\\s*#misU',
1096
- array( $this, '_media_concat' ),
 
 
1097
  $cssWithoutComments
1098
  );
1099
 
1100
  // filter the CSS
1101
- $search = array(
1102
  'import directives' => '/^\\s*@import\\s[^;]+;/misU',
1103
  'remaining media enclosures' => '/^\\s*@media\\s[^{]+{(.*)}\\s*}\\s/misU',
1104
- );
1105
 
1106
  $cleanedCss = preg_replace($search, '', $cssForAllowedMediaTypes);
1107
 
1108
- return array('css' => $cleanedCss, 'media' => self::$_media);
1109
- }
1110
-
1111
- private function _media_concat( $matches ) {
1112
- self::$_media .= $matches[0];
1113
  }
1114
 
1115
  /**
@@ -1117,9 +1263,9 @@ class Emogrifier
1117
  *
1118
  * @return \DOMDocument
1119
  */
1120
- private function createXmlDocument()
1121
  {
1122
- $xmlDocument = new DOMDocument;
1123
  $xmlDocument->encoding = 'UTF-8';
1124
  $xmlDocument->strictErrorChecking = false;
1125
  $xmlDocument->formatOutput = true;
@@ -1196,7 +1342,7 @@ class Emogrifier
1196
  */
1197
  private function addContentTypeMetaTag($html)
1198
  {
1199
- $hasContentTypeMetaTag = stristr($html, 'Content-Type') !== false;
1200
  if ($hasContentTypeMetaTag) {
1201
  return $html;
1202
  }
@@ -1251,7 +1397,7 @@ class Emogrifier
1251
  $precedence = 0;
1252
  $value = 100;
1253
  // ids: worth 100, classes: worth 10, elements: worth 1
1254
- $search = array('\\#','\\.','');
1255
 
1256
  foreach ($search as $s) {
1257
  if (trim($selector) === '') {
@@ -1268,10 +1414,6 @@ class Emogrifier
1268
  return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
1269
  }
1270
 
1271
- private function translateCssToXpath_callback( $matches ) {
1272
- return strtolower($matches[0]);
1273
- }
1274
-
1275
  /**
1276
  * Maps a CSS selector to an XPath query string.
1277
  *
@@ -1286,47 +1428,106 @@ class Emogrifier
1286
  $paddedSelector = ' ' . $cssSelector . ' ';
1287
  $lowercasePaddedSelector = preg_replace_callback(
1288
  '/\\s+\\w+\\s+/',
1289
- array( $this, 'translateCssToXpath_callback' ),
 
 
1290
  $paddedSelector
1291
  );
1292
-
1293
  $trimmedLowercaseSelector = trim($lowercasePaddedSelector);
1294
  $xPathKey = md5($trimmedLowercaseSelector);
1295
- if (!isset($this->caches[self::CACHE_KEY_XPATH][$xPathKey])) {
1296
- $roughXpath = '//' . preg_replace(
1297
- array_keys($this->xPathRules),
1298
- $this->xPathRules,
1299
- $trimmedLowercaseSelector
1300
- );
1301
- $xPathWithIdAttributeMatchers = preg_replace_callback(
1302
- self::ID_ATTRIBUTE_MATCHER,
1303
- array($this, 'matchIdAttributes'),
1304
- $roughXpath
1305
- );
1306
- $xPathWithIdAttributeAndClassMatchers = preg_replace_callback(
1307
- self::CLASS_ATTRIBUTE_MATCHER,
1308
- array($this, 'matchClassAttributes'),
1309
- $xPathWithIdAttributeMatchers
1310
- );
1311
-
1312
- // Advanced selectors are going to require a bit more advanced emogrification.
1313
- // When we required PHP 5.3, we could do this with closures.
1314
- $xPathWithIdAttributeAndClassMatchers = preg_replace_callback(
1315
- '/([^\\/]+):nth-child\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
1316
- array($this, 'translateNthChild'),
1317
- $xPathWithIdAttributeAndClassMatchers
1318
- );
1319
- $finalXpath = preg_replace_callback(
1320
- '/([^\\/]+):nth-of-type\\(\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
1321
- array($this, 'translateNthOfType'),
1322
- $xPathWithIdAttributeAndClassMatchers
1323
- );
1324
 
1325
- $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey] = $finalXpath;
 
 
 
 
 
 
 
 
 
 
 
 
1326
  }
 
 
1327
  return $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey];
1328
  }
1329
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1330
  /**
1331
  * @param string[] $match
1332
  *
@@ -1340,15 +1541,25 @@ class Emogrifier
1340
  /**
1341
  * @param string[] $match
1342
  *
1343
- * @return string
1344
  */
1345
  private function matchClassAttributes(array $match)
1346
  {
1347
- return ($match[1] !== '' ? $match[1] : '*') . '[contains(concat(" ",@class," "),concat(" ","' .
1348
- implode(
1349
- '"," "))][contains(concat(" ",@class," "),concat(" ","',
1350
- explode('.', substr($match[2], 1))
1351
- ) . '"," "))]';
 
 
 
 
 
 
 
 
 
 
1352
  }
1353
 
1354
  /**
@@ -1364,21 +1575,21 @@ class Emogrifier
1364
  if ($parseResult[self::MULTIPLIER] < 0) {
1365
  $parseResult[self::MULTIPLIER] = abs($parseResult[self::MULTIPLIER]);
1366
  $xPathExpression = sprintf(
1367
- '*[(last() - position()) mod %u = %u]/self::%s',
1368
  $parseResult[self::MULTIPLIER],
1369
  $parseResult[self::INDEX],
1370
  $match[1]
1371
  );
1372
  } else {
1373
  $xPathExpression = sprintf(
1374
- '*[position() mod %u = %u]/self::%s',
1375
  $parseResult[self::MULTIPLIER],
1376
  $parseResult[self::INDEX],
1377
  $match[1]
1378
  );
1379
  }
1380
  } else {
1381
- $xPathExpression = sprintf('*[%u]/self::%s', $parseResult[self::INDEX], $match[1]);
1382
  }
1383
 
1384
  return $xPathExpression;
@@ -1397,21 +1608,21 @@ class Emogrifier
1397
  if ($parseResult[self::MULTIPLIER] < 0) {
1398
  $parseResult[self::MULTIPLIER] = abs($parseResult[self::MULTIPLIER]);
1399
  $xPathExpression = sprintf(
1400
- '%s[(last() - position()) mod %u = %u]',
1401
  $match[1],
1402
  $parseResult[self::MULTIPLIER],
1403
  $parseResult[self::INDEX]
1404
  );
1405
  } else {
1406
  $xPathExpression = sprintf(
1407
- '%s[position() mod %u = %u]',
1408
  $match[1],
1409
  $parseResult[self::MULTIPLIER],
1410
  $parseResult[self::INDEX]
1411
  );
1412
  }
1413
  } else {
1414
- $xPathExpression = sprintf('%s[%u]', $match[1], $parseResult[self::INDEX]);
1415
  }
1416
 
1417
  return $xPathExpression;
@@ -1424,20 +1635,20 @@ class Emogrifier
1424
  */
1425
  private function parseNth(array $match)
1426
  {
1427
- if (in_array(strtolower($match[2]), array('even', 'odd'), true)) {
1428
  // we have "even" or "odd"
1429
  $index = strtolower($match[2]) === 'even' ? 0 : 1;
1430
- return array(self::MULTIPLIER => 2, self::INDEX => $index);
1431
  }
1432
  if (stripos($match[2], 'n') === false) {
1433
  // if there is a multiplier
1434
- $index = (int) str_replace(' ', '', $match[2]);
1435
- return array(self::INDEX => $index);
1436
  }
1437
 
1438
  if (isset($match[3])) {
1439
  $multipleTerm = str_replace($match[3], '', $match[2]);
1440
- $index = (int) str_replace(' ', '', $match[3]);
1441
  } else {
1442
  $multipleTerm = $match[2];
1443
  $index = 0;
@@ -1448,16 +1659,16 @@ class Emogrifier
1448
  if ($multiplier === '') {
1449
  $multiplier = 1;
1450
  } elseif ($multiplier === '0') {
1451
- return array(self::INDEX => $index);
1452
  } else {
1453
- $multiplier = (int) $multiplier;
1454
  }
1455
 
1456
  while ($index < 0) {
1457
  $index += abs($multiplier);
1458
  }
1459
 
1460
- return array(self::MULTIPLIER => $multiplier, self::INDEX => $index);
1461
  }
1462
 
1463
  /**
@@ -1485,11 +1696,11 @@ class Emogrifier
1485
  return $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock];
1486
  }
1487
 
1488
- $properties = array();
1489
  $declarations = preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock);
1490
 
1491
  foreach ($declarations as $declaration) {
1492
- $matches = array();
1493
  if (!preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/', trim($declaration), $matches)) {
1494
  continue;
1495
  }
@@ -1509,12 +1720,22 @@ class Emogrifier
1509
  * @param \DOMXPath $xPath
1510
  *
1511
  * @return \DOMElement[]
 
 
1512
  */
1513
- private function getNodesToExclude(DOMXPath $xPath)
1514
  {
1515
- $excludedNodes = array();
1516
  foreach (array_keys($this->excludedSelectors) as $selectorToExclude) {
1517
- foreach ($xPath->query($this->translateCssToXpath($selectorToExclude)) as $node) {
 
 
 
 
 
 
 
 
1518
  $excludedNodes[] = $node;
1519
  }
1520
  }
@@ -1523,9 +1744,9 @@ class Emogrifier
1523
  }
1524
 
1525
  /**
1526
- * Handles invalid xPath expression warnings, generated by process() method,
1527
- * during querying \DOMDocument and trigger \InvalidArgumentException
1528
- * with invalid selector.
1529
  *
1530
  * @param int $type
1531
  * @param string $message
@@ -1536,22 +1757,55 @@ class Emogrifier
1536
  * @return bool always false
1537
  *
1538
  * @throws \InvalidArgumentException
1539
- */
1540
- public function handleXpathError($type, $message, $file, $line, array $context)
1541
- {
1542
- if ($type === E_WARNING && isset($context['cssRule']['selector'])) {
1543
- throw new InvalidArgumentException(
1544
- sprintf(
1545
- '%s in selector >> %s << in %s on line %s',
1546
- $message,
1547
- $context['cssRule']['selector'],
1548
- $file,
1549
- $line
1550
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1551
  );
1552
  }
1553
 
1554
  // the normal error handling continues when handler return false
1555
  return false;
1556
  }
 
 
 
 
 
 
 
 
 
 
 
 
1557
  }
4
  *
5
  * For more information, please see the README.md file.
6
  *
7
+ * @version 2.0.0
8
  *
9
  * @author Cameron Brooks
10
  * @author Jaime Prado
11
+ * @author Oliver Klee <github@oliverklee.de>
12
  * @author Roman Ožana <ozana@omdesign.cz>
13
  * @author Sander Kruger <s.kruger@invessel.com>
14
+ * @author Zoli Szabó <zoli.szabo+github@gmail.com>
 
15
  */
 
16
  class Emogrifier
17
  {
18
  /**
87
  /**
88
  * @var bool[]
89
  */
90
+ private $excludedSelectors = [];
91
 
92
  /**
93
  * @var string[]
94
  */
95
+ private $unprocessableHtmlTags = ['wbr'];
96
 
97
  /**
98
  * @var bool[]
99
  */
100
+ private $allowedMediaTypes = ['all' => true, 'screen' => true, 'print' => true];
101
 
102
  /**
103
  * @var mixed[]
104
  */
105
+ private $caches = [
106
+ self::CACHE_KEY_CSS => [],
107
+ self::CACHE_KEY_SELECTOR => [],
108
+ self::CACHE_KEY_XPATH => [],
109
+ self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [],
110
+ self::CACHE_KEY_COMBINED_STYLES => [],
111
+ ];
112
 
113
  /**
114
  * the visited nodes with the XPath paths as array keys
115
  *
116
  * @var \DOMElement[]
117
  */
118
+ private $visitedNodes = [];
119
 
120
  /**
121
  * the styles to apply to the nodes with the XPath paths as array keys for the outer array
123
  *
124
  * @var string[][]
125
  */
126
+ private $styleAttributesForNodes = [];
127
 
128
  /**
129
  * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved.
156
  /**
157
  * @var string[]
158
  */
159
+ private $xPathRules = [
160
+ // attribute presence
161
+ '/^\\[(\\w+|\\w+\\=[\'"]?\\w+[\'"]?)\\]/' => '*[@\\1]',
162
+ // type and attribute exact value
 
 
 
 
 
 
 
 
 
 
 
 
163
  '/(\\w)\\[(\\w+)\\=[\'"]?([\\w\\s]+)[\'"]?\\]/' => '\\1[@\\2="\\3"]',
164
+ // type and attribute value with ~ (one word within a whitespace-separated list of words)
165
+ '/([\\w\\*]+)\\[(\\w+)[\\s]*\\~\\=[\\s]*[\'"]?([\\w\\-_\\/]+)[\'"]?\\]/'
166
  => '\\1[contains(concat(" ", @\\2, " "), concat(" ", "\\3", " "))]',
167
+ // type and attribute value with | (either exact value match or prefix followed by a hyphen)
168
+ '/([\\w\\*]+)\\[(\\w+)[\\s]*\\|\\=[\\s]*[\'"]?([\\w\\-_\\s\\/]+)[\'"]?\\]/'
 
 
 
 
 
 
 
169
  => '\\1[@\\2="\\3" or starts-with(@\\2, concat("\\3", "-"))]',
170
+ // type and attribute value with ^ (prefix match)
171
+ '/([\\w\\*]+)\\[(\\w+)[\\s]*\\^\\=[\\s]*[\'"]?([\\w\\-_\\/]+)[\'"]?\\]/' => '\\1[starts-with(@\\2, "\\3")]',
172
+ // type and attribute value with * (substring match)
173
+ '/([\\w\\*]+)\\[(\\w+)[\\s]*\\*\\=[\\s]*[\'"]?([\\w\\-_\\s\\/:;]+)[\'"]?\\]/' => '\\1[contains(@\\2, "\\3")]',
174
+ // adjacent sibling
175
+ '/\\s+\\+\\s+/' => '/following-sibling::*[1]/self::',
176
+ // child
177
+ '/\\s*>\\s*/' => '/',
178
+ // descendant
179
+ '/\\s+(?=.*[^\\]]{1}$)/' => '//',
180
+ // type and :first-child
181
+ '/([^\\/]+):first-child/i' => '*[1]/self::\\1',
182
+ // type and :last-child
183
+ '/([^\\/]+):last-child/i' => '*[last()]/self::\\1',
184
+
185
+ // The following matcher will break things if it is placed before the adjacent matcher.
186
+ // So one of the matchers matches either too much or not enough.
187
+ // type and attribute value with $ (suffix match)
188
+ '/([\\w\\*]+)\\[(\\w+)[\\s]*\\$\\=[\\s]*[\'"]?([\\w\\-_\\s\\/]+)[\'"]?\\]/'
189
+ => '\\1[substring(@\\2, string-length(@\\2) - string-length("\\3") + 1) = "\\3"]',
190
+ ];
191
 
192
  /**
193
  * Determines whether CSS styles that have an equivalent HTML attribute
205
  *
206
  * @var mixed[][]
207
  */
208
+ private $cssToHtmlMap = [
209
+ 'background-color' => [
210
  'attribute' => 'bgcolor',
211
+ ],
212
+ 'text-align' => [
213
  'attribute' => 'align',
214
+ 'nodes' => ['p', 'div', 'td'],
215
+ 'values' => ['left', 'right', 'center', 'justify'],
216
+ ],
217
+ 'float' => [
218
  'attribute' => 'align',
219
+ 'nodes' => ['table', 'img'],
220
+ 'values' => ['left', 'right'],
221
+ ],
222
+ 'border-spacing' => [
223
  'attribute' => 'cellspacing',
224
+ 'nodes' => ['table'],
225
+ ],
226
+ ];
227
 
228
+ /**
229
+ * Emogrifier will throw Exceptions when it encounters an error instead of silently ignoring them.
230
+ *
231
+ * @var bool
232
+ */
233
+ private $debug = false;
234
 
235
  /**
236
  * The constructor.
288
  */
289
  public function emogrify()
290
  {
291
+ return $this->createAndProcessXmlDocument()->saveHTML();
 
 
 
 
 
 
 
 
 
292
  }
293
 
294
  /**
302
  * @throws \BadMethodCallException
303
  */
304
  public function emogrifyBodyContent()
305
+ {
306
+ $xmlDocument = $this->createAndProcessXmlDocument();
307
+ $bodyNodeHtml = $xmlDocument->saveHTML($this->getBodyElement($xmlDocument));
308
+
309
+ return str_replace(['<body>', '</body>'], '', $bodyNodeHtml);
310
+ }
311
+
312
+ /**
313
+ * Creates an XML document from $this->html and emogrifies ist.
314
+ *
315
+ * @return \DOMDocument
316
+ *
317
+ * @throws \BadMethodCallException
318
+ */
319
+ private function createAndProcessXmlDocument()
320
  {
321
  if ($this->html === '') {
322
+ throw new \BadMethodCallException('Please set some HTML first.', 1390393096);
323
  }
324
 
325
+ $xmlDocument = $this->createRawXmlDocument();
326
+ $this->ensureExistenceOfBodyElement($xmlDocument);
327
  $this->process($xmlDocument);
328
 
329
+ return $xmlDocument;
 
 
 
 
 
330
  }
331
 
332
  /**
340
  *
341
  * @throws \InvalidArgumentException
342
  */
343
+ protected function process(\DOMDocument $xmlDocument)
344
  {
345
+ $xPath = new \DOMXPath($xmlDocument);
346
  $this->clearAllCaches();
 
 
 
 
 
347
  $this->purgeVisitedNodes();
348
+ set_error_handler([$this, 'handleXpathQueryWarnings'], E_WARNING);
349
 
350
+ $this->normalizeStyleAttributesOfAllNodes($xPath);
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
  // grab any existing style blocks from the html and append them to the existing CSS
353
  // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
354
  $allCss = $this->css;
 
355
  if ($this->isStyleBlocksParsingEnabled) {
356
  $allCss .= $this->getCssFromAllStyleNodes($xPath);
357
  }
360
  $excludedNodes = $this->getNodesToExclude($xPath);
361
  $cssRules = $this->parseCssRules($cssParts['css']);
362
  foreach ($cssRules as $cssRule) {
363
+ // There's no real way to test "PHP Warning" output generated by the following XPath query unless PHPUnit
364
+ // converts it to an exception. Unfortunately, this would only apply to tests and not work for production
365
+ // executions, which can still flood logs/output unnecessarily. Instead, Emogrifier's error handler should
366
+ // always throw an exception and it must be caught here and only rethrown if in debug mode.
367
+ try {
368
+ // \DOMXPath::query will always return a DOMNodeList or an exception when errors are caught.
369
+ $nodesMatchingCssSelectors = $xPath->query($this->translateCssToXpath($cssRule['selector']));
370
+ } catch (\InvalidArgumentException $e) {
371
+ if ($this->debug) {
372
+ throw $e;
373
+ }
374
  continue;
375
  }
376
 
379
  if (in_array($node, $excludedNodes, true)) {
380
  continue;
381
  }
 
382
  // if it has a style attribute, get it, process it, and append (overwrite) new stuff
383
  if ($node->hasAttribute('style')) {
384
  // break it up into an associative array
385
  $oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
386
  } else {
387
+ $oldStyleDeclarations = [];
388
  }
389
  $newStyleDeclarations = $this->parseCssDeclarationsBlock($cssRule['declarationsBlock']);
 
 
 
390
  $node->setAttribute(
391
  'style',
392
  $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations)
394
  }
395
  }
396
 
 
 
397
  if ($this->isInlineStyleAttributesParsingEnabled) {
398
  $this->fillStyleAttributesWithMergedStyles();
399
  }
400
 
401
+ if ($this->shouldMapCssToHtml) {
402
+ $this->mapAllInlineStylesToHtmlAttributes($xPath);
403
+ }
404
+
405
  if ($this->shouldKeepInvisibleNodes) {
406
  $this->removeInvisibleNodes($xPath);
407
  }
408
 
409
+ $this->removeImportantAnnotationFromAllInlineStyles($xPath);
410
+
411
  $this->copyCssWithMediaToStyleNode($xmlDocument, $xPath, $cssParts['media']);
412
+
413
+ restore_error_handler();
414
+ }
415
+
416
+ /**
417
+ * Searches for all nodes with a style attribute, transforms the CSS found
418
+ * to HTML attributes and adds those attributes to each node.
419
+ *
420
+ * @param \DOMXPath $xPath
421
+ *
422
+ * @return void
423
+ */
424
+ private function mapAllInlineStylesToHtmlAttributes(\DOMXPath $xPath)
425
+ {
426
+ /** @var \DOMElement $node */
427
+ foreach ($this->getAllNodesWithStyleAttribute($xPath) as $node) {
428
+ $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
429
+ $this->mapCssToHtmlAttributes($inlineStyleDeclarations, $node);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Searches for all nodes with a style attribute and removes the "!important" annotations out of
435
+ * the inline style declarations, eventually by rearranging declarations.
436
+ *
437
+ * @param \DOMXPath $xPath
438
+ *
439
+ * @return void
440
+ */
441
+ private function removeImportantAnnotationFromAllInlineStyles(\DOMXPath $xPath)
442
+ {
443
+ foreach ($this->getAllNodesWithStyleAttribute($xPath) as $node) {
444
+ $this->removeImportantAnnotationFromNodeInlineStyle($node);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Removes the "!important" annotations out of the inline style declarations,
450
+ * eventually by rearranging declarations.
451
+ * Rearranging needed when !important shorthand properties are followed by some of their
452
+ * not !important expanded-version properties.
453
+ * For example "font: 12px serif !important; font-size: 13px;" must be reordered
454
+ * to "font-size: 13px; font: 12px serif;" in order to remain correct.
455
+ *
456
+ * @param \DOMElement $node
457
+ *
458
+ * @return void
459
+ */
460
+ private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node)
461
+ {
462
+ $inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
463
+ $regularStyleDeclarations = [];
464
+ $importantStyleDeclarations = [];
465
+ foreach ($inlineStyleDeclarations as $property => $value) {
466
+ if ($this->attributeValueIsImportant($value)) {
467
+ $importantStyleDeclarations[$property] = trim(str_replace('!important', '', $value));
468
+ } else {
469
+ $regularStyleDeclarations[$property] = $value;
470
+ }
471
+ }
472
+ $inlineStyleDeclarationsInNewOrder = array_merge(
473
+ $regularStyleDeclarations,
474
+ $importantStyleDeclarations
475
+ );
476
+ $node->setAttribute(
477
+ 'style',
478
+ $this->generateStyleStringFromSingleDeclarationsArray($inlineStyleDeclarationsInNewOrder)
479
+ );
480
+ }
481
+
482
+ /**
483
+ * Returns a list with all DOM nodes that have a style attribute.
484
+ *
485
+ * @param \DOMXPath $xPath
486
+ *
487
+ * @return \DOMNodeList
488
+ */
489
+ private function getAllNodesWithStyleAttribute(\DOMXPath $xPath)
490
+ {
491
+ return $xPath->query('//*[@style]');
492
  }
493
 
494
  /**
498
  * node.
499
  *
500
  * @param string[] $styles the new CSS styles taken from the global styles to be applied to this node
501
+ * @param \DOMElement $node node to apply styles to
502
  *
503
  * @return void
504
  */
505
+ private function mapCssToHtmlAttributes(array $styles, \DOMElement $node)
506
  {
507
  foreach ($styles as $property => $value) {
508
  // Strip !important indicator
517
  * This method maps a CSS rule to HTML attributes and adds those to the node.
518
  *
519
  * @param string $property the name of the CSS property to map
520
+ * @param string $value the value of the style rule to map
521
+ * @param \DOMElement $node node to apply styles to
522
  *
523
  * @return void
524
  */
525
+ private function mapCssToHtmlAttribute($property, $value, \DOMElement $node)
526
  {
527
  if (!$this->mapSimpleCssProperty($property, $value, $node)) {
528
  $this->mapComplexCssProperty($property, $value, $node);
533
  * Looks up the CSS property in the mapping table and maps it if it matches the conditions.
534
  *
535
  * @param string $property the name of the CSS property to map
536
+ * @param string $value the value of the style rule to map
537
+ * @param \DOMElement $node node to apply styles to
538
  *
539
  * @return bool true if the property cab be mapped using the simple mapping table
540
  */
541
+ private function mapSimpleCssProperty($property, $value, \DOMElement $node)
542
  {
543
  if (!isset($this->cssToHtmlMap[$property])) {
544
  return false;
560
  * Maps CSS properties that need special transformation to an HTML attribute.
561
  *
562
  * @param string $property the name of the CSS property to map
563
+ * @param string $value the value of the style rule to map
564
+ * @param \DOMElement $node node to apply styles to
565
  *
566
  * @return void
567
  */
568
+ private function mapComplexCssProperty($property, $value, \DOMElement $node)
569
  {
570
  $nodeName = $node->nodeName;
571
  $isTable = $nodeName === 'table';
577
  // Parse out the color, if any
578
  $styles = explode(' ', $value);
579
  $first = $styles[0];
580
+ if (!is_numeric($first[0]) && strpos($first, 'url') !== 0) {
581
  // This is not a position or image, assume it's a color
582
  $node->setAttribute('bgcolor', $first);
583
  }
625
  {
626
  $values = preg_split('/\\s+/', $value);
627
 
628
+ $css = [];
629
  $css['top'] = $values[0];
630
  $css['right'] = (count($values) > 1) ? $values[1] : $css['top'];
631
  $css['bottom'] = (count($values) > 2) ? $values[2] : $css['top'];
650
  $cssKey = md5($css);
651
  if (!isset($this->caches[self::CACHE_KEY_CSS][$cssKey])) {
652
  // process the CSS file for selectors and definitions
653
+ preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mi', $css, $matches, PREG_SET_ORDER);
654
 
655
+ $cssRules = [];
656
+ /** @var string[][] $matches */
657
  /** @var string[] $cssRule */
658
  foreach ($matches as $key => $cssRule) {
659
  $cssDeclaration = trim($cssRule[2]);
666
  // don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
667
  // only allow structural pseudo-classes
668
  $hasPseudoElement = strpos($selector, '::') !== false;
669
+ $hasAnyPseudoClass = (bool)preg_match('/:[a-zA-Z]/', $selector);
670
+ $hasSupportedPseudoClass = (bool)preg_match(
671
+ '/:(\\S+\\-(child|type\\()|not\\([[:ascii:]]*\\))/i',
672
+ $selector
673
+ );
674
  if ($hasPseudoElement || ($hasAnyPseudoClass && !$hasSupportedPseudoClass)) {
675
  continue;
676
  }
677
 
678
+ $cssRules[] = [
679
  'selector' => trim($selector),
680
  'declarationsBlock' => $cssDeclaration,
681
  // keep track of where it appears in the file, since order is important
682
  'line' => $key,
683
+ ];
684
  }
685
  }
686
 
687
+ usort($cssRules, [$this, 'sortBySelectorPrecedence']);
688
 
689
  $this->caches[self::CACHE_KEY_CSS][$cssKey] = $cssRules;
690
  }
759
  */
760
  private function clearCache($key)
761
  {
762
+ $allowedCacheKeys = [
763
  self::CACHE_KEY_CSS,
764
  self::CACHE_KEY_SELECTOR,
765
  self::CACHE_KEY_XPATH,
766
  self::CACHE_KEY_CSS_DECLARATIONS_BLOCK,
767
  self::CACHE_KEY_COMBINED_STYLES,
768
+ ];
769
  if (!in_array($key, $allowedCacheKeys, true)) {
770
+ throw new \InvalidArgumentException('Invalid cache key: ' . $key, 1391822035);
771
  }
772
 
773
+ $this->caches[$key] = [];
774
  }
775
 
776
  /**
780
  */
781
  private function purgeVisitedNodes()
782
  {
783
+ $this->visitedNodes = [];
784
+ $this->styleAttributesForNodes = [];
785
  }
786
 
787
  /**
881
  *
882
  * @return void
883
  */
884
+ private function removeInvisibleNodes(\DOMXPath $xPath)
885
  {
886
  $nodesWithStyleDisplayNone = $xPath->query(
887
  '//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]'
894
  // we don't try to call removeChild on a nonexistent child node
895
  /** @var \DOMNode $node */
896
  foreach ($nodesWithStyleDisplayNone as $node) {
897
+ if ($node->parentNode && is_callable([$node->parentNode, 'removeChild'])) {
898
  $node->parentNode->removeChild($node);
899
  }
900
  }
901
  }
902
 
903
+ /**
904
+ * Parses the document and normalizes all existing CSS attributes.
905
+ * This changes 'DISPLAY: none' to 'display: none'.
906
+ * We wouldn't have to do this if DOMXPath supported XPath 2.0.
907
+ * Also stores a reference of nodes with existing inline styles so we don't overwrite them.
908
+ *
909
+ * @param \DOMXPath $xPath
910
+ *
911
+ * @return void
912
+ */
913
+ private function normalizeStyleAttributesOfAllNodes(\DOMXPath $xPath)
914
+ {
915
+ /** @var \DOMElement $node */
916
+ foreach ($this->getAllNodesWithStyleAttribute($xPath) as $node) {
917
+ if ($this->isInlineStyleAttributesParsingEnabled) {
918
+ $this->normalizeStyleAttributes($node);
919
+ }
920
+ // Remove style attribute in every case, so we can add them back (if inline style attributes
921
+ // parsing is enabled) to the end of the style list, thus keeping the right priority of CSS rules;
922
+ // else original inline style rules may remain at the beginning of the final inline style definition
923
+ // of a node, which may give not the desired results
924
+ $node->removeAttribute('style');
925
+ }
926
  }
927
 
928
  /**
932
  *
933
  * @return void
934
  */
935
+ private function normalizeStyleAttributes(\DOMElement $node)
936
  {
937
  $normalizedOriginalStyle = preg_replace_callback(
938
  '/[A-z\\-]+(?=\\:)/S',
939
+ function (array $m) {
940
+ return strtolower($m[0]);
941
+ },
942
  $node->getAttribute('style')
943
  );
944
 
999
 
1000
  $newAttributeValue = $newStyles[$attributeName];
1001
  if ($this->attributeValueIsImportant($attributeValue)
1002
+ && !$this->attributeValueIsImportant($newAttributeValue)
1003
  ) {
1004
  $combinedStyles[$attributeName] = $attributeValue;
1005
  }
1016
  return $trimmedStyle;
1017
  }
1018
 
1019
+ /**
1020
+ * Generates a CSS style string suitable to be used inline from the $styleDeclarations property => value array.
1021
+ *
1022
+ * @param string[] $styleDeclarations
1023
+ *
1024
+ * @return string
1025
+ */
1026
+ private function generateStyleStringFromSingleDeclarationsArray(array $styleDeclarations)
1027
+ {
1028
+ return $this->generateStyleStringFromDeclarationsArrays([], $styleDeclarations);
1029
+ }
1030
+
1031
  /**
1032
  * Checks whether $attributeValue is marked as !important.
1033
  *
1049
  *
1050
  * @return void
1051
  */
1052
+ private function copyCssWithMediaToStyleNode(\DOMDocument $xmlDocument, \DOMXPath $xPath, $css)
1053
  {
1054
  if ($css === '') {
1055
  return;
1056
  }
1057
 
1058
+ $mediaQueriesRelevantForDocument = [];
1059
 
1060
  foreach ($this->extractMediaQueriesFromCss($css) as $mediaQuery) {
1061
  foreach ($this->parseCssRules($mediaQuery['css']) as $selector) {
1079
  private function extractMediaQueriesFromCss($css)
1080
  {
1081
  preg_match_all('/@media\\b[^{]*({((?:[^{}]+|(?1))*)})/', $css, $rawMediaQueries, PREG_SET_ORDER);
1082
+ $parsedQueries = [];
1083
 
1084
+ /** @var string[][] $rawMediaQueries */
1085
  foreach ($rawMediaQueries as $mediaQuery) {
1086
  if ($mediaQuery[2] !== '') {
1087
+ $parsedQueries[] = [
1088
+ 'css' => $mediaQuery[2],
1089
  'query' => $mediaQuery[0],
1090
+ ];
1091
  }
1092
  }
1093
 
1096
 
1097
  /**
1098
  * Checks whether there is at least one matching element for $cssSelector.
1099
+ * When not in debug mode, it returns true also for invalid selectors (because they may be valid,
1100
+ * just not implemented/recognized yet by Emogrifier).
1101
  *
1102
  * @param \DOMXPath $xPath
1103
  * @param string $cssSelector
1104
  *
1105
  * @return bool
1106
+ *
1107
+ * @throws \InvalidArgumentException
1108
  */
1109
+ private function existsMatchForCssSelector(\DOMXPath $xPath, $cssSelector)
1110
  {
1111
+ try {
1112
+ $nodesMatchingSelector = $xPath->query($this->translateCssToXpath($cssSelector));
1113
+ } catch (\InvalidArgumentException $e) {
1114
+ if ($this->debug) {
1115
+ throw $e;
1116
+ }
1117
+ return true;
1118
+ }
1119
 
1120
  return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0;
1121
  }
1127
  *
1128
  * @return string
1129
  */
1130
+ private function getCssFromAllStyleNodes(\DOMXPath $xPath)
1131
  {
1132
  $styleNodes = $xPath->query('//style');
1133
 
1157
  *
1158
  * @return void
1159
  */
1160
+ protected function addStyleElementToDocument(\DOMDocument $document, $css)
1161
  {
1162
  $styleElement = $document->createElement('style', $css);
1163
  $styleAttribute = $document->createAttribute('type');
1164
  $styleAttribute->value = 'text/css';
1165
  $styleElement->appendChild($styleAttribute);
1166
 
1167
+ $bodyElement = $this->getBodyElement($document);
1168
+ $bodyElement->appendChild($styleElement);
1169
  }
1170
 
1171
  /**
1172
+ * Checks that $document has a BODY element and adds it if it is missing.
1173
  *
1174
  * @param \DOMDocument $document
 
 
1175
  */
1176
+ private function ensureExistenceOfBodyElement(\DOMDocument $document)
1177
  {
1178
+ if ($document->getElementsByTagName('body')->item(0) !== null) {
1179
+ return;
1180
+ }
1181
 
1182
+ $htmlElement = $document->getElementsByTagName('html')->item(0);
1183
+
1184
+ $htmlElement->appendChild($document->createElement('body'));
1185
+ }
1186
+
1187
+ /**
1188
+ * Returns the BODY element.
1189
+ *
1190
+ * This method assumes that there always is a BODY element.
1191
+ *
1192
+ * @param \DOMDocument $document
1193
+ *
1194
+ * @return \DOMElement
1195
+ *
1196
+ * @throws \BadMethodCallException
1197
+ */
1198
+ private function getBodyElement(\DOMDocument $document)
1199
+ {
1200
+ $bodyElement = $document->getElementsByTagName('body')->item(0);
1201
+ if ($bodyElement === null) {
1202
+ throw new \BadMethodCallException(
1203
+ 'getBodyElement method may only be called after ensureExistenceOfBodyElement has been called.',
1204
+ 1508173775427
1205
+ );
1206
  }
1207
 
1208
+ return $bodyElement;
1209
  }
1210
 
1211
  /**
1238
  $mediaTypesExpression = '|' . implode('|', array_keys($this->allowedMediaTypes));
1239
  }
1240
 
1241
+ $media = '';
1242
  $cssForAllowedMediaTypes = preg_replace_callback(
1243
+ '#@media\\s+(?:only\\s)?(?:[\\s{\\(]\\s*' . $mediaTypesExpression . ')\\s*[^{]*+{.*}\\s*}\\s*#misU',
1244
+ function ($matches) use (&$media) {
1245
+ $media .= $matches[0];
1246
+ },
1247
  $cssWithoutComments
1248
  );
1249
 
1250
  // filter the CSS
1251
+ $search = [
1252
  'import directives' => '/^\\s*@import\\s[^;]+;/misU',
1253
  'remaining media enclosures' => '/^\\s*@media\\s[^{]+{(.*)}\\s*}\\s/misU',
1254
+ ];
1255
 
1256
  $cleanedCss = preg_replace($search, '', $cssForAllowedMediaTypes);
1257
 
1258
+ return ['css' => $cleanedCss, 'media' => $media];
 
 
 
 
1259
  }
1260
 
1261
  /**
1263
  *
1264
  * @return \DOMDocument
1265
  */
1266
+ private function createRawXmlDocument()
1267
  {
1268
+ $xmlDocument = new \DOMDocument;
1269
  $xmlDocument->encoding = 'UTF-8';
1270
  $xmlDocument->strictErrorChecking = false;
1271
  $xmlDocument->formatOutput = true;
1342
  */
1343
  private function addContentTypeMetaTag($html)
1344
  {
1345
+ $hasContentTypeMetaTag = stripos($html, 'Content-Type') !== false;
1346
  if ($hasContentTypeMetaTag) {
1347
  return $html;
1348
  }
1397
  $precedence = 0;
1398
  $value = 100;
1399
  // ids: worth 100, classes: worth 10, elements: worth 1
1400
+ $search = ['\\#', '\\.', ''];
1401
 
1402
  foreach ($search as $s) {
1403
  if (trim($selector) === '') {
1414
  return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
1415
  }
1416
 
 
 
 
 
1417
  /**
1418
  * Maps a CSS selector to an XPath query string.
1419
  *
1428
  $paddedSelector = ' ' . $cssSelector . ' ';
1429
  $lowercasePaddedSelector = preg_replace_callback(
1430
  '/\\s+\\w+\\s+/',
1431
+ function (array $matches) {
1432
+ return strtolower($matches[0]);
1433
+ },
1434
  $paddedSelector
1435
  );
 
1436
  $trimmedLowercaseSelector = trim($lowercasePaddedSelector);
1437
  $xPathKey = md5($trimmedLowercaseSelector);
1438
+ if (isset($this->caches[self::CACHE_KEY_XPATH][$xPathKey])) {
1439
+ return $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey];
1440
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1441
 
1442
+ $hasNotSelector = (bool)preg_match(
1443
+ '/^([^:]+):not\\(\\s*([[:ascii:]]+)\\s*\\)$/',
1444
+ $trimmedLowercaseSelector,
1445
+ $matches
1446
+ );
1447
+ if (!$hasNotSelector) {
1448
+ $xPath = '//' . $this->translateCssToXpathPass($trimmedLowercaseSelector);
1449
+ } else {
1450
+ /** @var string[] $matches */
1451
+ $partBeforeNot = $matches[1];
1452
+ $notContents = $matches[2];
1453
+ $xPath = '//' . $this->translateCssToXpathPass($partBeforeNot) .
1454
+ '[not(' . $this->translateCssToXpathPassInline($notContents) . ')]';
1455
  }
1456
+ $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey] = $xPath;
1457
+
1458
  return $this->caches[self::CACHE_KEY_SELECTOR][$xPathKey];
1459
  }
1460
 
1461
+ /**
1462
+ * Flexibly translates the CSS selector $trimmedLowercaseSelector to an xPath selector.
1463
+ *
1464
+ * @param string $trimmedLowercaseSelector
1465
+ *
1466
+ * @return string
1467
+ */
1468
+ private function translateCssToXpathPass($trimmedLowercaseSelector)
1469
+ {
1470
+ return $this->translateCssToXpathPassWithMatchClassAttributesCallback(
1471
+ $trimmedLowercaseSelector,
1472
+ [$this, 'matchClassAttributes']
1473
+ );
1474
+ }
1475
+
1476
+ /**
1477
+ * Flexibly translates the CSS selector $trimmedLowercaseSelector to an xPath selector for inline usage.
1478
+ *
1479
+ * @param string $trimmedLowercaseSelector
1480
+ *
1481
+ * @return string
1482
+ */
1483
+ private function translateCssToXpathPassInline($trimmedLowercaseSelector)
1484
+ {
1485
+ return $this->translateCssToXpathPassWithMatchClassAttributesCallback(
1486
+ $trimmedLowercaseSelector,
1487
+ [$this, 'matchClassAttributesInline']
1488
+ );
1489
+ }
1490
+
1491
+ /**
1492
+ * Flexibly translates the CSS selector $trimmedLowercaseSelector to an xPath selector while using
1493
+ * $matchClassAttributesCallback as to match the class attributes.
1494
+ *
1495
+ * @param string $trimmedLowercaseSelector
1496
+ * @param callable $matchClassAttributesCallback
1497
+ *
1498
+ * @return string
1499
+ */
1500
+ private function translateCssToXpathPassWithMatchClassAttributesCallback(
1501
+ $trimmedLowercaseSelector,
1502
+ callable $matchClassAttributesCallback
1503
+ ) {
1504
+ $roughXpath = preg_replace(array_keys($this->xPathRules), $this->xPathRules, $trimmedLowercaseSelector);
1505
+ $xPathWithIdAttributeMatchers = preg_replace_callback(
1506
+ self::ID_ATTRIBUTE_MATCHER,
1507
+ [$this, 'matchIdAttributes'],
1508
+ $roughXpath
1509
+ );
1510
+ $xPathWithIdAttributeAndClassMatchers = preg_replace_callback(
1511
+ self::CLASS_ATTRIBUTE_MATCHER,
1512
+ $matchClassAttributesCallback,
1513
+ $xPathWithIdAttributeMatchers
1514
+ );
1515
+
1516
+ // Advanced selectors are going to require a bit more advanced emogrification.
1517
+ $xPathWithIdAttributeAndClassMatchers = preg_replace_callback(
1518
+ '/([^\\/]+):nth-child\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
1519
+ [$this, 'translateNthChild'],
1520
+ $xPathWithIdAttributeAndClassMatchers
1521
+ );
1522
+ $finalXpath = preg_replace_callback(
1523
+ '/([^\\/]+):nth-of-type\\(\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
1524
+ [$this, 'translateNthOfType'],
1525
+ $xPathWithIdAttributeAndClassMatchers
1526
+ );
1527
+
1528
+ return $finalXpath;
1529
+ }
1530
+
1531
  /**
1532
  * @param string[] $match
1533
  *
1541
  /**
1542
  * @param string[] $match
1543
  *
1544
+ * @return string xPath class attribute query wrapped in element selector
1545
  */
1546
  private function matchClassAttributes(array $match)
1547
  {
1548
+ return ($match[1] !== '' ? $match[1] : '*') . '[' . $this->matchClassAttributesInline($match) . ']';
1549
+ }
1550
+
1551
+ /**
1552
+ * @param string[] $match
1553
+ *
1554
+ * @return string xPath class attribute query
1555
+ */
1556
+ private function matchClassAttributesInline(array $match)
1557
+ {
1558
+ return 'contains(concat(" ",@class," "),concat(" ","' .
1559
+ implode(
1560
+ '"," "))][contains(concat(" ",@class," "),concat(" ","',
1561
+ explode('.', substr($match[2], 1))
1562
+ ) . '"," "))';
1563
  }
1564
 
1565
  /**
1575
  if ($parseResult[self::MULTIPLIER] < 0) {
1576
  $parseResult[self::MULTIPLIER] = abs($parseResult[self::MULTIPLIER]);
1577
  $xPathExpression = sprintf(
1578
+ '*[(last() - position()) mod %1%u = %2$u]/self::%3$s',
1579
  $parseResult[self::MULTIPLIER],
1580
  $parseResult[self::INDEX],
1581
  $match[1]
1582
  );
1583
  } else {
1584
  $xPathExpression = sprintf(
1585
+ '*[position() mod %1$u = %2$u]/self::%3$s',
1586
  $parseResult[self::MULTIPLIER],
1587
  $parseResult[self::INDEX],
1588
  $match[1]
1589
  );
1590
  }
1591
  } else {
1592
+ $xPathExpression = sprintf('*[%1$u]/self::%2$s', $parseResult[self::INDEX], $match[1]);
1593
  }
1594
 
1595
  return $xPathExpression;
1608
  if ($parseResult[self::MULTIPLIER] < 0) {
1609
  $parseResult[self::MULTIPLIER] = abs($parseResult[self::MULTIPLIER]);
1610
  $xPathExpression = sprintf(
1611
+ '%1$s[(last() - position()) mod %2$u = %3$u]',
1612
  $match[1],
1613
  $parseResult[self::MULTIPLIER],
1614
  $parseResult[self::INDEX]
1615
  );
1616
  } else {
1617
  $xPathExpression = sprintf(
1618
+ '%1$s[position() mod %2$u = %3$u]',
1619
  $match[1],
1620
  $parseResult[self::MULTIPLIER],
1621
  $parseResult[self::INDEX]
1622
  );
1623
  }
1624
  } else {
1625
+ $xPathExpression = sprintf('%1$s[%2$u]', $match[1], $parseResult[self::INDEX]);
1626
  }
1627
 
1628
  return $xPathExpression;
1635
  */
1636
  private function parseNth(array $match)
1637
  {
1638
+ if (in_array(strtolower($match[2]), ['even', 'odd'], true)) {
1639
  // we have "even" or "odd"
1640
  $index = strtolower($match[2]) === 'even' ? 0 : 1;
1641
+ return [self::MULTIPLIER => 2, self::INDEX => $index];
1642
  }
1643
  if (stripos($match[2], 'n') === false) {
1644
  // if there is a multiplier
1645
+ $index = (int)str_replace(' ', '', $match[2]);
1646
+ return [self::INDEX => $index];
1647
  }
1648
 
1649
  if (isset($match[3])) {
1650
  $multipleTerm = str_replace($match[3], '', $match[2]);
1651
+ $index = (int)str_replace(' ', '', $match[3]);
1652
  } else {
1653
  $multipleTerm = $match[2];
1654
  $index = 0;
1659
  if ($multiplier === '') {
1660
  $multiplier = 1;
1661
  } elseif ($multiplier === '0') {
1662
+ return [self::INDEX => $index];
1663
  } else {
1664
+ $multiplier = (int)$multiplier;
1665
  }
1666
 
1667
  while ($index < 0) {
1668
  $index += abs($multiplier);
1669
  }
1670
 
1671
+ return [self::MULTIPLIER => $multiplier, self::INDEX => $index];
1672
  }
1673
 
1674
  /**
1696
  return $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock];
1697
  }
1698
 
1699
+ $properties = [];
1700
  $declarations = preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock);
1701
 
1702
  foreach ($declarations as $declaration) {
1703
+ $matches = [];
1704
  if (!preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/', trim($declaration), $matches)) {
1705
  continue;
1706
  }
1720
  * @param \DOMXPath $xPath
1721
  *
1722
  * @return \DOMElement[]
1723
+ *
1724
+ * @throws \InvalidArgumentException
1725
  */
1726
+ private function getNodesToExclude(\DOMXPath $xPath)
1727
  {
1728
+ $excludedNodes = [];
1729
  foreach (array_keys($this->excludedSelectors) as $selectorToExclude) {
1730
+ try {
1731
+ $matchingNodes = $xPath->query($this->translateCssToXpath($selectorToExclude));
1732
+ } catch (\InvalidArgumentException $e) {
1733
+ if ($this->debug) {
1734
+ throw $e;
1735
+ }
1736
+ continue;
1737
+ }
1738
+ foreach ($matchingNodes as $node) {
1739
  $excludedNodes[] = $node;
1740
  }
1741
  }
1744
  }
1745
 
1746
  /**
1747
+ * Handles invalid xPath expression warnings, generated during the process() method,
1748
+ * during querying \DOMDocument and trigger \InvalidArgumentException with invalid selector
1749
+ * or \RuntimeException, depending on the source of the warning.
1750
  *
1751
  * @param int $type
1752
  * @param string $message
1757
  * @return bool always false
1758
  *
1759
  * @throws \InvalidArgumentException
1760
+ * @throws \RuntimeException
1761
+ */
1762
+ public function handleXpathQueryWarnings( // @codingStandardsIgnoreLine
1763
+ $type,
1764
+ $message,
1765
+ $file,
1766
+ $line,
1767
+ array $context
1768
+ ) {
1769
+ $selector = '';
1770
+ if (isset($context['cssRule']['selector'])) {
1771
+ // warnings generated by invalid/unrecognized selectors in method process()
1772
+ $selector = $context['cssRule']['selector'];
1773
+ } elseif (isset($context['selectorToExclude'])) {
1774
+ // warnings generated by invalid/unrecognized selectors in method getNodesToExclude()
1775
+ $selector = $context['selectorToExclude'];
1776
+ } elseif (isset($context['cssSelector'])) {
1777
+ // warnings generated by invalid/unrecognized selectors in method existsMatchForCssSelector()
1778
+ $selector = $context['cssSelector'];
1779
+ }
1780
+
1781
+ if ($selector !== '') {
1782
+ throw new \InvalidArgumentException(
1783
+ sprintf('%1$s in selector >> %2$s << in %3$s on line %4$u', $message, $selector, $file, $line),
1784
+ 1509279985
1785
+ );
1786
+ }
1787
+
1788
+ // Catches eventual warnings generated by method getAllNodesWithStyleAttribute()
1789
+ if (isset($context['xPath'])) {
1790
+ throw new \RuntimeException(
1791
+ sprintf('%1$s in %2$s on line %3$u', $message, $file, $line),
1792
+ 1509280067
1793
  );
1794
  }
1795
 
1796
  // the normal error handling continues when handler return false
1797
  return false;
1798
  }
1799
+
1800
+ /**
1801
+ * Sets the debug mode.
1802
+ *
1803
+ * @param bool $debug set to true to enable debug mode
1804
+ *
1805
+ * @return void
1806
+ */
1807
+ public function setDebug($debug)
1808
+ {
1809
+ $this->debug = $debug;
1810
+ }
1811
  }
readme.txt CHANGED
@@ -3,7 +3,7 @@ Contributors: mikejolley, automattic, adamkheckler, alexsanford1, annezazu, cena
3
  Tags: job manager, job listing, job board, job management, job lists, job list, job, jobs, company, hiring, employment, employer, employees, candidate, freelance, internship, job listings, positions, board, application, hiring, listing, manager, recruiting, recruitment, talent
4
  Requires at least: 4.7.0
5
  Tested up to: 5.0
6
- Stable tag: 1.32.0
7
  License: GPLv3
8
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
9
 
@@ -152,10 +152,14 @@ It then creates a database based on the parameters passed to it.
152
 
153
  == Changelog ==
154
 
 
 
 
 
155
  = 1.32.0 =
156
  * Enhancement: Switched from Chosen to Select2 for enhanced dropdown handling and better mobile support. May require theme update.
157
  * Enhancement: Draft and unsubmitted job listings now appear in `[job_dashboard]`, allowing users to complete their submission.
158
- * Enhancement: Filled and expired positions are now hidden from WordPress search. (@felipeelia)
159
  * Enhancement: Adds additional support for the new block editor. Restricted to classic block for compatibility with frontend editor.
160
  * Enhancement: Job types can be preselected in `[jobs]` shortcode with `?search_job_type=term-slug`. (@felipeelia)
161
  * Enhancement: Author selection in WP admin now uses a searchable dropdown.
3
  Tags: job manager, job listing, job board, job management, job lists, job list, job, jobs, company, hiring, employment, employer, employees, candidate, freelance, internship, job listings, positions, board, application, hiring, listing, manager, recruiting, recruitment, talent
4
  Requires at least: 4.7.0
5
  Tested up to: 5.0
6
+ Stable tag: 1.32.1
7
  License: GPLv3
8
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
9
 
152
 
153
  == Changelog ==
154
 
155
+ = 1.32.1 =
156
+ * Fix: Adds compatibility with PHP 7.3
157
+ * Fix: Restores original site search functionality.
158
+
159
  = 1.32.0 =
160
  * Enhancement: Switched from Chosen to Select2 for enhanced dropdown handling and better mobile support. May require theme update.
161
  * Enhancement: Draft and unsubmitted job listings now appear in `[job_dashboard]`, allowing users to complete their submission.
162
+ * Enhancement: [REVERTED IN 1.32.1] Filled and expired positions are now hidden from WordPress search. (@felipeelia)
163
  * Enhancement: Adds additional support for the new block editor. Restricted to classic block for compatibility with frontend editor.
164
  * Enhancement: Job types can be preselected in `[jobs]` shortcode with `?search_job_type=term-slug`. (@felipeelia)
165
  * Enhancement: Author selection in WP admin now uses a searchable dropdown.
wp-job-manager.php CHANGED
@@ -3,7 +3,7 @@
3
  * Plugin Name: WP Job Manager
4
  * Plugin URI: https://wpjobmanager.com/
5
  * Description: Manage job listings from the WordPress admin panel, and allow users to post jobs directly to your site.
6
- * Version: 1.32.0
7
  * Author: Automattic
8
  * Author URI: https://wpjobmanager.com/
9
  * Requires at least: 4.7.0
@@ -63,7 +63,7 @@ class WP_Job_Manager {
63
  */
64
  public function __construct() {
65
  // Define constants.
66
- define( 'JOB_MANAGER_VERSION', '1.32.0' );
67
  define( 'JOB_MANAGER_MINIMUM_WP_VERSION', '4.7.0' );
68
  define( 'JOB_MANAGER_PLUGIN_DIR', untrailingslashit( plugin_dir_path( __FILE__ ) ) );
69
  define( 'JOB_MANAGER_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) ) );
3
  * Plugin Name: WP Job Manager
4
  * Plugin URI: https://wpjobmanager.com/
5
  * Description: Manage job listings from the WordPress admin panel, and allow users to post jobs directly to your site.
6
+ * Version: 1.32.1
7
  * Author: Automattic
8
  * Author URI: https://wpjobmanager.com/
9
  * Requires at least: 4.7.0
63
  */
64
  public function __construct() {
65
  // Define constants.
66
+ define( 'JOB_MANAGER_VERSION', '1.32.1' );
67
  define( 'JOB_MANAGER_MINIMUM_WP_VERSION', '4.7.0' );
68
  define( 'JOB_MANAGER_PLUGIN_DIR', untrailingslashit( plugin_dir_path( __FILE__ ) ) );
69
  define( 'JOB_MANAGER_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) ) );