Version Description
- Fix: Adds compatibility with PHP 7.3
- Fix: Restores original site search functionality.
Download this release
Release Info
Developer | jakeom |
Plugin | 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 +5 -1
- includes/admin/class-wp-job-manager-writepanels.php +1 -1
- includes/class-wp-job-manager-email-notifications.php +1 -1
- includes/class-wp-job-manager-post-types.php +1 -31
- includes/class-wp-job-manager-usage-tracking.php +1 -1
- languages/wp-job-manager.pot +57 -57
- lib/emogrifier/class-emogrifier.php +509 -255
- readme.txt +6 -2
- wp-job-manager.php +2 -2
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 |
-
|
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' =>
|
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
|
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)
|
2 |
# This file is distributed under the GPL2+.
|
3 |
msgid ""
|
4 |
msgstr ""
|
5 |
-
"Project-Id-Version: WP Job Manager 1.32.
|
6 |
"Report-Msgid-Bugs-To: https://github.com/Automattic/WP-Job-Manager/issues\n"
|
7 |
-
"POT-Creation-Date:
|
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:
|
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:
|
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:
|
353 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
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:
|
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:
|
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:
|
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:
|
1189 |
msgid "Job categories"
|
1190 |
msgstr ""
|
1191 |
|
1192 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1193 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1194 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
1205 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1206 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
1217 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1218 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
1229 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
1238 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1239 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
1250 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
1259 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
1268 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
1277 |
msgid "Job types"
|
1278 |
msgstr ""
|
1279 |
|
1280 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1281 |
msgid "Job"
|
1282 |
msgstr ""
|
1283 |
|
1284 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1285 |
msgid "Jobs"
|
1286 |
msgstr ""
|
1287 |
|
1288 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1289 |
msgid "Add New"
|
1290 |
msgstr ""
|
1291 |
|
1292 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
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:
|
1305 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
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:
|
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:
|
1324 |
msgid "Set company logo"
|
1325 |
msgstr ""
|
1326 |
|
1327 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1328 |
msgid "Remove company logo"
|
1329 |
msgstr ""
|
1330 |
|
1331 |
-
#: includes/class-wp-job-manager-post-types.php:
|
1332 |
msgid "Use as company logo"
|
1333 |
msgstr ""
|
1334 |
|
1335 |
-
#: includes/class-wp-job-manager-post-types.php:
|
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:
|
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:
|
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:
|
2233 |
msgid "Load previous listings"
|
2234 |
msgstr ""
|
2235 |
|
2236 |
-
#: wp-job-manager.php:
|
2237 |
msgid "Invalid file type. Accepted types:"
|
2238 |
msgstr ""
|
2239 |
|
2240 |
-
#: wp-job-manager.php:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
8 |
*
|
9 |
* @author Cameron Brooks
|
10 |
* @author Jaime Prado
|
11 |
-
* @author Oliver Klee <
|
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 =
|
93 |
|
94 |
/**
|
95 |
* @var string[]
|
96 |
*/
|
97 |
-
private $unprocessableHtmlTags =
|
98 |
|
99 |
/**
|
100 |
* @var bool[]
|
101 |
*/
|
102 |
-
private $allowedMediaTypes =
|
103 |
|
104 |
/**
|
105 |
* @var mixed[]
|
106 |
*/
|
107 |
-
private $caches =
|
108 |
-
self::CACHE_KEY_CSS =>
|
109 |
-
self::CACHE_KEY_SELECTOR =>
|
110 |
-
self::CACHE_KEY_XPATH =>
|
111 |
-
self::CACHE_KEY_CSS_DECLARATIONS_BLOCK =>
|
112 |
-
self::CACHE_KEY_COMBINED_STYLES =>
|
113 |
-
|
114 |
|
115 |
/**
|
116 |
* the visited nodes with the XPath paths as array keys
|
117 |
*
|
118 |
* @var \DOMElement[]
|
119 |
*/
|
120 |
-
private $visitedNodes =
|
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 =
|
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 =
|
162 |
-
//
|
163 |
-
'
|
164 |
-
//
|
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 |
-
//
|
179 |
-
'/([\\w\\*]+)\\[(\\w+)[\\s]*\\~\\=[\\s]*[\'"]?([\\w
|
180 |
=> '\\1[contains(concat(" ", @\\2, " "), concat(" ", "\\3", " "))]',
|
181 |
-
//
|
182 |
-
'/([\\w\\*]+)\\[(\\w+)[\\s]
|
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 =
|
210 |
-
'background-color' =>
|
211 |
'attribute' => 'bgcolor',
|
212 |
-
|
213 |
-
'text-align' =>
|
214 |
'attribute' => 'align',
|
215 |
-
'nodes' =>
|
216 |
-
'values' =>
|
217 |
-
|
218 |
-
'float' =>
|
219 |
'attribute' => 'align',
|
220 |
-
'nodes' =>
|
221 |
-
'values' =>
|
222 |
-
|
223 |
-
'border-spacing' =>
|
224 |
'attribute' => 'cellspacing',
|
225 |
-
'nodes' =>
|
226 |
-
|
227 |
-
|
228 |
|
229 |
-
|
|
|
|
|
|
|
|
|
|
|
230 |
|
231 |
/**
|
232 |
* The constructor.
|
@@ -284,16 +288,7 @@ class Emogrifier
|
|
284 |
*/
|
285 |
public function emogrify()
|
286 |
{
|
287 |
-
|
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
|
313 |
}
|
314 |
|
315 |
-
$xmlDocument = $this->
|
|
|
316 |
$this->process($xmlDocument);
|
317 |
|
318 |
-
$
|
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 |
-
|
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 |
-
//
|
375 |
-
|
376 |
-
//
|
377 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 =
|
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 \
|
426 |
*
|
427 |
* @return void
|
428 |
*/
|
429 |
-
private function mapCssToHtmlAttributes(array $styles,
|
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
|
445 |
-
* @param \
|
446 |
*
|
447 |
* @return void
|
448 |
*/
|
449 |
-
private function mapCssToHtmlAttribute($property, $value,
|
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
|
461 |
-
* @param \
|
462 |
*
|
463 |
* @return bool true if the property cab be mapped using the simple mapping table
|
464 |
*/
|
465 |
-
private function mapSimpleCssProperty($property, $value,
|
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
|
488 |
-
* @param \
|
489 |
*
|
490 |
* @return void
|
491 |
*/
|
492 |
-
private function mapComplexCssProperty($property, $value,
|
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(
|
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 =
|
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^{}]*)([^{]+){([^}]*)}/
|
578 |
|
579 |
-
$cssRules =
|
|
|
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)
|
593 |
-
$hasSupportedPseudoClass = (bool)
|
|
|
|
|
|
|
594 |
if ($hasPseudoElement || ($hasAnyPseudoClass && !$hasSupportedPseudoClass)) {
|
595 |
continue;
|
596 |
}
|
597 |
|
598 |
-
$cssRules[] =
|
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,
|
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 =
|
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] =
|
694 |
}
|
695 |
|
696 |
/**
|
@@ -700,8 +780,8 @@ class Emogrifier
|
|
700 |
*/
|
701 |
private function purgeVisitedNodes()
|
702 |
{
|
703 |
-
$this->visitedNodes =
|
704 |
-
$this->styleAttributesForNodes =
|
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(
|
818 |
$node->parentNode->removeChild($node);
|
819 |
}
|
820 |
}
|
821 |
}
|
822 |
|
823 |
-
|
824 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
839 |
$node->getAttribute('style')
|
840 |
);
|
841 |
|
@@ -896,7 +999,7 @@ class Emogrifier
|
|
896 |
|
897 |
$newAttributeValue = $newStyles[$attributeName];
|
898 |
if ($this->attributeValueIsImportant($attributeValue)
|
899 |
-
|
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 =
|
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 =
|
968 |
|
|
|
969 |
foreach ($rawMediaQueries as $mediaQuery) {
|
970 |
if ($mediaQuery[2] !== '') {
|
971 |
-
$parsedQueries[] =
|
972 |
-
'css'
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
$
|
1041 |
-
$
|
1042 |
}
|
1043 |
|
1044 |
/**
|
1045 |
-
*
|
1046 |
*
|
1047 |
* @param \DOMDocument $document
|
1048 |
-
*
|
1049 |
-
* @return \DOMNode the head element
|
1050 |
*/
|
1051 |
-
private function
|
1052 |
{
|
1053 |
-
|
|
|
|
|
1054 |
|
1055 |
-
|
1056 |
-
|
1057 |
-
|
1058 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1059 |
}
|
1060 |
|
1061 |
-
return $
|
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
|
1096 |
-
|
|
|
|
|
1097 |
$cssWithoutComments
|
1098 |
);
|
1099 |
|
1100 |
// filter the CSS
|
1101 |
-
$search =
|
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
|
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
|
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 =
|
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 =
|
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
|
|
|
|
|
1290 |
$paddedSelector
|
1291 |
);
|
1292 |
-
|
1293 |
$trimmedLowercaseSelector = trim($lowercasePaddedSelector);
|
1294 |
$xPathKey = md5($trimmedLowercaseSelector);
|
1295 |
-
if (
|
1296 |
-
$
|
1297 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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] : '*') . '[
|
1348 |
-
|
1349 |
-
|
1350 |
-
|
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]),
|
1428 |
// we have "even" or "odd"
|
1429 |
$index = strtolower($match[2]) === 'even' ? 0 : 1;
|
1430 |
-
return
|
1431 |
}
|
1432 |
if (stripos($match[2], 'n') === false) {
|
1433 |
// if there is a multiplier
|
1434 |
-
$index = (int)
|
1435 |
-
return
|
1436 |
}
|
1437 |
|
1438 |
if (isset($match[3])) {
|
1439 |
$multipleTerm = str_replace($match[3], '', $match[2]);
|
1440 |
-
$index = (int)
|
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
|
1452 |
} else {
|
1453 |
-
$multiplier = (int)
|
1454 |
}
|
1455 |
|
1456 |
while ($index < 0) {
|
1457 |
$index += abs($multiplier);
|
1458 |
}
|
1459 |
|
1460 |
-
return
|
1461 |
}
|
1462 |
|
1463 |
/**
|
@@ -1485,11 +1696,11 @@ class Emogrifier
|
|
1485 |
return $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock];
|
1486 |
}
|
1487 |
|
1488 |
-
$properties =
|
1489 |
$declarations = preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock);
|
1490 |
|
1491 |
foreach ($declarations as $declaration) {
|
1492 |
-
$matches =
|
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 =
|
1516 |
foreach (array_keys($this->excludedSelectors) as $selectorToExclude) {
|
1517 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1518 |
$excludedNodes[] = $node;
|
1519 |
}
|
1520 |
}
|
@@ -1523,9 +1744,9 @@ class Emogrifier
|
|
1523 |
}
|
1524 |
|
1525 |
/**
|
1526 |
-
* Handles invalid xPath expression warnings, generated
|
1527 |
-
* during querying \DOMDocument and trigger \InvalidArgumentException
|
1528 |
-
*
|
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 |
-
|
1541 |
-
|
1542 |
-
|
1543 |
-
|
1544 |
-
|
1545 |
-
|
1546 |
-
|
1547 |
-
|
1548 |
-
|
1549 |
-
|
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.
|
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.
|
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.
|
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__ ) ) ) );
|